Merge "Add service type clients to servicediscovery dumpsys" into main
diff --git a/Tethering/common/TetheringLib/Android.bp b/Tethering/common/TetheringLib/Android.bp
index 9c2a59d..47227e3 100644
--- a/Tethering/common/TetheringLib/Android.bp
+++ b/Tethering/common/TetheringLib/Android.bp
@@ -59,6 +59,9 @@
lint: {
strict_updatability_linting: true,
},
+ aconfig_declarations: [
+ "com.android.net.flags-aconfig",
+ ],
}
java_library {
diff --git a/common/FlaggedApi.bp b/common/FlaggedApi.bp
index c382e76..56625c5 100644
--- a/common/FlaggedApi.bp
+++ b/common/FlaggedApi.bp
@@ -21,3 +21,19 @@
srcs: ["flags.aconfig"],
visibility: ["//packages/modules/Connectivity:__subpackages__"],
}
+
+aconfig_declarations {
+ name: "com.android.net.thread.flags-aconfig",
+ package: "com.android.net.thread.flags",
+ container: "system",
+ srcs: ["thread_flags.aconfig"],
+ visibility: ["//packages/modules/Connectivity:__subpackages__"],
+}
+
+aconfig_declarations {
+ name: "nearby_flags",
+ package: "com.android.nearby.flags",
+ container: "system",
+ srcs: ["nearby_flags.aconfig"],
+ visibility: ["//packages/modules/Connectivity:__subpackages__"],
+}
diff --git a/common/OWNERS b/common/OWNERS
new file mode 100644
index 0000000..e7f5d11
--- /dev/null
+++ b/common/OWNERS
@@ -0,0 +1 @@
+per-file thread_flags.aconfig = file:platform/packages/modules/Connectivity:main:/thread/OWNERS
diff --git a/common/flags.aconfig b/common/flags.aconfig
index 965354d..19b522c 100644
--- a/common/flags.aconfig
+++ b/common/flags.aconfig
@@ -5,13 +5,6 @@
# Flags used from platform code must be in under frameworks
flag {
- name: "forbidden_capability"
- namespace: "android_core_networking"
- description: "This flag controls the forbidden capability API"
- bug: "302997505"
-}
-
-flag {
name: "set_data_saver_via_cm"
namespace: "android_core_networking"
description: "Set data saver through ConnectivityManager API"
@@ -33,13 +26,6 @@
}
flag {
- name: "register_nsd_offload_engine"
- namespace: "android_core_networking"
- description: "The flag controls the access for registerOffloadEngine API in NsdManager"
- bug: "294777050"
-}
-
-flag {
name: "ipsec_transform_state"
namespace: "android_core_networking_ipsec"
description: "The flag controls the access for getIpSecTransformState and IpSecTransformState"
@@ -52,3 +38,38 @@
description: "The flag controls the access for the parcelable TetheringRequest with getSoftApConfiguration/setSoftApConfiguration API"
bug: "216524590"
}
+
+flag {
+ name: "request_restricted_wifi"
+ namespace: "android_core_networking"
+ description: "Flag for API to support requesting restricted wifi"
+ bug: "315835605"
+}
+
+flag {
+ name: "net_capability_local_network"
+ namespace: "android_core_networking"
+ description: "Flag for local network capability API"
+ bug: "313000440"
+}
+
+flag {
+ name: "support_transport_satellite"
+ namespace: "android_core_networking"
+ description: "Flag for satellite transport API"
+ bug: "320514105"
+}
+
+flag {
+ name: "nsd_subtypes_support_enabled"
+ namespace: "android_core_networking"
+ description: "Flag for API to support nsd subtypes"
+ bug: "265095929"
+}
+
+flag {
+ name: "register_nsd_offload_engine_api"
+ namespace: "android_core_networking"
+ description: "Flag for API to register nsd offload engine"
+ bug: "301713539"
+}
diff --git a/common/nearby_flags.aconfig b/common/nearby_flags.aconfig
new file mode 100644
index 0000000..b957d33
--- /dev/null
+++ b/common/nearby_flags.aconfig
@@ -0,0 +1,9 @@
+package: "com.android.nearby.flags"
+container: "system"
+
+flag {
+ name: "powered_off_finding"
+ namespace: "nearby"
+ description: "Controls whether the Powered Off Finding feature is enabled"
+ bug: "307898240"
+}
diff --git a/thread/flags/thread_base.aconfig b/common/thread_flags.aconfig
similarity index 100%
rename from thread/flags/thread_base.aconfig
rename to common/thread_flags.aconfig
diff --git a/framework-t/Android.bp b/framework-t/Android.bp
index 9203a3e..468cee4 100644
--- a/framework-t/Android.bp
+++ b/framework-t/Android.bp
@@ -197,6 +197,8 @@
],
aconfig_declarations: [
"com.android.net.flags-aconfig",
+ "com.android.net.thread.flags-aconfig",
+ "nearby_flags",
],
}
diff --git a/framework/Android.bp b/framework/Android.bp
index 1356eea..f76bbe1 100644
--- a/framework/Android.bp
+++ b/framework/Android.bp
@@ -96,7 +96,6 @@
],
impl_only_static_libs: [
"net-utils-device-common-bpf",
- "net-utils-device-common-struct",
],
libs: [
"androidx.annotation_annotation",
@@ -125,7 +124,6 @@
// Even if the library is included in "impl_only_static_libs" of defaults. This is still
// needed because java_library which doesn't understand "impl_only_static_libs".
"net-utils-device-common-bpf",
- "net-utils-device-common-struct",
],
libs: [
// This cannot be in the defaults clause above because if it were, it would be used
@@ -195,6 +193,9 @@
lint: {
baseline_filename: "lint-baseline.xml",
},
+ aconfig_declarations: [
+ "com.android.net.flags-aconfig",
+ ],
}
platform_compat_config {
diff --git a/framework/src/android/net/ConnectivityManager.java b/framework/src/android/net/ConnectivityManager.java
index 1ea1815..915ec52 100644
--- a/framework/src/android/net/ConnectivityManager.java
+++ b/framework/src/android/net/ConnectivityManager.java
@@ -74,6 +74,7 @@
import android.util.SparseIntArray;
import com.android.internal.annotations.GuardedBy;
+import com.android.modules.utils.build.SdkLevel;
import libcore.net.event.NetworkEventDispatcher;
@@ -6278,9 +6279,13 @@
// Only the system server process and the network stack have access.
@FlaggedApi(Flags.SUPPORT_IS_UID_NETWORKING_BLOCKED)
@SystemApi(client = MODULE_LIBRARIES)
- @RequiresApi(Build.VERSION_CODES.TIRAMISU) // BPF maps were only mainlined in T
+ // Note b/326143935 kernel bug can trigger crash on some T device.
+ @RequiresApi(VERSION_CODES.UPSIDE_DOWN_CAKE)
@RequiresPermission(NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK)
public boolean isUidNetworkingBlocked(int uid, boolean isNetworkMetered) {
+ if (!SdkLevel.isAtLeastU()) {
+ Log.wtf(TAG, "isUidNetworkingBlocked is not supported on pre-U devices");
+ }
final BpfNetMapsReader reader = BpfNetMapsReader.getInstance();
// Note that before V, the data saver status in bpf is written by ConnectivityService
// when receiving {@link #ACTION_RESTRICT_BACKGROUND_CHANGED}. Thus,
diff --git a/framework/src/android/net/NetworkCapabilities.java b/framework/src/android/net/NetworkCapabilities.java
index f6ef75e..84a0d29 100644
--- a/framework/src/android/net/NetworkCapabilities.java
+++ b/framework/src/android/net/NetworkCapabilities.java
@@ -1770,9 +1770,13 @@
public @NonNull NetworkCapabilities setNetworkSpecifier(
@NonNull NetworkSpecifier networkSpecifier) {
if (networkSpecifier != null
- // Transport can be test, or test + a single other transport
+ // Transport can be test, or test + a single other transport or cellular + satellite
+ // transport. Note: cellular + satellite combination is allowed since both transport
+ // use the same specifier, TelephonyNetworkSpecifier.
&& mTransportTypes != (1L << TRANSPORT_TEST)
- && Long.bitCount(mTransportTypes & ~(1L << TRANSPORT_TEST)) != 1) {
+ && Long.bitCount(mTransportTypes & ~(1L << TRANSPORT_TEST)) != 1
+ && (mTransportTypes & ~(1L << TRANSPORT_TEST))
+ != (1 << TRANSPORT_CELLULAR | 1 << TRANSPORT_SATELLITE)) {
throw new IllegalStateException("Must have a single non-test transport specified to "
+ "use setNetworkSpecifier");
}
diff --git a/netbpfload/NetBpfLoad.cpp b/netbpfload/NetBpfLoad.cpp
index 2bfaee4..9dc7cdc 100644
--- a/netbpfload/NetBpfLoad.cpp
+++ b/netbpfload/NetBpfLoad.cpp
@@ -259,9 +259,11 @@
}
if (is_platform) {
+ ALOGI("Executing apex netbpfload...");
const char * args[] = { apexNetBpfLoad, NULL, };
execve(args[0], (char**)args, envp);
- ALOGW("exec '%s' fail: %d[%s]", apexNetBpfLoad, errno, strerror(errno));
+ ALOGE("exec '%s' fail: %d[%s]", apexNetBpfLoad, errno, strerror(errno));
+ return 1;
}
if (!has_platform_bpfloader_rc && !has_platform_netbpfload_rc) {
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 fe9bbba..56202fd 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsFeatureFlags.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsFeatureFlags.java
@@ -118,6 +118,14 @@
}
/**
+ * Indicates whether {@link #NSD_KNOWN_ANSWER_SUPPRESSION} is enabled, including for testing.
+ */
+ public boolean isKnownAnswerSuppressionEnabled() {
+ return mIsKnownAnswerSuppressionEnabled
+ || isForceEnabledForTest(NSD_KNOWN_ANSWER_SUPPRESSION);
+ }
+
+ /**
* The constructor for {@link MdnsFeatureFlags}.
*/
public MdnsFeatureFlags(boolean isOffloadFeatureEnabled,
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsRecordRepository.java b/service-t/src/com/android/server/connectivity/mdns/MdnsRecordRepository.java
index fb45454..ed0bde2 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsRecordRepository.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsRecordRepository.java
@@ -388,7 +388,8 @@
"Service ID must not be reused across registrations: " + serviceId);
}
- final int existing = getServiceByName(serviceInfo.getServiceName());
+ final int existing =
+ getServiceByNameAndType(serviceInfo.getServiceName(), serviceInfo.getServiceType());
// It's OK to re-add a service that is exiting
if (existing >= 0 && !mServices.get(existing).exiting) {
throw new NameConflictException(existing);
@@ -405,16 +406,17 @@
}
/**
- * @return The ID of the service identified by its name, or -1 if none.
+ * @return The ID of the service identified by its name and type, or -1 if none.
*/
- private int getServiceByName(@Nullable String serviceName) {
- if (TextUtils.isEmpty(serviceName)) {
+ private int getServiceByNameAndType(
+ @Nullable String serviceName, @Nullable String serviceType) {
+ if (TextUtils.isEmpty(serviceName) || TextUtils.isEmpty(serviceType)) {
return -1;
}
for (int i = 0; i < mServices.size(); i++) {
- final ServiceRegistration registration = mServices.valueAt(i);
- if (MdnsUtils.equalsIgnoreDnsCase(
- serviceName, registration.serviceInfo.getServiceName())) {
+ final NsdServiceInfo info = mServices.valueAt(i).serviceInfo;
+ if (MdnsUtils.equalsIgnoreDnsCase(serviceName, info.getServiceName())
+ && MdnsUtils.equalsIgnoreDnsCase(serviceType, info.getServiceType())) {
return mServices.keyAt(i);
}
}
@@ -536,7 +538,7 @@
}
private boolean isTruncatedKnownAnswerPacket(MdnsPacket packet) {
- if (!mMdnsFeatureFlags.mIsKnownAnswerSuppressionEnabled
+ if (!mMdnsFeatureFlags.isKnownAnswerSuppressionEnabled()
// Should ignore the response packet.
|| (packet.flags & MdnsConstants.FLAGS_RESPONSE) != 0) {
return false;
@@ -743,7 +745,7 @@
// RR TTL as known by the Multicast DNS responder, the responder MUST
// send an answer so as to update the querier's cache before the record
// becomes in danger of expiration.
- if (mMdnsFeatureFlags.mIsKnownAnswerSuppressionEnabled
+ if (mMdnsFeatureFlags.isKnownAnswerSuppressionEnabled()
&& isKnownAnswer(info.record, knownAnswerRecords)) {
continue;
}
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsReplySender.java b/service-t/src/com/android/server/connectivity/mdns/MdnsReplySender.java
index a46be3b..db3845a 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsReplySender.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsReplySender.java
@@ -145,7 +145,7 @@
public void queueReply(@NonNull MdnsReplyInfo reply) {
ensureRunningOnHandlerThread(mHandler);
- if (mMdnsFeatureFlags.mIsKnownAnswerSuppressionEnabled) {
+ if (mMdnsFeatureFlags.isKnownAnswerSuppressionEnabled()) {
mDependencies.removeMessages(mHandler, MSG_SEND, reply.source);
final MdnsReplyInfo queuingReply = mSrcReplies.remove(reply.source);
@@ -231,7 +231,7 @@
@Override
public void handleMessage(@NonNull Message msg) {
final MdnsReplyInfo replyInfo;
- if (mMdnsFeatureFlags.mIsKnownAnswerSuppressionEnabled) {
+ if (mMdnsFeatureFlags.isKnownAnswerSuppressionEnabled()) {
// Retrieve the MdnsReplyInfo from the map via a source address, as the reply info
// will be combined or updated.
final InetSocketAddress source = (InetSocketAddress) msg.obj;
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 52fe06c..33b5ea4 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsServiceTypeClient.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsServiceTypeClient.java
@@ -141,8 +141,7 @@
// before sending the query, it needs to be called just before sending it.
final List<MdnsResponse> servicesToResolve = makeResponsesForResolve(socketKey);
final QueryTask queryTask = new QueryTask(taskArgs, servicesToResolve,
- getAllDiscoverySubtypes(),
- servicesToResolve.size() < listeners.size() /* sendDiscoveryQueries */);
+ getAllDiscoverySubtypes(), needSendDiscoveryQueries(listeners));
executor.submit(queryTask);
break;
}
@@ -389,8 +388,7 @@
final QueryTask queryTask = new QueryTask(
mdnsQueryScheduler.scheduleFirstRun(taskConfig, now,
minRemainingTtl, currentSessionId), servicesToResolve,
- getAllDiscoverySubtypes(),
- servicesToResolve.size() < listeners.size() /* sendDiscoveryQueries */);
+ getAllDiscoverySubtypes(), needSendDiscoveryQueries(listeners));
executor.submit(queryTask);
}
@@ -628,6 +626,10 @@
if (resolveName == null) {
continue;
}
+ if (CollectionUtils.any(resolveResponses,
+ r -> MdnsUtils.equalsIgnoreDnsCase(resolveName, r.getServiceInstanceName()))) {
+ continue;
+ }
MdnsResponse knownResponse =
serviceCache.getCachedService(resolveName, cacheKey);
if (knownResponse == null) {
@@ -644,6 +646,17 @@
return resolveResponses;
}
+ private static boolean needSendDiscoveryQueries(
+ @NonNull ArrayMap<MdnsServiceBrowserListener, ListenerInfo> listeners) {
+ // Note iterators are discouraged on ArrayMap as per its documentation
+ for (int i = 0; i < listeners.size(); i++) {
+ if (listeners.valueAt(i).searchOptions.getResolveInstanceName() == null) {
+ return true;
+ }
+ }
+ return false;
+ }
+
private void tryRemoveServiceAfterTtlExpires() {
if (!shouldRemoveServiceAfterTtlExpires()) return;
diff --git a/service-t/src/com/android/server/net/NetworkStatsService.java b/service-t/src/com/android/server/net/NetworkStatsService.java
index 80c4033..9684d18 100644
--- a/service-t/src/com/android/server/net/NetworkStatsService.java
+++ b/service-t/src/com/android/server/net/NetworkStatsService.java
@@ -2231,7 +2231,7 @@
.setDefaultNetwork(true)
.setOemManaged(ident.getOemManaged())
.setSubId(ident.getSubId()).build();
- final String ifaceVt = IFACE_VT + getSubIdForMobile(snapshot);
+ final String ifaceVt = IFACE_VT + getSubIdForCellularOrSatellite(snapshot);
findOrCreateNetworkIdentitySet(mActiveIfaces, ifaceVt).add(vtIdent);
findOrCreateNetworkIdentitySet(mActiveUidIfaces, ifaceVt).add(vtIdent);
}
@@ -2300,9 +2300,15 @@
mMobileIfaces = mobileIfaces.toArray(new String[0]);
}
- private static int getSubIdForMobile(@NonNull NetworkStateSnapshot state) {
- if (!state.getNetworkCapabilities().hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR)) {
- throw new IllegalArgumentException("Mobile state need capability TRANSPORT_CELLULAR");
+ private static int getSubIdForCellularOrSatellite(@NonNull NetworkStateSnapshot state) {
+ if (!state.getNetworkCapabilities().hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR)
+ // Both cellular and satellite are 2 different network transport at Mobile using
+ // same telephony network specifier. So adding satellite transport to consider
+ // for, when satellite network is active at mobile.
+ && !state.getNetworkCapabilities().hasTransport(
+ NetworkCapabilities.TRANSPORT_SATELLITE)) {
+ throw new IllegalArgumentException(
+ "Mobile state need capability TRANSPORT_CELLULAR or TRANSPORT_SATELLITE");
}
final NetworkSpecifier spec = state.getNetworkCapabilities().getNetworkSpecifier();
diff --git a/service-t/src/com/android/server/net/TrafficStatsRateLimitCache.java b/service-t/src/com/android/server/net/TrafficStatsRateLimitCache.java
new file mode 100644
index 0000000..8598ac4
--- /dev/null
+++ b/service-t/src/com/android/server/net/TrafficStatsRateLimitCache.java
@@ -0,0 +1,134 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.net;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.net.NetworkStats;
+
+import com.android.internal.annotations.GuardedBy;
+
+import java.time.Clock;
+import java.util.HashMap;
+import java.util.Objects;
+
+/**
+ * A thread-safe cache for storing and retrieving {@link NetworkStats.Entry} objects,
+ * with an adjustable expiry duration to manage data freshness.
+ */
+class TrafficStatsRateLimitCache {
+ private final Clock mClock;
+ private final long mExpiryDurationMs;
+
+ /**
+ * Constructs a new {@link TrafficStatsRateLimitCache} with the specified expiry duration.
+ *
+ * @param clock The {@link Clock} to use for determining timestamps.
+ * @param expiryDurationMs The expiry duration in milliseconds.
+ */
+ TrafficStatsRateLimitCache(@NonNull Clock clock, long expiryDurationMs) {
+ mClock = clock;
+ mExpiryDurationMs = expiryDurationMs;
+ }
+
+ private static class TrafficStatsCacheKey {
+ @Nullable
+ public final String iface;
+ public final int uid;
+
+ TrafficStatsCacheKey(@Nullable String iface, int uid) {
+ this.iface = iface;
+ this.uid = uid;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (!(o instanceof TrafficStatsCacheKey)) return false;
+ TrafficStatsCacheKey that = (TrafficStatsCacheKey) o;
+ return uid == that.uid && Objects.equals(iface, that.iface);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(iface, uid);
+ }
+ }
+
+ private static class TrafficStatsCacheValue {
+ public final long timestamp;
+ @NonNull
+ public final NetworkStats.Entry entry;
+
+ TrafficStatsCacheValue(long timestamp, NetworkStats.Entry entry) {
+ this.timestamp = timestamp;
+ this.entry = entry;
+ }
+ }
+
+ @GuardedBy("mMap")
+ private final HashMap<TrafficStatsCacheKey, TrafficStatsCacheValue> mMap = new HashMap<>();
+
+ /**
+ * Retrieves a {@link NetworkStats.Entry} from the cache, associated with the given key.
+ *
+ * @param iface The interface name to include in the cache key. Null if not applicable.
+ * @param uid The UID to include in the cache key. {@code UID_ALL} if not applicable.
+ * @return The cached {@link NetworkStats.Entry}, or null if not found or expired.
+ */
+ @Nullable
+ NetworkStats.Entry get(String iface, int uid) {
+ final TrafficStatsCacheKey key = new TrafficStatsCacheKey(iface, uid);
+ synchronized (mMap) { // Synchronize for thread-safety
+ final TrafficStatsCacheValue value = mMap.get(key);
+ if (value != null && !isExpired(value.timestamp)) {
+ return value.entry;
+ } else {
+ mMap.remove(key); // Remove expired entries
+ return null;
+ }
+ }
+ }
+
+ /**
+ * Stores a {@link NetworkStats.Entry} in the cache, associated with the given key.
+ *
+ * @param iface The interface name to include in the cache key. Null if not applicable.
+ * @param uid The UID to include in the cache key. {@code UID_ALL} if not applicable.
+ * @param entry The {@link NetworkStats.Entry} to store in the cache.
+ */
+ void put(String iface, int uid, @NonNull final NetworkStats.Entry entry) {
+ Objects.requireNonNull(entry);
+ final TrafficStatsCacheKey key = new TrafficStatsCacheKey(iface, uid);
+ synchronized (mMap) { // Synchronize for thread-safety
+ mMap.put(key, new TrafficStatsCacheValue(mClock.millis(), entry));
+ }
+ }
+
+ /**
+ * Clear the cache.
+ */
+ void clear() {
+ synchronized (mMap) {
+ mMap.clear();
+ }
+ }
+
+ private boolean isExpired(long timestamp) {
+ return mClock.millis() > timestamp + mExpiryDurationMs;
+ }
+}
diff --git a/service/Android.bp b/service/Android.bp
index 403ba7d..c35c4f8 100644
--- a/service/Android.bp
+++ b/service/Android.bp
@@ -199,6 +199,7 @@
"PlatformProperties",
"service-connectivity-protos",
"service-connectivity-stats-protos",
+ "net-utils-multicast-forwarding-structs",
],
apex_available: [
"com.android.tethering",
diff --git a/service/ServiceConnectivityResources/res/values/config_thread.xml b/service/ServiceConnectivityResources/res/values/config_thread.xml
index 14b5427..f7e47f5 100644
--- a/service/ServiceConnectivityResources/res/values/config_thread.xml
+++ b/service/ServiceConnectivityResources/res/values/config_thread.xml
@@ -20,10 +20,15 @@
-->
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <!-- Sets to {@code true} to enable Thread on the device by default. Note this is the default
+ value, the actual Thread enabled state can be changed by the {@link
+ ThreadNetworkController#setEnabled} API.
+ -->
+ <bool name="config_thread_default_enabled">true</bool>
+
<!-- Whether to use location APIs in the algorithm to determine country code or not.
If disabled, will use other sources (telephony, wifi, etc) to determine device location for
Thread Network regulatory purposes.
-->
<bool name="config_thread_location_use_for_country_code_enabled">true</bool>
-
</resources>
diff --git a/service/ServiceConnectivityResources/res/values/overlayable.xml b/service/ServiceConnectivityResources/res/values/overlayable.xml
index f2c4d91..d9af5a3 100644
--- a/service/ServiceConnectivityResources/res/values/overlayable.xml
+++ b/service/ServiceConnectivityResources/res/values/overlayable.xml
@@ -46,6 +46,7 @@
<item type="integer" name="config_netstats_validate_import" />
<!-- Configuration values for ThreadNetworkService -->
+ <item type="bool" name="config_thread_default_enabled" />
<item type="bool" name="config_thread_location_use_for_country_code_enabled" />
</policy>
</overlayable>
diff --git a/service/src/com/android/server/ConnectivityService.java b/service/src/com/android/server/ConnectivityService.java
index e6287bc..6839c22 100755
--- a/service/src/com/android/server/ConnectivityService.java
+++ b/service/src/com/android/server/ConnectivityService.java
@@ -114,7 +114,6 @@
import static com.android.net.module.util.PermissionUtils.enforceNetworkStackPermissionOr;
import static com.android.net.module.util.PermissionUtils.hasAnyPermissionOf;
import static com.android.server.ConnectivityStatsLog.CONNECTIVITY_STATE_SAMPLE;
-import static com.android.server.connectivity.CarrierPrivilegeAuthenticator.CarrierPrivilegesLostListener;
import static com.android.server.connectivity.ConnectivityFlags.REQUEST_RESTRICTED_WIFI;
import android.Manifest;
@@ -257,6 +256,7 @@
import android.stats.connectivity.ValidatedState;
import android.sysprop.NetworkProperties;
import android.system.ErrnoException;
+import android.telephony.SubscriptionManager;
import android.telephony.TelephonyManager;
import android.text.TextUtils;
import android.util.ArrayMap;
@@ -377,6 +377,7 @@
import java.util.TreeSet;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
+import java.util.function.BiConsumer;
import java.util.function.Consumer;
/**
@@ -1287,18 +1288,14 @@
}
private final LegacyTypeTracker mLegacyTypeTracker = new LegacyTypeTracker(this);
- private final CarrierPrivilegesLostListenerImpl mCarrierPrivilegesLostListenerImpl =
- new CarrierPrivilegesLostListenerImpl();
-
- private class CarrierPrivilegesLostListenerImpl implements CarrierPrivilegesLostListener {
- @Override
- public void onCarrierPrivilegesLost(int uid) {
- if (mRequestRestrictedWifiEnabled) {
- mHandler.sendMessage(mHandler.obtainMessage(
- EVENT_UID_CARRIER_PRIVILEGES_LOST, uid, 0 /* arg2 */));
- }
+ @VisibleForTesting
+ void onCarrierPrivilegesLost(Integer uid, Integer subId) {
+ if (mRequestRestrictedWifiEnabled) {
+ mHandler.sendMessage(mHandler.obtainMessage(
+ EVENT_UID_CARRIER_PRIVILEGES_LOST, uid, subId));
}
}
+
final LocalPriorityDump mPriorityDumper = new LocalPriorityDump();
/**
* Helper class which parses out priority arguments and dumps sections according to their
@@ -1357,11 +1354,6 @@
}
}
- @VisibleForTesting
- CarrierPrivilegesLostListener getCarrierPrivilegesLostListener() {
- return mCarrierPrivilegesLostListenerImpl;
- }
-
/**
* Dependencies of ConnectivityService, for injection in tests.
*/
@@ -1525,7 +1517,7 @@
@NonNull final Context context,
@NonNull final TelephonyManager tm,
boolean requestRestrictedWifiEnabled,
- @NonNull CarrierPrivilegesLostListener listener) {
+ @NonNull BiConsumer<Integer, Integer> listener) {
if (isAtLeastT()) {
return new CarrierPrivilegeAuthenticator(
context, tm, requestRestrictedWifiEnabled, listener);
@@ -1813,7 +1805,7 @@
&& mDeps.isFeatureEnabled(context, REQUEST_RESTRICTED_WIFI);
mCarrierPrivilegeAuthenticator = mDeps.makeCarrierPrivilegeAuthenticator(
mContext, mTelephonyManager, mRequestRestrictedWifiEnabled,
- mCarrierPrivilegesLostListenerImpl);
+ this::onCarrierPrivilegesLost);
if (mDeps.isAtLeastU()
&& mDeps
@@ -5401,6 +5393,13 @@
return false;
}
+ private int getSubscriptionIdFromNetworkCaps(@NonNull final NetworkCapabilities caps) {
+ if (mCarrierPrivilegeAuthenticator != null) {
+ return mCarrierPrivilegeAuthenticator.getSubIdFromNetworkCapabilities(caps);
+ }
+ return SubscriptionManager.INVALID_SUBSCRIPTION_ID;
+ }
+
private void handleRegisterNetworkRequestWithIntent(@NonNull final Message msg) {
final NetworkRequestInfo nri = (NetworkRequestInfo) (msg.obj);
// handleRegisterNetworkRequestWithIntent() doesn't apply to multilayer requests.
@@ -6010,7 +6009,7 @@
if (nm == null) return;
if (request == CaptivePortal.APP_REQUEST_REEVALUATION_REQUIRED) {
- hasNetworkStackPermission();
+ enforceNetworkStackPermission(mContext);
nm.forceReevaluation(mDeps.getCallingUid());
}
}
@@ -6492,7 +6491,7 @@
handleFrozenUids(args.mUids, args.mFrozenStates);
break;
case EVENT_UID_CARRIER_PRIVILEGES_LOST:
- handleUidCarrierPrivilegesLost(msg.arg1);
+ handleUidCarrierPrivilegesLost(msg.arg1, msg.arg2);
break;
}
}
@@ -9155,7 +9154,7 @@
}
}
- private void handleUidCarrierPrivilegesLost(int uid) {
+ private void handleUidCarrierPrivilegesLost(int uid, int subId) {
ensureRunningOnConnectivityServiceThread();
// A NetworkRequest needs to be revoked when all the conditions are met
// 1. It requests restricted network
@@ -9166,6 +9165,7 @@
if ((nr.isRequest() || nr.isListen())
&& !nr.hasCapability(NET_CAPABILITY_NOT_RESTRICTED)
&& nr.getRequestorUid() == uid
+ && getSubscriptionIdFromNetworkCaps(nr.networkCapabilities) == subId
&& !hasConnectivityRestrictedNetworksPermission(uid, true)) {
declareNetworkRequestUnfulfillable(nr);
}
@@ -9174,7 +9174,8 @@
// A NetworkAgent's allowedUids may need to be updated if the app has lost
// carrier config
for (final NetworkAgentInfo nai : mNetworkAgentInfos) {
- if (nai.networkCapabilities.getAllowedUidsNoCopy().contains(uid)) {
+ if (nai.networkCapabilities.getAllowedUidsNoCopy().contains(uid)
+ && getSubscriptionIdFromNetworkCaps(nai.networkCapabilities) == subId) {
final NetworkCapabilities nc = new NetworkCapabilities(nai.networkCapabilities);
NetworkAgentInfo.restrictCapabilitiesFromNetworkAgent(
nc,
diff --git a/service/src/com/android/server/connectivity/CarrierPrivilegeAuthenticator.java b/service/src/com/android/server/connectivity/CarrierPrivilegeAuthenticator.java
index 533278e..04d0fc1 100644
--- a/service/src/com/android/server/connectivity/CarrierPrivilegeAuthenticator.java
+++ b/service/src/com/android/server/connectivity/CarrierPrivilegeAuthenticator.java
@@ -40,12 +40,13 @@
import android.telephony.SubscriptionManager;
import android.telephony.TelephonyManager;
import android.util.Log;
-import android.util.SparseIntArray;
+import android.util.SparseArray;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.IndentingPrintWriter;
import com.android.modules.utils.HandlerExecutor;
+import com.android.modules.utils.build.SdkLevel;
import com.android.net.module.util.DeviceConfigUtils;
import com.android.networkstack.apishim.TelephonyManagerShimImpl;
import com.android.networkstack.apishim.common.TelephonyManagerShim;
@@ -55,6 +56,7 @@
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Executor;
+import java.util.function.BiConsumer;
/**
* Tracks the uid of the carrier privileged app that provides the carrier config.
@@ -71,7 +73,8 @@
private final TelephonyManagerShim mTelephonyManagerShim;
private final TelephonyManager mTelephonyManager;
@GuardedBy("mLock")
- private final SparseIntArray mCarrierServiceUid = new SparseIntArray(2 /* initialCapacity */);
+ private final SparseArray<CarrierServiceUidWithSubId> mCarrierServiceUidWithSubId =
+ new SparseArray<>(2 /* initialCapacity */);
@GuardedBy("mLock")
private int mModemCount = 0;
private final Object mLock = new Object();
@@ -81,14 +84,14 @@
private final boolean mUseCallbacksForServiceChanged;
private final boolean mRequestRestrictedWifiEnabled;
@NonNull
- private final CarrierPrivilegesLostListener mListener;
+ private final BiConsumer<Integer, Integer> mListener;
public CarrierPrivilegeAuthenticator(@NonNull final Context c,
@NonNull final Dependencies deps,
@NonNull final TelephonyManager t,
@NonNull final TelephonyManagerShim telephonyManagerShim,
final boolean requestRestrictedWifiEnabled,
- @NonNull CarrierPrivilegesLostListener listener) {
+ @NonNull BiConsumer<Integer, Integer> listener) {
mContext = c;
mTelephonyManager = t;
mTelephonyManagerShim = telephonyManagerShim;
@@ -121,7 +124,7 @@
public CarrierPrivilegeAuthenticator(@NonNull final Context c,
@NonNull final TelephonyManager t, final boolean requestRestrictedWifiEnabled,
- @NonNull CarrierPrivilegesLostListener listener) {
+ @NonNull BiConsumer<Integer, Integer> listener) {
this(c, new Dependencies(), t, TelephonyManagerShimImpl.newInstance(t),
requestRestrictedWifiEnabled, listener);
}
@@ -142,18 +145,6 @@
}
}
- /**
- * Listener interface to get a notification when the carrier App lost its privileges.
- */
- public interface CarrierPrivilegesLostListener {
- /**
- * Called when the carrier App lost its privileges.
- *
- * @param uid The uid of the carrier app which has lost its privileges.
- */
- void onCarrierPrivilegesLost(int uid);
- }
-
private void simConfigChanged() {
synchronized (mLock) {
unregisterCarrierPrivilegesListeners();
@@ -163,6 +154,29 @@
}
}
+ private static class CarrierServiceUidWithSubId {
+ final int mUid;
+ final int mSubId;
+
+ CarrierServiceUidWithSubId(int uid, int subId) {
+ mUid = uid;
+ mSubId = subId;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (!(obj instanceof CarrierServiceUidWithSubId)) {
+ return false;
+ }
+ CarrierServiceUidWithSubId compare = (CarrierServiceUidWithSubId) obj;
+ return (mUid == compare.mUid && mSubId == compare.mSubId);
+ }
+
+ @Override
+ public int hashCode() {
+ return mUid * 31 + mSubId;
+ }
+ }
private class PrivilegeListener implements CarrierPrivilegesListenerShim {
public final int mLogicalSlot;
@@ -192,10 +206,17 @@
return;
}
synchronized (mLock) {
- int oldUid = mCarrierServiceUid.get(mLogicalSlot);
- mCarrierServiceUid.put(mLogicalSlot, carrierServiceUid);
- if (oldUid != 0 && oldUid != carrierServiceUid) {
- mListener.onCarrierPrivilegesLost(oldUid);
+ CarrierServiceUidWithSubId oldPair =
+ mCarrierServiceUidWithSubId.get(mLogicalSlot);
+ int subId = getSubId(mLogicalSlot);
+ mCarrierServiceUidWithSubId.put(
+ mLogicalSlot,
+ new CarrierServiceUidWithSubId(carrierServiceUid, subId));
+ if (oldPair != null
+ && oldPair.mUid != Process.INVALID_UID
+ && oldPair.mSubId != SubscriptionManager.INVALID_SUBSCRIPTION_ID
+ && !oldPair.equals(mCarrierServiceUidWithSubId.get(mLogicalSlot))) {
+ mListener.accept(oldPair.mUid, oldPair.mSubId);
}
}
}
@@ -218,10 +239,13 @@
private void unregisterCarrierPrivilegesListeners() {
for (PrivilegeListener carrierPrivilegesListener : mCarrierPrivilegesChangedListeners) {
removeCarrierPrivilegesListener(carrierPrivilegesListener);
- int oldUid = mCarrierServiceUid.get(carrierPrivilegesListener.mLogicalSlot);
- mCarrierServiceUid.delete(carrierPrivilegesListener.mLogicalSlot);
- if (oldUid != 0) {
- mListener.onCarrierPrivilegesLost(oldUid);
+ CarrierServiceUidWithSubId oldPair =
+ mCarrierServiceUidWithSubId.get(carrierPrivilegesListener.mLogicalSlot);
+ mCarrierServiceUidWithSubId.remove(carrierPrivilegesListener.mLogicalSlot);
+ if (oldPair != null
+ && oldPair.mUid != Process.INVALID_UID
+ && oldPair.mSubId != SubscriptionManager.INVALID_SUBSCRIPTION_ID) {
+ mListener.accept(oldPair.mUid, oldPair.mSubId);
}
}
mCarrierPrivilegesChangedListeners.clear();
@@ -259,7 +283,23 @@
*/
public boolean isCarrierServiceUidForNetworkCapabilities(int callingUid,
@NonNull NetworkCapabilities networkCapabilities) {
- if (callingUid == Process.INVALID_UID) return false;
+ if (callingUid == Process.INVALID_UID) {
+ return false;
+ }
+ int subId = getSubIdFromNetworkCapabilities(networkCapabilities);
+ if (SubscriptionManager.INVALID_SUBSCRIPTION_ID == subId) {
+ return false;
+ }
+ return callingUid == getCarrierServiceUidForSubId(subId);
+ }
+
+ /**
+ * Extract the SubscriptionId from the NetworkCapabilities.
+ *
+ * @param networkCapabilities the network capabilities which may contains the SubscriptionId.
+ * @return the SubscriptionId.
+ */
+ public int getSubIdFromNetworkCapabilities(@NonNull NetworkCapabilities networkCapabilities) {
int subId;
if (networkCapabilities.hasSingleTransportBesidesTest(TRANSPORT_CELLULAR)) {
subId = getSubIdFromTelephonySpecifier(networkCapabilities.getNetworkSpecifier());
@@ -285,21 +325,42 @@
Log.wtf(TAG, "NetworkCapabilities subIds are inconsistent between "
+ "specifier/transportInfo and mSubIds : " + networkCapabilities);
}
- if (SubscriptionManager.INVALID_SUBSCRIPTION_ID == subId) return false;
- return callingUid == getCarrierServiceUidForSubId(subId);
+ return subId;
+ }
+
+ @VisibleForTesting
+ protected int getSubId(int slotIndex) {
+ if (SdkLevel.isAtLeastU()) {
+ return SubscriptionManager.getSubscriptionId(slotIndex);
+ } else {
+ SubscriptionManager sm = mContext.getSystemService(SubscriptionManager.class);
+ int[] subIds = sm.getSubscriptionIds(slotIndex);
+ if (subIds != null && subIds.length > 0) {
+ return subIds[0];
+ }
+ return SubscriptionManager.INVALID_SUBSCRIPTION_ID;
+ }
}
@VisibleForTesting
void updateCarrierServiceUid() {
synchronized (mLock) {
- SparseIntArray oldCarrierServiceUid = mCarrierServiceUid.clone();
- mCarrierServiceUid.clear();
+ SparseArray<CarrierServiceUidWithSubId> copy = mCarrierServiceUidWithSubId.clone();
+ mCarrierServiceUidWithSubId.clear();
for (int i = 0; i < mModemCount; i++) {
- mCarrierServiceUid.put(i, getCarrierServicePackageUidForSlot(i));
+ int subId = getSubId(i);
+ mCarrierServiceUidWithSubId.put(
+ i,
+ new CarrierServiceUidWithSubId(
+ getCarrierServicePackageUidForSlot(i), subId));
}
- for (int i = 0; i < oldCarrierServiceUid.size(); i++) {
- if (mCarrierServiceUid.indexOfValue(oldCarrierServiceUid.valueAt(i)) < 0) {
- mListener.onCarrierPrivilegesLost(oldCarrierServiceUid.valueAt(i));
+ for (int i = 0; i < copy.size(); ++i) {
+ CarrierServiceUidWithSubId oldPair = copy.valueAt(i);
+ CarrierServiceUidWithSubId newPair = mCarrierServiceUidWithSubId.get(copy.keyAt(i));
+ if (oldPair.mUid != Process.INVALID_UID
+ && oldPair.mSubId != SubscriptionManager.INVALID_SUBSCRIPTION_ID
+ && !oldPair.equals(newPair)) {
+ mListener.accept(oldPair.mUid, oldPair.mSubId);
}
}
}
@@ -307,18 +368,17 @@
@VisibleForTesting
int getCarrierServiceUidForSubId(int subId) {
- final int slotId = getSlotIndex(subId);
synchronized (mLock) {
- return mCarrierServiceUid.get(slotId, Process.INVALID_UID);
+ for (int i = 0; i < mCarrierServiceUidWithSubId.size(); ++i) {
+ if (mCarrierServiceUidWithSubId.valueAt(i).mSubId == subId) {
+ return mCarrierServiceUidWithSubId.valueAt(i).mUid;
+ }
+ }
+ return Process.INVALID_UID;
}
}
@VisibleForTesting
- protected int getSlotIndex(int subId) {
- return SubscriptionManager.getSlotIndex(subId);
- }
-
- @VisibleForTesting
int getUidForPackage(String pkgName) {
if (pkgName == null) {
return Process.INVALID_UID;
@@ -383,11 +443,12 @@
pw.println("CarrierPrivilegeAuthenticator:");
pw.println("mRequestRestrictedWifiEnabled = " + mRequestRestrictedWifiEnabled);
synchronized (mLock) {
- final int size = mCarrierServiceUid.size();
- for (int i = 0; i < size; ++i) {
- final int logicalSlot = mCarrierServiceUid.keyAt(i);
- final int serviceUid = mCarrierServiceUid.valueAt(i);
- pw.println("Logical slot = " + logicalSlot + " : uid = " + serviceUid);
+ for (int i = 0; i < mCarrierServiceUidWithSubId.size(); ++i) {
+ final int logicalSlot = mCarrierServiceUidWithSubId.keyAt(i);
+ final int serviceUid = mCarrierServiceUidWithSubId.valueAt(i).mUid;
+ final int subId = mCarrierServiceUidWithSubId.valueAt(i).mSubId;
+ pw.println("Logical slot = " + logicalSlot + " : uid = " + serviceUid
+ + " : subId = " + subId);
}
}
}
diff --git a/service/src/com/android/server/connectivity/SatelliteAccessController.java b/service/src/com/android/server/connectivity/SatelliteAccessController.java
index 0968aff..b53abce 100644
--- a/service/src/com/android/server/connectivity/SatelliteAccessController.java
+++ b/service/src/com/android/server/connectivity/SatelliteAccessController.java
@@ -26,8 +26,10 @@
import android.os.Handler;
import android.os.Process;
import android.os.UserHandle;
+import android.os.UserManager;
import android.util.ArraySet;
import android.util.Log;
+import android.util.SparseArray;
import com.android.internal.annotations.VisibleForTesting;
@@ -44,13 +46,18 @@
*/
public class SatelliteAccessController {
private static final String TAG = SatelliteAccessController.class.getSimpleName();
- private final PackageManager mPackageManager;
+ private final Context mContext;
private final Dependencies mDeps;
private final DefaultMessageRoleListener mDefaultMessageRoleListener;
+ private final UserManager mUserManager;
private final Consumer<Set<Integer>> mCallback;
- private final Set<Integer> mSatelliteNetworkPreferredUidCache = new ArraySet<>();
private final Handler mConnectivityServiceHandler;
+ // At this sparseArray, Key is userId and values are uids of SMS apps that are allowed
+ // to use satellite network as fallback.
+ private final SparseArray<Set<Integer>> mAllUsersSatelliteNetworkFallbackUidCache =
+ new SparseArray<>();
+
/**
* Monitor {@link android.app.role.OnRoleHoldersChangedListener#onRoleHoldersChanged(String,
* UserHandle)},
@@ -59,10 +66,10 @@
private final class DefaultMessageRoleListener
implements OnRoleHoldersChangedListener {
@Override
- public void onRoleHoldersChanged(String role, UserHandle user) {
+ public void onRoleHoldersChanged(String role, UserHandle userHandle) {
if (RoleManager.ROLE_SMS.equals(role)) {
Log.i(TAG, "ROLE_SMS Change detected ");
- onRoleSmsChanged();
+ onRoleSmsChanged(userHandle);
}
}
@@ -71,7 +78,7 @@
mDeps.addOnRoleHoldersChangedListenerAsUser(
mConnectivityServiceHandler::post, this, UserHandle.ALL);
} catch (RuntimeException e) {
- Log.e(TAG, "Could not register satellite controller listener due to " + e);
+ Log.wtf(TAG, "Could not register satellite controller listener due to " + e);
}
}
}
@@ -89,9 +96,9 @@
mRoleManager = context.getSystemService(RoleManager.class);
}
- /** See {@link RoleManager#getRoleHolders(String)} */
- public List<String> getRoleHolders(String roleName) {
- return mRoleManager.getRoleHolders(roleName);
+ /** See {@link RoleManager#getRoleHoldersAsUser(String, UserHandle)} */
+ public List<String> getRoleHoldersAsUser(String roleName, UserHandle userHandle) {
+ return mRoleManager.getRoleHoldersAsUser(roleName, userHandle);
}
/** See {@link RoleManager#addOnRoleHoldersChangedListenerAsUser} */
@@ -105,81 +112,107 @@
SatelliteAccessController(@NonNull final Context c, @NonNull final Dependencies deps,
Consumer<Set<Integer>> callback,
@NonNull final Handler connectivityServiceInternalHandler) {
+ mContext = c;
mDeps = deps;
- mPackageManager = c.getPackageManager();
+ mUserManager = mContext.getSystemService(UserManager.class);
mDefaultMessageRoleListener = new DefaultMessageRoleListener();
mCallback = callback;
mConnectivityServiceHandler = connectivityServiceInternalHandler;
}
- private void updateSatelliteNetworkPreferredUidListCache(List<String> packageNames) {
- for (String packageName : packageNames) {
- // Check if SATELLITE_COMMUNICATION permission is enabled for default sms application
- // package before adding it part of satellite network preferred uid cache list.
- if (isSatellitePermissionEnabled(packageName)) {
- mSatelliteNetworkPreferredUidCache.add(getUidForPackage(packageName));
+ private Set<Integer> updateSatelliteNetworkFallbackUidListCache(List<String> packageNames,
+ @NonNull UserHandle userHandle) {
+ Set<Integer> fallbackUids = new ArraySet<>();
+ PackageManager pm =
+ mContext.createContextAsUser(userHandle, 0).getPackageManager();
+ if (pm != null) {
+ for (String packageName : packageNames) {
+ // Check if SATELLITE_COMMUNICATION permission is enabled for default sms
+ // application package before adding it part of satellite network fallback uid
+ // cache list.
+ if (isSatellitePermissionEnabled(pm, packageName)) {
+ int uid = getUidForPackage(pm, packageName);
+ if (uid != Process.INVALID_UID) {
+ fallbackUids.add(uid);
+ }
+ }
}
+ } else {
+ Log.wtf(TAG, "package manager found null");
}
+ return fallbackUids;
}
//Check if satellite communication is enabled for the package
- private boolean isSatellitePermissionEnabled(String packageName) {
- if (mPackageManager != null) {
- return mPackageManager.checkPermission(
- Manifest.permission.SATELLITE_COMMUNICATION, packageName)
- == PackageManager.PERMISSION_GRANTED;
- }
- return false;
+ private boolean isSatellitePermissionEnabled(PackageManager packageManager,
+ String packageName) {
+ return packageManager.checkPermission(
+ Manifest.permission.SATELLITE_COMMUNICATION, packageName)
+ == PackageManager.PERMISSION_GRANTED;
}
- private int getUidForPackage(String pkgName) {
+ private int getUidForPackage(PackageManager packageManager, String pkgName) {
if (pkgName == null) {
return Process.INVALID_UID;
}
try {
- if (mPackageManager != null) {
- ApplicationInfo applicationInfo = mPackageManager.getApplicationInfo(pkgName, 0);
- if (applicationInfo != null) {
- return applicationInfo.uid;
- }
- }
+ ApplicationInfo applicationInfo = packageManager.getApplicationInfo(pkgName, 0);
+ return applicationInfo.uid;
} catch (PackageManager.NameNotFoundException exception) {
Log.e(TAG, "Unable to find uid for package: " + pkgName);
}
return Process.INVALID_UID;
}
- //on Role sms change triggered by OnRoleHoldersChangedListener()
- private void onRoleSmsChanged() {
- final List<String> packageNames = getRoleSmsChangedPackageName();
-
- // Create a new Set
- Set<Integer> previousSatellitePreferredUid = new ArraySet<>(
- mSatelliteNetworkPreferredUidCache);
-
- mSatelliteNetworkPreferredUidCache.clear();
-
- if (packageNames != null) {
- Log.i(TAG, "role_sms_packages: " + packageNames);
- // On Role change listener, update the satellite network preferred uid cache list
- updateSatelliteNetworkPreferredUidListCache(packageNames);
- Log.i(TAG, "satellite_preferred_uid: " + mSatelliteNetworkPreferredUidCache);
- } else {
- Log.wtf(TAG, "package name was found null");
+ // on Role sms change triggered by OnRoleHoldersChangedListener()
+ // TODO(b/326373613): using UserLifecycleListener, callback to be received when user removed for
+ // user delete scenario. This to be used to update uid list and ML Layer request can also be
+ // updated.
+ private void onRoleSmsChanged(@NonNull UserHandle userHandle) {
+ int userId = userHandle.getIdentifier();
+ if (userId == Process.INVALID_UID) {
+ Log.wtf(TAG, "Invalid User Id");
+ return;
}
+ //Returns empty list if no package exists
+ final List<String> packageNames =
+ mDeps.getRoleHoldersAsUser(RoleManager.ROLE_SMS, userHandle);
+
+ // Store previous satellite fallback uid available
+ final Set<Integer> prevUidsForUser =
+ mAllUsersSatelliteNetworkFallbackUidCache.get(userId, new ArraySet<>());
+
+ Log.i(TAG, "currentUser : role_sms_packages: " + userId + " : " + packageNames);
+ final Set<Integer> newUidsForUser = !packageNames.isEmpty()
+ ? updateSatelliteNetworkFallbackUidListCache(packageNames, userHandle)
+ : new ArraySet<>();
+ Log.i(TAG, "satellite_fallback_uid: " + newUidsForUser);
+
// on Role change, update the multilayer request at ConnectivityService with updated
- // satellite network preferred uid cache list if changed or to revoke for previous default
- // sms app
- if (!mSatelliteNetworkPreferredUidCache.equals(previousSatellitePreferredUid)) {
- Log.i(TAG, "update multi layer request");
- mCallback.accept(mSatelliteNetworkPreferredUidCache);
+ // satellite network fallback uid cache list of multiple users as applicable
+ if (newUidsForUser.equals(prevUidsForUser)) {
+ return;
}
+
+ mAllUsersSatelliteNetworkFallbackUidCache.put(userId, newUidsForUser);
+
+ // Merge all uids of multiple users available
+ Set<Integer> mergedSatelliteNetworkFallbackUidCache = new ArraySet<>();
+ for (int i = 0; i < mAllUsersSatelliteNetworkFallbackUidCache.size(); i++) {
+ mergedSatelliteNetworkFallbackUidCache.addAll(
+ mAllUsersSatelliteNetworkFallbackUidCache.valueAt(i));
+ }
+ Log.i(TAG, "merged uid list for multi layer request : "
+ + mergedSatelliteNetworkFallbackUidCache);
+
+ // trigger multiple layer request for satellite network fallback of multi user uids
+ mCallback.accept(mergedSatelliteNetworkFallbackUidCache);
}
- private List<String> getRoleSmsChangedPackageName() {
+ private List<String> getRoleSmsChangedPackageName(UserHandle userHandle) {
try {
- return mDeps.getRoleHolders(RoleManager.ROLE_SMS);
+ return mDeps.getRoleHoldersAsUser(RoleManager.ROLE_SMS, userHandle);
} catch (RuntimeException e) {
Log.wtf(TAG, "Could not get package name at role sms change update due to: " + e);
return null;
@@ -188,7 +221,16 @@
/** Register OnRoleHoldersChangedListener */
public void start() {
- mConnectivityServiceHandler.post(this::onRoleSmsChanged);
+ mConnectivityServiceHandler.post(this::updateAllUserRoleSmsUids);
mDefaultMessageRoleListener.register();
}
+
+ private void updateAllUserRoleSmsUids() {
+ List<UserHandle> existingUsers = mUserManager.getUserHandles(true /* excludeDying */);
+ // Iterate through the user handles and obtain their uids with role sms and satellite
+ // communication permission
+ for (UserHandle userHandle : existingUsers) {
+ onRoleSmsChanged(userHandle);
+ }
+ }
}
diff --git a/staticlibs/Android.bp b/staticlibs/Android.bp
index 47e897d..6790093 100644
--- a/staticlibs/Android.bp
+++ b/staticlibs/Android.bp
@@ -188,6 +188,33 @@
},
}
+// The net-utils-multicast-forwarding-structs library requires the callers to
+// contain net-utils-device-common-bpf.
+java_library {
+ name: "net-utils-multicast-forwarding-structs",
+ srcs: [
+ "device/com/android/net/module/util/structs/StructMf6cctl.java",
+ "device/com/android/net/module/util/structs/StructMif6ctl.java",
+ "device/com/android/net/module/util/structs/StructMrt6Msg.java",
+ ],
+ sdk_version: "module_current",
+ min_sdk_version: "30",
+ visibility: [
+ "//packages/modules/Connectivity:__subpackages__",
+ ],
+ libs: [
+ // Only Struct.java is needed from "net-utils-device-common-bpf"
+ "net-utils-device-common-bpf",
+ ],
+ apex_available: [
+ "com.android.tethering",
+ ],
+ lint: {
+ strict_updatability_linting: true,
+ error_checks: ["NewApi"],
+ },
+}
+
// The net-utils-device-common-netlink library requires the callers to contain
// net-utils-device-common-struct.
java_library {
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/ExternalPacketForwarder.kt b/staticlibs/testutils/devicetests/com/android/testutils/ExternalPacketForwarder.kt
new file mode 100644
index 0000000..36eb795
--- /dev/null
+++ b/staticlibs/testutils/devicetests/com/android/testutils/ExternalPacketForwarder.kt
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.testutils
+
+import java.io.FileDescriptor
+
+class ExternalPacketForwarder(
+ srcFd: FileDescriptor,
+ mtu: Int,
+ dstFd: FileDescriptor,
+ forwardMap: Map<Int, Int>
+) : PacketForwarderBase(srcFd, mtu, dstFd, forwardMap) {
+
+ /**
+ * Prepares a packet for forwarding by potentially updating the
+ * source port based on the specified port remapping rules.
+ *
+ * @param buf The packet data as a byte array.
+ * @param version The IP version of the packet (e.g., 4 for IPv4).
+ */
+ override fun remapPort(buf: ByteArray, version: Int) {
+ val transportOffset = getTransportOffset(version)
+ val intPort = getRemappedPort(buf, transportOffset)
+
+ // Copy remapped source port.
+ if (intPort != 0) {
+ setPortAt(intPort, buf, transportOffset)
+ }
+ }
+}
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/InternalPacketForwarder.kt b/staticlibs/testutils/devicetests/com/android/testutils/InternalPacketForwarder.kt
new file mode 100644
index 0000000..58829dc
--- /dev/null
+++ b/staticlibs/testutils/devicetests/com/android/testutils/InternalPacketForwarder.kt
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.testutils
+
+import java.io.FileDescriptor
+
+class InternalPacketForwarder(
+ srcFd: FileDescriptor,
+ mtu: Int,
+ dstFd: FileDescriptor,
+ forwardMap: Map<Int, Int>
+) : PacketForwarderBase(srcFd, mtu, dstFd, forwardMap) {
+ /**
+ * Prepares a packet for forwarding by potentially updating the
+ * destination port based on the specified port remapping rules.
+ *
+ * @param buf The packet data as a byte array.
+ * @param version The IP version of the packet (e.g., 4 for IPv4).
+ */
+ override fun remapPort(buf: ByteArray, version: Int) {
+ val transportOffset = getTransportOffset(version) + DESTINATION_PORT_OFFSET
+ val extPort = getRemappedPort(buf, transportOffset)
+
+ // Copy remapped destination port.
+ if (extPort != 0) {
+ setPortAt(extPort, buf, transportOffset)
+ }
+ }
+}
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/PacketBridge.kt b/staticlibs/testutils/devicetests/com/android/testutils/PacketBridge.kt
index 1a2cc88..0b736d1 100644
--- a/staticlibs/testutils/devicetests/com/android/testutils/PacketBridge.kt
+++ b/staticlibs/testutils/devicetests/com/android/testutils/PacketBridge.kt
@@ -40,7 +40,8 @@
class PacketBridge(
context: Context,
addresses: List<LinkAddress>,
- dnsAddr: InetAddress
+ dnsAddr: InetAddress,
+ portMapping: List<Pair<Int, Int>>
) {
private val binder = Binder()
@@ -56,6 +57,10 @@
// Register test networks to ConnectivityService.
private val internalNetworkCallback: TestableNetworkCallback
private val externalNetworkCallback: TestableNetworkCallback
+
+ private val internalForwardMap = HashMap<Int, Int>()
+ private val externalForwardMap = HashMap<Int, Int>()
+
val internalNetwork: Network
val externalNetwork: Network
init {
@@ -65,14 +70,28 @@
externalNetworkCallback = exCb
internalNetwork = inNet
externalNetwork = exNet
+ for (mapping in portMapping) {
+ internalForwardMap[mapping.first] = mapping.second
+ externalForwardMap[mapping.second] = mapping.first
+ }
}
// Set up the packet bridge.
private val internalFd = internalIface.fileDescriptor.fileDescriptor
private val externalFd = externalIface.fileDescriptor.fileDescriptor
- private val pr1 = PacketForwarder(internalFd, 1500, externalFd)
- private val pr2 = PacketForwarder(externalFd, 1500, internalFd)
+ private val pr1 = InternalPacketForwarder(
+ internalFd,
+ 1500,
+ externalFd,
+ internalForwardMap
+ )
+ private val pr2 = ExternalPacketForwarder(
+ externalFd,
+ 1500,
+ internalFd,
+ externalForwardMap
+ )
fun start() {
IoUtils.setBlocking(internalFd, true /* blocking */)
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/PacketForwarder.java b/staticlibs/testutils/devicetests/com/android/testutils/PacketForwarderBase.java
similarity index 68%
rename from staticlibs/testutils/devicetests/com/android/testutils/PacketForwarder.java
rename to staticlibs/testutils/devicetests/com/android/testutils/PacketForwarderBase.java
index d8efb7d..5c79eb0 100644
--- a/staticlibs/testutils/devicetests/com/android/testutils/PacketForwarder.java
+++ b/staticlibs/testutils/devicetests/com/android/testutils/PacketForwarderBase.java
@@ -32,6 +32,7 @@
import java.io.FileDescriptor;
import java.io.IOException;
+import java.util.Map;
import java.util.Objects;
/**
@@ -57,8 +58,9 @@
* from the http server, the same mechanism is applied but in a different direction,
* where the source and destination will be swapped.
*/
-public class PacketForwarder extends Thread {
+public abstract class PacketForwarderBase extends Thread {
private static final String TAG = "PacketForwarder";
+ static final int DESTINATION_PORT_OFFSET = 2;
// The source fd to read packets from.
@NonNull
@@ -70,8 +72,10 @@
@NonNull
final FileDescriptor mDstFd;
+ @NonNull
+ final Map<Integer, Integer> mPortRemapRules;
/**
- * Construct a {@link PacketForwarder}.
+ * Construct a {@link PacketForwarderBase}.
*
* This class reads packets from {@code srcFd} of a {@link TestNetworkInterface}, and
* forwards them to the {@code dstFd} of another {@link TestNetworkInterface}.
@@ -82,13 +86,49 @@
* @param srcFd {@link FileDescriptor} to read packets from.
* @param mtu MTU of the test network.
* @param dstFd {@link FileDescriptor} to write packets to.
+ * @param portRemapRules port remap rules
*/
- public PacketForwarder(@NonNull FileDescriptor srcFd, int mtu,
- @NonNull FileDescriptor dstFd) {
+ public PacketForwarderBase(@NonNull FileDescriptor srcFd, int mtu,
+ @NonNull FileDescriptor dstFd,
+ @NonNull Map<Integer, Integer> portRemapRules) {
super(TAG);
mSrcFd = Objects.requireNonNull(srcFd);
mBuf = new byte[mtu];
mDstFd = Objects.requireNonNull(dstFd);
+ mPortRemapRules = Objects.requireNonNull(portRemapRules);
+ }
+
+ /**
+ * A method to prepare forwarding packets between two instances of {@link TestNetworkInterface},
+ * which includes ports mapping.
+ * Subclasses should override this method to implement the needed port remapping.
+ * For internal forwarder will remapped destination port,
+ * external forwarder will remapped source port.
+ * Example:
+ * An outgoing packet from the internal interface with
+ * source 1.2.3.4:1234 and destination 8.8.8.8:80
+ * might be translated to 8.8.8.8:1234 -> 1.2.3.4:8080 before forwarding.
+ * An outgoing packet from the external interface with
+ * source 1.2.3.4:8080 and destination 8.8.8.8:1234
+ * might be translated to 8.8.8.8:80 -> 1.2.3.4:1234 before forwarding.
+ */
+ abstract void remapPort(@NonNull byte[] buf, int version);
+
+ /**
+ * Retrieves a potentially remapped port number from a packet.
+ *
+ * @param buf The packet data as a byte array.
+ * @param transportOffset The offset within the packet where the transport layer port begins.
+ * @return The remapped port if a mapping exists in the internal forwarding map,
+ * otherwise returns 0 (indicating no remapping).
+ */
+ int getRemappedPort(@NonNull byte[] buf, int transportOffset) {
+ int port = PacketReflectorUtil.getPortAt(buf, transportOffset);
+ return mPortRemapRules.getOrDefault(port, 0);
+ }
+
+ int getTransportOffset(int version) {
+ return version == 4 ? IPV4_HEADER_LENGTH : IPV6_HEADER_LENGTH;
}
private void forwardPacket(@NonNull byte[] buf, int len) {
@@ -99,7 +139,13 @@
}
}
- // Reads one packet from mSrcFd, and writes the packet to the mDstFd for supported protocols.
+ /**
+ * Reads one packet from mSrcFd, and writes the packet to the mDestFd for supported protocols.
+ * This includes:
+ * 1.Address Swapping: Swaps source and destination IP addresses.
+ * 2.Port Remapping: Remap port if necessary.
+ * 3.Checksum Recalculation: Updates IP and transport layer checksums to reflect changes.
+ */
private void processPacket() {
final int len = PacketReflectorUtil.readPacket(mSrcFd, mBuf);
if (len < 1) {
@@ -142,13 +188,19 @@
if (len < ipHdrLen + transportHdrLen) {
throw new IllegalStateException("Unexpected buffer length: " + len);
}
- // Swap addresses.
+
+ // Swap source and destination address.
PacketReflectorUtil.swapAddresses(mBuf, version);
+ // Remapping the port.
+ remapPort(mBuf, version);
+
+ // Fix IP and Transport layer checksum.
+ PacketReflectorUtil.fixPacketChecksum(mBuf, len, version, proto);
+
// Send the packet to the destination fd.
forwardPacket(mBuf, len);
}
-
@Override
public void run() {
Log.i(TAG, "starting fd=" + mSrcFd + " valid=" + mSrcFd.valid());
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/TestHttpServer.kt b/staticlibs/testutils/devicetests/com/android/testutils/TestHttpServer.kt
index 740bf63..f1f0c1c 100644
--- a/staticlibs/testutils/devicetests/com/android/testutils/TestHttpServer.kt
+++ b/staticlibs/testutils/devicetests/com/android/testutils/TestHttpServer.kt
@@ -25,8 +25,10 @@
* A minimal HTTP server running on a random available port.
*
* @param host The host to listen to, or null to listen on all hosts
+ * @param port The port to listen to, or 0 to auto select
*/
-class TestHttpServer(host: String? = null) : NanoHTTPD(host, 0 /* auto-select the port */) {
+class TestHttpServer
+ @JvmOverloads constructor(host: String? = null, port: Int = 0) : NanoHTTPD(host, port) {
// Map of URL path -> HTTP response code
private val responses = HashMap<Request, Response>()
diff --git a/tests/common/java/android/net/NetworkCapabilitiesTest.java b/tests/common/java/android/net/NetworkCapabilitiesTest.java
index 3a3459b..3124b1b 100644
--- a/tests/common/java/android/net/NetworkCapabilitiesTest.java
+++ b/tests/common/java/android/net/NetworkCapabilitiesTest.java
@@ -54,6 +54,7 @@
import static android.net.NetworkCapabilities.SIGNAL_STRENGTH_UNSPECIFIED;
import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR;
import static android.net.NetworkCapabilities.TRANSPORT_ETHERNET;
+import static android.net.NetworkCapabilities.TRANSPORT_SATELLITE;
import static android.net.NetworkCapabilities.TRANSPORT_TEST;
import static android.net.NetworkCapabilities.TRANSPORT_USB;
import static android.net.NetworkCapabilities.TRANSPORT_VPN;
@@ -761,6 +762,47 @@
}
@Test
+ public void testSetNetworkSpecifierWithCellularAndSatelliteMultiTransportNc() {
+ final TelephonyNetworkSpecifier specifier = new TelephonyNetworkSpecifier(1);
+ NetworkCapabilities nc = new NetworkCapabilities.Builder()
+ .addTransportType(TRANSPORT_CELLULAR)
+ .addTransportType(TRANSPORT_SATELLITE)
+ .setNetworkSpecifier(specifier)
+ .build();
+ // Adding a specifier did not crash with 2 transports if it is cellular + satellite
+ assertEquals(specifier, nc.getNetworkSpecifier());
+ }
+
+ @Test
+ public void testSetNetworkSpecifierWithWifiAndSatelliteMultiTransportNc() {
+ final TelephonyNetworkSpecifier specifier = new TelephonyNetworkSpecifier(1);
+ NetworkCapabilities.Builder nc1 = new NetworkCapabilities.Builder();
+ nc1.addTransportType(TRANSPORT_SATELLITE).addTransportType(TRANSPORT_WIFI);
+ // Adding multiple transports specifier to crash, apart from cellular + satellite
+ // combination
+ assertThrows("Cannot set NetworkSpecifier on a NetworkCapability with multiple transports!",
+ IllegalStateException.class,
+ () -> nc1.build().setNetworkSpecifier(specifier));
+ assertThrows("Cannot set NetworkSpecifier on a NetworkCapability with multiple transports!",
+ IllegalStateException.class,
+ () -> nc1.setNetworkSpecifier(specifier));
+ }
+
+ @Test
+ public void testSetNetworkSpecifierOnTestWithCellularAndSatelliteMultiTransportNc() {
+ final TelephonyNetworkSpecifier specifier = new TelephonyNetworkSpecifier(1);
+ NetworkCapabilities nc = new NetworkCapabilities.Builder()
+ .addTransportType(TRANSPORT_TEST)
+ .addTransportType(TRANSPORT_CELLULAR)
+ .addTransportType(TRANSPORT_SATELLITE)
+ .setNetworkSpecifier(specifier)
+ .build();
+ // Adding a specifier did not crash with 3 transports , TEST + CELLULAR + SATELLITE and if
+ // one is test
+ assertEquals(specifier, nc.getNetworkSpecifier());
+ }
+
+ @Test
public void testSetNetworkSpecifierOnTestMultiTransportNc() {
final NetworkSpecifier specifier = CompatUtil.makeEthernetNetworkSpecifier("eth0");
NetworkCapabilities nc = new NetworkCapabilities.Builder()
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 1241e18..2ca8832 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
@@ -160,10 +160,6 @@
private static final long BROADCAST_TIMEOUT_MS = 5_000;
- // Should be kept in sync with the constant in NetworkPolicyManagerService.
- // TODO: b/322115994 - remove once the feature is in staging.
- private static final boolean ALWAYS_RESTRICT_BACKGROUND_NETWORK = false;
-
protected Context mContext;
protected Instrumentation mInstrumentation;
protected ConnectivityManager mCm;
@@ -233,8 +229,9 @@
}
final String output = executeShellCommand("device_config get backstage_power"
+ " com.android.server.net.network_blocked_for_top_sleeping_and_above");
- return Boolean.parseBoolean(output) && ALWAYS_RESTRICT_BACKGROUND_NETWORK;
+ return Boolean.parseBoolean(output);
}
+
protected int getUid(String packageName) throws Exception {
return mContext.getPackageManager().getPackageUid(packageName, 0);
}
diff --git a/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java b/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java
index f0edee2..cdf8340 100644
--- a/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java
+++ b/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java
@@ -802,7 +802,9 @@
assertNull(redactedNormal.getUids());
assertNull(redactedNormal.getSsid());
assertNull(redactedNormal.getUnderlyingNetworks());
- assertEquals(0, redactedNormal.getSubscriptionIds().size());
+ // TODO: Make subIds public and update to verify the size is 2
+ final int subIdsSize = redactedNormal.getSubscriptionIds().size();
+ assertTrue(subIdsSize == 0 || subIdsSize == 2);
assertEquals(WifiInfo.DEFAULT_MAC_ADDRESS,
((WifiInfo) redactedNormal.getTransportInfo()).getBSSID());
assertEquals(rssi, ((WifiInfo) redactedNormal.getTransportInfo()).getRssi());
diff --git a/tests/cts/net/src/android/net/cts/DnsResolverTest.java b/tests/cts/net/src/android/net/cts/DnsResolverTest.java
index 9ff0f2f..752891f 100644
--- a/tests/cts/net/src/android/net/cts/DnsResolverTest.java
+++ b/tests/cts/net/src/android/net/cts/DnsResolverTest.java
@@ -23,6 +23,7 @@
import static android.net.DnsResolver.TYPE_AAAA;
import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR;
import static android.net.cts.util.CtsNetUtils.TestNetworkCallback;
+import static android.provider.DeviceConfig.NAMESPACE_CONNECTIVITY;
import static android.system.OsConstants.ETIMEDOUT;
import static com.android.testutils.DevSdkIgnoreRuleKt.SC_V2;
@@ -59,11 +60,14 @@
import com.android.net.module.util.DnsPacket;
import com.android.testutils.DevSdkIgnoreRule;
import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
+import com.android.testutils.DeviceConfigRule;
import com.android.testutils.DnsResolverModuleTest;
import com.android.testutils.SkipPresubmit;
import org.junit.After;
import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.ClassRule;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -80,6 +84,8 @@
@AppModeFull(reason = "WRITE_SECURE_SETTINGS permission can't be granted to instant apps")
@RunWith(AndroidJUnit4.class)
public class DnsResolverTest {
+ @ClassRule
+ public static final DeviceConfigRule DEVICE_CONFIG_CLASS_RULE = new DeviceConfigRule();
@Rule
public final DevSdkIgnoreRule ignoreRule = new DevSdkIgnoreRule();
@@ -123,6 +129,20 @@
private TestNetworkCallback mWifiRequestCallback = null;
+ /**
+ * @see BeforeClass
+ */
+ @BeforeClass
+ public static void beforeClass() throws Exception {
+ // Use async private DNS resolution to avoid flakes due to races applying the setting
+ DEVICE_CONFIG_CLASS_RULE.setConfig(NAMESPACE_CONNECTIVITY,
+ "networkmonitor_async_privdns_resolution", "1");
+ // Make sure NetworkMonitor is restarted before and after the test so the flag is applied
+ // and cleaned up.
+ maybeToggleWifiAndCell();
+ DEVICE_CONFIG_CLASS_RULE.runAfterNextCleanup(DnsResolverTest::maybeToggleWifiAndCell);
+ }
+
@Before
public void setUp() throws Exception {
mContext = InstrumentationRegistry.getContext();
@@ -144,6 +164,12 @@
}
}
+ private static void maybeToggleWifiAndCell() throws Exception {
+ final CtsNetUtils utils = new CtsNetUtils(InstrumentationRegistry.getContext());
+ utils.reconnectWifiIfSupported();
+ utils.reconnectCellIfSupported();
+ }
+
private static String byteArrayToHexString(byte[] bytes) {
char[] hexChars = new char[bytes.length * 2];
for (int i = 0; i < bytes.length; ++i) {
diff --git a/tests/cts/net/src/android/net/cts/IpSecTransformStateTest.java b/tests/cts/net/src/android/net/cts/IpSecTransformStateTest.java
new file mode 100644
index 0000000..7b42306
--- /dev/null
+++ b/tests/cts/net/src/android/net/cts/IpSecTransformStateTest.java
@@ -0,0 +1,99 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.cts;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.fail;
+
+import android.net.IpSecTransformState;
+import android.os.Build;
+import android.os.SystemClock;
+
+import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
+import com.android.testutils.DevSdkIgnoreRunner;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@IgnoreUpTo(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+@RunWith(DevSdkIgnoreRunner.class)
+public class IpSecTransformStateTest {
+ @Rule public final DevSdkIgnoreRule ignoreRule = new DevSdkIgnoreRule();
+
+ private static final long TIMESTAMP_MILLIS = 1000L;
+ private static final long HIGHEST_SEQ_NUMBER_TX = 10000L;
+ private static final long HIGHEST_SEQ_NUMBER_RX = 20000L;
+ private static final long PACKET_COUNT = 9000L;
+ private static final long BYTE_COUNT = 900000L;
+
+ private static final int REPLAY_BITMAP_LEN_BYTE = 512;
+ private static final byte[] REPLAY_BITMAP_NO_PACKETS = new byte[REPLAY_BITMAP_LEN_BYTE];
+ private static final byte[] REPLAY_BITMAP_ALL_RECEIVED = new byte[REPLAY_BITMAP_LEN_BYTE];
+
+ static {
+ for (int i = 0; i < REPLAY_BITMAP_ALL_RECEIVED.length; i++) {
+ REPLAY_BITMAP_ALL_RECEIVED[i] = (byte) 0xff;
+ }
+ }
+
+ @Test
+ public void testBuildAndGet() {
+ final IpSecTransformState state =
+ new IpSecTransformState.Builder()
+ .setTimestampMillis(TIMESTAMP_MILLIS)
+ .setTxHighestSequenceNumber(HIGHEST_SEQ_NUMBER_TX)
+ .setRxHighestSequenceNumber(HIGHEST_SEQ_NUMBER_RX)
+ .setPacketCount(PACKET_COUNT)
+ .setByteCount(BYTE_COUNT)
+ .setReplayBitmap(REPLAY_BITMAP_ALL_RECEIVED)
+ .build();
+
+ assertEquals(TIMESTAMP_MILLIS, state.getTimestampMillis());
+ assertEquals(HIGHEST_SEQ_NUMBER_TX, state.getTxHighestSequenceNumber());
+ assertEquals(HIGHEST_SEQ_NUMBER_RX, state.getRxHighestSequenceNumber());
+ assertEquals(PACKET_COUNT, state.getPacketCount());
+ assertEquals(BYTE_COUNT, state.getByteCount());
+ assertArrayEquals(REPLAY_BITMAP_ALL_RECEIVED, state.getReplayBitmap());
+ }
+
+ @Test
+ public void testSelfGeneratedTimestampMillis() {
+ final long elapsedRealtimeBefore = SystemClock.elapsedRealtime();
+
+ final IpSecTransformState state =
+ new IpSecTransformState.Builder().setReplayBitmap(REPLAY_BITMAP_NO_PACKETS).build();
+
+ final long elapsedRealtimeAfter = SystemClock.elapsedRealtime();
+
+ // Verify elapsedRealtimeBefore <= state.getTimestampMillis() <= elapsedRealtimeAfter
+ assertFalse(elapsedRealtimeBefore > state.getTimestampMillis());
+ assertFalse(elapsedRealtimeAfter < state.getTimestampMillis());
+ }
+
+ @Test
+ public void testBuildWithoutReplayBitmap() throws Exception {
+ try {
+ new IpSecTransformState.Builder().build();
+ fail("Expected expcetion if replay bitmap is not set");
+ } catch (NullPointerException expected) {
+ }
+ }
+}
diff --git a/tests/cts/net/src/android/net/cts/MultinetworkApiTest.java b/tests/cts/net/src/android/net/cts/MultinetworkApiTest.java
index 17a9ca2..bca18f5 100644
--- a/tests/cts/net/src/android/net/cts/MultinetworkApiTest.java
+++ b/tests/cts/net/src/android/net/cts/MultinetworkApiTest.java
@@ -17,6 +17,12 @@
package android.net.cts;
import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR;
+import static android.provider.DeviceConfig.NAMESPACE_CONNECTIVITY;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.fail;
import android.content.ContentResolver;
import android.content.Context;
@@ -28,9 +34,21 @@
import android.platform.test.annotations.AppModeFull;
import android.system.ErrnoException;
import android.system.OsConstants;
-import android.test.AndroidTestCase;
-public class MultinetworkApiTest extends AndroidTestCase {
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import com.android.testutils.DeviceConfigRule;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+public class MultinetworkApiTest {
+ @Rule
+ public final DeviceConfigRule mDeviceConfigRule = new DeviceConfigRule();
static {
System.loadLibrary("nativemultinetwork_jni");
@@ -58,20 +76,17 @@
private CtsNetUtils mCtsNetUtils;
private String mOldMode;
private String mOldDnsSpecifier;
+ private Context mContext;
- @Override
- protected void setUp() throws Exception {
- super.setUp();
- mCM = (ConnectivityManager) getContext().getSystemService(Context.CONNECTIVITY_SERVICE);
- mCR = getContext().getContentResolver();
- mCtsNetUtils = new CtsNetUtils(getContext());
+ @Before
+ public void setUp() throws Exception {
+ mContext = InstrumentationRegistry.getInstrumentation().getContext();
+ mCM = mContext.getSystemService(ConnectivityManager.class);
+ mCR = mContext.getContentResolver();
+ mCtsNetUtils = new CtsNetUtils(mContext);
}
- @Override
- protected void tearDown() throws Exception {
- super.tearDown();
- }
-
+ @Test
public void testGetaddrinfo() throws ErrnoException {
for (Network network : mCtsNetUtils.getTestableNetworks()) {
int errno = runGetaddrinfoCheck(network.getNetworkHandle());
@@ -82,6 +97,7 @@
}
}
+ @Test
@AppModeFull(reason = "CHANGE_NETWORK_STATE permission can't be granted to instant apps")
public void testSetprocnetwork() throws ErrnoException {
// Hopefully no prior test in this process space has set a default network.
@@ -125,6 +141,7 @@
}
}
+ @Test
@AppModeFull(reason = "CHANGE_NETWORK_STATE permission can't be granted to instant apps")
public void testSetsocknetwork() throws ErrnoException {
for (Network network : mCtsNetUtils.getTestableNetworks()) {
@@ -136,6 +153,7 @@
}
}
+ @Test
public void testNativeDatagramTransmission() throws ErrnoException {
for (Network network : mCtsNetUtils.getTestableNetworks()) {
int errno = runDatagramCheck(network.getNetworkHandle());
@@ -146,6 +164,7 @@
}
}
+ @Test
public void testNoSuchNetwork() {
final Network eNoNet = new Network(54321);
assertNull(mCM.getNetworkInfo(eNoNet));
@@ -158,6 +177,7 @@
// assertEquals(-OsConstants.ENONET, runGetaddrinfoCheck(eNoNetHandle));
}
+ @Test
public void testNetworkHandle() {
// Test Network -> NetworkHandle -> Network results in the same Network.
for (Network network : mCtsNetUtils.getTestableNetworks()) {
@@ -181,6 +201,7 @@
} catch (IllegalArgumentException e) {}
}
+ @Test
public void testResNApi() throws Exception {
final Network[] testNetworks = mCtsNetUtils.getTestableNetworks();
@@ -201,9 +222,21 @@
}
}
+ @Test
@AppModeFull(reason = "WRITE_SECURE_SETTINGS permission can't be granted to instant apps")
- public void testResNApiNXDomainPrivateDns() throws InterruptedException {
+ public void testResNApiNXDomainPrivateDns() throws Exception {
+ // Use async private DNS resolution to avoid flakes due to races applying the setting
+ mDeviceConfigRule.setConfig(NAMESPACE_CONNECTIVITY,
+ "networkmonitor_async_privdns_resolution", "1");
+ mCtsNetUtils.reconnectWifiIfSupported();
+ mCtsNetUtils.reconnectCellIfSupported();
+
mCtsNetUtils.storePrivateDnsSetting();
+
+ mDeviceConfigRule.runAfterNextCleanup(() -> {
+ mCtsNetUtils.reconnectWifiIfSupported();
+ mCtsNetUtils.reconnectCellIfSupported();
+ });
// Enable private DNS strict mode and set server to dns.google before doing NxDomain test.
// b/144521720
try {
diff --git a/tests/cts/net/src/android/net/cts/NsdManagerTest.kt b/tests/cts/net/src/android/net/cts/NsdManagerTest.kt
index 43aa8a6..ce2c2c1 100644
--- a/tests/cts/net/src/android/net/cts/NsdManagerTest.kt
+++ b/tests/cts/net/src/android/net/cts/NsdManagerTest.kt
@@ -684,6 +684,48 @@
}
}
+ @Test
+ fun testRegisterService_twoServicesWithSameNameButDifferentTypes_registeredAndDiscoverable() {
+ val si1 = NsdServiceInfo().also {
+ it.network = testNetwork1.network
+ it.serviceName = serviceName
+ it.serviceType = serviceType
+ it.port = TEST_PORT
+ }
+ val si2 = NsdServiceInfo().also {
+ it.network = testNetwork1.network
+ it.serviceName = serviceName
+ it.serviceType = serviceType2
+ it.port = TEST_PORT + 1
+ }
+ val registrationRecord1 = NsdRegistrationRecord()
+ val registrationRecord2 = NsdRegistrationRecord()
+ val discoveryRecord1 = NsdDiscoveryRecord()
+ val discoveryRecord2 = NsdDiscoveryRecord()
+ tryTest {
+ registerService(registrationRecord1, si1)
+ registerService(registrationRecord2, si2)
+
+ nsdManager.discoverServices(serviceType,
+ NsdManager.PROTOCOL_DNS_SD,
+ testNetwork1.network, Executor { it.run() }, discoveryRecord1)
+ nsdManager.discoverServices(serviceType2,
+ NsdManager.PROTOCOL_DNS_SD,
+ testNetwork1.network, Executor { it.run() }, discoveryRecord2)
+
+ discoveryRecord1.waitForServiceDiscovered(serviceName, serviceType,
+ testNetwork1.network)
+ discoveryRecord2.waitForServiceDiscovered(serviceName, serviceType2,
+ testNetwork1.network)
+ } cleanupStep {
+ nsdManager.stopServiceDiscovery(discoveryRecord1)
+ nsdManager.stopServiceDiscovery(discoveryRecord2)
+ } cleanup {
+ nsdManager.unregisterService(registrationRecord1)
+ nsdManager.unregisterService(registrationRecord2)
+ }
+ }
+
fun checkOffloadServiceInfo(serviceInfo: OffloadServiceInfo, si: NsdServiceInfo) {
val expectedServiceType = si.serviceType.split(",")[0]
assertEquals(si.serviceName, serviceInfo.key.serviceName)
@@ -1674,6 +1716,177 @@
}
}
+ @Test
+ fun testReplyWhenKnownAnswerSuppressionFlagSet() {
+ // The flag may be removed in the future but known-answer suppression should be enabled by
+ // default in that case. The rule will reset flags automatically on teardown.
+ deviceConfigRule.setConfig(NAMESPACE_TETHERING, "test_nsd_known_answer_suppression", "1")
+ deviceConfigRule.setConfig(NAMESPACE_TETHERING, "test_nsd_unicast_reply_enabled", "1")
+
+ val si = makeTestServiceInfo(testNetwork1.network)
+
+ // Register service on testNetwork1
+ val registrationRecord = NsdRegistrationRecord()
+ var nsResponder: NSResponder? = null
+ tryTest {
+ registerService(registrationRecord, si)
+ val packetReader = TapPacketReader(Handler(handlerThread.looper),
+ testNetwork1.iface.fileDescriptor.fileDescriptor, 1500 /* maxPacketSize */)
+ packetReader.startAsyncForTest()
+
+ handlerThread.waitForIdle(TIMEOUT_MS)
+ /*
+ Send a query with a known answer. Expect to receive a response containing TXT record
+ only.
+ Generated with:
+ scapy.raw(scapy.DNS(rd=0, qr=0, aa=0, qd =
+ scapy.DNSQR(qname='_nmt123456789._tcp.local', qtype='PTR',
+ qclass=0x8001) /
+ scapy.DNSQR(qname='NsdTest123456789._nmt123456789._tcp.local', qtype='TXT',
+ qclass=0x8001),
+ an = scapy.DNSRR(rrname='_nmt123456789._tcp.local', type='PTR', ttl=4500,
+ rdata='NsdTest123456789._nmt123456789._tcp.local')
+ )).hex()
+ */
+ val query = HexDump.hexStringToByteArray("0000000000020001000000000d5f6e6d74313233343" +
+ "536373839045f746370056c6f63616c00000c8001104e7364546573743132333435363738390" +
+ "d5f6e6d74313233343536373839045f746370056c6f63616c00001080010d5f6e6d743132333" +
+ "43536373839045f746370056c6f63616c00000c000100001194002b104e73645465737431323" +
+ "33435363738390d5f6e6d74313233343536373839045f746370056c6f63616c00")
+ replaceServiceNameAndTypeWithTestSuffix(query)
+
+ val testSrcAddr = makeLinkLocalAddressOfOtherDeviceOnPrefix(testNetwork1.network)
+ nsResponder = NSResponder(packetReader, mapOf(
+ testSrcAddr to MacAddress.fromString("01:02:03:04:05:06")
+ )).apply { start() }
+
+ packetReader.sendResponse(buildMdnsPacket(query, testSrcAddr))
+ // The reply is sent unicast to the source address. There may be announcements sent
+ // multicast around this time, so filter by destination address.
+ val reply = packetReader.pollForMdnsPacket { pkt ->
+ pkt.isReplyFor("$serviceName.$serviceType.local", DnsResolver.TYPE_TXT) &&
+ !pkt.isReplyFor("$serviceType.local", DnsResolver.TYPE_PTR) &&
+ pkt.dstAddr == testSrcAddr
+ }
+ assertNotNull(reply)
+
+ /*
+ Send a query with a known answer (TTL is less than half). Expect to receive a response
+ containing both PTR and TXT records.
+ Generated with:
+ scapy.raw(scapy.DNS(rd=0, qr=0, aa=0, qd =
+ scapy.DNSQR(qname='_nmt123456789._tcp.local', qtype='PTR',
+ qclass=0x8001) /
+ scapy.DNSQR(qname='NsdTest123456789._nmt123456789._tcp.local', qtype='TXT',
+ qclass=0x8001),
+ an = scapy.DNSRR(rrname='_nmt123456789._tcp.local', type='PTR', ttl=2150,
+ rdata='NsdTest123456789._nmt123456789._tcp.local')
+ )).hex()
+ */
+ val query2 = HexDump.hexStringToByteArray("0000000000020001000000000d5f6e6d7431323334" +
+ "3536373839045f746370056c6f63616c00000c8001104e736454657374313233343536373839" +
+ "0d5f6e6d74313233343536373839045f746370056c6f63616c00001080010d5f6e6d74313233" +
+ "343536373839045f746370056c6f63616c00000c000100000866002b104e7364546573743132" +
+ "333435363738390d5f6e6d74313233343536373839045f746370056c6f63616c00")
+ replaceServiceNameAndTypeWithTestSuffix(query2)
+
+ packetReader.sendResponse(buildMdnsPacket(query2, testSrcAddr))
+ // The reply is sent unicast to the source address. There may be announcements sent
+ // multicast around this time, so filter by destination address.
+ val reply2 = packetReader.pollForMdnsPacket { pkt ->
+ pkt.isReplyFor("$serviceName.$serviceType.local", DnsResolver.TYPE_TXT) &&
+ pkt.isReplyFor("$serviceType.local", DnsResolver.TYPE_PTR) &&
+ pkt.dstAddr == testSrcAddr
+ }
+ assertNotNull(reply2)
+ } cleanup {
+ nsResponder?.stop()
+ nsdManager.unregisterService(registrationRecord)
+ registrationRecord.expectCallback<ServiceUnregistered>()
+ }
+ }
+
+ @Test
+ fun testReplyWithMultipacketWhenKnownAnswerSuppressionFlagSet() {
+ // The flag may be removed in the future but known-answer suppression should be enabled by
+ // default in that case. The rule will reset flags automatically on teardown.
+ deviceConfigRule.setConfig(NAMESPACE_TETHERING, "test_nsd_known_answer_suppression", "1")
+ deviceConfigRule.setConfig(NAMESPACE_TETHERING, "test_nsd_unicast_reply_enabled", "1")
+
+ val si = makeTestServiceInfo(testNetwork1.network)
+
+ // Register service on testNetwork1
+ val registrationRecord = NsdRegistrationRecord()
+ var nsResponder: NSResponder? = null
+ tryTest {
+ registerService(registrationRecord, si)
+ val packetReader = TapPacketReader(Handler(handlerThread.looper),
+ testNetwork1.iface.fileDescriptor.fileDescriptor, 1500 /* maxPacketSize */)
+ packetReader.startAsyncForTest()
+
+ handlerThread.waitForIdle(TIMEOUT_MS)
+ /*
+ Send a query with truncated bit set.
+ Generated with:
+ scapy.raw(scapy.DNS(rd=0, qr=0, aa=0, tc=1, qd=
+ scapy.DNSQR(qname='_nmt123456789._tcp.local', qtype='PTR',
+ qclass=0x8001) /
+ scapy.DNSQR(qname='NsdTest123456789._nmt123456789._tcp.local', qtype='TXT',
+ qclass=0x8001)
+ )).hex()
+ */
+ val query = HexDump.hexStringToByteArray("0000020000020000000000000d5f6e6d74313233343" +
+ "536373839045f746370056c6f63616c00000c8001104e7364546573743132333435363738390" +
+ "d5f6e6d74313233343536373839045f746370056c6f63616c0000108001")
+ replaceServiceNameAndTypeWithTestSuffix(query)
+ /*
+ Send a known answer packet (other service) with truncated bit set.
+ Generated with:
+ scapy.raw(scapy.DNS(rd=0, qr=0, aa=0, tc=1, qd=None,
+ an = scapy.DNSRR(rrname='_test._tcp.local', type='PTR', ttl=4500,
+ rdata='NsdTest._test._tcp.local')
+ )).hex()
+ */
+ val knownAnswer1 = HexDump.hexStringToByteArray("000002000000000100000000055f74657374" +
+ "045f746370056c6f63616c00000c000100001194001a074e736454657374055f74657374045f" +
+ "746370056c6f63616c00")
+ replaceServiceNameAndTypeWithTestSuffix(knownAnswer1)
+ /*
+ Send a known answer packet.
+ Generated with:
+ scapy.raw(scapy.DNS(rd=0, qr=0, aa=0, qd=None,
+ an = scapy.DNSRR(rrname='_nmt123456789._tcp.local', type='PTR', ttl=4500,
+ rdata='NsdTest123456789._nmt123456789._tcp.local')
+ )).hex()
+ */
+ val knownAnswer2 = HexDump.hexStringToByteArray("0000000000000001000000000d5f6e6d7431" +
+ "3233343536373839045f746370056c6f63616c00000c000100001194002b104e736454657374" +
+ "3132333435363738390d5f6e6d74313233343536373839045f746370056c6f63616c00")
+ replaceServiceNameAndTypeWithTestSuffix(knownAnswer2)
+
+ val testSrcAddr = makeLinkLocalAddressOfOtherDeviceOnPrefix(testNetwork1.network)
+ nsResponder = NSResponder(packetReader, mapOf(
+ testSrcAddr to MacAddress.fromString("01:02:03:04:05:06")
+ )).apply { start() }
+
+ packetReader.sendResponse(buildMdnsPacket(query, testSrcAddr))
+ packetReader.sendResponse(buildMdnsPacket(knownAnswer1, testSrcAddr))
+ packetReader.sendResponse(buildMdnsPacket(knownAnswer2, testSrcAddr))
+ // The reply is sent unicast to the source address. There may be announcements sent
+ // multicast around this time, so filter by destination address.
+ val reply = packetReader.pollForMdnsPacket { pkt ->
+ pkt.isReplyFor("$serviceName.$serviceType.local", DnsResolver.TYPE_TXT) &&
+ !pkt.isReplyFor("$serviceType.local", DnsResolver.TYPE_PTR) &&
+ pkt.dstAddr == testSrcAddr
+ }
+ assertNotNull(reply)
+ } cleanup {
+ nsResponder?.stop()
+ nsdManager.unregisterService(registrationRecord)
+ registrationRecord.expectCallback<ServiceUnregistered>()
+ }
+ }
+
private fun makeLinkLocalAddressOfOtherDeviceOnPrefix(network: Network): Inet6Address {
val lp = cm.getLinkProperties(network) ?: fail("No LinkProperties for net $network")
// Expect to have a /64 link-local address
diff --git a/tests/integration/AndroidManifest.xml b/tests/integration/AndroidManifest.xml
index cea83c7..1821329 100644
--- a/tests/integration/AndroidManifest.xml
+++ b/tests/integration/AndroidManifest.xml
@@ -42,6 +42,9 @@
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES"/>
<!-- Register UidFrozenStateChangedCallback -->
<uses-permission android:name="android.permission.PACKAGE_USAGE_STATS"/>
+ <!-- Permission required for CTS test - NetworkStatsIntegrationTest -->
+ <uses-permission android:name="android.permission.INTERNET"/>
+ <uses-permission android:name="android.permission.CONNECTIVITY_USE_RESTRICTED_NETWORKS"/>
<application android:debuggable="true">
<uses-library android:name="android.test.runner"/>
diff --git a/tests/integration/src/com/android/server/net/integrationtests/ConnectivityServiceIntegrationTest.kt b/tests/integration/src/com/android/server/net/integrationtests/ConnectivityServiceIntegrationTest.kt
index 9148770..361d68c 100644
--- a/tests/integration/src/com/android/server/net/integrationtests/ConnectivityServiceIntegrationTest.kt
+++ b/tests/integration/src/com/android/server/net/integrationtests/ConnectivityServiceIntegrationTest.kt
@@ -56,7 +56,6 @@
import com.android.server.NetworkAgentWrapper
import com.android.server.TestNetIdManager
import com.android.server.connectivity.CarrierPrivilegeAuthenticator
-import com.android.server.connectivity.CarrierPrivilegeAuthenticator.CarrierPrivilegesLostListener
import com.android.server.connectivity.ConnectivityResources
import com.android.server.connectivity.MockableSystemProperties
import com.android.server.connectivity.MultinetworkPolicyTracker
@@ -89,6 +88,7 @@
import org.mockito.MockitoAnnotations
import org.mockito.Spy
import java.util.function.Consumer
+import java.util.function.BiConsumer
const val SERVICE_BIND_TIMEOUT_MS = 5_000L
const val TEST_TIMEOUT_MS = 10_000L
@@ -245,7 +245,7 @@
context: Context,
tm: TelephonyManager,
requestRestrictedWifiEnabled: Boolean,
- listener: CarrierPrivilegesLostListener
+ listener: BiConsumer<Int, Int>
): CarrierPrivilegeAuthenticator {
return CarrierPrivilegeAuthenticator(context,
object : CarrierPrivilegeAuthenticator.Dependencies() {
diff --git a/tests/integration/src/com/android/server/net/integrationtests/NetworkStatsIntegrationTest.kt b/tests/integration/src/com/android/server/net/integrationtests/NetworkStatsIntegrationTest.kt
new file mode 100644
index 0000000..765e56e
--- /dev/null
+++ b/tests/integration/src/com/android/server/net/integrationtests/NetworkStatsIntegrationTest.kt
@@ -0,0 +1,597 @@
+/*
+ * 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.net.integrationtests
+
+import android.Manifest.permission.MANAGE_TEST_NETWORKS
+import android.annotation.TargetApi
+import android.app.usage.NetworkStats
+import android.app.usage.NetworkStats.Bucket
+import android.app.usage.NetworkStats.Bucket.TAG_NONE
+import android.app.usage.NetworkStatsManager
+import android.content.Context
+import android.net.ConnectivityManager
+import android.net.ConnectivityManager.TYPE_TEST
+import android.net.InetAddresses
+import android.net.IpPrefix
+import android.net.LinkAddress
+import android.net.Network
+import android.net.NetworkCapabilities
+import android.net.NetworkRequest
+import android.net.NetworkTemplate
+import android.net.NetworkTemplate.MATCH_TEST
+import android.net.TestNetworkSpecifier
+import android.net.TrafficStats
+import android.os.Build
+import android.os.Process
+import androidx.test.platform.app.InstrumentationRegistry
+import com.android.server.net.integrationtests.NetworkStatsIntegrationTest.Direction.DOWNLOAD
+import com.android.server.net.integrationtests.NetworkStatsIntegrationTest.Direction.UPLOAD
+import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo
+import com.android.testutils.DevSdkIgnoreRunner
+import com.android.testutils.PacketBridge
+import com.android.testutils.RecorderCallback.CallbackEntry.LinkPropertiesChanged
+import com.android.testutils.TestDnsServer
+import com.android.testutils.TestHttpServer
+import com.android.testutils.TestableNetworkCallback
+import com.android.testutils.runAsShell
+import fi.iki.elonen.NanoHTTPD
+import java.io.BufferedInputStream
+import java.io.BufferedOutputStream
+import java.net.HttpURLConnection
+import java.net.HttpURLConnection.HTTP_OK
+import java.net.InetSocketAddress
+import java.net.URL
+import java.nio.charset.Charset
+import kotlin.math.ceil
+import kotlin.test.assertEquals
+import kotlin.test.assertTrue
+import org.junit.After
+import org.junit.Assume.assumeTrue
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+private const val TEST_TAG = 0xF00D
+
+@RunWith(DevSdkIgnoreRunner::class)
+@TargetApi(Build.VERSION_CODES.S)
+@IgnoreUpTo(Build.VERSION_CODES.TIRAMISU)
+class NetworkStatsIntegrationTest {
+ private val TAG = NetworkStatsIntegrationTest::class.java.simpleName
+ private val LOCAL_V6ADDR =
+ LinkAddress(InetAddresses.parseNumericAddress("2001:db8::1234"), 64)
+
+ // Remote address, both the client and server will have a hallucination that
+ // they are talking to this address.
+ private val REMOTE_V6ADDR =
+ LinkAddress(InetAddresses.parseNumericAddress("dead:beef::808:808"), 64)
+ private val REMOTE_V4ADDR =
+ LinkAddress(InetAddresses.parseNumericAddress("8.8.8.8"), 32)
+ private val DEFAULT_MTU = 1500
+ private val DEFAULT_BUFFER_SIZE = 1500 // Any size greater than or equal to mtu
+ private val CONNECTION_TIMEOUT_MILLIS = 15000
+ private val TEST_DOWNLOAD_SIZE = 10000L
+ private val TEST_UPLOAD_SIZE = 20000L
+ private val HTTP_SERVER_NAME = "test.com"
+ private val HTTP_SERVER_PORT = 8080 // Use port > 1024 to avoid restrictions on system ports
+ private val DNS_INTERNAL_SERVER_PORT = 53
+ private val DNS_EXTERNAL_SERVER_PORT = 1053
+ private val TCP_ACK_SIZE = 72
+
+ // Packet overheads that are not part of the actual data transmission, these
+ // include DNS packets, TCP handshake/termination packets, and HTTP header
+ // packets. These overheads were gathered from real samples and may not
+ // be perfectly accurate because of DNS caches and TCP retransmissions, etc.
+ private val CONSTANT_PACKET_OVERHEAD = 8
+
+ // 130 is an observed average.
+ private val CONSTANT_BYTES_OVERHEAD = 130 * CONSTANT_PACKET_OVERHEAD
+ private val TOLERANCE = 1.3
+
+ // Set up the packet bridge with two IPv6 address only test networks.
+ private val inst = InstrumentationRegistry.getInstrumentation()
+ private val context = inst.getContext()
+ private val packetBridge = runAsShell(MANAGE_TEST_NETWORKS) {
+ PacketBridge(
+ context,
+ listOf(LOCAL_V6ADDR),
+ REMOTE_V6ADDR.address,
+ listOf(
+ Pair(DNS_INTERNAL_SERVER_PORT, DNS_EXTERNAL_SERVER_PORT)
+ )
+ )
+ }
+ private val cm = context.getSystemService(ConnectivityManager::class.java)!!
+
+ // Set up DNS server for testing server and DNS64.
+ private val fakeDns = TestDnsServer(
+ packetBridge.externalNetwork,
+ InetSocketAddress(LOCAL_V6ADDR.address, DNS_EXTERNAL_SERVER_PORT)
+ ).apply {
+ start()
+ setAnswer(
+ "ipv4only.arpa",
+ listOf(IpPrefix(REMOTE_V6ADDR.address, REMOTE_V6ADDR.prefixLength).address)
+ )
+ setAnswer(HTTP_SERVER_NAME, listOf(REMOTE_V4ADDR.address))
+ }
+
+ // Start up test http server.
+ private val httpServer = TestHttpServer(
+ LOCAL_V6ADDR.address.hostAddress,
+ HTTP_SERVER_PORT
+ ).apply {
+ start()
+ }
+
+ @Before
+ fun setUp() {
+ assumeTrue(shouldRunTests())
+ packetBridge.start()
+ }
+
+ // For networkstack tests, it is not guaranteed that the tethering module will be
+ // updated at the same time. If the tethering module is not new enough, it may not contain
+ // the necessary abilities to run these tests. For example, The tests depends on test
+ // network stats being counted, which can only be achieved when they are marked as TYPE_TEST.
+ // If the tethering module does not support TYPE_TEST stats, then these tests will need
+ // to be skipped.
+ fun shouldRunTests() = cm.getNetworkInfo(packetBridge.internalNetwork)!!.type == TYPE_TEST
+
+ @After
+ fun tearDown() {
+ packetBridge.stop()
+ fakeDns.stop()
+ httpServer.stop()
+ }
+
+ private fun waitFor464XlatReady(network: Network): String {
+ val iface = cm.getLinkProperties(network)!!.interfaceName!!
+
+ // Make a network request to listen to the specific test network.
+ val nr = NetworkRequest.Builder()
+ .clearCapabilities()
+ .addTransportType(NetworkCapabilities.TRANSPORT_TEST)
+ .setNetworkSpecifier(TestNetworkSpecifier(iface))
+ .build()
+ val testCb = TestableNetworkCallback()
+ cm.registerNetworkCallback(nr, testCb)
+
+ // Wait for the stacked address to be available.
+ testCb.eventuallyExpect<LinkPropertiesChanged> {
+ it.lp.stackedLinks.getOrNull(0)?.linkAddresses?.getOrNull(0) != null
+ }
+
+ return iface
+ }
+
+ private val Network.mtu: Int get() {
+ val lp = cm.getLinkProperties(this)!!
+ val mtuStacked = if (lp.stackedLinks[0]?.mtu != 0) lp.stackedLinks[0].mtu else DEFAULT_MTU
+ val mtuInterface = if (lp.mtu != 0) lp.mtu else DEFAULT_MTU
+ return mtuInterface.coerceAtMost(mtuStacked)
+ }
+
+ /**
+ * Verify data usage download stats with test 464xlat networks.
+ *
+ * This test starts two test networks and binds them together, the internal one is for the
+ * client to make http traffic on the test network, and the external one is for the mocked
+ * http and dns server to bind to and provide responses.
+ *
+ * After Clat setup, the client will use clat v4 address to send packets to the mocked
+ * server v4 address, which will be translated into a v6 packet by the clat daemon with
+ * NAT64 prefix learned from the mocked DNS64 response. And send to the interface.
+ *
+ * While the packets are being forwarded to the external interface, the servers will see
+ * the packets originated from the mocked v6 address, and destined to a local v6 address.
+ */
+ @Test
+ fun test464XlatTcpStats() {
+ // Wait for 464Xlat to be ready.
+ val internalInterfaceName = waitFor464XlatReady(packetBridge.internalNetwork)
+ val mtu = packetBridge.internalNetwork.mtu
+
+ val snapshotBeforeTest = StatsSnapshot(context, internalInterfaceName)
+
+ // Generate the download traffic.
+ genHttpTraffic(packetBridge.internalNetwork, uploadSize = 0L, TEST_DOWNLOAD_SIZE)
+
+ // In practice, for one way 10k download payload, the download usage is about
+ // 11222~12880 bytes, with 14~17 packets. And the upload usage is about 1279~1626 bytes
+ // with 14~17 packets, which is majorly contributed by TCP ACK packets.
+ val snapshotAfterDownload = StatsSnapshot(context, internalInterfaceName)
+ val (expectedDownloadLower, expectedDownloadUpper) = getExpectedStatsBounds(
+ TEST_DOWNLOAD_SIZE,
+ mtu,
+ DOWNLOAD
+ )
+ assertOnlyNonTaggedStatsIncreases(
+ snapshotBeforeTest,
+ snapshotAfterDownload,
+ expectedDownloadLower,
+ expectedDownloadUpper
+ )
+
+ // Generate upload traffic with tag to verify tagged data accounting as well.
+ genHttpTrafficWithTag(
+ packetBridge.internalNetwork,
+ TEST_UPLOAD_SIZE,
+ downloadSize = 0L,
+ TEST_TAG
+ )
+
+ // Verify upload data usage accounting.
+ val snapshotAfterUpload = StatsSnapshot(context, internalInterfaceName)
+ val (expectedUploadLower, expectedUploadUpper) = getExpectedStatsBounds(
+ TEST_UPLOAD_SIZE,
+ mtu,
+ UPLOAD
+ )
+ assertAllStatsIncreases(
+ snapshotAfterDownload,
+ snapshotAfterUpload,
+ expectedUploadLower,
+ expectedUploadUpper
+ )
+ }
+
+ private enum class Direction {
+ DOWNLOAD,
+ UPLOAD
+ }
+
+ private fun getExpectedStatsBounds(
+ transmittedSize: Long,
+ mtu: Int,
+ direction: Direction
+ ): Pair<BareStats, BareStats> {
+ // This is already an underestimated value since the input doesn't include TCP/IP
+ // layer overhead.
+ val txBytesLower = transmittedSize
+ // Include TCP/IP header overheads and retransmissions in the upper bound.
+ val txBytesUpper = (transmittedSize * TOLERANCE).toLong()
+ val txPacketsLower = txBytesLower / mtu + (CONSTANT_PACKET_OVERHEAD / TOLERANCE).toLong()
+ val estTransmissionPacketsUpper = ceil(txBytesUpper / mtu.toDouble()).toLong()
+ val txPacketsUpper = estTransmissionPacketsUpper +
+ (CONSTANT_PACKET_OVERHEAD * TOLERANCE).toLong()
+ // Assume ACK only sent once for the entire transmission.
+ val rxPacketsLower = 1L + (CONSTANT_PACKET_OVERHEAD / TOLERANCE).toLong()
+ // Assume ACK sent for every RX packet.
+ val rxPacketsUpper = txPacketsUpper
+ val rxBytesLower = 1L * TCP_ACK_SIZE + (CONSTANT_BYTES_OVERHEAD / TOLERANCE).toLong()
+ val rxBytesUpper = estTransmissionPacketsUpper * TCP_ACK_SIZE +
+ (CONSTANT_BYTES_OVERHEAD * TOLERANCE).toLong()
+
+ return if (direction == UPLOAD) {
+ BareStats(rxBytesLower, rxPacketsLower, txBytesLower, txPacketsLower) to
+ BareStats(rxBytesUpper, rxPacketsUpper, txBytesUpper, txPacketsUpper)
+ } else {
+ BareStats(txBytesLower, txPacketsLower, rxBytesLower, rxPacketsLower) to
+ BareStats(txBytesUpper, txPacketsUpper, rxBytesUpper, rxPacketsUpper)
+ }
+ }
+
+ private fun genHttpTraffic(network: Network, uploadSize: Long, downloadSize: Long) =
+ genHttpTrafficWithTag(network, uploadSize, downloadSize, NetworkStats.Bucket.TAG_NONE)
+
+ private fun genHttpTrafficWithTag(
+ network: Network,
+ uploadSize: Long,
+ downloadSize: Long,
+ tag: Int
+ ) {
+ val path = "/test_upload_download"
+ val buf = ByteArray(DEFAULT_BUFFER_SIZE)
+
+ httpServer.addResponse(
+ TestHttpServer.Request(path, NanoHTTPD.Method.POST), NanoHTTPD.Response.Status.OK,
+ content = getRandomString(downloadSize)
+ )
+ var httpConnection: HttpURLConnection? = null
+ try {
+ TrafficStats.setThreadStatsTag(tag)
+ val spec = "http://$HTTP_SERVER_NAME:${httpServer.listeningPort}$path"
+ val url = URL(spec)
+ httpConnection = network.openConnection(url) as HttpURLConnection
+ httpConnection.connectTimeout = CONNECTION_TIMEOUT_MILLIS
+ httpConnection.requestMethod = "POST"
+ httpConnection.doOutput = true
+ // Tell the server that the response should not be compressed. Otherwise, the data usage
+ // accounted will be less than expected.
+ httpConnection.setRequestProperty("Accept-Encoding", "identity")
+ // Tell the server that to close connection after this request, this is needed to
+ // prevent from reusing the same socket that has different tagging requirement.
+ httpConnection.setRequestProperty("Connection", "close")
+
+ // Send http body.
+ val outputStream = BufferedOutputStream(httpConnection.outputStream)
+ outputStream.write(getRandomString(uploadSize).toByteArray(Charset.forName("UTF-8")))
+ outputStream.close()
+ assertEquals(HTTP_OK, httpConnection.responseCode)
+
+ // Receive response from the server.
+ val inputStream = BufferedInputStream(httpConnection.getInputStream())
+ var total = 0L
+ while (true) {
+ val count = inputStream.read(buf)
+ if (count == -1) break // End-of-Stream
+ total += count
+ }
+ assertEquals(downloadSize, total)
+ } finally {
+ httpConnection?.inputStream?.close()
+ TrafficStats.clearThreadStatsTag()
+ }
+ }
+
+ // NetworkStats.Bucket cannot be written. So another class is needed to
+ // perform arithmetic operations.
+ data class BareStats(
+ val rxBytes: Long,
+ val rxPackets: Long,
+ val txBytes: Long,
+ val txPackets: Long
+ ) {
+ operator fun plus(other: BareStats): BareStats {
+ return BareStats(
+ this.rxBytes + other.rxBytes, this.rxPackets + other.rxPackets,
+ this.txBytes + other.txBytes, this.txPackets + other.txPackets
+ )
+ }
+
+ operator fun minus(other: BareStats): BareStats {
+ return BareStats(
+ this.rxBytes - other.rxBytes, this.rxPackets - other.rxPackets,
+ this.txBytes - other.txBytes, this.txPackets - other.txPackets
+ )
+ }
+
+ fun reverse(): BareStats =
+ BareStats(
+ rxBytes = txBytes,
+ rxPackets = txPackets,
+ txBytes = rxBytes,
+ txPackets = rxPackets
+ )
+
+ override fun toString(): String {
+ return "BareStats{rx/txBytes=$rxBytes/$txBytes, rx/txPackets=$rxPackets/$txPackets}"
+ }
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (other !is BareStats) return false
+
+ if (rxBytes != other.rxBytes) return false
+ if (rxPackets != other.rxPackets) return false
+ if (txBytes != other.txBytes) return false
+ if (txPackets != other.txPackets) return false
+
+ return true
+ }
+
+ override fun hashCode(): Int {
+ return (rxBytes * 11 + rxPackets * 13 + txBytes * 17 + txPackets * 19).toInt()
+ }
+
+ companion object {
+ val EMPTY = BareStats(0L, 0L, 0L, 0L)
+ }
+ }
+
+ data class StatsSnapshot(val context: Context, val iface: String) {
+ val statsSummary = getNetworkSummary(iface)
+ val statsUid = getUidDetail(iface, TAG_NONE)
+ val taggedSummary = getTaggedNetworkSummary(iface, TEST_TAG)
+ val taggedUid = getUidDetail(iface, TEST_TAG)
+ val trafficStatsIface = getTrafficStatsIface(iface)
+ val trafficStatsUid = getTrafficStatsUid(Process.myUid())
+
+ private fun getUidDetail(iface: String, tag: Int): BareStats {
+ return getNetworkStatsThat(iface, tag) { nsm, template ->
+ nsm.queryDetailsForUidTagState(
+ template, Long.MIN_VALUE, Long.MAX_VALUE,
+ Process.myUid(), tag, Bucket.STATE_ALL
+ )
+ }
+ }
+
+ private fun getNetworkSummary(iface: String): BareStats {
+ return getNetworkStatsThat(iface, TAG_NONE) { nsm, template ->
+ nsm.querySummary(template, Long.MIN_VALUE, Long.MAX_VALUE)
+ }
+ }
+
+ private fun getTaggedNetworkSummary(iface: String, tag: Int): BareStats {
+ return getNetworkStatsThat(iface, tag) { nsm, template ->
+ nsm.queryTaggedSummary(template, Long.MIN_VALUE, Long.MAX_VALUE)
+ }
+ }
+
+ private fun getNetworkStatsThat(
+ iface: String,
+ tag: Int,
+ queryApi: (nsm: NetworkStatsManager, template: NetworkTemplate) -> NetworkStats
+ ): BareStats {
+ val nsm = context.getSystemService(NetworkStatsManager::class.java)!!
+ nsm.forceUpdate()
+ val testTemplate = NetworkTemplate.Builder(MATCH_TEST)
+ .setWifiNetworkKeys(setOf(iface)).build()
+ val stats = queryApi.invoke(nsm, testTemplate)
+ val filteredBuckets =
+ stats.buckets().filter { it.uid == Process.myUid() && it.tag == tag }
+ return filteredBuckets.fold(BareStats.EMPTY) { acc, it ->
+ acc + BareStats(
+ it.rxBytes,
+ it.rxPackets,
+ it.txBytes,
+ it.txPackets
+ )
+ }
+ }
+
+ // Helper function to iterate buckets in app.usage.NetworkStats.
+ private fun NetworkStats.buckets() = object : Iterable<NetworkStats.Bucket> {
+ override fun iterator() = object : Iterator<NetworkStats.Bucket> {
+ override operator fun hasNext() = hasNextBucket()
+ override operator fun next() =
+ NetworkStats.Bucket().also { assertTrue(getNextBucket(it)) }
+ }
+ }
+
+ private fun getTrafficStatsIface(iface: String): BareStats = BareStats(
+ TrafficStats.getRxBytes(iface),
+ TrafficStats.getRxPackets(iface),
+ TrafficStats.getTxBytes(iface),
+ TrafficStats.getTxPackets(iface)
+ )
+
+ private fun getTrafficStatsUid(uid: Int): BareStats = BareStats(
+ TrafficStats.getUidRxBytes(uid),
+ TrafficStats.getUidRxPackets(uid),
+ TrafficStats.getUidTxBytes(uid),
+ TrafficStats.getUidTxPackets(uid)
+ )
+ }
+
+ private fun assertAllStatsIncreases(
+ before: StatsSnapshot,
+ after: StatsSnapshot,
+ lower: BareStats,
+ upper: BareStats
+ ) {
+ assertNonTaggedStatsIncreases(before, after, lower, upper)
+ assertTaggedStatsIncreases(before, after, lower, upper)
+ }
+
+ private fun assertOnlyNonTaggedStatsIncreases(
+ before: StatsSnapshot,
+ after: StatsSnapshot,
+ lower: BareStats,
+ upper: BareStats
+ ) {
+ assertNonTaggedStatsIncreases(before, after, lower, upper)
+ assertTaggedStatsEquals(before, after)
+ }
+
+ private fun assertNonTaggedStatsIncreases(
+ before: StatsSnapshot,
+ after: StatsSnapshot,
+ lower: BareStats,
+ upper: BareStats
+ ) {
+ assertInRange(
+ "Unexpected iface traffic stats",
+ after.iface,
+ before.trafficStatsIface, after.trafficStatsIface,
+ lower, upper
+ )
+ // Uid traffic stats are counted in both direction because the external network
+ // traffic is also attributed to the test uid.
+ assertInRange(
+ "Unexpected uid traffic stats",
+ after.iface,
+ before.trafficStatsUid, after.trafficStatsUid,
+ lower + lower.reverse(), upper + upper.reverse()
+ )
+ assertInRange(
+ "Unexpected non-tagged summary stats",
+ after.iface,
+ before.statsSummary, after.statsSummary,
+ lower, upper
+ )
+ assertInRange(
+ "Unexpected non-tagged uid stats",
+ after.iface,
+ before.statsUid, after.statsUid,
+ lower, upper
+ )
+ }
+
+ private fun assertTaggedStatsEquals(before: StatsSnapshot, after: StatsSnapshot) {
+ // Increment of tagged data should be zero since no tagged traffic was generated.
+ assertEquals(
+ before.taggedSummary,
+ after.taggedSummary,
+ "Unexpected tagged summary stats: ${after.iface}"
+ )
+ assertEquals(
+ before.taggedUid,
+ after.taggedUid,
+ "Unexpected tagged uid stats: ${Process.myUid()} on ${after.iface}"
+ )
+ }
+
+ private fun assertTaggedStatsIncreases(
+ before: StatsSnapshot,
+ after: StatsSnapshot,
+ lower: BareStats,
+ upper: BareStats
+ ) {
+ assertInRange(
+ "Unexpected tagged summary stats",
+ after.iface,
+ before.taggedSummary, after.taggedSummary,
+ lower,
+ upper
+ )
+ assertInRange(
+ "Unexpected tagged uid stats: ${Process.myUid()}",
+ after.iface,
+ before.taggedUid, after.taggedUid,
+ lower,
+ upper
+ )
+ }
+
+ /** Verify the given BareStats is in range [lower, upper] */
+ private fun assertInRange(
+ tag: String,
+ iface: String,
+ before: BareStats,
+ after: BareStats,
+ lower: BareStats,
+ upper: BareStats
+ ) {
+ // Passing the value after operation and the value before operation to dump the actual
+ // numbers if it fails.
+ assertTrue(checkInRange(before, after, lower, upper),
+ "$tag on $iface: $after - $before is not within range [$lower, $upper]"
+ )
+ }
+
+ private fun checkInRange(
+ before: BareStats,
+ after: BareStats,
+ lower: BareStats,
+ upper: BareStats
+ ): Boolean {
+ val value = after - before
+ return value.rxBytes in lower.rxBytes..upper.rxBytes &&
+ value.rxPackets in lower.rxPackets..upper.rxPackets &&
+ value.txBytes in lower.txBytes..upper.txBytes &&
+ value.txPackets in lower.txPackets..upper.txPackets
+ }
+
+ fun getRandomString(length: Long): String {
+ val allowedChars = ('A'..'Z') + ('a'..'z') + ('0'..'9')
+ return (1..length)
+ .map { allowedChars.random() }
+ .joinToString("")
+ }
+}
diff --git a/tests/unit/java/com/android/metrics/ConnectivitySampleMetricsTest.kt b/tests/unit/java/com/android/metrics/ConnectivitySampleMetricsTest.kt
index 3043d50..53baee1 100644
--- a/tests/unit/java/com/android/metrics/ConnectivitySampleMetricsTest.kt
+++ b/tests/unit/java/com/android/metrics/ConnectivitySampleMetricsTest.kt
@@ -16,6 +16,7 @@
import android.net.NetworkCapabilities.TRANSPORT_CELLULAR
import android.net.NetworkCapabilities.TRANSPORT_WIFI
import android.net.NetworkScore
+import android.net.NetworkScore.KEEP_CONNECTED_FOR_TEST
import android.net.NetworkScore.POLICY_EXITING
import android.net.NetworkScore.POLICY_TRANSPORT_PRIMARY
import android.os.Build
@@ -86,7 +87,10 @@
.addCapability(NET_CAPABILITY_NOT_SUSPENDED)
.addCapability(NET_CAPABILITY_NOT_ROAMING)
.build()
- val wifi1Score = NetworkScore.Builder().setExiting(true).build()
+ val wifi1Score = NetworkScore.Builder()
+ .setKeepConnectedReason(KEEP_CONNECTED_FOR_TEST)
+ .setExiting(true)
+ .build()
val agentWifi1 = Agent(nc = wifi1Caps, score = FromS(wifi1Score)).also { it.connect() }
val wifi2Caps = NetworkCapabilities.Builder()
@@ -96,7 +100,10 @@
.addCapability(NET_CAPABILITY_NOT_ROAMING)
.addEnterpriseId(NET_ENTERPRISE_ID_3)
.build()
- val wifi2Score = NetworkScore.Builder().setTransportPrimary(true).build()
+ val wifi2Score = NetworkScore.Builder()
+ .setKeepConnectedReason(KEEP_CONNECTED_FOR_TEST)
+ .setTransportPrimary(true)
+ .build()
val agentWifi2 = Agent(nc = wifi2Caps, score = FromS(wifi2Score)).also { it.connect() }
val cellCaps = NetworkCapabilities.Builder()
@@ -107,7 +114,9 @@
.addCapability(NET_CAPABILITY_NOT_ROAMING)
.addEnterpriseId(NET_ENTERPRISE_ID_1)
.build()
- val cellScore = NetworkScore.Builder().build()
+ val cellScore = NetworkScore.Builder()
+ .setKeepConnectedReason(KEEP_CONNECTED_FOR_TEST)
+ .build()
val agentCell = Agent(nc = cellCaps, score = FromS(cellScore)).also { it.connect() }
val stats = csHandler.onHandler { service.sampleConnectivityState() }
diff --git a/tests/unit/java/com/android/server/ConnectivityServiceTest.java b/tests/unit/java/com/android/server/ConnectivityServiceTest.java
index 6623bbd..c534025 100755
--- a/tests/unit/java/com/android/server/ConnectivityServiceTest.java
+++ b/tests/unit/java/com/android/server/ConnectivityServiceTest.java
@@ -173,7 +173,6 @@
import static com.android.server.ConnectivityServiceTestUtils.transportToLegacyType;
import static com.android.server.NetworkAgentWrapper.CallbackType.OnQosCallbackRegister;
import static com.android.server.NetworkAgentWrapper.CallbackType.OnQosCallbackUnregister;
-import static com.android.server.connectivity.CarrierPrivilegeAuthenticator.CarrierPrivilegesLostListener;
import static com.android.testutils.Cleanup.testAndCleanup;
import static com.android.testutils.ConcurrentUtils.await;
import static com.android.testutils.ConcurrentUtils.durationOf;
@@ -488,6 +487,7 @@
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
+import java.util.function.BiConsumer;
import java.util.function.Consumer;
import java.util.function.Predicate;
import java.util.function.Supplier;
@@ -526,7 +526,7 @@
// between a LOST callback that arrives immediately and a LOST callback that arrives after
// the linger/nascent timeout. For this, our assertions should run fast enough to leave
// less than (mService.mLingerDelayMs - TEST_CALLBACK_TIMEOUT_MS) between the time callbacks are
- // supposedly fired, and the time we call expectCallback.
+ // supposedly fired, and the time we call expectCapChanged.
private static final int TEST_CALLBACK_TIMEOUT_MS = 250;
// Chosen to be less than TEST_CALLBACK_TIMEOUT_MS. This ensures that requests have time to
// complete before callbacks are verified.
@@ -565,6 +565,7 @@
private static final int TEST_PACKAGE_UID2 = 321;
private static final int TEST_PACKAGE_UID3 = 456;
private static final int NETWORK_ACTIVITY_NO_UID = -1;
+ private static final int TEST_SUBSCRIPTION_ID = 1;
private static final int PACKET_WAKEUP_MARK_MASK = 0x80000000;
@@ -2059,7 +2060,7 @@
@NonNull final Context context,
@NonNull final TelephonyManager tm,
final boolean requestRestrictedWifiEnabled,
- CarrierPrivilegesLostListener listener) {
+ BiConsumer<Integer, Integer> listener) {
return mDeps.isAtLeastT() ? mCarrierPrivilegeAuthenticator : null;
}
@@ -11486,7 +11487,7 @@
doTestInterfaceClassActivityChanged(TRANSPORT_CELLULAR);
}
- private void doTestOnNetworkActive_NewNetworkConnects(int transportType, boolean expectCallback)
+ private void doTestOnNetworkActive_NewNetworkConnects(int transportType, boolean expectCapChanged)
throws Exception {
final ConditionVariable onNetworkActiveCv = new ConditionVariable();
final ConnectivityManager.OnNetworkActiveListener listener = onNetworkActiveCv::open;
@@ -11498,7 +11499,7 @@
testAndCleanup(() -> {
mCm.addDefaultNetworkActiveListener(listener);
agent.connect(true);
- if (expectCallback) {
+ if (expectCapChanged) {
assertTrue(onNetworkActiveCv.block(TEST_CALLBACK_TIMEOUT_MS));
} else {
assertFalse(onNetworkActiveCv.block(TEST_CALLBACK_TIMEOUT_MS));
@@ -11513,7 +11514,7 @@
@Test
public void testOnNetworkActive_NewCellConnects_CallbackCalled() throws Exception {
- doTestOnNetworkActive_NewNetworkConnects(TRANSPORT_CELLULAR, true /* expectCallback */);
+ doTestOnNetworkActive_NewNetworkConnects(TRANSPORT_CELLULAR, true /* expectCapChanged */);
}
@Test
@@ -11522,8 +11523,8 @@
// 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
- final boolean expectCallback = mDeps.isAtLeastV();
- doTestOnNetworkActive_NewNetworkConnects(TRANSPORT_ETHERNET, expectCallback);
+ final boolean expectCapChanged = mDeps.isAtLeastV();
+ doTestOnNetworkActive_NewNetworkConnects(TRANSPORT_ETHERNET, expectCapChanged);
}
@Test
@@ -17375,7 +17376,7 @@
return new NetworkRequest.Builder()
.addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
.removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED)
- .setSubscriptionIds(Collections.singleton(Process.myUid()))
+ .setSubscriptionIds(Collections.singleton(TEST_SUBSCRIPTION_ID))
.build();
}
@@ -17422,32 +17423,46 @@
final NetworkCallback networkCallback1 = new NetworkCallback();
final NetworkCallback networkCallback2 = new NetworkCallback();
- mCm.requestNetwork(getRestrictedRequestForWifiWithSubIds(), networkCallback1);
- mCm.requestNetwork(getRestrictedRequestForWifiWithSubIds(), pendingIntent);
- mCm.registerNetworkCallback(getRestrictedRequestForWifiWithSubIds(), networkCallback2);
+ mCm.requestNetwork(
+ getRestrictedRequestForWifiWithSubIds(), networkCallback1);
+ mCm.requestNetwork(
+ getRestrictedRequestForWifiWithSubIds(), pendingIntent);
+ mCm.registerNetworkCallback(
+ getRestrictedRequestForWifiWithSubIds(), networkCallback2);
mCm.unregisterNetworkCallback(networkCallback1);
mCm.releaseNetworkRequest(pendingIntent);
mCm.unregisterNetworkCallback(networkCallback2);
}
- @Test
- @IgnoreUpTo(Build.VERSION_CODES.TIRAMISU)
- public void testRestrictedRequestRemovedDueToCarrierPrivilegesLost() throws Exception {
- mServiceContext.setPermission(CONNECTIVITY_USE_RESTRICTED_NETWORKS, PERMISSION_DENIED);
- NetworkCapabilities filter = getRestrictedRequestForWifiWithSubIds().networkCapabilities;
+ private void doTestNetworkRequestWithCarrierPrivilegesLost(
+ boolean shouldGrantRestrictedNetworkPermission,
+ int lostPrivilegeUid,
+ int lostPrivilegeSubId,
+ boolean expectUnavailable,
+ boolean expectCapChanged) throws Exception {
+ if (shouldGrantRestrictedNetworkPermission) {
+ mServiceContext.setPermission(CONNECTIVITY_USE_RESTRICTED_NETWORKS, PERMISSION_GRANTED);
+ } else {
+ mServiceContext.setPermission(CONNECTIVITY_USE_RESTRICTED_NETWORKS, PERMISSION_DENIED);
+ }
+
+ NetworkCapabilities filter =
+ getRestrictedRequestForWifiWithSubIds().networkCapabilities;
final HandlerThread handlerThread = new HandlerThread("testRestrictedFactoryRequests");
handlerThread.start();
+
final MockNetworkFactory testFactory = new MockNetworkFactory(handlerThread.getLooper(),
mServiceContext, "testFactory", filter, mCsHandlerThread);
testFactory.register();
-
testFactory.assertRequestCountEquals(0);
+
doReturn(true).when(mCarrierPrivilegeAuthenticator)
.isCarrierServiceUidForNetworkCapabilities(eq(Process.myUid()), any());
- final TestNetworkCallback networkCallback1 = new TestNetworkCallback();
- final NetworkRequest networkrequest1 = getRestrictedRequestForWifiWithSubIds();
- mCm.requestNetwork(networkrequest1, networkCallback1);
+ final TestNetworkCallback networkCallback = new TestNetworkCallback();
+ final NetworkRequest networkrequest =
+ getRestrictedRequestForWifiWithSubIds();
+ mCm.requestNetwork(networkrequest, networkCallback);
testFactory.expectRequestAdd();
testFactory.assertRequestCountEquals(1);
@@ -17455,24 +17470,36 @@
.setAllowedUids(Set.of(Process.myUid()))
.build();
mWiFiAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI, new LinkProperties(), nc);
- mWiFiAgent.connect(true);
- networkCallback1.expectAvailableThenValidatedCallbacks(mWiFiAgent);
-
+ mWiFiAgent.connect(false);
+ networkCallback.expectAvailableCallbacksUnvalidated(mWiFiAgent);
final NetworkAgentInfo nai = mService.getNetworkAgentInfoForNetwork(
mWiFiAgent.getNetwork());
doReturn(false).when(mCarrierPrivilegeAuthenticator)
.isCarrierServiceUidForNetworkCapabilities(eq(Process.myUid()), any());
- final CarrierPrivilegesLostListener carrierPrivilegesLostListener =
- mService.getCarrierPrivilegesLostListener();
- carrierPrivilegesLostListener.onCarrierPrivilegesLost(Process.myUid());
+ doReturn(TEST_SUBSCRIPTION_ID).when(mCarrierPrivilegeAuthenticator)
+ .getSubIdFromNetworkCapabilities(any());
+ mService.onCarrierPrivilegesLost(lostPrivilegeUid, lostPrivilegeSubId);
waitForIdle();
- testFactory.expectRequestRemove();
- testFactory.assertRequestCountEquals(0);
- assertTrue(nai.networkCapabilities.getAllowedUidsNoCopy().isEmpty());
- networkCallback1.expect(NETWORK_CAPS_UPDATED);
- networkCallback1.expect(UNAVAILABLE);
+ if (expectCapChanged) {
+ networkCallback.expect(NETWORK_CAPS_UPDATED);
+ }
+ if (expectUnavailable) {
+ networkCallback.expect(UNAVAILABLE);
+ }
+ if (!expectCapChanged && !expectUnavailable) {
+ networkCallback.assertNoCallback();
+ }
+
+ mWiFiAgent.disconnect();
+ waitForIdle();
+
+ if (expectUnavailable) {
+ testFactory.assertRequestCountEquals(0);
+ } else {
+ testFactory.assertRequestCountEquals(1);
+ }
handlerThread.quitSafely();
handlerThread.join();
@@ -17480,64 +17507,45 @@
@Test
@IgnoreUpTo(Build.VERSION_CODES.TIRAMISU)
+ public void testRestrictedRequestRemovedDueToCarrierPrivilegesLost() throws Exception {
+ doTestNetworkRequestWithCarrierPrivilegesLost(
+ false /* shouldGrantRestrictedNetworkPermission */,
+ Process.myUid(),
+ TEST_SUBSCRIPTION_ID,
+ true /* expectUnavailable */,
+ true /* expectCapChanged */);
+ }
+
+ @Test
+ @IgnoreUpTo(Build.VERSION_CODES.TIRAMISU)
+ public void testRequestNotRemoved_MismatchSubId() throws Exception {
+ doTestNetworkRequestWithCarrierPrivilegesLost(
+ false /* shouldGrantRestrictedNetworkPermission */,
+ Process.myUid(),
+ TEST_SUBSCRIPTION_ID + 1,
+ false /* expectUnavailable */,
+ false /* expectCapChanged */);
+ }
+ @Test
+ @IgnoreUpTo(Build.VERSION_CODES.TIRAMISU)
public void testRequestNotRemoved_MismatchUid() throws Exception {
- mServiceContext.setPermission(CONNECTIVITY_USE_RESTRICTED_NETWORKS, PERMISSION_DENIED);
- NetworkCapabilities filter = getRestrictedRequestForWifiWithSubIds().networkCapabilities;
- final HandlerThread handlerThread = new HandlerThread("testRestrictedFactoryRequests");
- handlerThread.start();
-
- final MockNetworkFactory testFactory = new MockNetworkFactory(handlerThread.getLooper(),
- mServiceContext, "testFactory", filter, mCsHandlerThread);
- testFactory.register();
-
- doReturn(true).when(mCarrierPrivilegeAuthenticator)
- .isCarrierServiceUidForNetworkCapabilities(anyInt(), any());
- final TestNetworkCallback networkCallback1 = new TestNetworkCallback();
- final NetworkRequest networkrequest1 = getRestrictedRequestForWifiWithSubIds();
- mCm.requestNetwork(networkrequest1, networkCallback1);
- testFactory.expectRequestAdd();
- testFactory.assertRequestCountEquals(1);
-
- doReturn(false).when(mCarrierPrivilegeAuthenticator)
- .isCarrierServiceUidForNetworkCapabilities(eq(Process.myUid()), any());
- final CarrierPrivilegesLostListener carrierPrivilegesLostListener =
- mService.getCarrierPrivilegesLostListener();
- carrierPrivilegesLostListener.onCarrierPrivilegesLost(Process.myUid() + 1);
- expectNoRequestChanged(testFactory);
-
- handlerThread.quitSafely();
- handlerThread.join();
+ doTestNetworkRequestWithCarrierPrivilegesLost(
+ false /* shouldGrantRestrictedNetworkPermission */,
+ Process.myUid() + 1,
+ TEST_SUBSCRIPTION_ID,
+ false /* expectUnavailable */,
+ false /* expectCapChanged */);
}
@Test
@IgnoreUpTo(Build.VERSION_CODES.TIRAMISU)
public void testRequestNotRemoved_HasRestrictedNetworkPermission() throws Exception {
- mServiceContext.setPermission(CONNECTIVITY_USE_RESTRICTED_NETWORKS, PERMISSION_GRANTED);
- NetworkCapabilities filter = getRestrictedRequestForWifiWithSubIds().networkCapabilities;
- final HandlerThread handlerThread = new HandlerThread("testRestrictedFactoryRequests");
- handlerThread.start();
-
- final MockNetworkFactory testFactory = new MockNetworkFactory(handlerThread.getLooper(),
- mServiceContext, "testFactory", filter, mCsHandlerThread);
- testFactory.register();
-
- doReturn(true).when(mCarrierPrivilegeAuthenticator)
- .isCarrierServiceUidForNetworkCapabilities(anyInt(), any());
- final TestNetworkCallback networkCallback1 = new TestNetworkCallback();
- final NetworkRequest networkrequest1 = getRestrictedRequestForWifiWithSubIds();
- mCm.requestNetwork(networkrequest1, networkCallback1);
- testFactory.expectRequestAdd();
- testFactory.assertRequestCountEquals(1);
-
- doReturn(false).when(mCarrierPrivilegeAuthenticator)
- .isCarrierServiceUidForNetworkCapabilities(eq(Process.myUid()), any());
- final CarrierPrivilegesLostListener carrierPrivilegesLostListener =
- mService.getCarrierPrivilegesLostListener();
- carrierPrivilegesLostListener.onCarrierPrivilegesLost(Process.myUid());
- expectNoRequestChanged(testFactory);
-
- handlerThread.quitSafely();
- handlerThread.join();
+ doTestNetworkRequestWithCarrierPrivilegesLost(
+ true /* shouldGrantRestrictedNetworkPermission */,
+ Process.myUid(),
+ TEST_SUBSCRIPTION_ID,
+ false /* expectUnavailable */,
+ true /* expectCapChanged */);
}
@Test
public void testAllowedUids() throws Exception {
diff --git a/tests/unit/java/com/android/server/connectivity/CarrierPrivilegeAuthenticatorTest.java b/tests/unit/java/com/android/server/connectivity/CarrierPrivilegeAuthenticatorTest.java
index 9f0ec30..7bd2b56 100644
--- a/tests/unit/java/com/android/server/connectivity/CarrierPrivilegeAuthenticatorTest.java
+++ b/tests/unit/java/com/android/server/connectivity/CarrierPrivilegeAuthenticatorTest.java
@@ -20,7 +20,6 @@
import static android.net.NetworkCapabilities.TRANSPORT_WIFI;
import static android.telephony.TelephonyManager.ACTION_MULTI_SIM_CONFIG_CHANGED;
-import static com.android.server.connectivity.CarrierPrivilegeAuthenticator.CarrierPrivilegesLostListener;
import static com.android.server.connectivity.ConnectivityFlags.CARRIER_SERVICE_CHANGED_USE_CALLBACK;
import static org.junit.Assert.assertEquals;
@@ -47,7 +46,6 @@
import android.net.TelephonyNetworkSpecifier;
import android.os.Build;
import android.os.HandlerThread;
-import android.telephony.SubscriptionManager;
import android.telephony.TelephonyManager;
import com.android.net.module.util.CollectionUtils;
@@ -71,6 +69,7 @@
import java.util.Collections;
import java.util.Map;
import java.util.Set;
+import java.util.function.BiConsumer;
/**
* Tests for CarrierPrivilegeAuthenticatorTest.
@@ -92,7 +91,7 @@
@NonNull private final TelephonyManagerShimImpl mTelephonyManagerShim;
@NonNull private final PackageManager mPackageManager;
@NonNull private TestCarrierPrivilegeAuthenticator mCarrierPrivilegeAuthenticator;
- @NonNull private final CarrierPrivilegesLostListener mListener;
+ @NonNull private final BiConsumer<Integer, Integer> mListener;
private final int mCarrierConfigPkgUid = 12345;
private final boolean mUseCallbacks;
private final String mTestPkg = "com.android.server.connectivity.test";
@@ -107,9 +106,8 @@
mListener);
}
@Override
- protected int getSlotIndex(int subId) {
- if (SubscriptionManager.DEFAULT_SUBSCRIPTION_ID == subId) return TEST_SUBSCRIPTION_ID;
- return subId;
+ protected int getSubId(int slotIndex) {
+ return TEST_SUBSCRIPTION_ID;
}
}
@@ -129,7 +127,7 @@
mTelephonyManager = mock(TelephonyManager.class);
mTelephonyManagerShim = mock(TelephonyManagerShimImpl.class);
mPackageManager = mock(PackageManager.class);
- mListener = mock(CarrierPrivilegesLostListener.class);
+ mListener = mock(BiConsumer.class);
mHandlerThread = new HandlerThread(CarrierPrivilegeAuthenticatorTest.class.getSimpleName());
mUseCallbacks = useCallbacks;
final Dependencies deps = mock(Dependencies.class);
@@ -184,7 +182,7 @@
final NetworkCapabilities.Builder ncBuilder = new NetworkCapabilities.Builder()
.addTransportType(TRANSPORT_CELLULAR)
- .setNetworkSpecifier(new TelephonyNetworkSpecifier(0));
+ .setNetworkSpecifier(new TelephonyNetworkSpecifier(TEST_SUBSCRIPTION_ID));
assertTrue(mCarrierPrivilegeAuthenticator.isCarrierServiceUidForNetworkCapabilities(
mCarrierConfigPkgUid, ncBuilder.build()));
@@ -220,7 +218,8 @@
newListeners.get(0).onCarrierServiceChanged(null, mCarrierConfigPkgUid);
- final TelephonyNetworkSpecifier specifier = new TelephonyNetworkSpecifier(0);
+ final TelephonyNetworkSpecifier specifier =
+ new TelephonyNetworkSpecifier(TEST_SUBSCRIPTION_ID);
final NetworkCapabilities nc = new NetworkCapabilities.Builder()
.addTransportType(TRANSPORT_CELLULAR)
.setNetworkSpecifier(specifier)
@@ -239,7 +238,11 @@
l.onCarrierServiceChanged(null, mCarrierConfigPkgUid);
l.onCarrierServiceChanged(null, mCarrierConfigPkgUid + 1);
if (mUseCallbacks) {
- verify(mListener).onCarrierPrivilegesLost(eq(mCarrierConfigPkgUid));
+ verify(mListener).accept(eq(mCarrierConfigPkgUid), eq(TEST_SUBSCRIPTION_ID));
+ }
+ l.onCarrierServiceChanged(null, mCarrierConfigPkgUid + 2);
+ if (mUseCallbacks) {
+ verify(mListener).accept(eq(mCarrierConfigPkgUid + 1), eq(TEST_SUBSCRIPTION_ID));
}
}
@@ -247,7 +250,8 @@
public void testOnCarrierPrivilegesChanged() throws Exception {
final CarrierPrivilegesListenerShim listener = getCarrierPrivilegesListeners().get(0);
- final TelephonyNetworkSpecifier specifier = new TelephonyNetworkSpecifier(0);
+ final TelephonyNetworkSpecifier specifier =
+ new TelephonyNetworkSpecifier(TEST_SUBSCRIPTION_ID);
final NetworkCapabilities nc = new NetworkCapabilities.Builder()
.addTransportType(TRANSPORT_CELLULAR)
.setNetworkSpecifier(specifier)
@@ -275,7 +279,7 @@
assertFalse(mCarrierPrivilegeAuthenticator.isCarrierServiceUidForNetworkCapabilities(
mCarrierConfigPkgUid, ncBuilder.build()));
- ncBuilder.setNetworkSpecifier(new TelephonyNetworkSpecifier(0));
+ ncBuilder.setNetworkSpecifier(new TelephonyNetworkSpecifier(TEST_SUBSCRIPTION_ID));
assertTrue(mCarrierPrivilegeAuthenticator.isCarrierServiceUidForNetworkCapabilities(
mCarrierConfigPkgUid, ncBuilder.build()));
@@ -284,7 +288,7 @@
ncBuilder.setNetworkSpecifier(null);
ncBuilder.removeTransportType(TRANSPORT_CELLULAR);
ncBuilder.addTransportType(TRANSPORT_WIFI);
- ncBuilder.setNetworkSpecifier(new TelephonyNetworkSpecifier(0));
+ ncBuilder.setNetworkSpecifier(new TelephonyNetworkSpecifier(TEST_SUBSCRIPTION_ID));
assertFalse(mCarrierPrivilegeAuthenticator.isCarrierServiceUidForNetworkCapabilities(
mCarrierConfigPkgUid, ncBuilder.build()));
}
@@ -298,7 +302,7 @@
final NetworkCapabilities.Builder ncBuilder = new NetworkCapabilities.Builder();
ncBuilder.addTransportType(TRANSPORT_WIFI);
ncBuilder.removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED);
- ncBuilder.setSubscriptionIds(Set.of(0));
+ ncBuilder.setSubscriptionIds(Set.of(TEST_SUBSCRIPTION_ID));
assertTrue(mCarrierPrivilegeAuthenticator.isCarrierServiceUidForNetworkCapabilities(
mCarrierConfigPkgUid, ncBuilder.build()));
}
diff --git a/tests/unit/java/com/android/server/connectivity/SatelliteAccessControllerTest.kt b/tests/unit/java/com/android/server/connectivity/SatelliteAccessControllerTest.kt
index 64a515a..193078b 100644
--- a/tests/unit/java/com/android/server/connectivity/SatelliteAccessControllerTest.kt
+++ b/tests/unit/java/com/android/server/connectivity/SatelliteAccessControllerTest.kt
@@ -21,9 +21,12 @@
import android.content.Context
import android.content.pm.ApplicationInfo
import android.content.pm.PackageManager
+import android.content.pm.UserInfo
import android.os.Build
import android.os.Handler
import android.os.UserHandle
+import android.util.ArraySet
+import com.android.server.makeMockUserManager
import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo
import com.android.testutils.DevSdkIgnoreRunner
import org.junit.Before
@@ -36,18 +39,31 @@
import org.mockito.Mockito.doReturn
import org.mockito.Mockito.mock
import org.mockito.Mockito.never
-import org.mockito.Mockito.times
import org.mockito.Mockito.verify
import java.util.concurrent.Executor
import java.util.function.Consumer
-import kotlin.test.assertEquals
-import kotlin.test.assertFalse
-import kotlin.test.assertTrue
-private const val DEFAULT_MESSAGING_APP1 = "default_messaging_app_1"
-private const val DEFAULT_MESSAGING_APP2 = "default_messaging_app_2"
-private const val DEFAULT_MESSAGING_APP1_UID = 1234
-private const val DEFAULT_MESSAGING_APP2_UID = 5678
+private const val USER = 0
+val USER_INFO = UserInfo(USER, "" /* name */, UserInfo.FLAG_PRIMARY)
+val USER_HANDLE = UserHandle(USER)
+private const val PRIMARY_USER = 0
+private const val SECONDARY_USER = 10
+private val PRIMARY_USER_HANDLE = UserHandle.of(PRIMARY_USER)
+private val SECONDARY_USER_HANDLE = UserHandle.of(SECONDARY_USER)
+// sms app names
+private const val SMS_APP1 = "sms_app_1"
+private const val SMS_APP2 = "sms_app_2"
+// sms app ids
+private const val SMS_APP_ID1 = 100
+private const val SMS_APP_ID2 = 101
+// UID for app1 and app2 on primary user
+// These app could become default sms app for user1
+private val PRIMARY_USER_SMS_APP_UID1 = UserHandle.getUid(PRIMARY_USER, SMS_APP_ID1)
+private val PRIMARY_USER_SMS_APP_UID2 = UserHandle.getUid(PRIMARY_USER, SMS_APP_ID2)
+// UID for app1 and app2 on secondary user
+// These app could become default sms app for user2
+private val SECONDARY_USER_SMS_APP_UID1 = UserHandle.getUid(SECONDARY_USER, SMS_APP_ID1)
+private val SECONDARY_USER_SMS_APP_UID2 = UserHandle.getUid(SECONDARY_USER, SMS_APP_ID2)
@RunWith(DevSdkIgnoreRunner::class)
@IgnoreUpTo(Build.VERSION_CODES.TIRAMISU)
@@ -58,33 +74,36 @@
private val mRoleManager =
mock(SatelliteAccessController.Dependencies::class.java)
private val mCallback = mock(Consumer::class.java) as Consumer<Set<Int>>
- private val mSatelliteAccessController by lazy {
- SatelliteAccessController(context, mRoleManager, mCallback, mHandler)}
- private var mRoleHolderChangedListener: OnRoleHoldersChangedListener? = null
+ private val mSatelliteAccessController =
+ SatelliteAccessController(context, mRoleManager, mCallback, mHandler)
+ private lateinit var mRoleHolderChangedListener: OnRoleHoldersChangedListener
@Before
@Throws(PackageManager.NameNotFoundException::class)
fun setup() {
+ makeMockUserManager(USER_INFO, USER_HANDLE)
+ doReturn(context).`when`(context).createContextAsUser(any(), anyInt())
doReturn(mPackageManager).`when`(context).packageManager
- doReturn(PackageManager.PERMISSION_GRANTED)
- .`when`(mPackageManager)
- .checkPermission(Manifest.permission.SATELLITE_COMMUNICATION, DEFAULT_MESSAGING_APP1)
- doReturn(PackageManager.PERMISSION_GRANTED)
- .`when`(mPackageManager)
- .checkPermission(Manifest.permission.SATELLITE_COMMUNICATION, DEFAULT_MESSAGING_APP2)
- // Initialise default message application package1
+ doReturn(PackageManager.PERMISSION_GRANTED)
+ .`when`(mPackageManager)
+ .checkPermission(Manifest.permission.SATELLITE_COMMUNICATION, SMS_APP1)
+ doReturn(PackageManager.PERMISSION_GRANTED)
+ .`when`(mPackageManager)
+ .checkPermission(Manifest.permission.SATELLITE_COMMUNICATION, SMS_APP2)
+
+ // Initialise default message application primary user package1
val applicationInfo1 = ApplicationInfo()
- applicationInfo1.uid = DEFAULT_MESSAGING_APP1_UID
+ applicationInfo1.uid = PRIMARY_USER_SMS_APP_UID1
doReturn(applicationInfo1)
.`when`(mPackageManager)
- .getApplicationInfo(eq(DEFAULT_MESSAGING_APP1), anyInt())
+ .getApplicationInfo(eq(SMS_APP1), anyInt())
- // Initialise default message application package2
+ // Initialise default message application primary user package2
val applicationInfo2 = ApplicationInfo()
- applicationInfo2.uid = DEFAULT_MESSAGING_APP2_UID
+ applicationInfo2.uid = PRIMARY_USER_SMS_APP_UID2
doReturn(applicationInfo2)
.`when`(mPackageManager)
- .getApplicationInfo(eq(DEFAULT_MESSAGING_APP2), anyInt())
+ .getApplicationInfo(eq(SMS_APP2), anyInt())
// Get registered listener using captor
val listenerCaptor = ArgumentCaptor.forClass(
@@ -97,80 +116,107 @@
}
@Test
- fun test_onRoleHoldersChanged_SatellitePreferredUid_Changed() {
- doReturn(listOf<String>()).`when`(mRoleManager).getRoleHolders(RoleManager.ROLE_SMS)
- val satelliteNetworkPreferredSet =
- ArgumentCaptor.forClass(Set::class.java) as ArgumentCaptor<Set<Int>>
- mRoleHolderChangedListener?.onRoleHoldersChanged(RoleManager.ROLE_SMS, UserHandle.ALL)
- verify(mCallback, never()).accept(satelliteNetworkPreferredSet.capture())
+ fun test_onRoleHoldersChanged_SatelliteFallbackUid_Changed_SingleUser() {
+ doReturn(listOf<String>()).`when`(mRoleManager).getRoleHoldersAsUser(RoleManager.ROLE_SMS,
+ PRIMARY_USER_HANDLE)
+ mRoleHolderChangedListener.onRoleHoldersChanged(RoleManager.ROLE_SMS, PRIMARY_USER_HANDLE)
+ verify(mCallback, never()).accept(any())
- // check DEFAULT_MESSAGING_APP1 is available as satellite network preferred uid
- doReturn(listOf(DEFAULT_MESSAGING_APP1))
- .`when`(mRoleManager).getRoleHolders(RoleManager.ROLE_SMS)
- mRoleHolderChangedListener?.onRoleHoldersChanged(RoleManager.ROLE_SMS, UserHandle.ALL)
- verify(mCallback).accept(satelliteNetworkPreferredSet.capture())
- var satelliteNetworkPreferredUids = satelliteNetworkPreferredSet.value
- assertEquals(1, satelliteNetworkPreferredUids.size)
- assertTrue(satelliteNetworkPreferredUids.contains(DEFAULT_MESSAGING_APP1_UID))
- assertFalse(satelliteNetworkPreferredUids.contains(DEFAULT_MESSAGING_APP2_UID))
+ // check DEFAULT_MESSAGING_APP1 is available as satellite network fallback uid
+ doReturn(listOf(SMS_APP1))
+ .`when`(mRoleManager).getRoleHoldersAsUser(RoleManager.ROLE_SMS, PRIMARY_USER_HANDLE)
+ mRoleHolderChangedListener.onRoleHoldersChanged(RoleManager.ROLE_SMS, PRIMARY_USER_HANDLE)
+ verify(mCallback).accept(setOf(PRIMARY_USER_SMS_APP_UID1))
- // check DEFAULT_MESSAGING_APP1 and DEFAULT_MESSAGING_APP2 is available
- // as satellite network preferred uid
- val dmas: MutableList<String> = ArrayList()
- dmas.add(DEFAULT_MESSAGING_APP1)
- dmas.add(DEFAULT_MESSAGING_APP2)
- doReturn(dmas).`when`(mRoleManager).getRoleHolders(RoleManager.ROLE_SMS)
- mRoleHolderChangedListener?.onRoleHoldersChanged(RoleManager.ROLE_SMS, UserHandle.ALL)
- verify(mCallback, times(2))
- .accept(satelliteNetworkPreferredSet.capture())
- satelliteNetworkPreferredUids = satelliteNetworkPreferredSet.value
- assertEquals(2, satelliteNetworkPreferredUids.size)
- assertTrue(satelliteNetworkPreferredUids.contains(DEFAULT_MESSAGING_APP1_UID))
- assertTrue(satelliteNetworkPreferredUids.contains(DEFAULT_MESSAGING_APP2_UID))
+ // check SMS_APP2 is available as satellite network Fallback uid
+ doReturn(listOf(SMS_APP2)).`when`(mRoleManager).getRoleHoldersAsUser(RoleManager.ROLE_SMS,
+ PRIMARY_USER_HANDLE)
+ mRoleHolderChangedListener.onRoleHoldersChanged(RoleManager.ROLE_SMS, PRIMARY_USER_HANDLE)
+ verify(mCallback).accept(setOf(PRIMARY_USER_SMS_APP_UID2))
- // check no uid is available as satellite network preferred uid
- doReturn(listOf<String>()).`when`(mRoleManager).getRoleHolders(RoleManager.ROLE_SMS)
- mRoleHolderChangedListener?.onRoleHoldersChanged(RoleManager.ROLE_SMS, UserHandle.ALL)
- verify(mCallback, times(3))
- .accept(satelliteNetworkPreferredSet.capture())
- satelliteNetworkPreferredUids = satelliteNetworkPreferredSet.value
- assertEquals(0, satelliteNetworkPreferredUids.size)
- assertFalse(satelliteNetworkPreferredUids.contains(DEFAULT_MESSAGING_APP1_UID))
- assertFalse(satelliteNetworkPreferredUids.contains(DEFAULT_MESSAGING_APP2_UID))
-
- // No Change received at OnRoleSmsChanged, check callback not triggered
- doReturn(listOf<String>()).`when`(mRoleManager).getRoleHolders(RoleManager.ROLE_SMS)
- mRoleHolderChangedListener?.onRoleHoldersChanged(RoleManager.ROLE_SMS, UserHandle.ALL)
- verify(mCallback, times(3))
- .accept(satelliteNetworkPreferredSet.capture())
+ // check no uid is available as satellite network fallback uid
+ doReturn(listOf<String>()).`when`(mRoleManager).getRoleHoldersAsUser(RoleManager.ROLE_SMS,
+ PRIMARY_USER_HANDLE)
+ mRoleHolderChangedListener.onRoleHoldersChanged(RoleManager.ROLE_SMS, PRIMARY_USER_HANDLE)
+ verify(mCallback).accept(ArraySet())
}
@Test
fun test_onRoleHoldersChanged_NoSatelliteCommunicationPermission() {
- doReturn(listOf<Any>()).`when`(mRoleManager).getRoleHolders(RoleManager.ROLE_SMS)
- val satelliteNetworkPreferredSet =
- ArgumentCaptor.forClass(Set::class.java) as ArgumentCaptor<Set<Int>>
- mRoleHolderChangedListener?.onRoleHoldersChanged(RoleManager.ROLE_SMS, UserHandle.ALL)
- verify(mCallback, never()).accept(satelliteNetworkPreferredSet.capture())
+ doReturn(listOf<Any>()).`when`(mRoleManager).getRoleHoldersAsUser(RoleManager.ROLE_SMS,
+ PRIMARY_USER_HANDLE)
+ mRoleHolderChangedListener.onRoleHoldersChanged(RoleManager.ROLE_SMS, PRIMARY_USER_HANDLE)
+ verify(mCallback, never()).accept(any())
- // check DEFAULT_MESSAGING_APP1 is not available as satellite network preferred uid
+ // check DEFAULT_MESSAGING_APP1 is not available as satellite network fallback uid
// since satellite communication permission not available.
doReturn(PackageManager.PERMISSION_DENIED)
.`when`(mPackageManager)
- .checkPermission(Manifest.permission.SATELLITE_COMMUNICATION, DEFAULT_MESSAGING_APP1)
- doReturn(listOf(DEFAULT_MESSAGING_APP1))
- .`when`(mRoleManager).getRoleHolders(RoleManager.ROLE_SMS)
- mRoleHolderChangedListener?.onRoleHoldersChanged(RoleManager.ROLE_SMS, UserHandle.ALL)
- verify(mCallback, never()).accept(satelliteNetworkPreferredSet.capture())
+ .checkPermission(Manifest.permission.SATELLITE_COMMUNICATION, SMS_APP1)
+ doReturn(listOf(SMS_APP1))
+ .`when`(mRoleManager).getRoleHoldersAsUser(RoleManager.ROLE_SMS, PRIMARY_USER_HANDLE)
+ mRoleHolderChangedListener.onRoleHoldersChanged(RoleManager.ROLE_SMS, PRIMARY_USER_HANDLE)
+ verify(mCallback, never()).accept(any())
}
@Test
fun test_onRoleHoldersChanged_RoleSms_NotAvailable() {
- doReturn(listOf(DEFAULT_MESSAGING_APP1))
- .`when`(mRoleManager).getRoleHolders(RoleManager.ROLE_SMS)
- val satelliteNetworkPreferredSet =
- ArgumentCaptor.forClass(Set::class.java) as ArgumentCaptor<Set<Int>>
- mRoleHolderChangedListener?.onRoleHoldersChanged(RoleManager.ROLE_BROWSER, UserHandle.ALL)
- verify(mCallback, never()).accept(satelliteNetworkPreferredSet.capture())
+ doReturn(listOf(SMS_APP1))
+ .`when`(mRoleManager).getRoleHoldersAsUser(RoleManager.ROLE_SMS, PRIMARY_USER_HANDLE)
+ mRoleHolderChangedListener.onRoleHoldersChanged(RoleManager.ROLE_BROWSER,
+ PRIMARY_USER_HANDLE)
+ verify(mCallback, never()).accept(any())
+ }
+
+ @Test
+ fun test_onRoleHoldersChanged_SatelliteNetworkFallbackUid_Changed_multiUser() {
+ doReturn(listOf<String>()).`when`(mRoleManager).getRoleHoldersAsUser(RoleManager.ROLE_SMS,
+ PRIMARY_USER_HANDLE)
+ mRoleHolderChangedListener.onRoleHoldersChanged(RoleManager.ROLE_SMS, PRIMARY_USER_HANDLE)
+ verify(mCallback, never()).accept(any())
+
+ // check SMS_APP1 is available as satellite network fallback uid at primary user
+ doReturn(listOf(SMS_APP1))
+ .`when`(mRoleManager).getRoleHoldersAsUser(RoleManager.ROLE_SMS, PRIMARY_USER_HANDLE)
+ mRoleHolderChangedListener.onRoleHoldersChanged(RoleManager.ROLE_SMS, PRIMARY_USER_HANDLE)
+ verify(mCallback).accept(setOf(PRIMARY_USER_SMS_APP_UID1))
+
+ // check SMS_APP2 is available as satellite network fallback uid at primary user
+ doReturn(listOf(SMS_APP2)).`when`(mRoleManager).getRoleHoldersAsUser(RoleManager.ROLE_SMS,
+ PRIMARY_USER_HANDLE)
+ mRoleHolderChangedListener.onRoleHoldersChanged(RoleManager.ROLE_SMS, PRIMARY_USER_HANDLE)
+ verify(mCallback).accept(setOf(PRIMARY_USER_SMS_APP_UID2))
+
+ // check SMS_APP1 is available as satellite network fallback uid at secondary user
+ val applicationInfo1 = ApplicationInfo()
+ applicationInfo1.uid = SECONDARY_USER_SMS_APP_UID1
+ doReturn(applicationInfo1).`when`(mPackageManager)
+ .getApplicationInfo(eq(SMS_APP1), anyInt())
+ doReturn(listOf(SMS_APP1)).`when`(mRoleManager).getRoleHoldersAsUser(RoleManager.ROLE_SMS,
+ SECONDARY_USER_HANDLE)
+ mRoleHolderChangedListener.onRoleHoldersChanged(RoleManager.ROLE_SMS, SECONDARY_USER_HANDLE)
+ verify(mCallback).accept(setOf(PRIMARY_USER_SMS_APP_UID2, SECONDARY_USER_SMS_APP_UID1))
+
+ // check no uid is available as satellite network fallback uid at primary user
+ doReturn(listOf<String>()).`when`(mRoleManager).getRoleHoldersAsUser(RoleManager.ROLE_SMS,
+ PRIMARY_USER_HANDLE)
+ mRoleHolderChangedListener.onRoleHoldersChanged(RoleManager.ROLE_SMS,
+ PRIMARY_USER_HANDLE)
+ verify(mCallback).accept(setOf(SECONDARY_USER_SMS_APP_UID1))
+
+ // check SMS_APP2 is available as satellite network fallback uid at secondary user
+ applicationInfo1.uid = SECONDARY_USER_SMS_APP_UID2
+ doReturn(applicationInfo1).`when`(mPackageManager)
+ .getApplicationInfo(eq(SMS_APP2), anyInt())
+ doReturn(listOf(SMS_APP2))
+ .`when`(mRoleManager).getRoleHoldersAsUser(RoleManager.ROLE_SMS, SECONDARY_USER_HANDLE)
+ mRoleHolderChangedListener.onRoleHoldersChanged(RoleManager.ROLE_SMS, SECONDARY_USER_HANDLE)
+ verify(mCallback).accept(setOf(SECONDARY_USER_SMS_APP_UID2))
+
+ // check no uid is available as satellite network fallback uid at secondary user
+ doReturn(listOf<String>()).`when`(mRoleManager).getRoleHoldersAsUser(RoleManager.ROLE_SMS,
+ SECONDARY_USER_HANDLE)
+ mRoleHolderChangedListener.onRoleHoldersChanged(RoleManager.ROLE_SMS, SECONDARY_USER_HANDLE)
+ verify(mCallback).accept(ArraySet())
}
}
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 58124f3..09236b1 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/MdnsServiceTypeClientTests.java
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsServiceTypeClientTests.java
@@ -43,6 +43,7 @@
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;
import static java.nio.charset.StandardCharsets.UTF_8;
@@ -1207,10 +1208,14 @@
final String ipV4Address = "192.0.2.0";
final String ipV6Address = "2001:db8::";
- final MdnsSearchOptions resolveOptions = MdnsSearchOptions.newBuilder()
+ final MdnsSearchOptions resolveOptions1 = MdnsSearchOptions.newBuilder()
+ .setResolveInstanceName(instanceName).build();
+ final MdnsSearchOptions resolveOptions2 = MdnsSearchOptions.newBuilder()
.setResolveInstanceName(instanceName).build();
- startSendAndReceive(mockListenerOne, resolveOptions);
+ startSendAndReceive(mockListenerOne, resolveOptions1);
+ startSendAndReceive(mockListenerTwo, resolveOptions2);
+ // No need to verify order for both listeners; and order is not guaranteed between them
InOrder inOrder = inOrder(mockListenerOne, mockSocketClient);
// Verify a query for SRV/TXT was sent, but no PTR query
@@ -1223,13 +1228,19 @@
eq(socketKey), eq(false));
verify(mockDeps, times(1)).sendMessage(any(), any(Message.class));
assertNotNull(delayMessage);
+ inOrder.verify(mockListenerOne).onDiscoveryQuerySent(any(), anyInt());
+ verify(mockListenerTwo).onDiscoveryQuerySent(any(), anyInt());
final MdnsPacket srvTxtQueryPacket = MdnsPacket.parse(
new MdnsPacketReader(srvTxtQueryCaptor.getValue()));
final String[] serviceName = getTestServiceName(instanceName);
+ assertEquals(1, srvTxtQueryPacket.questions.size());
assertFalse(hasQuestion(srvTxtQueryPacket, MdnsRecord.TYPE_PTR));
assertTrue(hasQuestion(srvTxtQueryPacket, MdnsRecord.TYPE_ANY, serviceName));
+ assertEquals(0, srvTxtQueryPacket.answers.size());
+ assertEquals(0, srvTxtQueryPacket.authorityRecords.size());
+ assertEquals(0, srvTxtQueryPacket.additionalRecords.size());
// Process a response with SRV+TXT
final MdnsPacket srvTxtResponse = new MdnsPacket(
@@ -1246,6 +1257,10 @@
Collections.emptyList() /* additionalRecords */);
processResponse(srvTxtResponse, socketKey);
+ inOrder.verify(mockListenerOne).onServiceNameDiscovered(
+ matchServiceName(instanceName), eq(false) /* isServiceFromCache */);
+ verify(mockListenerTwo).onServiceNameDiscovered(
+ matchServiceName(instanceName), eq(false) /* isServiceFromCache */);
// Expect a query for A/AAAA
dispatchMessage();
@@ -1255,11 +1270,18 @@
inOrder.verify(mockSocketClient, times(2)).sendPacketRequestingMulticastResponse(
addressQueryCaptor.capture(),
eq(socketKey), eq(false));
+ inOrder.verify(mockListenerOne).onDiscoveryQuerySent(any(), anyInt());
+ // onDiscoveryQuerySent was called 2 times in total
+ verify(mockListenerTwo, times(2)).onDiscoveryQuerySent(any(), anyInt());
final MdnsPacket addressQueryPacket = MdnsPacket.parse(
new MdnsPacketReader(addressQueryCaptor.getValue()));
+ assertEquals(2, addressQueryPacket.questions.size());
assertTrue(hasQuestion(addressQueryPacket, MdnsRecord.TYPE_A, hostname));
assertTrue(hasQuestion(addressQueryPacket, MdnsRecord.TYPE_AAAA, hostname));
+ assertEquals(0, addressQueryPacket.answers.size());
+ assertEquals(0, addressQueryPacket.authorityRecords.size());
+ assertEquals(0, addressQueryPacket.additionalRecords.size());
// Process a response with address records
final MdnsPacket addressResponse = new MdnsPacket(
@@ -1276,10 +1298,12 @@
Collections.emptyList() /* additionalRecords */);
inOrder.verify(mockListenerOne, never()).onServiceNameDiscovered(any(), anyBoolean());
+ verifyNoMoreInteractions(mockListenerTwo);
processResponse(addressResponse, socketKey);
inOrder.verify(mockListenerOne).onServiceFound(
serviceInfoCaptor.capture(), eq(false) /* isServiceFromCache */);
+ verify(mockListenerTwo).onServiceFound(any(), anyBoolean());
verifyServiceInfo(serviceInfoCaptor.getValue(),
instanceName,
SERVICE_TYPE_LABELS,
diff --git a/tests/unit/java/com/android/server/connectivityservice/CSCaptivePortalAppTest.kt b/tests/unit/java/com/android/server/connectivityservice/CSCaptivePortalAppTest.kt
new file mode 100644
index 0000000..be2b29c
--- /dev/null
+++ b/tests/unit/java/com/android/server/connectivityservice/CSCaptivePortalAppTest.kt
@@ -0,0 +1,127 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server
+
+import android.Manifest.permission.NETWORK_STACK
+import android.content.Intent
+import android.content.pm.PackageManager.PERMISSION_DENIED
+import android.content.pm.PackageManager.PERMISSION_GRANTED
+import android.net.ConnectivityManager.ACTION_CAPTIVE_PORTAL_SIGN_IN
+import android.net.ConnectivityManager.EXTRA_CAPTIVE_PORTAL
+import android.net.IpPrefix
+import android.net.LinkAddress
+import android.net.LinkProperties
+import android.net.NetworkCapabilities
+import android.net.NetworkCapabilities.NET_CAPABILITY_CAPTIVE_PORTAL
+import android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET
+import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED
+import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_ROAMING
+import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_SUSPENDED
+import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_VCN_MANAGED
+import android.net.NetworkCapabilities.TRANSPORT_WIFI
+import android.net.NetworkStack
+import android.net.CaptivePortal
+import android.net.NetworkRequest
+import android.net.NetworkScore
+import android.net.NetworkScore.KEEP_CONNECTED_FOR_TEST
+import android.net.RouteInfo
+import android.os.Build
+import android.os.Bundle
+import androidx.test.filters.SmallTest
+import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo
+import com.android.testutils.DevSdkIgnoreRunner
+import com.android.testutils.assertThrows
+import com.android.testutils.TestableNetworkCallback
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.verify
+import kotlin.test.assertEquals
+
+// This allows keeping all the networks connected without having to file individual requests
+// for them.
+private fun keepScore() = FromS(
+ NetworkScore.Builder().setKeepConnectedReason(KEEP_CONNECTED_FOR_TEST).build()
+)
+
+private fun nc(transport: Int, vararg caps: Int) = NetworkCapabilities.Builder().apply {
+ addTransportType(transport)
+ caps.forEach {
+ addCapability(it)
+ }
+ // Useful capabilities for everybody
+ addCapability(NET_CAPABILITY_NOT_RESTRICTED)
+ addCapability(NET_CAPABILITY_NOT_SUSPENDED)
+ addCapability(NET_CAPABILITY_NOT_ROAMING)
+ addCapability(NET_CAPABILITY_NOT_VCN_MANAGED)
+}.build()
+
+private fun lp(iface: String) = LinkProperties().apply {
+ interfaceName = iface
+ addLinkAddress(LinkAddress(LOCAL_IPV4_ADDRESS, 32))
+ addRoute(RouteInfo(IpPrefix("0.0.0.0/0"), null, null))
+}
+
+@DevSdkIgnoreRunner.MonitorThreadLeak
+@RunWith(DevSdkIgnoreRunner::class)
+@SmallTest
+@IgnoreUpTo(Build.VERSION_CODES.R)
+class CSCaptivePortalAppTest : CSTest() {
+ private val WIFI_IFACE = "wifi0"
+ private val TEST_REDIRECT_URL = "http://example.com/firstPath"
+ private val TIMEOUT_MS = 2_000L
+
+ @Test
+ fun testCaptivePortalApp_Reevaluate_Nopermission() {
+ val captivePortalCallback = TestableNetworkCallback()
+ val captivePortalRequest = NetworkRequest.Builder()
+ .addCapability(NET_CAPABILITY_CAPTIVE_PORTAL).build()
+ cm.registerNetworkCallback(captivePortalRequest, captivePortalCallback)
+ val wifiAgent = createWifiAgent()
+ wifiAgent.connectWithCaptivePortal(TEST_REDIRECT_URL)
+ captivePortalCallback.expectAvailableCallbacksUnvalidated(wifiAgent)
+ val signInIntent = startCaptivePortalApp(wifiAgent)
+ // Remove the granted permissions
+ context.setPermission(NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK,
+ PERMISSION_DENIED)
+ context.setPermission(NETWORK_STACK, PERMISSION_DENIED)
+ val captivePortal: CaptivePortal? = signInIntent.getParcelableExtra(EXTRA_CAPTIVE_PORTAL)
+ assertThrows(SecurityException::class.java, { captivePortal?.reevaluateNetwork() })
+ }
+
+ private fun createWifiAgent(): CSAgentWrapper {
+ return Agent(score = keepScore(), lp = lp(WIFI_IFACE),
+ nc = nc(TRANSPORT_WIFI, NET_CAPABILITY_INTERNET))
+ }
+
+ private fun startCaptivePortalApp(networkAgent: CSAgentWrapper): Intent {
+ val network = networkAgent.network
+ cm.startCaptivePortalApp(network)
+ waitForIdle()
+ verify(networkAgent.networkMonitor).launchCaptivePortalApp()
+
+ val testBundle = Bundle()
+ val testKey = "testkey"
+ val testValue = "testvalue"
+ testBundle.putString(testKey, testValue)
+ context.setPermission(NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK, PERMISSION_GRANTED)
+ cm.startCaptivePortalApp(network, testBundle)
+ val signInIntent: Intent = context.expectStartActivityIntent(TIMEOUT_MS)
+ assertEquals(ACTION_CAPTIVE_PORTAL_SIGN_IN, signInIntent.getAction())
+ assertEquals(testValue, signInIntent.getStringExtra(testKey))
+ return signInIntent
+ }
+}
diff --git a/tests/unit/java/com/android/server/connectivityservice/base/CSAgentWrapper.kt b/tests/unit/java/com/android/server/connectivityservice/base/CSAgentWrapper.kt
index d41c742..d7343b1 100644
--- a/tests/unit/java/com/android/server/connectivityservice/base/CSAgentWrapper.kt
+++ b/tests/unit/java/com/android/server/connectivityservice/base/CSAgentWrapper.kt
@@ -19,6 +19,8 @@
import android.content.Context
import android.net.ConnectivityManager
import android.net.INetworkMonitor
+import android.net.INetworkMonitor.NETWORK_VALIDATION_PROBE_DNS
+import android.net.INetworkMonitor.NETWORK_VALIDATION_PROBE_HTTP
import android.net.INetworkMonitorCallbacks
import android.net.LinkProperties
import android.net.LocalNetworkConfig
@@ -75,10 +77,15 @@
) : TestableNetworkCallback.HasNetwork {
private val TAG = "CSAgent${nextAgentId()}"
private val VALIDATION_RESULT_INVALID = 0
+ private val NO_PROBE_RESULT = 0
private val VALIDATION_TIMESTAMP = 1234L
private val agent: NetworkAgent
private val nmCallbacks: INetworkMonitorCallbacks
val networkMonitor = mock<INetworkMonitor>()
+ private var nmValidationRedirectUrl: String? = null
+ private var nmValidationResult = NO_PROBE_RESULT
+ private var nmProbesCompleted = NO_PROBE_RESULT
+ private var nmProbesSucceeded = NO_PROBE_RESULT
override val network: Network get() = agent.network!!
@@ -120,10 +127,10 @@
}
nmCallbacks.notifyProbeStatusChanged(0 /* completed */, 0 /* succeeded */)
val p = NetworkTestResultParcelable()
- p.result = VALIDATION_RESULT_INVALID
- p.probesAttempted = 0
- p.probesSucceeded = 0
- p.redirectUrl = null
+ p.result = nmValidationResult
+ p.probesAttempted = nmProbesCompleted
+ p.probesSucceeded = nmProbesSucceeded
+ p.redirectUrl = nmValidationRedirectUrl
p.timestampMillis = VALIDATION_TIMESTAMP
nmCallbacks.notifyNetworkTestedWithExtras(p)
}
@@ -171,4 +178,26 @@
fun sendLocalNetworkConfig(lnc: LocalNetworkConfig) = agent.sendLocalNetworkConfig(lnc)
fun sendNetworkCapabilities(nc: NetworkCapabilities) = agent.sendNetworkCapabilities(nc)
+
+ fun connectWithCaptivePortal(redirectUrl: String) {
+ setCaptivePortal(redirectUrl)
+ connect()
+ }
+
+ fun setProbesStatus(probesCompleted: Int, probesSucceeded: Int) {
+ nmProbesCompleted = probesCompleted
+ nmProbesSucceeded = probesSucceeded
+ }
+
+ fun setCaptivePortal(redirectUrl: String) {
+ nmValidationResult = VALIDATION_RESULT_INVALID
+ nmValidationRedirectUrl = redirectUrl
+ // Suppose the portal is found when NetworkMonitor probes NETWORK_VALIDATION_PROBE_HTTP
+ // in the beginning. Because NETWORK_VALIDATION_PROBE_HTTP is the decisive probe for captive
+ // portal, considering the NETWORK_VALIDATION_PROBE_HTTPS hasn't probed yet and set only
+ // DNS and HTTP probes completed.
+ setProbesStatus(
+ NETWORK_VALIDATION_PROBE_DNS or NETWORK_VALIDATION_PROBE_HTTP /* probesCompleted */,
+ VALIDATION_RESULT_INVALID /* probesSucceeded */)
+ }
}
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 b15c684..595ca47 100644
--- a/tests/unit/java/com/android/server/connectivityservice/base/CSTest.kt
+++ b/tests/unit/java/com/android/server/connectivityservice/base/CSTest.kt
@@ -47,8 +47,10 @@
import android.os.Bundle
import android.os.Handler
import android.os.HandlerThread
+import android.os.Process
import android.os.UserHandle
import android.os.UserManager
+import android.permission.PermissionManager.PermissionResult
import android.telephony.TelephonyManager
import android.testing.TestableContext
import android.util.ArraySet
@@ -60,7 +62,6 @@
import com.android.networkstack.apishim.common.UnsupportedApiLevelException
import com.android.server.connectivity.AutomaticOnOffKeepaliveTracker
import com.android.server.connectivity.CarrierPrivilegeAuthenticator
-import com.android.server.connectivity.CarrierPrivilegeAuthenticator.CarrierPrivilegesLostListener
import com.android.server.connectivity.ClatCoordinator
import com.android.server.connectivity.ConnectivityFlags
import com.android.server.connectivity.MulticastRoutingCoordinatorService
@@ -72,7 +73,11 @@
import com.android.testutils.visibleOnHandlerThread
import com.android.testutils.waitForIdle
import java.util.concurrent.Executors
+import java.util.concurrent.LinkedBlockingQueue
+import java.util.concurrent.TimeUnit
import java.util.function.Consumer
+import java.util.function.BiConsumer
+import kotlin.test.assertNotNull
import kotlin.test.assertNull
import kotlin.test.fail
import org.junit.After
@@ -82,7 +87,7 @@
import org.mockito.Mockito.doReturn
import org.mockito.Mockito.mock
-internal const val HANDLER_TIMEOUT_MS = 2_000
+internal const val HANDLER_TIMEOUT_MS = 2_000L
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"
@@ -222,7 +227,7 @@
context: Context,
tm: TelephonyManager,
requestRestrictedWifiEnabled: Boolean,
- listener: CarrierPrivilegesLostListener
+ listener: BiConsumer<Int, Int>
) = if (SdkLevel.isAtLeastT()) mock<CarrierPrivilegeAuthenticator>() else null
var satelliteNetworkFallbackUidUpdate: Consumer<Set<Int>>? = null
@@ -300,13 +305,65 @@
val pacProxyManager = mock<PacProxyManager>()
val networkPolicyManager = mock<NetworkPolicyManager>()
+ // Map of permission name -> PermissionManager.Permission_{GRANTED|DENIED} constant
+ // For permissions granted across the board, the key is only the permission name.
+ // For permissions only granted to a combination of uid/pid, the key
+ // is "<permission name>,<pid>,<uid>". PID+UID permissions have priority over generic ones.
+ private val mMockedPermissions: HashMap<String, Int> = HashMap()
+ private val mStartedActivities = LinkedBlockingQueue<Intent>()
override fun getPackageManager() = this@CSTest.packageManager
override fun getContentResolver() = this@CSTest.contentResolver
- // TODO : buff up the capabilities of this permission scheme to allow checking for
- // permission rejections
- override fun checkPermission(permission: String, pid: Int, uid: Int) = PERMISSION_GRANTED
- override fun checkCallingOrSelfPermission(permission: String) = PERMISSION_GRANTED
+ // If the permission result does not set in the mMockedPermissions, it will be
+ // considered as PERMISSION_GRANTED as existing design to prevent breaking other tests.
+ override fun checkPermission(permission: String, pid: Int, uid: Int) =
+ checkMockedPermission(permission, pid, uid, PERMISSION_GRANTED)
+
+ override fun enforceCallingOrSelfPermission(permission: String, message: String?) {
+ // If the permission result does not set in the mMockedPermissions, it will be
+ // considered as PERMISSION_GRANTED as existing design to prevent breaking other tests.
+ val granted = checkMockedPermission(permission, Process.myPid(), Process.myUid(),
+ PERMISSION_GRANTED)
+ if (!granted.equals(PERMISSION_GRANTED)) {
+ throw SecurityException("[Test] permission denied: " + permission)
+ }
+ }
+
+ // If the permission result does not set in the mMockedPermissions, it will be
+ // considered as PERMISSION_GRANTED as existing design to prevent breaking other tests.
+ override fun checkCallingOrSelfPermission(permission: String) =
+ checkMockedPermission(permission, Process.myPid(), Process.myUid(), PERMISSION_GRANTED)
+
+ private fun checkMockedPermission(permission: String, pid: Int, uid: Int, default: Int):
+ Int {
+ val processSpecificKey = "$permission,$pid,$uid"
+ return mMockedPermissions[processSpecificKey]
+ ?: mMockedPermissions[permission] ?: default
+ }
+
+ /**
+ * Mock checks for the specified permission, and have them behave as per `granted` or
+ * `denied`.
+ *
+ * This will apply to all calls no matter what the checked UID and PID are.
+ *
+ * @param granted One of {@link PackageManager#PermissionResult}.
+ */
+ fun setPermission(permission: String, @PermissionResult granted: Int) {
+ mMockedPermissions.put(permission, granted)
+ }
+
+ /**
+ * Mock checks for the specified permission, and have them behave as per `granted` or
+ * `denied`.
+ *
+ * This will only apply to the passed UID and PID.
+ *
+ * @param granted One of {@link PackageManager#PermissionResult}.
+ */
+ fun setPermission(permission: String, pid: Int, uid: Int, @PermissionResult granted: Int) {
+ mMockedPermissions.put("$permission,$pid,$uid", granted)
+ }
// Necessary for MultinetworkPolicyTracker, which tries to register a receiver for
// all users. The test can't do that since it doesn't hold INTERACT_ACROSS_USERS.
@@ -364,6 +421,16 @@
) {
orderedBroadcastAsUserHistory.add(intent)
}
+
+ override fun startActivityAsUser(intent: Intent, handle: UserHandle) {
+ mStartedActivities.put(intent)
+ }
+
+ fun expectStartActivityIntent(timeoutMs: Long = HANDLER_TIMEOUT_MS): Intent {
+ val intent = mStartedActivities.poll(timeoutMs, TimeUnit.MILLISECONDS)
+ assertNotNull(intent, "Did not receive sign-in intent after " + timeoutMs + "ms")
+ return intent
+ }
}
// Utility methods for subclasses to use
diff --git a/tests/unit/java/com/android/server/net/TrafficStatsRateLimitCacheTest.kt b/tests/unit/java/com/android/server/net/TrafficStatsRateLimitCacheTest.kt
new file mode 100644
index 0000000..27e6f96
--- /dev/null
+++ b/tests/unit/java/com/android/server/net/TrafficStatsRateLimitCacheTest.kt
@@ -0,0 +1,89 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.net
+
+import android.net.NetworkStats
+import com.android.testutils.DevSdkIgnoreRunner
+import java.time.Clock
+import kotlin.test.assertEquals
+import kotlin.test.assertNull
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.mock
+import org.mockito.Mockito.`when`
+
+@RunWith(DevSdkIgnoreRunner::class)
+class TrafficStatsRateLimitCacheTest {
+ companion object {
+ private const val expiryDurationMs = 1000L
+ }
+
+ private val clock = mock(Clock::class.java)
+ private val entry = mock(NetworkStats.Entry::class.java)
+ private val cache = TrafficStatsRateLimitCache(clock, expiryDurationMs)
+
+ @Test
+ fun testGet_returnsEntryIfNotExpired() {
+ cache.put("iface", 2, entry)
+ `when`(clock.millis()).thenReturn(500L) // Set clock to before expiry
+ val result = cache.get("iface", 2)
+ assertEquals(entry, result)
+ }
+
+ @Test
+ fun testGet_returnsNullIfExpired() {
+ cache.put("iface", 2, entry)
+ `when`(clock.millis()).thenReturn(2000L) // Set clock to after expiry
+ assertNull(cache.get("iface", 2))
+ }
+
+ @Test
+ fun testGet_returnsNullForNonExistentKey() {
+ val result = cache.get("otherIface", 99)
+ assertNull(result)
+ }
+
+ @Test
+ fun testPutAndGet_retrievesCorrectEntryForDifferentKeys() {
+ val entry1 = mock(NetworkStats.Entry::class.java)
+ val entry2 = mock(NetworkStats.Entry::class.java)
+
+ cache.put("iface1", 2, entry1)
+ cache.put("iface2", 4, entry2)
+
+ assertEquals(entry1, cache.get("iface1", 2))
+ assertEquals(entry2, cache.get("iface2", 4))
+ }
+
+ @Test
+ fun testPut_overridesExistingEntry() {
+ val entry1 = mock(NetworkStats.Entry::class.java)
+ val entry2 = mock(NetworkStats.Entry::class.java)
+
+ cache.put("iface", 2, entry1)
+ cache.put("iface", 2, entry2) // Put with the same key
+
+ assertEquals(entry2, cache.get("iface", 2))
+ }
+
+ @Test
+ fun testClear() {
+ cache.put("iface", 2, entry)
+ cache.clear()
+ assertNull(cache.get("iface", 2))
+ }
+}
diff --git a/tests/unit/vpn-jarjar-rules.txt b/tests/unit/vpn-jarjar-rules.txt
index 1a6bddc..f74eab8 100644
--- a/tests/unit/vpn-jarjar-rules.txt
+++ b/tests/unit/vpn-jarjar-rules.txt
@@ -1,4 +1,2 @@
# Only keep classes imported by ConnectivityServiceTest
-keep com.android.server.connectivity.Vpn
keep com.android.server.connectivity.VpnProfileStore
-keep com.android.server.net.LockdownVpnTracker
diff --git a/thread/framework/java/android/net/thread/ThreadNetworkException.java b/thread/framework/java/android/net/thread/ThreadNetworkException.java
index 66f13ce..4def0fb 100644
--- a/thread/framework/java/android/net/thread/ThreadNetworkException.java
+++ b/thread/framework/java/android/net/thread/ThreadNetworkException.java
@@ -89,8 +89,9 @@
/**
* The operation failed because required preconditions were not satisfied. For example, trying
- * to schedule a network migration when this device is not attached will receive this error. The
- * caller should not retry the same operation before the precondition is satisfied.
+ * to schedule a network migration when this device is not attached will receive this error or
+ * enable Thread while User Resitration has disabled it. The caller should not retry the same
+ * operation before the precondition is satisfied.
*/
public static final int ERROR_FAILED_PRECONDITION = 6;
diff --git a/thread/framework/java/android/net/thread/ThreadNetworkManager.java b/thread/framework/java/android/net/thread/ThreadNetworkManager.java
index 28012a7..150b759 100644
--- a/thread/framework/java/android/net/thread/ThreadNetworkManager.java
+++ b/thread/framework/java/android/net/thread/ThreadNetworkManager.java
@@ -79,6 +79,17 @@
public static final String PERMISSION_THREAD_NETWORK_PRIVILEGED =
"android.permission.THREAD_NETWORK_PRIVILEGED";
+ /**
+ * This user restriction specifies if Thread network is disallowed on the device. If Thread
+ * network is disallowed it cannot be turned on via Settings.
+ *
+ * <p>this is a mirror of {@link UserManager#DISALLOW_THREAD_NETWORK} which is not available on
+ * Android U devices.
+ *
+ * @hide
+ */
+ public static final String DISALLOW_THREAD_NETWORK = "no_thread_network";
+
@NonNull private final Context mContext;
@NonNull private final List<ThreadNetworkController> mUnmodifiableControllerServices;
diff --git a/thread/service/java/com/android/server/thread/ActiveOperationalDatasetReceiverWrapper.java b/thread/service/java/com/android/server/thread/ActiveOperationalDatasetReceiverWrapper.java
new file mode 100644
index 0000000..e3b4e1a
--- /dev/null
+++ b/thread/service/java/com/android/server/thread/ActiveOperationalDatasetReceiverWrapper.java
@@ -0,0 +1,87 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.thread;
+
+import static android.net.thread.ThreadNetworkException.ERROR_UNAVAILABLE;
+
+import android.net.thread.ActiveOperationalDataset;
+import android.net.thread.IActiveOperationalDatasetReceiver;
+import android.os.RemoteException;
+
+import com.android.internal.annotations.GuardedBy;
+
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * A {@link IActiveOperationalDatasetReceiver} wrapper which makes it easier to invoke the
+ * callbacks.
+ */
+final class ActiveOperationalDatasetReceiverWrapper {
+ private final IActiveOperationalDatasetReceiver mReceiver;
+
+ private static final Object sPendingReceiversLock = new Object();
+
+ @GuardedBy("sPendingReceiversLock")
+ private static final Set<ActiveOperationalDatasetReceiverWrapper> sPendingReceivers =
+ new HashSet<>();
+
+ public ActiveOperationalDatasetReceiverWrapper(IActiveOperationalDatasetReceiver receiver) {
+ this.mReceiver = receiver;
+
+ synchronized (sPendingReceiversLock) {
+ sPendingReceivers.add(this);
+ }
+ }
+
+ public static void onOtDaemonDied() {
+ synchronized (sPendingReceiversLock) {
+ for (ActiveOperationalDatasetReceiverWrapper receiver : sPendingReceivers) {
+ try {
+ receiver.mReceiver.onError(ERROR_UNAVAILABLE, "Thread daemon died");
+ } catch (RemoteException e) {
+ // The client is dead, do nothing
+ }
+ }
+ sPendingReceivers.clear();
+ }
+ }
+
+ public void onSuccess(ActiveOperationalDataset dataset) {
+ synchronized (sPendingReceiversLock) {
+ sPendingReceivers.remove(this);
+ }
+
+ try {
+ mReceiver.onSuccess(dataset);
+ } catch (RemoteException e) {
+ // The client is dead, do nothing
+ }
+ }
+
+ public void onError(int errorCode, String errorMessage) {
+ synchronized (sPendingReceiversLock) {
+ sPendingReceivers.remove(this);
+ }
+
+ try {
+ mReceiver.onError(errorCode, errorMessage);
+ } catch (RemoteException e) {
+ // The client is dead, do nothing
+ }
+ }
+}
diff --git a/thread/service/java/com/android/server/thread/ThreadNetworkControllerService.java b/thread/service/java/com/android/server/thread/ThreadNetworkControllerService.java
index 21e3927..0623b87 100644
--- a/thread/service/java/com/android/server/thread/ThreadNetworkControllerService.java
+++ b/thread/service/java/com/android/server/thread/ThreadNetworkControllerService.java
@@ -16,7 +16,6 @@
import static android.Manifest.permission.NETWORK_SETTINGS;
import static android.net.MulticastRoutingConfig.CONFIG_FORWARD_NONE;
-import static android.net.MulticastRoutingConfig.FORWARD_NONE;
import static android.net.MulticastRoutingConfig.FORWARD_SELECTED;
import static android.net.MulticastRoutingConfig.FORWARD_WITH_MIN_SCOPE;
import static android.net.thread.ActiveOperationalDataset.CHANNEL_PAGE_24_GHZ;
@@ -41,11 +40,12 @@
import static android.net.thread.ThreadNetworkException.ERROR_THREAD_DISABLED;
import static android.net.thread.ThreadNetworkException.ERROR_TIMEOUT;
import static android.net.thread.ThreadNetworkException.ERROR_UNSUPPORTED_CHANNEL;
+import static android.net.thread.ThreadNetworkManager.DISALLOW_THREAD_NETWORK;
import static android.net.thread.ThreadNetworkManager.PERMISSION_THREAD_NETWORK_PRIVILEGED;
import static com.android.server.thread.openthread.IOtDaemon.ErrorCode.OT_ERROR_ABORT;
import static com.android.server.thread.openthread.IOtDaemon.ErrorCode.OT_ERROR_BUSY;
-import static com.android.server.thread.openthread.IOtDaemon.ErrorCode.OT_ERROR_DETACHED;
+import static com.android.server.thread.openthread.IOtDaemon.ErrorCode.OT_ERROR_FAILED_PRECONDITION;
import static com.android.server.thread.openthread.IOtDaemon.ErrorCode.OT_ERROR_INVALID_STATE;
import static com.android.server.thread.openthread.IOtDaemon.ErrorCode.OT_ERROR_NO_BUFS;
import static com.android.server.thread.openthread.IOtDaemon.ErrorCode.OT_ERROR_PARSE;
@@ -64,8 +64,12 @@
import android.annotation.Nullable;
import android.annotation.RequiresPermission;
import android.annotation.TargetApi;
+import android.content.BroadcastReceiver;
import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
import android.net.ConnectivityManager;
+import android.net.InetAddresses;
import android.net.LinkAddress;
import android.net.LinkProperties;
import android.net.LocalNetworkConfig;
@@ -98,12 +102,15 @@
import android.os.Looper;
import android.os.RemoteException;
import android.os.SystemClock;
+import android.os.UserManager;
import android.util.Log;
import android.util.SparseArray;
import com.android.internal.annotations.VisibleForTesting;
import com.android.server.ServiceManagerWrapper;
+import com.android.server.thread.openthread.BackboneRouterState;
import com.android.server.thread.openthread.BorderRouterConfigurationParcel;
+import com.android.server.thread.openthread.IChannelMasksReceiver;
import com.android.server.thread.openthread.IOtDaemon;
import com.android.server.thread.openthread.IOtDaemonCallback;
import com.android.server.thread.openthread.IOtStatusReceiver;
@@ -117,6 +124,7 @@
import java.security.SecureRandom;
import java.time.Instant;
import java.util.HashMap;
+import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Random;
@@ -152,9 +160,6 @@
private final NsdPublisher mNsdPublisher;
private final OtDaemonCallbackProxy mOtDaemonCallbackProxy = new OtDaemonCallbackProxy();
- // TODO(b/308310823): read supported channel from Thread dameon
- private final int mSupportedChannelMask = 0x07FFF800; // from channel 11 to 26
-
@Nullable private IOtDaemon mOtDaemon;
@Nullable private NetworkAgent mNetworkAgent;
@Nullable private NetworkAgent mTestNetworkAgent;
@@ -167,6 +172,8 @@
private TestNetworkSpecifier mUpstreamTestNetworkSpecifier;
private final HashMap<Network, String> mNetworkToInterface;
private final ThreadPersistentSettings mPersistentSettings;
+ private final UserManager mUserManager;
+ private boolean mUserRestricted;
private BorderRouterConfigurationParcel mBorderRouterConfig;
@@ -180,7 +187,8 @@
TunInterfaceController tunIfController,
InfraInterfaceController infraIfController,
ThreadPersistentSettings persistentSettings,
- NsdPublisher nsdPublisher) {
+ NsdPublisher nsdPublisher,
+ UserManager userManager) {
mContext = context;
mHandler = handler;
mNetworkProvider = networkProvider;
@@ -193,6 +201,7 @@
mBorderRouterConfig = new BorderRouterConfigurationParcel();
mPersistentSettings = persistentSettings;
mNsdPublisher = nsdPublisher;
+ mUserManager = userManager;
}
public static ThreadNetworkControllerService newInstance(
@@ -212,7 +221,8 @@
new TunInterfaceController(TUN_IF_NAME),
new InfraInterfaceController(),
persistentSettings,
- NsdPublisher.newInstance(context, handler));
+ NsdPublisher.newInstance(context, handler),
+ context.getSystemService(UserManager.class));
}
private static Inet6Address bytesToInet6Address(byte[] addressBytes) {
@@ -288,10 +298,7 @@
if (otDaemon == null) {
throw new RemoteException("Internal error: failed to start OT daemon");
}
- otDaemon.initialize(
- mTunIfController.getTunFd(),
- mPersistentSettings.get(ThreadPersistentSettings.THREAD_ENABLED),
- mNsdPublisher);
+ otDaemon.initialize(mTunIfController.getTunFd(), isEnabled(), mNsdPublisher);
otDaemon.registerStateCallback(mOtDaemonCallbackProxy, -1);
otDaemon.asBinder().linkToDeath(() -> mHandler.post(this::onOtDaemonDied), 0);
mOtDaemon = otDaemon;
@@ -323,23 +330,39 @@
mConnectivityManager.registerNetworkProvider(mNetworkProvider);
requestUpstreamNetwork();
requestThreadNetwork();
-
+ mUserRestricted = isThreadUserRestricted();
+ registerUserRestrictionsReceiver();
initializeOtDaemon();
});
}
- public void setEnabled(@NonNull boolean isEnabled, @NonNull IOperationReceiver receiver) {
+ public void setEnabled(boolean isEnabled, @NonNull IOperationReceiver receiver) {
enforceAllPermissionsGranted(PERMISSION_THREAD_NETWORK_PRIVILEGED);
- mHandler.post(() -> setEnabledInternal(isEnabled, new OperationReceiverWrapper(receiver)));
+ mHandler.post(
+ () ->
+ setEnabledInternal(
+ isEnabled,
+ true /* persist */,
+ new OperationReceiverWrapper(receiver)));
}
private void setEnabledInternal(
- @NonNull boolean isEnabled, @Nullable OperationReceiverWrapper receiver) {
- // The persistent setting keeps the desired enabled state, thus it's set regardless
- // the otDaemon set enabled state operation succeeded or not, so that it can recover
- // to the desired value after reboot.
- mPersistentSettings.put(ThreadPersistentSettings.THREAD_ENABLED.key, isEnabled);
+ boolean isEnabled, boolean persist, @NonNull OperationReceiverWrapper receiver) {
+ if (isEnabled && isThreadUserRestricted()) {
+ receiver.onError(
+ ERROR_FAILED_PRECONDITION,
+ "Cannot enable Thread: forbidden by user restriction");
+ return;
+ }
+
+ if (persist) {
+ // The persistent setting keeps the desired enabled state, thus it's set regardless
+ // the otDaemon set enabled state operation succeeded or not, so that it can recover
+ // to the desired value after reboot.
+ mPersistentSettings.put(ThreadPersistentSettings.THREAD_ENABLED.key, isEnabled);
+ }
+
try {
getOtDaemon().setThreadEnabled(isEnabled, newOtStatusReceiver(receiver));
} catch (RemoteException e) {
@@ -348,6 +371,67 @@
}
}
+ private void registerUserRestrictionsReceiver() {
+ mContext.registerReceiver(
+ new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ onUserRestrictionsChanged(isThreadUserRestricted());
+ }
+ },
+ new IntentFilter(UserManager.ACTION_USER_RESTRICTIONS_CHANGED),
+ null /* broadcastPermission */,
+ mHandler);
+ }
+
+ private void onUserRestrictionsChanged(boolean newUserRestrictedState) {
+ checkOnHandlerThread();
+ if (mUserRestricted == newUserRestrictedState) {
+ return;
+ }
+ Log.i(
+ TAG,
+ "Thread user restriction changed: "
+ + mUserRestricted
+ + " -> "
+ + newUserRestrictedState);
+ mUserRestricted = newUserRestrictedState;
+
+ final boolean isEnabled = isEnabled();
+ final IOperationReceiver receiver =
+ new IOperationReceiver.Stub() {
+ @Override
+ public void onSuccess() {
+ Log.d(
+ TAG,
+ (isEnabled ? "Enabled" : "Disabled")
+ + " Thread due to user restriction change");
+ }
+
+ @Override
+ public void onError(int otError, String messages) {
+ Log.e(
+ TAG,
+ "Failed to "
+ + (isEnabled ? "enable" : "disable")
+ + " Thread for user restriction change");
+ }
+ };
+ // Do not save the user restriction state to persistent settings so that the user
+ // configuration won't be overwritten
+ setEnabledInternal(isEnabled, false /* persist */, new OperationReceiverWrapper(receiver));
+ }
+
+ /** Returns {@code true} if Thread is set enabled. */
+ private boolean isEnabled() {
+ return !mUserRestricted && mPersistentSettings.get(ThreadPersistentSettings.THREAD_ENABLED);
+ }
+
+ /** Returns {@code true} if Thread has been restricted for the user. */
+ private boolean isThreadUserRestricted() {
+ return mUserManager.hasUserRestriction(DISALLOW_THREAD_NETWORK);
+ }
+
private void requestUpstreamNetwork() {
if (mUpstreamNetworkCallback != null) {
throw new AssertionError("The upstream network request is already there.");
@@ -509,26 +593,51 @@
@Override
public void createRandomizedDataset(
String networkName, IActiveOperationalDatasetReceiver receiver) {
- mHandler.post(
- () -> {
- ActiveOperationalDataset dataset =
- createRandomizedDatasetInternal(
- networkName,
- mSupportedChannelMask,
- Instant.now(),
- new Random(),
- new SecureRandom());
- try {
- receiver.onSuccess(dataset);
- } catch (RemoteException e) {
- // The client is dead, do nothing
- }
- });
+ ActiveOperationalDatasetReceiverWrapper receiverWrapper =
+ new ActiveOperationalDatasetReceiverWrapper(receiver);
+ mHandler.post(() -> createRandomizedDatasetInternal(networkName, receiverWrapper));
}
- private static ActiveOperationalDataset createRandomizedDatasetInternal(
+ private void createRandomizedDatasetInternal(
+ String networkName, @NonNull ActiveOperationalDatasetReceiverWrapper receiver) {
+ checkOnHandlerThread();
+
+ try {
+ getOtDaemon().getChannelMasks(newChannelMasksReceiver(networkName, receiver));
+ } catch (RemoteException e) {
+ Log.e(TAG, "otDaemon.getChannelMasks failed", e);
+ receiver.onError(ERROR_INTERNAL_ERROR, "Thread stack error");
+ }
+ }
+
+ private IChannelMasksReceiver newChannelMasksReceiver(
+ String networkName, ActiveOperationalDatasetReceiverWrapper receiver) {
+ return new IChannelMasksReceiver.Stub() {
+ @Override
+ public void onSuccess(int supportedChannelMask, int preferredChannelMask) {
+ ActiveOperationalDataset dataset =
+ createRandomizedDataset(
+ networkName,
+ supportedChannelMask,
+ preferredChannelMask,
+ Instant.now(),
+ new Random(),
+ new SecureRandom());
+
+ receiver.onSuccess(dataset);
+ }
+
+ @Override
+ public void onError(int errorCode, String errorMessage) {
+ receiver.onError(otErrorToAndroidError(errorCode), errorMessage);
+ }
+ };
+ }
+
+ private static ActiveOperationalDataset createRandomizedDataset(
String networkName,
int supportedChannelMask,
+ int preferredChannelMask,
Instant now,
Random random,
SecureRandom secureRandom) {
@@ -538,6 +647,7 @@
final SparseArray<byte[]> channelMask = new SparseArray<>(1);
channelMask.put(CHANNEL_PAGE_24_GHZ, channelMaskToByteArray(supportedChannelMask));
+ final int channel = selectChannel(supportedChannelMask, preferredChannelMask, random);
final byte[] securityFlags = new byte[] {(byte) 0xff, (byte) 0xf8};
@@ -548,7 +658,7 @@
.setExtendedPanId(newRandomBytes(random, LENGTH_EXTENDED_PAN_ID))
.setPanId(panId)
.setNetworkName(networkName)
- .setChannel(CHANNEL_PAGE_24_GHZ, selectRandomChannel(supportedChannelMask, random))
+ .setChannel(CHANNEL_PAGE_24_GHZ, channel)
.setChannelMask(channelMask)
.setPskc(newRandomBytes(secureRandom, LENGTH_PSKC))
.setNetworkKey(newRandomBytes(secureRandom, LENGTH_NETWORK_KEY))
@@ -557,6 +667,18 @@
.build();
}
+ private static int selectChannel(
+ int supportedChannelMask, int preferredChannelMask, Random random) {
+ // If the preferred channel mask is not empty, select a random channel from it, otherwise
+ // choose one from the supported channel mask.
+ preferredChannelMask = preferredChannelMask & supportedChannelMask;
+ if (preferredChannelMask == 0) {
+ preferredChannelMask = supportedChannelMask;
+ }
+
+ return selectRandomChannel(preferredChannelMask, random);
+ }
+
private static byte[] newRandomBytes(Random random, int length) {
byte[] result = new byte[length];
random.nextBytes(result);
@@ -656,9 +778,6 @@
return ERROR_ABORTED;
case OT_ERROR_BUSY:
return ERROR_BUSY;
- case OT_ERROR_DETACHED:
- case OT_ERROR_INVALID_STATE:
- return ERROR_FAILED_PRECONDITION;
case OT_ERROR_NO_BUFS:
return ERROR_RESOURCE_EXHAUSTED;
case OT_ERROR_PARSE:
@@ -672,6 +791,9 @@
return ERROR_UNSUPPORTED_CHANNEL;
case OT_ERROR_THREAD_DISABLED:
return ERROR_THREAD_DISABLED;
+ case OT_ERROR_FAILED_PRECONDITION:
+ return ERROR_FAILED_PRECONDITION;
+ case OT_ERROR_INVALID_STATE:
default:
return ERROR_INTERNAL_ERROR;
}
@@ -881,11 +1003,6 @@
}
}
- private boolean isMulticastForwardingEnabled() {
- return !(mUpstreamMulticastRoutingConfig.getForwardingMode() == FORWARD_NONE
- && mDownstreamMulticastRoutingConfig.getForwardingMode() == FORWARD_NONE);
- }
-
private void sendLocalNetworkConfig() {
if (mNetworkAgent == null) {
return;
@@ -895,72 +1012,44 @@
Log.d(TAG, "Sent localNetworkConfig: " + localNetworkConfig);
}
- private void handleMulticastForwardingStateChanged(boolean isEnabled) {
- if (isMulticastForwardingEnabled() == isEnabled) {
- return;
- }
+ private void handleMulticastForwardingChanged(BackboneRouterState state) {
+ MulticastRoutingConfig upstreamMulticastRoutingConfig;
+ MulticastRoutingConfig downstreamMulticastRoutingConfig;
- Log.i(TAG, "Multicast forwaring is " + (isEnabled ? "enabled" : "disabled"));
-
- if (isEnabled) {
+ if (state.multicastForwardingEnabled) {
// When multicast forwarding is enabled, setup upstream forwarding to any address
// with minimal scope 4
// setup downstream forwarding with addresses subscribed from Thread network
- mUpstreamMulticastRoutingConfig =
+ upstreamMulticastRoutingConfig =
new MulticastRoutingConfig.Builder(FORWARD_WITH_MIN_SCOPE, 4).build();
- mDownstreamMulticastRoutingConfig =
- new MulticastRoutingConfig.Builder(FORWARD_SELECTED).build();
+ downstreamMulticastRoutingConfig =
+ buildDownstreamMulticastRoutingConfigSelected(state.listeningAddresses);
} else {
// When multicast forwarding is disabled, set both upstream and downstream
// forwarding config to FORWARD_NONE.
- mUpstreamMulticastRoutingConfig = CONFIG_FORWARD_NONE;
- mDownstreamMulticastRoutingConfig = CONFIG_FORWARD_NONE;
+ upstreamMulticastRoutingConfig = CONFIG_FORWARD_NONE;
+ downstreamMulticastRoutingConfig = CONFIG_FORWARD_NONE;
}
+
+ if (upstreamMulticastRoutingConfig.equals(mUpstreamMulticastRoutingConfig)
+ && downstreamMulticastRoutingConfig.equals(mDownstreamMulticastRoutingConfig)) {
+ return;
+ }
+
+ mUpstreamMulticastRoutingConfig = upstreamMulticastRoutingConfig;
+ mDownstreamMulticastRoutingConfig = downstreamMulticastRoutingConfig;
sendLocalNetworkConfig();
}
- private void handleMulticastForwardingAddressChanged(byte[] addressBytes, boolean isAdded) {
- Inet6Address address = bytesToInet6Address(addressBytes);
- MulticastRoutingConfig newDownstreamConfig;
- MulticastRoutingConfig.Builder builder;
-
- if (mDownstreamMulticastRoutingConfig.getForwardingMode()
- != MulticastRoutingConfig.FORWARD_SELECTED) {
- Log.e(
- TAG,
- "Ignore multicast listening address updates when downstream multicast "
- + "forwarding mode is not FORWARD_SELECTED");
- // Don't update the address set if downstream multicast forwarding is disabled.
- return;
- }
- if (isAdded
- == mDownstreamMulticastRoutingConfig.getListeningAddresses().contains(address)) {
- return;
- }
-
- builder = new MulticastRoutingConfig.Builder(FORWARD_SELECTED);
- for (Inet6Address listeningAddress :
- mDownstreamMulticastRoutingConfig.getListeningAddresses()) {
- builder.addListeningAddress(listeningAddress);
- }
-
- if (isAdded) {
+ private MulticastRoutingConfig buildDownstreamMulticastRoutingConfigSelected(
+ List<String> listeningAddresses) {
+ MulticastRoutingConfig.Builder builder =
+ new MulticastRoutingConfig.Builder(FORWARD_SELECTED);
+ for (String addressStr : listeningAddresses) {
+ Inet6Address address = (Inet6Address) InetAddresses.parseNumericAddress(addressStr);
builder.addListeningAddress(address);
- } else {
- builder.clearListeningAddress(address);
}
-
- newDownstreamConfig = builder.build();
- if (!newDownstreamConfig.equals(mDownstreamMulticastRoutingConfig)) {
- Log.d(
- TAG,
- "Multicast listening address "
- + address.getHostAddress()
- + " is "
- + (isAdded ? "added" : "removed"));
- mDownstreamMulticastRoutingConfig = newDownstreamConfig;
- sendLocalNetworkConfig();
- }
+ return builder.build();
}
private static final class CallbackMetadata {
@@ -1128,7 +1217,6 @@
onInterfaceStateChanged(newState.isInterfaceUp);
onDeviceRoleChanged(newState.deviceRole, listenerId);
onPartitionIdChanged(newState.partitionId, listenerId);
- onMulticastForwardingStateChanged(newState.multicastForwardingEnabled);
mState = newState;
ActiveOperationalDataset newActiveDataset;
@@ -1237,19 +1325,14 @@
}
}
- private void onMulticastForwardingStateChanged(boolean isEnabled) {
- checkOnHandlerThread();
- handleMulticastForwardingStateChanged(isEnabled);
- }
-
@Override
public void onAddressChanged(Ipv6AddressInfo addressInfo, boolean isAdded) {
mHandler.post(() -> handleAddressChanged(addressInfo, isAdded));
}
@Override
- public void onMulticastForwardingAddressChanged(byte[] address, boolean isAdded) {
- mHandler.post(() -> handleMulticastForwardingAddressChanged(address, isAdded));
+ public void onBackboneRouterStateChanged(BackboneRouterState state) {
+ mHandler.post(() -> handleMulticastForwardingChanged(state));
}
}
}
diff --git a/thread/service/java/com/android/server/thread/ThreadNetworkService.java b/thread/service/java/com/android/server/thread/ThreadNetworkService.java
index 5cf27f7..5664922 100644
--- a/thread/service/java/com/android/server/thread/ThreadNetworkService.java
+++ b/thread/service/java/com/android/server/thread/ThreadNetworkService.java
@@ -18,21 +18,16 @@
import static android.content.pm.PackageManager.PERMISSION_GRANTED;
-import static com.android.net.module.util.DeviceConfigUtils.TETHERING_MODULE_NAME;
-
import android.annotation.NonNull;
import android.annotation.Nullable;
-import android.content.ApexEnvironment;
import android.content.Context;
import android.net.thread.IThreadNetworkController;
import android.net.thread.IThreadNetworkManager;
import android.os.Binder;
import android.os.ParcelFileDescriptor;
-import android.util.AtomicFile;
import com.android.server.SystemService;
-import java.io.File;
import java.io.FileDescriptor;
import java.io.PrintWriter;
import java.util.Collections;
@@ -51,12 +46,7 @@
/** Creates a new {@link ThreadNetworkService} object. */
public ThreadNetworkService(Context context) {
mContext = context;
- mPersistentSettings =
- new ThreadPersistentSettings(
- new AtomicFile(
- new File(
- getOrCreateThreadnetworkDir(),
- ThreadPersistentSettings.FILE_NAME)));
+ mPersistentSettings = ThreadPersistentSettings.newInstance(context);
}
/**
@@ -123,19 +113,4 @@
pw.println();
}
-
- /** Get device protected storage dir for the tethering apex. */
- private static File getOrCreateThreadnetworkDir() {
- final File threadnetworkDir;
- final File apexDataDir =
- ApexEnvironment.getApexEnvironment(TETHERING_MODULE_NAME)
- .getDeviceProtectedDataDir();
- threadnetworkDir = new File(apexDataDir, "thread");
-
- if (threadnetworkDir.exists() || threadnetworkDir.mkdirs()) {
- return threadnetworkDir;
- }
- throw new IllegalStateException(
- "Cannot write into thread network data directory: " + threadnetworkDir);
- }
}
diff --git a/thread/service/java/com/android/server/thread/ThreadPersistentSettings.java b/thread/service/java/com/android/server/thread/ThreadPersistentSettings.java
index d32f0bf..aba4193 100644
--- a/thread/service/java/com/android/server/thread/ThreadPersistentSettings.java
+++ b/thread/service/java/com/android/server/thread/ThreadPersistentSettings.java
@@ -16,15 +16,23 @@
package com.android.server.thread;
+import static com.android.net.module.util.DeviceConfigUtils.TETHERING_MODULE_NAME;
+
import android.annotation.Nullable;
+import android.content.ApexEnvironment;
+import android.content.Context;
import android.os.PersistableBundle;
import android.util.AtomicFile;
import android.util.Log;
+import com.android.connectivity.resources.R;
import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.connectivity.ConnectivityResources;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
+import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
@@ -39,7 +47,7 @@
public class ThreadPersistentSettings {
private static final String TAG = "ThreadPersistentSettings";
/** File name used for storing settings. */
- public static final String FILE_NAME = "ThreadPersistentSettings.xml";
+ private static final String FILE_NAME = "ThreadPersistentSettings.xml";
/** Current config store data version. This will be incremented for any additions. */
private static final int CURRENT_SETTINGS_STORE_DATA_VERSION = 1;
/**
@@ -62,16 +70,29 @@
@GuardedBy("mLock")
private final PersistableBundle mSettings = new PersistableBundle();
- public ThreadPersistentSettings(AtomicFile atomicFile) {
+ private final ConnectivityResources mResources;
+
+ public static ThreadPersistentSettings newInstance(Context context) {
+ return new ThreadPersistentSettings(
+ new AtomicFile(new File(getOrCreateThreadNetworkDir(), FILE_NAME)),
+ new ConnectivityResources(context));
+ }
+
+ @VisibleForTesting
+ ThreadPersistentSettings(AtomicFile atomicFile, ConnectivityResources resources) {
mAtomicFile = atomicFile;
+ mResources = resources;
}
/** Initialize the settings by reading from the settings file. */
public void initialize() {
readFromStoreFile();
synchronized (mLock) {
- if (mSettings.isEmpty()) {
- put(THREAD_ENABLED.key, THREAD_ENABLED.defaultValue);
+ if (!mSettings.containsKey(THREAD_ENABLED.key)) {
+ Log.i(TAG, "\"thread_enabled\" is missing in settings file, using default value");
+ put(
+ THREAD_ENABLED.key,
+ mResources.get().getBoolean(R.bool.config_thread_default_enabled));
}
}
}
@@ -240,4 +261,19 @@
throw e;
}
}
+
+ /** Get device protected storage dir for the tethering apex. */
+ private static File getOrCreateThreadNetworkDir() {
+ final File threadnetworkDir;
+ final File apexDataDir =
+ ApexEnvironment.getApexEnvironment(TETHERING_MODULE_NAME)
+ .getDeviceProtectedDataDir();
+ threadnetworkDir = new File(apexDataDir, "thread");
+
+ if (threadnetworkDir.exists() || threadnetworkDir.mkdirs()) {
+ return threadnetworkDir;
+ }
+ throw new IllegalStateException(
+ "Cannot write into thread network data directory: " + threadnetworkDir);
+ }
}
diff --git a/thread/tests/cts/Android.bp b/thread/tests/cts/Android.bp
index 522120c..676eb0e 100644
--- a/thread/tests/cts/Android.bp
+++ b/thread/tests/cts/Android.bp
@@ -32,6 +32,7 @@
test_suites: [
"cts",
"general-tests",
+ "mcts-tethering",
"mts-tethering",
],
static_libs: [
diff --git a/thread/tests/integration/src/android/net/thread/BorderRoutingTest.java b/thread/tests/integration/src/android/net/thread/BorderRoutingTest.java
index 29ada1b..e8ef346 100644
--- a/thread/tests/integration/src/android/net/thread/BorderRoutingTest.java
+++ b/thread/tests/integration/src/android/net/thread/BorderRoutingTest.java
@@ -18,28 +18,39 @@
import static android.Manifest.permission.MANAGE_TEST_NETWORKS;
import static android.Manifest.permission.NETWORK_SETTINGS;
-import static android.net.thread.ThreadNetworkController.DEVICE_ROLE_LEADER;
import static android.net.thread.ThreadNetworkManager.PERMISSION_THREAD_NETWORK_PRIVILEGED;
import static android.net.thread.utils.IntegrationTestUtils.JOIN_TIMEOUT;
+import static android.net.thread.utils.IntegrationTestUtils.RESTART_JOIN_TIMEOUT;
import static android.net.thread.utils.IntegrationTestUtils.isExpectedIcmpv6Packet;
+import static android.net.thread.utils.IntegrationTestUtils.isFromIpv6Source;
+import static android.net.thread.utils.IntegrationTestUtils.isInMulticastGroup;
import static android.net.thread.utils.IntegrationTestUtils.isSimulatedThreadRadioSupported;
+import static android.net.thread.utils.IntegrationTestUtils.isToIpv6Destination;
import static android.net.thread.utils.IntegrationTestUtils.newPacketReader;
-import static android.net.thread.utils.IntegrationTestUtils.readPacketFrom;
+import static android.net.thread.utils.IntegrationTestUtils.pollForPacket;
+import static android.net.thread.utils.IntegrationTestUtils.sendUdpMessage;
import static android.net.thread.utils.IntegrationTestUtils.waitFor;
-import static android.net.thread.utils.IntegrationTestUtils.waitForStateAnyOf;
import static com.android.net.module.util.NetworkStackConstants.ICMPV6_ECHO_REPLY_TYPE;
+import static com.android.net.module.util.NetworkStackConstants.ICMPV6_ECHO_REQUEST_TYPE;
+import static com.android.testutils.DeviceInfoUtils.isKernelVersionAtLeast;
import static com.android.testutils.TestNetworkTrackerKt.initTestNetwork;
import static com.android.testutils.TestPermissionUtil.runAsShell;
import static com.google.common.io.BaseEncoding.base16;
import static com.google.common.util.concurrent.MoreExecutors.directExecutor;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
import static org.junit.Assume.assumeNotNull;
import static org.junit.Assume.assumeTrue;
+import static java.util.concurrent.TimeUnit.MILLISECONDS;
+
import android.content.Context;
+import android.net.InetAddresses;
import android.net.LinkProperties;
import android.net.MacAddress;
import android.net.thread.utils.FullThreadDevice;
@@ -61,9 +72,12 @@
import java.net.Inet6Address;
import java.time.Duration;
+import java.util.ArrayList;
import java.util.List;
+import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
+import java.util.function.Predicate;
/** Integration test cases for Thread Border Routing feature. */
@RunWith(AndroidJUnit4.class)
@@ -75,6 +89,18 @@
private HandlerThread mHandlerThread;
private Handler mHandler;
private TestNetworkTracker mInfraNetworkTracker;
+ private List<FullThreadDevice> mFtds;
+ private TapPacketReader mInfraNetworkReader;
+ private InfraNetworkDevice mInfraDevice;
+
+ private static final int NUM_FTD = 2;
+ private static final String KERNEL_VERSION_MULTICAST_ROUTING_SUPPORTED = "5.15.0";
+ private static final Inet6Address GROUP_ADDR_SCOPE_5 =
+ (Inet6Address) InetAddresses.parseNumericAddress("ff05::1234");
+ private static final Inet6Address GROUP_ADDR_SCOPE_4 =
+ (Inet6Address) InetAddresses.parseNumericAddress("ff04::1234");
+ private static final Inet6Address GROUP_ADDR_SCOPE_3 =
+ (Inet6Address) InetAddresses.parseNumericAddress("ff03::1234");
// A valid Thread Active Operational Dataset generated from OpenThread CLI "dataset init new".
private static final byte[] DEFAULT_DATASET_TLVS =
@@ -89,6 +115,7 @@
@Before
public void setUp() throws Exception {
+ assumeTrue(isSimulatedThreadRadioSupported());
final ThreadNetworkManager manager = mContext.getSystemService(ThreadNetworkManager.class);
if (manager != null) {
mController = manager.getAllThreadNetworkControllers().get(0);
@@ -100,24 +127,21 @@
mHandlerThread = new HandlerThread(getClass().getSimpleName());
mHandlerThread.start();
mHandler = new Handler(mHandlerThread.getLooper());
+ mFtds = new ArrayList<>();
- mInfraNetworkTracker =
- runAsShell(
- MANAGE_TEST_NETWORKS,
- () ->
- initTestNetwork(
- mContext, new LinkProperties(), 5000 /* timeoutMs */));
- runAsShell(
- PERMISSION_THREAD_NETWORK_PRIVILEGED,
- NETWORK_SETTINGS,
- () -> {
- CountDownLatch latch = new CountDownLatch(1);
- mController.setTestNetworkAsUpstream(
- mInfraNetworkTracker.getTestIface().getInterfaceName(),
- directExecutor(),
- v -> latch.countDown());
- latch.await();
- });
+ setUpInfraNetwork();
+
+ // BR forms a network.
+ startBrLeader();
+
+ // Creates a infra network device.
+ mInfraNetworkReader = newPacketReader(mInfraNetworkTracker.getTestIface(), mHandler);
+ startInfraDevice();
+
+ // Create Ftds
+ for (int i = 0; i < NUM_FTD; ++i) {
+ mFtds.add(new FullThreadDevice(15 + i /* node ID */));
+ }
}
@After
@@ -136,16 +160,19 @@
mController.leave(directExecutor(), v -> latch.countDown());
latch.await(10, TimeUnit.SECONDS);
});
- runAsShell(MANAGE_TEST_NETWORKS, () -> mInfraNetworkTracker.teardown());
+ tearDownInfraNetwork();
mHandlerThread.quitSafely();
mHandlerThread.join();
+
+ for (var ftd : mFtds) {
+ ftd.destroy();
+ }
+ mFtds.clear();
}
@Test
public void unicastRouting_infraDevicePingTheadDeviceOmr_replyReceived() throws Exception {
- assumeTrue(isSimulatedThreadRadioSupported());
-
/*
* <pre>
* Topology:
@@ -155,36 +182,485 @@
* </pre>
*/
+ // Let ftd join the network.
+ FullThreadDevice ftd = mFtds.get(0);
+ startFtdChild(ftd);
+
+ // Infra device sends an echo request to FTD's OMR.
+ mInfraDevice.sendEchoRequest(ftd.getOmrAddress());
+
+ // Infra device receives an echo reply sent by FTD.
+ assertNotNull(pollForPacketOnInfraNetwork(ICMPV6_ECHO_REPLY_TYPE, null /* srcAddress */));
+ }
+
+ @Test
+ public void unicastRouting_borderRouterSendsUdpToThreadDevice_datagramReceived()
+ throws Exception {
+ assumeTrue(isSimulatedThreadRadioSupported());
+
+ /*
+ * <pre>
+ * Topology:
+ * Thread
+ * Border Router -------------- Full Thread device
+ * (Cuttlefish)
+ * </pre>
+ */
+
// BR forms a network.
+ CompletableFuture<Void> joinFuture = new CompletableFuture<>();
runAsShell(
PERMISSION_THREAD_NETWORK_PRIVILEGED,
- () -> mController.join(DEFAULT_DATASET, directExecutor(), result -> {}));
- waitForStateAnyOf(mController, List.of(DEVICE_ROLE_LEADER), JOIN_TIMEOUT);
+ () -> mController.join(DEFAULT_DATASET, directExecutor(), joinFuture::complete));
+ joinFuture.get(RESTART_JOIN_TIMEOUT.toMillis(), MILLISECONDS);
// Creates a Full Thread Device (FTD) and lets it join the network.
- FullThreadDevice ftd = new FullThreadDevice(5 /* node ID */);
+ FullThreadDevice ftd = mFtds.get(0);
+ startFtdChild(ftd);
+ Inet6Address ftdOmr = ftd.getOmrAddress();
+ Inet6Address ftdMlEid = ftd.getMlEid();
+ assertNotNull(ftdMlEid);
+
+ ftd.udpBind(ftdOmr, 12345);
+ sendUdpMessage(ftdOmr, 12345, "aaaaaaaa");
+ assertEquals("aaaaaaaa", ftd.udpReceive());
+
+ ftd.udpBind(ftdMlEid, 12345);
+ sendUdpMessage(ftdMlEid, 12345, "bbbbbbbb");
+ assertEquals("bbbbbbbb", ftd.udpReceive());
+ }
+
+ @Test
+ public void multicastRouting_ftdSubscribedMulticastAddress_infraLinkJoinsMulticastGroup()
+ throws Exception {
+ assumeTrue(isKernelVersionAtLeast(KERNEL_VERSION_MULTICAST_ROUTING_SUPPORTED));
+ /*
+ * <pre>
+ * Topology:
+ * infra network Thread
+ * infra device -------------------- Border Router -------------- Full Thread device
+ * (Cuttlefish)
+ * </pre>
+ */
+
+ FullThreadDevice ftd = mFtds.get(0);
+ startFtdChild(ftd);
+
+ ftd.subscribeMulticastAddress(GROUP_ADDR_SCOPE_5);
+
+ assertInfraLinkMemberOfGroup(GROUP_ADDR_SCOPE_5);
+ }
+
+ @Test
+ public void
+ multicastRouting_ftdSubscribedScope3MulticastAddress_infraLinkNotJoinMulticastGroup()
+ throws Exception {
+ assumeTrue(isKernelVersionAtLeast(KERNEL_VERSION_MULTICAST_ROUTING_SUPPORTED));
+ /*
+ * <pre>
+ * Topology:
+ * infra network Thread
+ * infra device -------------------- Border Router -------------- Full Thread device
+ * (Cuttlefish)
+ * </pre>
+ */
+
+ FullThreadDevice ftd = mFtds.get(0);
+ startFtdChild(ftd);
+
+ ftd.subscribeMulticastAddress(GROUP_ADDR_SCOPE_3);
+
+ assertInfraLinkNotMemberOfGroup(GROUP_ADDR_SCOPE_3);
+ }
+
+ @Test
+ public void multicastRouting_ftdSubscribedMulticastAddress_canPingfromInfraLink()
+ throws Exception {
+ assumeTrue(isKernelVersionAtLeast(KERNEL_VERSION_MULTICAST_ROUTING_SUPPORTED));
+ /*
+ * <pre>
+ * Topology:
+ * infra network Thread
+ * infra device -------------------- Border Router -------------- Full Thread device
+ * (Cuttlefish)
+ * </pre>
+ */
+
+ FullThreadDevice ftd = mFtds.get(0);
+ startFtdChild(ftd);
+ subscribeMulticastAddressAndWait(ftd, GROUP_ADDR_SCOPE_5);
+
+ mInfraDevice.sendEchoRequest(GROUP_ADDR_SCOPE_5);
+
+ assertNotNull(pollForPacketOnInfraNetwork(ICMPV6_ECHO_REPLY_TYPE, ftd.getOmrAddress()));
+ }
+
+ @Test
+ public void multicastRouting_inboundForwarding_afterBrRejoinFtdRepliesSubscribedAddress()
+ throws Exception {
+ assumeTrue(isKernelVersionAtLeast(KERNEL_VERSION_MULTICAST_ROUTING_SUPPORTED));
+
+ // TODO (b/327311034): Testing bbr state switch from primary mode to secondary mode and back
+ // to primary mode requires an additional BR in the Thread network. This is not currently
+ // supported, to be implemented when possible.
+ }
+
+ @Test
+ public void multicastRouting_ftdSubscribedScope3MulticastAddress_cannotPingfromInfraLink()
+ throws Exception {
+ assumeTrue(isKernelVersionAtLeast(KERNEL_VERSION_MULTICAST_ROUTING_SUPPORTED));
+ /*
+ * <pre>
+ * Topology:
+ * infra network Thread
+ * infra device -------------------- Border Router -------------- Full Thread device
+ * (Cuttlefish)
+ * </pre>
+ */
+
+ FullThreadDevice ftd = mFtds.get(0);
+ startFtdChild(ftd);
+ ftd.subscribeMulticastAddress(GROUP_ADDR_SCOPE_3);
+
+ mInfraDevice.sendEchoRequest(GROUP_ADDR_SCOPE_3);
+
+ assertNull(pollForPacketOnInfraNetwork(ICMPV6_ECHO_REPLY_TYPE, ftd.getOmrAddress()));
+ }
+
+ @Test
+ public void multicastRouting_ftdNotSubscribedMulticastAddress_cannotPingFromInfraDevice()
+ throws Exception {
+ assumeTrue(isKernelVersionAtLeast(KERNEL_VERSION_MULTICAST_ROUTING_SUPPORTED));
+ /*
+ * <pre>
+ * Topology:
+ * infra network Thread
+ * infra device -------------------- Border Router -------------- Full Thread device
+ * (Cuttlefish)
+ * </pre>
+ */
+
+ FullThreadDevice ftd = mFtds.get(0);
+ startFtdChild(ftd);
+
+ mInfraDevice.sendEchoRequest(GROUP_ADDR_SCOPE_4);
+
+ assertNull(pollForPacketOnInfraNetwork(ICMPV6_ECHO_REPLY_TYPE, ftd.getOmrAddress()));
+ }
+
+ @Test
+ public void multicastRouting_multipleFtdsSubscribedDifferentAddresses_canPingFromInfraDevice()
+ throws Exception {
+ assumeTrue(isKernelVersionAtLeast(KERNEL_VERSION_MULTICAST_ROUTING_SUPPORTED));
+ /*
+ * <pre>
+ * Topology:
+ * infra network Thread
+ * infra device -------------------- Border Router -------------- Full Thread device 1
+ * (Cuttlefish)
+ * |
+ * | Thread
+ * |
+ * Full Thread device 2
+ * </pre>
+ */
+
+ FullThreadDevice ftd1 = mFtds.get(0);
+ startFtdChild(ftd1);
+ subscribeMulticastAddressAndWait(ftd1, GROUP_ADDR_SCOPE_5);
+
+ FullThreadDevice ftd2 = mFtds.get(1);
+ startFtdChild(ftd2);
+ subscribeMulticastAddressAndWait(ftd2, GROUP_ADDR_SCOPE_4);
+
+ mInfraDevice.sendEchoRequest(GROUP_ADDR_SCOPE_5);
+
+ assertNotNull(pollForPacketOnInfraNetwork(ICMPV6_ECHO_REPLY_TYPE, ftd1.getOmrAddress()));
+
+ // Verify ping reply from ftd1 and ftd2 separately as the order of replies can't be
+ // predicted.
+ mInfraDevice.sendEchoRequest(GROUP_ADDR_SCOPE_4);
+
+ assertNotNull(pollForPacketOnInfraNetwork(ICMPV6_ECHO_REPLY_TYPE, ftd2.getOmrAddress()));
+ }
+
+ @Test
+ public void multicastRouting_multipleFtdsSubscribedSameAddress_canPingFromInfraDevice()
+ throws Exception {
+ assumeTrue(isKernelVersionAtLeast(KERNEL_VERSION_MULTICAST_ROUTING_SUPPORTED));
+ /*
+ * <pre>
+ * Topology:
+ * infra network Thread
+ * infra device -------------------- Border Router -------------- Full Thread device 1
+ * (Cuttlefish)
+ * |
+ * | Thread
+ * |
+ * Full Thread device 2
+ * </pre>
+ */
+
+ FullThreadDevice ftd1 = mFtds.get(0);
+ startFtdChild(ftd1);
+ subscribeMulticastAddressAndWait(ftd1, GROUP_ADDR_SCOPE_5);
+
+ FullThreadDevice ftd2 = mFtds.get(1);
+ startFtdChild(ftd2);
+ subscribeMulticastAddressAndWait(ftd2, GROUP_ADDR_SCOPE_5);
+
+ mInfraDevice.sendEchoRequest(GROUP_ADDR_SCOPE_5);
+
+ assertNotNull(pollForPacketOnInfraNetwork(ICMPV6_ECHO_REPLY_TYPE, ftd1.getOmrAddress()));
+
+ // Send the request twice as the order of replies from ftd1 and ftd2 are not guaranteed
+ mInfraDevice.sendEchoRequest(GROUP_ADDR_SCOPE_5);
+
+ assertNotNull(pollForPacketOnInfraNetwork(ICMPV6_ECHO_REPLY_TYPE, ftd2.getOmrAddress()));
+ }
+
+ @Test
+ public void multicastRouting_outboundForwarding_scopeLargerThan3IsForwarded() throws Exception {
+ assumeTrue(isKernelVersionAtLeast(KERNEL_VERSION_MULTICAST_ROUTING_SUPPORTED));
+ /*
+ * <pre>
+ * Topology:
+ * infra network Thread
+ * infra device -------------------- Border Router -------------- Full Thread device
+ * (Cuttlefish)
+ * </pre>
+ */
+
+ FullThreadDevice ftd = mFtds.get(0);
+ startFtdChild(ftd);
+ Inet6Address ftdOmr = ftd.getOmrAddress();
+
+ ftd.ping(GROUP_ADDR_SCOPE_5);
+ ftd.ping(GROUP_ADDR_SCOPE_4);
+
+ assertNotNull(
+ pollForPacketOnInfraNetwork(ICMPV6_ECHO_REQUEST_TYPE, ftdOmr, GROUP_ADDR_SCOPE_5));
+ assertNotNull(
+ pollForPacketOnInfraNetwork(ICMPV6_ECHO_REQUEST_TYPE, ftdOmr, GROUP_ADDR_SCOPE_4));
+ }
+
+ @Test
+ public void multicastRouting_outboundForwarding_scopeSmallerThan4IsNotForwarded()
+ throws Exception {
+ assumeTrue(isKernelVersionAtLeast(KERNEL_VERSION_MULTICAST_ROUTING_SUPPORTED));
+ /*
+ * <pre>
+ * Topology:
+ * infra network Thread
+ * infra device -------------------- Border Router -------------- Full Thread device
+ * (Cuttlefish)
+ * </pre>
+ */
+
+ FullThreadDevice ftd = mFtds.get(0);
+ startFtdChild(ftd);
+
+ ftd.ping(GROUP_ADDR_SCOPE_3);
+
+ assertNull(
+ pollForPacketOnInfraNetwork(
+ ICMPV6_ECHO_REQUEST_TYPE, ftd.getOmrAddress(), GROUP_ADDR_SCOPE_3));
+ }
+
+ @Test
+ public void multicastRouting_outboundForwarding_llaToScope4IsNotForwarded() throws Exception {
+ assumeTrue(isKernelVersionAtLeast(KERNEL_VERSION_MULTICAST_ROUTING_SUPPORTED));
+ /*
+ * <pre>
+ * Topology:
+ * infra network Thread
+ * infra device -------------------- Border Router -------------- Full Thread device
+ * (Cuttlefish)
+ * </pre>
+ */
+
+ FullThreadDevice ftd = mFtds.get(0);
+ startFtdChild(ftd);
+ Inet6Address ftdLla = ftd.getLinkLocalAddress();
+ assertNotNull(ftdLla);
+
+ ftd.ping(GROUP_ADDR_SCOPE_4, ftdLla, 100 /* size */, 1 /* count */);
+
+ assertNull(
+ pollForPacketOnInfraNetwork(ICMPV6_ECHO_REQUEST_TYPE, ftdLla, GROUP_ADDR_SCOPE_4));
+ }
+
+ @Test
+ public void multicastRouting_outboundForwarding_mlaToScope4IsNotForwarded() throws Exception {
+ assumeTrue(isKernelVersionAtLeast(KERNEL_VERSION_MULTICAST_ROUTING_SUPPORTED));
+ /*
+ * <pre>
+ * Topology:
+ * infra network Thread
+ * infra device -------------------- Border Router -------------- Full Thread device
+ * (Cuttlefish)
+ * </pre>
+ */
+
+ FullThreadDevice ftd = mFtds.get(0);
+ startFtdChild(ftd);
+ List<Inet6Address> ftdMlas = ftd.getMeshLocalAddresses();
+ assertFalse(ftdMlas.isEmpty());
+
+ for (Inet6Address ftdMla : ftdMlas) {
+ ftd.ping(GROUP_ADDR_SCOPE_4, ftdMla, 100 /* size */, 1 /* count */);
+
+ assertNull(
+ pollForPacketOnInfraNetwork(
+ ICMPV6_ECHO_REQUEST_TYPE, ftdMla, GROUP_ADDR_SCOPE_4));
+ }
+ }
+
+ @Test
+ public void multicastRouting_infraNetworkSwitch_ftdRepliesToSubscribedAddress()
+ throws Exception {
+ assumeTrue(isKernelVersionAtLeast(KERNEL_VERSION_MULTICAST_ROUTING_SUPPORTED));
+ /*
+ * <pre>
+ * Topology:
+ * infra network Thread
+ * infra device -------------------- Border Router -------------- Full Thread device
+ * (Cuttlefish)
+ * </pre>
+ */
+
+ FullThreadDevice ftd = mFtds.get(0);
+ startFtdChild(ftd);
+ subscribeMulticastAddressAndWait(ftd, GROUP_ADDR_SCOPE_5);
+ Inet6Address ftdOmr = ftd.getOmrAddress();
+
+ // Destroy infra link and re-create
+ tearDownInfraNetwork();
+ setUpInfraNetwork();
+ mInfraNetworkReader = newPacketReader(mInfraNetworkTracker.getTestIface(), mHandler);
+ startInfraDevice();
+
+ mInfraDevice.sendEchoRequest(GROUP_ADDR_SCOPE_5);
+
+ assertNotNull(pollForPacketOnInfraNetwork(ICMPV6_ECHO_REPLY_TYPE, ftdOmr));
+ }
+
+ @Test
+ public void multicastRouting_infraNetworkSwitch_outboundPacketIsForwarded() throws Exception {
+ assumeTrue(isKernelVersionAtLeast(KERNEL_VERSION_MULTICAST_ROUTING_SUPPORTED));
+ /*
+ * <pre>
+ * Topology:
+ * infra network Thread
+ * infra device -------------------- Border Router -------------- Full Thread device
+ * (Cuttlefish)
+ * </pre>
+ */
+
+ FullThreadDevice ftd = mFtds.get(0);
+ startFtdChild(ftd);
+ Inet6Address ftdOmr = ftd.getOmrAddress();
+
+ // Destroy infra link and re-create
+ tearDownInfraNetwork();
+ setUpInfraNetwork();
+ mInfraNetworkReader = newPacketReader(mInfraNetworkTracker.getTestIface(), mHandler);
+ startInfraDevice();
+
+ ftd.ping(GROUP_ADDR_SCOPE_5);
+ ftd.ping(GROUP_ADDR_SCOPE_4);
+
+ assertNotNull(
+ pollForPacketOnInfraNetwork(ICMPV6_ECHO_REQUEST_TYPE, ftdOmr, GROUP_ADDR_SCOPE_5));
+ assertNotNull(
+ pollForPacketOnInfraNetwork(ICMPV6_ECHO_REQUEST_TYPE, ftdOmr, GROUP_ADDR_SCOPE_4));
+ }
+
+ private void setUpInfraNetwork() {
+ mInfraNetworkTracker =
+ runAsShell(
+ MANAGE_TEST_NETWORKS,
+ () ->
+ initTestNetwork(
+ mContext, new LinkProperties(), 5000 /* timeoutMs */));
+ runAsShell(
+ PERMISSION_THREAD_NETWORK_PRIVILEGED,
+ NETWORK_SETTINGS,
+ () -> {
+ CompletableFuture<Void> future = new CompletableFuture<>();
+ mController.setTestNetworkAsUpstream(
+ mInfraNetworkTracker.getTestIface().getInterfaceName(),
+ directExecutor(),
+ future::complete);
+ future.get(5, TimeUnit.SECONDS);
+ });
+ }
+
+ private void tearDownInfraNetwork() {
+ runAsShell(MANAGE_TEST_NETWORKS, () -> mInfraNetworkTracker.teardown());
+ }
+
+ private void startBrLeader() throws Exception {
+ CompletableFuture<Void> joinFuture = new CompletableFuture<>();
+ runAsShell(
+ PERMISSION_THREAD_NETWORK_PRIVILEGED,
+ () -> mController.join(DEFAULT_DATASET, directExecutor(), joinFuture::complete));
+ joinFuture.get(RESTART_JOIN_TIMEOUT.toSeconds(), TimeUnit.SECONDS);
+ }
+
+ private void startFtdChild(FullThreadDevice ftd) throws Exception {
ftd.factoryReset();
ftd.joinNetwork(DEFAULT_DATASET);
ftd.waitForStateAnyOf(List.of("router", "child"), JOIN_TIMEOUT);
waitFor(() -> ftd.getOmrAddress() != null, Duration.ofSeconds(60));
Inet6Address ftdOmr = ftd.getOmrAddress();
assertNotNull(ftdOmr);
+ }
- // Creates a infra network device.
- TapPacketReader infraNetworkReader =
- newPacketReader(mInfraNetworkTracker.getTestIface(), mHandler);
- InfraNetworkDevice infraDevice =
- new InfraNetworkDevice(MacAddress.fromString("1:2:3:4:5:6"), infraNetworkReader);
- infraDevice.runSlaac(Duration.ofSeconds(60));
- assertNotNull(infraDevice.ipv6Addr);
+ private void startInfraDevice() throws Exception {
+ mInfraDevice =
+ new InfraNetworkDevice(MacAddress.fromString("1:2:3:4:5:6"), mInfraNetworkReader);
+ mInfraDevice.runSlaac(Duration.ofSeconds(60));
+ assertNotNull(mInfraDevice.ipv6Addr);
+ }
- // Infra device sends an echo request to FTD's OMR.
- infraDevice.sendEchoRequest(ftdOmr);
+ private void assertInfraLinkMemberOfGroup(Inet6Address address) throws Exception {
+ waitFor(
+ () ->
+ isInMulticastGroup(
+ mInfraNetworkTracker.getTestIface().getInterfaceName(), address),
+ Duration.ofSeconds(3));
+ }
- // Infra device receives an echo reply sent by FTD.
- assertNotNull(
- readPacketFrom(
- infraNetworkReader,
- p -> isExpectedIcmpv6Packet(p, ICMPV6_ECHO_REPLY_TYPE)));
+ private void assertInfraLinkNotMemberOfGroup(Inet6Address address) throws Exception {
+ waitFor(
+ () ->
+ !isInMulticastGroup(
+ mInfraNetworkTracker.getTestIface().getInterfaceName(), address),
+ Duration.ofSeconds(3));
+ }
+
+ private void subscribeMulticastAddressAndWait(FullThreadDevice ftd, Inet6Address address)
+ throws Exception {
+ ftd.subscribeMulticastAddress(address);
+
+ assertInfraLinkMemberOfGroup(address);
+ }
+
+ private byte[] pollForPacketOnInfraNetwork(int type, Inet6Address srcAddress) {
+ return pollForPacketOnInfraNetwork(type, srcAddress, null);
+ }
+
+ private byte[] pollForPacketOnInfraNetwork(
+ int type, Inet6Address srcAddress, Inet6Address destAddress) {
+ Predicate<byte[]> filter;
+ filter =
+ p ->
+ (isExpectedIcmpv6Packet(p, type)
+ && (srcAddress == null ? true : isFromIpv6Source(p, srcAddress))
+ && (destAddress == null
+ ? true
+ : isToIpv6Destination(p, destAddress)));
+ return pollForPacket(mInfraNetworkReader, filter);
}
}
diff --git a/thread/tests/integration/src/android/net/thread/utils/FullThreadDevice.java b/thread/tests/integration/src/android/net/thread/utils/FullThreadDevice.java
index 031d205..6cb1675 100644
--- a/thread/tests/integration/src/android/net/thread/utils/FullThreadDevice.java
+++ b/thread/tests/integration/src/android/net/thread/utils/FullThreadDevice.java
@@ -35,6 +35,8 @@
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeoutException;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
/**
* A class that launches and controls a simulation Full Thread Device (FTD).
@@ -73,6 +75,10 @@
mActiveOperationalDataset = null;
}
+ public void destroy() {
+ mProcess.destroy();
+ }
+
/**
* Returns an OMR (Off-Mesh-Routable) address on this device if any.
*
@@ -94,6 +100,45 @@
return null;
}
+ /** Returns the Mesh-local EID address on this device if any. */
+ public Inet6Address getMlEid() {
+ List<String> addresses = executeCommand("ipaddr mleid");
+ return (Inet6Address) InetAddresses.parseNumericAddress(addresses.get(0));
+ }
+
+ /**
+ * Returns the link-local address of the device.
+ *
+ * <p>This methods goes through all unicast addresses on the device and returns the address that
+ * begins with fe80.
+ */
+ public Inet6Address getLinkLocalAddress() {
+ List<String> output = executeCommand("ipaddr linklocal");
+ if (!output.isEmpty() && output.get(0).startsWith("fe80:")) {
+ return (Inet6Address) InetAddresses.parseNumericAddress(output.get(0));
+ }
+ return null;
+ }
+
+ /**
+ * Returns the mesh-local addresses of the device.
+ *
+ * <p>This methods goes through all unicast addresses on the device and returns the address that
+ * begins with mesh-local prefix.
+ */
+ public List<Inet6Address> getMeshLocalAddresses() {
+ List<String> addresses = executeCommand("ipaddr");
+ List<Inet6Address> meshLocalAddresses = new ArrayList<>();
+ IpPrefix meshLocalPrefix = mActiveOperationalDataset.getMeshLocalPrefix();
+ for (String address : addresses) {
+ Inet6Address addr = (Inet6Address) InetAddresses.parseNumericAddress(address);
+ if (meshLocalPrefix.contains(addr)) {
+ meshLocalAddresses.add(addr);
+ }
+ }
+ return meshLocalAddresses;
+ }
+
/**
* Joins the Thread network using the given {@link ActiveOperationalDataset}.
*
@@ -132,6 +177,33 @@
return executeCommand("state").get(0);
}
+ /** Closes the UDP socket. */
+ public void udpClose() {
+ executeCommand("udp close");
+ }
+
+ /** Opens the UDP socket. */
+ public void udpOpen() {
+ executeCommand("udp open");
+ }
+
+ /** Opens the UDP socket and binds it to a specific address and port. */
+ public void udpBind(Inet6Address address, int port) {
+ udpClose();
+ udpOpen();
+ executeCommand(String.format("udp bind %s %d", address.getHostAddress(), port));
+ }
+
+ /** Returns the message received on the UDP socket. */
+ public String udpReceive() throws IOException {
+ Pattern pattern =
+ Pattern.compile("> (\\d+) bytes from ([\\da-f:]+) (\\d+) ([\\x00-\\x7F]+)");
+ Matcher matcher = pattern.matcher(mReader.readLine());
+ matcher.matches();
+
+ return matcher.group(4);
+ }
+
/** Runs the "factoryreset" command on the device. */
public void factoryReset() {
try {
@@ -147,6 +219,27 @@
}
}
+ public void subscribeMulticastAddress(Inet6Address address) {
+ executeCommand("ipmaddr add " + address.getHostAddress());
+ }
+
+ public void ping(Inet6Address address, Inet6Address source, int size, int count) {
+ String cmd =
+ "ping"
+ + ((source == null) ? "" : (" -I " + source.getHostAddress()))
+ + " "
+ + address.getHostAddress()
+ + " "
+ + size
+ + " "
+ + count;
+ executeCommand(cmd);
+ }
+
+ public void ping(Inet6Address address) {
+ ping(address, null, 100 /* size */, 1 /* count */);
+ }
+
private List<String> executeCommand(String command) {
try {
mWriter.write(command + "\n");
diff --git a/thread/tests/integration/src/android/net/thread/utils/InfraNetworkDevice.java b/thread/tests/integration/src/android/net/thread/utils/InfraNetworkDevice.java
index 3081f9f..72a278c 100644
--- a/thread/tests/integration/src/android/net/thread/utils/InfraNetworkDevice.java
+++ b/thread/tests/integration/src/android/net/thread/utils/InfraNetworkDevice.java
@@ -16,7 +16,7 @@
package android.net.thread.utils;
import static android.net.thread.utils.IntegrationTestUtils.getRaPios;
-import static android.net.thread.utils.IntegrationTestUtils.readPacketFrom;
+import static android.net.thread.utils.IntegrationTestUtils.pollForPacket;
import static android.net.thread.utils.IntegrationTestUtils.waitFor;
import static com.android.net.module.util.NetworkStackConstants.ICMPV6_ND_OPTION_SLLA;
@@ -109,7 +109,7 @@
try {
sendRsPacket();
- final byte[] raPacket = readPacketFrom(packetReader, p -> !getRaPios(p).isEmpty());
+ final byte[] raPacket = pollForPacket(packetReader, p -> !getRaPios(p).isEmpty());
final List<PrefixInformationOption> options = getRaPios(raPacket);
diff --git a/thread/tests/integration/src/android/net/thread/utils/IntegrationTestUtils.java b/thread/tests/integration/src/android/net/thread/utils/IntegrationTestUtils.java
index f223367..74251a6 100644
--- a/thread/tests/integration/src/android/net/thread/utils/IntegrationTestUtils.java
+++ b/thread/tests/integration/src/android/net/thread/utils/IntegrationTestUtils.java
@@ -17,6 +17,7 @@
import static android.system.OsConstants.IPPROTO_ICMPV6;
+import static com.android.compatibility.common.util.SystemUtil.runShellCommandOrThrow;
import static com.android.net.module.util.NetworkStackConstants.ICMPV6_ND_OPTION_PIO;
import static com.android.net.module.util.NetworkStackConstants.ICMPV6_ROUTER_ADVERTISEMENT;
@@ -39,6 +40,13 @@
import com.google.common.util.concurrent.SettableFuture;
import java.io.FileDescriptor;
+import java.io.IOException;
+import java.net.DatagramPacket;
+import java.net.DatagramSocket;
+import java.net.Inet6Address;
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.net.SocketAddress;
import java.nio.ByteBuffer;
import java.time.Duration;
import java.util.ArrayList;
@@ -143,17 +151,17 @@
}
/**
- * Reads a packet from a given {@link TapPacketReader} that satisfies the {@code filter}.
+ * Polls for a packet from a given {@link TapPacketReader} that satisfies the {@code filter}.
*
* @param packetReader a TUN packet reader
* @param filter the filter to be applied on the packet
* @return the first IPv6 packet that satisfies the {@code filter}. If it has waited for more
* than 3000ms to read the next packet, the method will return null
*/
- public static byte[] readPacketFrom(TapPacketReader packetReader, Predicate<byte[]> filter) {
+ public static byte[] pollForPacket(TapPacketReader packetReader, Predicate<byte[]> filter) {
byte[] packet;
- while ((packet = packetReader.poll(3000 /* timeoutMs */)) != null) {
- if (filter.test(packet)) return packet;
+ while ((packet = packetReader.poll(3000 /* timeoutMs */, filter)) != null) {
+ return packet;
}
return null;
}
@@ -176,6 +184,34 @@
return false;
}
+ public static boolean isFromIpv6Source(byte[] packet, Inet6Address src) {
+ if (packet == null) {
+ return false;
+ }
+ ByteBuffer buf = ByteBuffer.wrap(packet);
+ try {
+ return Struct.parse(Ipv6Header.class, buf).srcIp.equals(src);
+ } catch (IllegalArgumentException ignored) {
+ // It's fine that the passed in packet is malformed because it's could be sent
+ // by anybody.
+ }
+ return false;
+ }
+
+ public static boolean isToIpv6Destination(byte[] packet, Inet6Address dest) {
+ if (packet == null) {
+ return false;
+ }
+ ByteBuffer buf = ByteBuffer.wrap(packet);
+ try {
+ return Struct.parse(Ipv6Header.class, buf).dstIp.equals(dest);
+ } catch (IllegalArgumentException ignored) {
+ // It's fine that the passed in packet is malformed because it's could be sent
+ // by anybody.
+ }
+ return false;
+ }
+
/** Returns the Prefix Information Options (PIO) extracted from an ICMPv6 RA message. */
public static List<PrefixInformationOption> getRaPios(byte[] raMsg) {
final ArrayList<PrefixInformationOption> pioList = new ArrayList<>();
@@ -219,4 +255,38 @@
}
return pioList;
}
+
+ /**
+ * Sends a UDP message to a destination.
+ *
+ * @param dstAddress the IP address of the destination
+ * @param dstPort the port of the destination
+ * @param message the message in UDP payload
+ * @throws IOException if failed to send the message
+ */
+ public static void sendUdpMessage(InetAddress dstAddress, int dstPort, String message)
+ throws IOException {
+ SocketAddress dstSockAddr = new InetSocketAddress(dstAddress, dstPort);
+
+ try (DatagramSocket socket = new DatagramSocket()) {
+ socket.connect(dstSockAddr);
+
+ byte[] msgBytes = message.getBytes();
+ DatagramPacket packet = new DatagramPacket(msgBytes, msgBytes.length);
+
+ socket.send(packet);
+ }
+ }
+
+ public static boolean isInMulticastGroup(String interfaceName, Inet6Address address) {
+ final String cmd = "ip -6 maddr show dev " + interfaceName;
+ final String output = runShellCommandOrThrow(cmd);
+ final String addressStr = address.getHostAddress();
+ for (final String line : output.split("\\n")) {
+ if (line.contains(addressStr)) {
+ return true;
+ }
+ }
+ return false;
+ }
}
diff --git a/thread/tests/unit/AndroidTest.xml b/thread/tests/unit/AndroidTest.xml
index 26813c1..d16e423 100644
--- a/thread/tests/unit/AndroidTest.xml
+++ b/thread/tests/unit/AndroidTest.xml
@@ -19,6 +19,18 @@
<option name="test-tag" value="ThreadNetworkUnitTests" />
<option name="test-suite-tag" value="apct" />
+ <!--
+ Only run tests if the device under test is SDK version 34 (Android 14) or above.
+ -->
+ <object type="module_controller"
+ class="com.android.tradefed.testtype.suite.module.Sdk34ModuleController" />
+
+ <!-- Run tests in MTS only if the Tethering Mainline module is installed. -->
+ <object type="module_controller"
+ class="com.android.tradefed.testtype.suite.module.MainlineTestModuleController">
+ <option name="mainline-module-package-name" value="com.google.android.tethering" />
+ </object>
+
<target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
<option name="test-file-name" value="ThreadNetworkUnitTests.apk" />
<option name="check-min-sdk" value="true" />
diff --git a/thread/tests/unit/src/com/android/server/thread/ThreadNetworkControllerServiceTest.java b/thread/tests/unit/src/com/android/server/thread/ThreadNetworkControllerServiceTest.java
index f626edf..4948c22 100644
--- a/thread/tests/unit/src/com/android/server/thread/ThreadNetworkControllerServiceTest.java
+++ b/thread/tests/unit/src/com/android/server/thread/ThreadNetworkControllerServiceTest.java
@@ -16,32 +16,50 @@
package com.android.server.thread;
+import static android.Manifest.permission.ACCESS_NETWORK_STATE;
+import static android.net.thread.ActiveOperationalDataset.CHANNEL_PAGE_24_GHZ;
+import static android.net.thread.ThreadNetworkController.STATE_DISABLED;
+import static android.net.thread.ThreadNetworkController.STATE_ENABLED;
+import static android.net.thread.ThreadNetworkException.ERROR_FAILED_PRECONDITION;
import static android.net.thread.ThreadNetworkException.ERROR_INTERNAL_ERROR;
+import static android.net.thread.ThreadNetworkManager.DISALLOW_THREAD_NETWORK;
import static android.net.thread.ThreadNetworkManager.PERMISSION_THREAD_NETWORK_PRIVILEGED;
-import static com.android.testutils.TestPermissionUtil.runAsShell;
+import static com.android.server.thread.openthread.IOtDaemon.ErrorCode.OT_ERROR_INVALID_STATE;
import static com.google.common.io.BaseEncoding.base16;
import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.assertThrows;
import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
+import android.content.BroadcastReceiver;
import android.content.Context;
+import android.content.Intent;
import android.net.ConnectivityManager;
import android.net.NetworkAgent;
import android.net.NetworkProvider;
import android.net.thread.ActiveOperationalDataset;
+import android.net.thread.IActiveOperationalDatasetReceiver;
import android.net.thread.IOperationReceiver;
+import android.net.thread.ThreadNetworkException;
import android.os.Handler;
+import android.os.IBinder;
import android.os.ParcelFileDescriptor;
import android.os.RemoteException;
+import android.os.UserManager;
import android.os.test.TestLooper;
import androidx.test.core.app.ApplicationProvider;
@@ -53,9 +71,15 @@
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.atomic.AtomicReference;
+
/** Unit tests for {@link ThreadNetworkControllerService}. */
@SmallTest
@RunWith(AndroidJUnit4.class)
@@ -80,6 +104,12 @@
+ "B9D351B40C0402A0FFF8");
private static final ActiveOperationalDataset DEFAULT_ACTIVE_DATASET =
ActiveOperationalDataset.fromThreadTlvs(DEFAULT_ACTIVE_DATASET_TLVS);
+ private static final String DEFAULT_NETWORK_NAME = "thread-wpan0";
+ private static final int OT_ERROR_NONE = 0;
+ private static final int DEFAULT_SUPPORTED_CHANNEL_MASK = 0x07FFF800; // from channel 11 to 26
+ private static final int DEFAULT_PREFERRED_CHANNEL_MASK = 0x00000800; // channel 11
+ private static final int DEFAULT_SELECTED_CHANNEL = 11;
+ private static final byte[] DEFAULT_SUPPORTED_CHANNEL_MASK_ARRAY = base16().decode("001FFFE0");
@Mock private ConnectivityManager mMockConnectivityManager;
@Mock private NetworkAgent mMockNetworkAgent;
@@ -88,30 +118,38 @@
@Mock private InfraInterfaceController mMockInfraIfController;
@Mock private ThreadPersistentSettings mMockPersistentSettings;
@Mock private NsdPublisher mMockNsdPublisher;
+ @Mock private UserManager mMockUserManager;
+ @Mock private IBinder mIBinder;
private Context mContext;
private TestLooper mTestLooper;
private FakeOtDaemon mFakeOtDaemon;
private ThreadNetworkControllerService mService;
+ @Captor private ArgumentCaptor<ActiveOperationalDataset> mActiveDatasetCaptor;
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
- mContext = ApplicationProvider.getApplicationContext();
+ mContext = spy(ApplicationProvider.getApplicationContext());
+ doNothing()
+ .when(mContext)
+ .enforceCallingOrSelfPermission(
+ eq(PERMISSION_THREAD_NETWORK_PRIVILEGED), anyString());
+
mTestLooper = new TestLooper();
final Handler handler = new Handler(mTestLooper.getLooper());
NetworkProvider networkProvider =
new NetworkProvider(mContext, mTestLooper.getLooper(), "ThreadNetworkProvider");
mFakeOtDaemon = new FakeOtDaemon(handler);
-
when(mMockTunIfController.getTunFd()).thenReturn(mMockTunFd);
when(mMockPersistentSettings.get(any())).thenReturn(true);
+ when(mMockUserManager.hasUserRestriction(eq(DISALLOW_THREAD_NETWORK))).thenReturn(false);
mService =
new ThreadNetworkControllerService(
- ApplicationProvider.getApplicationContext(),
+ mContext,
handler,
networkProvider,
() -> mFakeOtDaemon,
@@ -119,7 +157,8 @@
mMockTunIfController,
mMockInfraIfController,
mMockPersistentSettings,
- mMockNsdPublisher);
+ mMockNsdPublisher,
+ mMockUserManager);
mService.setTestNetworkAgent(mMockNetworkAgent);
}
@@ -141,9 +180,7 @@
final IOperationReceiver mockReceiver = mock(IOperationReceiver.class);
mFakeOtDaemon.setJoinException(new RemoteException("ot-daemon join() throws"));
- runAsShell(
- PERMISSION_THREAD_NETWORK_PRIVILEGED,
- () -> mService.join(DEFAULT_ACTIVE_DATASET, mockReceiver));
+ mService.join(DEFAULT_ACTIVE_DATASET, mockReceiver);
mTestLooper.dispatchAll();
verify(mockReceiver, never()).onSuccess();
@@ -155,9 +192,7 @@
mService.initialize();
final IOperationReceiver mockReceiver = mock(IOperationReceiver.class);
- runAsShell(
- PERMISSION_THREAD_NETWORK_PRIVILEGED,
- () -> mService.join(DEFAULT_ACTIVE_DATASET, mockReceiver));
+ mService.join(DEFAULT_ACTIVE_DATASET, mockReceiver);
// Here needs to call Testlooper#dispatchAll twices because TestLooper#moveTimeForward
// operates on only currently enqueued messages but the delayed message is posted from
// another Handler task.
@@ -168,4 +203,134 @@
verify(mockReceiver, times(1)).onSuccess();
verify(mMockNetworkAgent, times(1)).register();
}
+
+ @Test
+ public void userRestriction_initWithUserRestricted_threadIsDisabled() {
+ when(mMockUserManager.hasUserRestriction(eq(DISALLOW_THREAD_NETWORK))).thenReturn(true);
+
+ mService.initialize();
+ mTestLooper.dispatchAll();
+
+ assertThat(mFakeOtDaemon.getEnabledState()).isEqualTo(STATE_DISABLED);
+ }
+
+ @Test
+ public void userRestriction_initWithUserNotRestricted_threadIsEnabled() {
+ when(mMockUserManager.hasUserRestriction(eq(DISALLOW_THREAD_NETWORK))).thenReturn(false);
+
+ mService.initialize();
+ mTestLooper.dispatchAll();
+
+ assertThat(mFakeOtDaemon.getEnabledState()).isEqualTo(STATE_ENABLED);
+ }
+
+ @Test
+ public void userRestriction_userBecomesRestricted_stateIsDisabledButNotPersisted() {
+ AtomicReference<BroadcastReceiver> receiverRef = new AtomicReference<>();
+ when(mMockUserManager.hasUserRestriction(eq(DISALLOW_THREAD_NETWORK))).thenReturn(false);
+ doAnswer(
+ invocation -> {
+ receiverRef.set((BroadcastReceiver) invocation.getArguments()[0]);
+ return null;
+ })
+ .when(mContext)
+ .registerReceiver(any(BroadcastReceiver.class), any(), any(), any());
+ mService.initialize();
+ mTestLooper.dispatchAll();
+
+ when(mMockUserManager.hasUserRestriction(eq(DISALLOW_THREAD_NETWORK))).thenReturn(true);
+ receiverRef.get().onReceive(mContext, new Intent());
+ mTestLooper.dispatchAll();
+
+ assertThat(mFakeOtDaemon.getEnabledState()).isEqualTo(STATE_DISABLED);
+ verify(mMockPersistentSettings, never())
+ .put(eq(ThreadPersistentSettings.THREAD_ENABLED.key), eq(false));
+ }
+
+ @Test
+ public void userRestriction_userBecomesNotRestricted_stateIsEnabledButNotPersisted() {
+ AtomicReference<BroadcastReceiver> receiverRef = new AtomicReference<>();
+ when(mMockUserManager.hasUserRestriction(eq(DISALLOW_THREAD_NETWORK))).thenReturn(true);
+ doAnswer(
+ invocation -> {
+ receiverRef.set((BroadcastReceiver) invocation.getArguments()[0]);
+ return null;
+ })
+ .when(mContext)
+ .registerReceiver(any(BroadcastReceiver.class), any(), any(), any());
+ mService.initialize();
+ mTestLooper.dispatchAll();
+
+ when(mMockUserManager.hasUserRestriction(eq(DISALLOW_THREAD_NETWORK))).thenReturn(false);
+ receiverRef.get().onReceive(mContext, new Intent());
+ mTestLooper.dispatchAll();
+
+ assertThat(mFakeOtDaemon.getEnabledState()).isEqualTo(STATE_ENABLED);
+ verify(mMockPersistentSettings, never())
+ .put(eq(ThreadPersistentSettings.THREAD_ENABLED.key), eq(true));
+ }
+
+ @Test
+ public void userRestriction_setEnabledWhenUserRestricted_failedPreconditionError() {
+ when(mMockUserManager.hasUserRestriction(eq(DISALLOW_THREAD_NETWORK))).thenReturn(true);
+ mService.initialize();
+
+ CompletableFuture<Void> setEnabledFuture = new CompletableFuture<>();
+ mService.setEnabled(true, newOperationReceiver(setEnabledFuture));
+ mTestLooper.dispatchAll();
+
+ var thrown = assertThrows(ExecutionException.class, () -> setEnabledFuture.get());
+ ThreadNetworkException failure = (ThreadNetworkException) thrown.getCause();
+ assertThat(failure.getErrorCode()).isEqualTo(ERROR_FAILED_PRECONDITION);
+ }
+
+ private static IOperationReceiver newOperationReceiver(CompletableFuture<Void> future) {
+ return new IOperationReceiver.Stub() {
+ @Override
+ public void onSuccess() {
+ future.complete(null);
+ }
+
+ @Override
+ public void onError(int errorCode, String errorMessage) {
+ future.completeExceptionally(new ThreadNetworkException(errorCode, errorMessage));
+ }
+ };
+ }
+
+ @Test
+ public void createRandomizedDataset_succeed_activeDatasetCreated() throws Exception {
+ final IActiveOperationalDatasetReceiver mockReceiver =
+ mock(IActiveOperationalDatasetReceiver.class);
+ mFakeOtDaemon.setChannelMasks(
+ DEFAULT_SUPPORTED_CHANNEL_MASK, DEFAULT_PREFERRED_CHANNEL_MASK);
+ mFakeOtDaemon.setChannelMasksReceiverOtError(OT_ERROR_NONE);
+
+ mService.createRandomizedDataset(DEFAULT_NETWORK_NAME, mockReceiver);
+ mTestLooper.dispatchAll();
+
+ verify(mockReceiver, never()).onError(anyInt(), anyString());
+ verify(mockReceiver, times(1)).onSuccess(mActiveDatasetCaptor.capture());
+ ActiveOperationalDataset activeDataset = mActiveDatasetCaptor.getValue();
+ assertThat(activeDataset.getNetworkName()).isEqualTo(DEFAULT_NETWORK_NAME);
+ assertThat(activeDataset.getChannelMask().size()).isEqualTo(1);
+ assertThat(activeDataset.getChannelMask().get(CHANNEL_PAGE_24_GHZ))
+ .isEqualTo(DEFAULT_SUPPORTED_CHANNEL_MASK_ARRAY);
+ assertThat(activeDataset.getChannel()).isEqualTo(DEFAULT_SELECTED_CHANNEL);
+ }
+
+ @Test
+ public void createRandomizedDataset_otDaemonRemoteFailure_returnsPreconditionError()
+ throws Exception {
+ final IActiveOperationalDatasetReceiver mockReceiver =
+ mock(IActiveOperationalDatasetReceiver.class);
+ mFakeOtDaemon.setChannelMasksReceiverOtError(OT_ERROR_INVALID_STATE);
+ when(mockReceiver.asBinder()).thenReturn(mIBinder);
+
+ mService.createRandomizedDataset(DEFAULT_NETWORK_NAME, mockReceiver);
+ mTestLooper.dispatchAll();
+
+ verify(mockReceiver, never()).onSuccess(any(ActiveOperationalDataset.class));
+ verify(mockReceiver, times(1)).onError(eq(ERROR_INTERNAL_ERROR), anyString());
+ }
}
diff --git a/thread/tests/unit/src/android/net/thread/ThreadPersistentSettingsTest.java b/thread/tests/unit/src/com/android/server/thread/ThreadPersistentSettingsTest.java
similarity index 67%
rename from thread/tests/unit/src/android/net/thread/ThreadPersistentSettingsTest.java
rename to thread/tests/unit/src/com/android/server/thread/ThreadPersistentSettingsTest.java
index 11aabb8..927b5ae 100644
--- a/thread/tests/unit/src/android/net/thread/ThreadPersistentSettingsTest.java
+++ b/thread/tests/unit/src/com/android/server/thread/ThreadPersistentSettingsTest.java
@@ -23,18 +23,22 @@
import static org.mockito.Mockito.any;
import static org.mockito.Mockito.anyInt;
import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.eq;
import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.never;
import static org.mockito.Mockito.validateMockitoUsage;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
+import android.content.res.Resources;
import android.os.PersistableBundle;
import android.test.suitebuilder.annotation.SmallTest;
import android.util.AtomicFile;
import androidx.test.runner.AndroidJUnit4;
+import com.android.connectivity.resources.R;
+import com.android.server.connectivity.ConnectivityResources;
+
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
@@ -51,16 +55,22 @@
@SmallTest
public class ThreadPersistentSettingsTest {
@Mock private AtomicFile mAtomicFile;
+ @Mock Resources mResources;
+ @Mock ConnectivityResources mConnectivityResources;
- private ThreadPersistentSettings mThreadPersistentSetting;
+ private ThreadPersistentSettings mThreadPersistentSettings;
@Before
public void setUp() throws Exception {
MockitoAnnotations.initMocks(this);
+ when(mConnectivityResources.get()).thenReturn(mResources);
+ when(mResources.getBoolean(eq(R.bool.config_thread_default_enabled))).thenReturn(true);
+
FileOutputStream fos = mock(FileOutputStream.class);
when(mAtomicFile.startWrite()).thenReturn(fos);
- mThreadPersistentSetting = new ThreadPersistentSettings(mAtomicFile);
+ mThreadPersistentSettings =
+ new ThreadPersistentSettings(mAtomicFile, mConnectivityResources);
}
/** Called after each test */
@@ -70,10 +80,42 @@
}
@Test
- public void put_ThreadFeatureEnabledTrue_returnsTrue() throws Exception {
- mThreadPersistentSetting.put(THREAD_ENABLED.key, true);
+ public void initialize_readsFromFile() throws Exception {
+ byte[] data = createXmlForParsing(THREAD_ENABLED.key, false);
+ setupAtomicFileMockForRead(data);
- assertThat(mThreadPersistentSetting.get(THREAD_ENABLED)).isTrue();
+ mThreadPersistentSettings.initialize();
+
+ assertThat(mThreadPersistentSettings.get(THREAD_ENABLED)).isFalse();
+ }
+
+ @Test
+ public void initialize_ThreadDisabledInResources_returnsThreadDisabled() throws Exception {
+ when(mResources.getBoolean(eq(R.bool.config_thread_default_enabled))).thenReturn(false);
+ setupAtomicFileMockForRead(new byte[0]);
+
+ mThreadPersistentSettings.initialize();
+
+ assertThat(mThreadPersistentSettings.get(THREAD_ENABLED)).isFalse();
+ }
+
+ @Test
+ public void initialize_ThreadDisabledInResourcesButEnabledInXml_returnsThreadEnabled()
+ throws Exception {
+ when(mResources.getBoolean(eq(R.bool.config_thread_default_enabled))).thenReturn(false);
+ byte[] data = createXmlForParsing(THREAD_ENABLED.key, true);
+ setupAtomicFileMockForRead(data);
+
+ mThreadPersistentSettings.initialize();
+
+ assertThat(mThreadPersistentSettings.get(THREAD_ENABLED)).isTrue();
+ }
+
+ @Test
+ public void put_ThreadFeatureEnabledTrue_returnsTrue() throws Exception {
+ mThreadPersistentSettings.put(THREAD_ENABLED.key, true);
+
+ assertThat(mThreadPersistentSettings.get(THREAD_ENABLED)).isTrue();
// Confirm that file writes have been triggered.
verify(mAtomicFile).startWrite();
verify(mAtomicFile).finishWrite(any());
@@ -81,26 +123,14 @@
@Test
public void put_ThreadFeatureEnabledFalse_returnsFalse() throws Exception {
- mThreadPersistentSetting.put(THREAD_ENABLED.key, false);
+ mThreadPersistentSettings.put(THREAD_ENABLED.key, false);
- assertThat(mThreadPersistentSetting.get(THREAD_ENABLED)).isFalse();
+ assertThat(mThreadPersistentSettings.get(THREAD_ENABLED)).isFalse();
// Confirm that file writes have been triggered.
verify(mAtomicFile).startWrite();
verify(mAtomicFile).finishWrite(any());
}
- @Test
- public void initialize_readsFromFile() throws Exception {
- byte[] data = createXmlForParsing(THREAD_ENABLED.key, false);
- setupAtomicFileMockForRead(data);
-
- // Trigger file read.
- mThreadPersistentSetting.initialize();
-
- assertThat(mThreadPersistentSetting.get(THREAD_ENABLED)).isFalse();
- verify(mAtomicFile, never()).startWrite();
- }
-
private byte[] createXmlForParsing(String key, Boolean value) throws Exception {
PersistableBundle bundle = new PersistableBundle();
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
diff --git a/tools/Android.bp b/tools/Android.bp
index b7b2aaa..9216b5b 100644
--- a/tools/Android.bp
+++ b/tools/Android.bp
@@ -42,6 +42,7 @@
name: "jarjar-rules-generator-testjavalib",
srcs: ["testdata/java/**/*.java"],
libs: ["unsupportedappusage"],
+ sdk_version: "core_platform",
visibility: ["//visibility:private"],
}
@@ -56,6 +57,7 @@
static_libs: [
"framework-connectivity.stubs.module_lib",
],
+ sdk_version: "module_current",
// Not strictly necessary but specified as this MUST not have generate
// a dex jar as that will break the tests.
compile_dex: false,
@@ -67,6 +69,7 @@
static_libs: [
"framework-connectivity-t.stubs.module_lib",
],
+ sdk_version: "module_current",
// Not strictly necessary but specified as this MUST not have generate
// a dex jar as that will break the tests.
compile_dex: false,