Merge "Remove unused library visibility" into main
diff --git a/bpf_progs/netd.c b/bpf_progs/netd.c
index c4b27b8..5e401aa 100644
--- a/bpf_progs/netd.c
+++ b/bpf_progs/netd.c
@@ -550,7 +550,7 @@
bpf_cgroup_egress_trace_user, KVER_5_8, KVER_INF,
BPFLOADER_IGNORED_ON_VERSION, BPFLOADER_MAX_VER, OPTIONAL,
"fs_bpf_netd_readonly", "",
- LOAD_ON_ENG, IGNORE_ON_USER, LOAD_ON_USERDEBUG)
+ IGNORE_ON_ENG, LOAD_ON_USER, IGNORE_ON_USERDEBUG)
(struct __sk_buff* skb) {
return bpf_traffic_account(skb, EGRESS, TRACE_ON, KVER_5_8);
}
diff --git a/common/FlaggedApi.bp b/common/FlaggedApi.bp
index 449d7ae..56625c5 100644
--- a/common/FlaggedApi.bp
+++ b/common/FlaggedApi.bp
@@ -23,6 +23,14 @@
}
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",
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 8c448e6..19b522c 100644
--- a/common/flags.aconfig
+++ b/common/flags.aconfig
@@ -26,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"
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 9d00878..bc919ac 100644
--- a/framework-t/Android.bp
+++ b/framework-t/Android.bp
@@ -198,6 +198,7 @@
],
aconfig_declarations: [
"com.android.net.flags-aconfig",
+ "com.android.net.thread.flags-aconfig",
"nearby_flags",
],
}
diff --git a/framework-t/src/android/net/nsd/AdvertisingRequest.java b/framework-t/src/android/net/nsd/AdvertisingRequest.java
index b1ef98f..2895b0c 100644
--- a/framework-t/src/android/net/nsd/AdvertisingRequest.java
+++ b/framework-t/src/android/net/nsd/AdvertisingRequest.java
@@ -17,11 +17,13 @@
import android.annotation.LongDef;
import android.annotation.NonNull;
+import android.annotation.Nullable;
import android.os.Parcel;
import android.os.Parcelable;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
+import java.time.Duration;
import java.util.Objects;
/**
@@ -34,7 +36,7 @@
/**
* Only update the registration without sending exit and re-announcement.
*/
- public static final int NSD_ADVERTISING_UPDATE_ONLY = 1;
+ public static final long NSD_ADVERTISING_UPDATE_ONLY = 1;
@NonNull
@@ -46,7 +48,9 @@
NsdServiceInfo.class.getClassLoader(), NsdServiceInfo.class);
final int protocolType = in.readInt();
final long advertiseConfig = in.readLong();
- return new AdvertisingRequest(serviceInfo, protocolType, advertiseConfig);
+ final long ttlSeconds = in.readLong();
+ final Duration ttl = ttlSeconds < 0 ? null : Duration.ofSeconds(ttlSeconds);
+ return new AdvertisingRequest(serviceInfo, protocolType, advertiseConfig, ttl);
}
@Override
@@ -60,6 +64,9 @@
// Bitmask of @AdvertisingConfig flags. Uses a long to allow 64 possible flags in the future.
private final long mAdvertisingConfig;
+ @Nullable
+ private final Duration mTtl;
+
/**
* @hide
*/
@@ -73,10 +80,11 @@
* The constructor for the advertiseRequest
*/
private AdvertisingRequest(@NonNull NsdServiceInfo serviceInfo, int protocolType,
- long advertisingConfig) {
+ long advertisingConfig, @NonNull Duration ttl) {
mServiceInfo = serviceInfo;
mProtocolType = protocolType;
mAdvertisingConfig = advertisingConfig;
+ mTtl = ttl;
}
/**
@@ -101,12 +109,25 @@
return mAdvertisingConfig;
}
+ /**
+ * Returns the time interval that the resource records may be cached on a DNS resolver or
+ * {@code null} if not specified.
+ *
+ * @hide
+ */
+ // @FlaggedApi(NsdManager.Flags.NSD_CUSTOM_TTL_ENABLED)
+ @Nullable
+ public Duration getTtl() {
+ return mTtl;
+ }
+
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append("serviceInfo: ").append(mServiceInfo)
.append(", protocolType: ").append(mProtocolType)
- .append(", advertisingConfig: ").append(mAdvertisingConfig);
+ .append(", advertisingConfig: ").append(mAdvertisingConfig)
+ .append(", ttl: ").append(mTtl);
return sb.toString();
}
@@ -120,13 +141,14 @@
final AdvertisingRequest otherRequest = (AdvertisingRequest) other;
return mServiceInfo.equals(otherRequest.mServiceInfo)
&& mProtocolType == otherRequest.mProtocolType
- && mAdvertisingConfig == otherRequest.mAdvertisingConfig;
+ && mAdvertisingConfig == otherRequest.mAdvertisingConfig
+ && Objects.equals(mTtl, otherRequest.mTtl);
}
}
@Override
public int hashCode() {
- return Objects.hash(mServiceInfo, mProtocolType, mAdvertisingConfig);
+ return Objects.hash(mServiceInfo, mProtocolType, mAdvertisingConfig, mTtl);
}
@Override
@@ -139,6 +161,7 @@
dest.writeParcelable(mServiceInfo, flags);
dest.writeInt(mProtocolType);
dest.writeLong(mAdvertisingConfig);
+ dest.writeLong(mTtl == null ? -1 : mTtl.getSeconds());
}
// @FlaggedApi(NsdManager.Flags.ADVERTISE_REQUEST_API)
@@ -151,6 +174,8 @@
private final NsdServiceInfo mServiceInfo;
private final int mProtocolType;
private long mAdvertisingConfig;
+ @Nullable
+ private Duration mTtl;
/**
* Creates a new {@link Builder} object.
*/
@@ -170,11 +195,44 @@
return this;
}
+ /**
+ * Sets the time interval that the resource records may be cached on a DNS resolver.
+ *
+ * If this method is not called or {@code ttl} is {@code null}, default TTL values
+ * will be used for the service when it's registered. Otherwise, the {@code ttl}
+ * will be used for all resource records of this service.
+ *
+ * When registering a service, {@link NsdManager#FAILURE_BAD_PARAMETERS} will be returned
+ * if {@code ttl} is smaller than 30 seconds.
+ *
+ * Note: only number of seconds of {@code ttl} is used.
+ *
+ * @param ttl the maximum duration that the DNS resource records will be cached
+ *
+ * @see AdvertisingRequest#getTtl
+ * @hide
+ */
+ // @FlaggedApi(NsdManager.Flags.NSD_CUSTOM_TTL_ENABLED)
+ @NonNull
+ public Builder setTtl(@Nullable Duration ttl) {
+ if (ttl == null) {
+ mTtl = null;
+ return this;
+ }
+ final long ttlSeconds = ttl.getSeconds();
+ if (ttlSeconds < 0 || ttlSeconds > 0xffffffffL) {
+ throw new IllegalArgumentException(
+ "ttlSeconds exceeds the allowed range (value = " + ttlSeconds
+ + ", allowedRanged = [0, 0xffffffffL])");
+ }
+ mTtl = Duration.ofSeconds(ttlSeconds);
+ return this;
+ }
/** Creates a new {@link AdvertisingRequest} object. */
@NonNull
public AdvertisingRequest build() {
- return new AdvertisingRequest(mServiceInfo, mProtocolType, mAdvertisingConfig);
+ return new AdvertisingRequest(mServiceInfo, mProtocolType, mAdvertisingConfig, mTtl);
}
}
}
diff --git a/framework-t/src/android/net/nsd/NsdManager.java b/framework-t/src/android/net/nsd/NsdManager.java
index f6e1324..1001423 100644
--- a/framework-t/src/android/net/nsd/NsdManager.java
+++ b/framework-t/src/android/net/nsd/NsdManager.java
@@ -160,6 +160,8 @@
"com.android.net.flags.advertise_request_api";
static final String NSD_CUSTOM_HOSTNAME_ENABLED =
"com.android.net.flags.nsd_custom_hostname_enabled";
+ static final String NSD_CUSTOM_TTL_ENABLED =
+ "com.android.net.flags.nsd_custom_ttl_enabled";
}
/**
@@ -327,6 +329,20 @@
/** Dns based service discovery protocol */
public static final int PROTOCOL_DNS_SD = 0x0001;
+ /**
+ * The minimum TTL seconds which is allowed for a service registration.
+ *
+ * @hide
+ */
+ public static final long TTL_SECONDS_MIN = 30L;
+
+ /**
+ * The maximum TTL seconds which is allowed for a service registration.
+ *
+ * @hide
+ */
+ public static final long TTL_SECONDS_MAX = 10 * 3600L;
+
private static final SparseArray<String> EVENT_NAMES = new SparseArray<>();
static {
EVENT_NAMES.put(DISCOVER_SERVICES, "DISCOVER_SERVICES");
diff --git a/framework-t/src/android/net/nsd/NsdServiceInfo.java b/framework-t/src/android/net/nsd/NsdServiceInfo.java
index 146d4ca..f4cc2ac 100644
--- a/framework-t/src/android/net/nsd/NsdServiceInfo.java
+++ b/framework-t/src/android/net/nsd/NsdServiceInfo.java
@@ -16,8 +16,6 @@
package android.net.nsd;
-import static java.nio.charset.StandardCharsets.UTF_8;
-
import android.annotation.FlaggedApi;
import android.annotation.NonNull;
import android.annotation.Nullable;
@@ -35,6 +33,7 @@
import java.io.UnsupportedEncodingException;
import java.net.InetAddress;
import java.nio.charset.StandardCharsets;
+import java.time.Instant;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
@@ -71,6 +70,10 @@
private int mInterfaceIndex;
+ // The timestamp that all resource records associated with this service are considered invalid.
+ @Nullable
+ private Instant mExpirationTime;
+
public NsdServiceInfo() {
mSubtypes = new ArraySet<>();
mTxtRecord = new ArrayMap<>();
@@ -99,6 +102,7 @@
mPort = other.getPort();
mNetwork = other.getNetwork();
mInterfaceIndex = other.getInterfaceIndex();
+ mExpirationTime = other.getExpirationTime();
}
/** Get the service name */
@@ -490,6 +494,38 @@
return Collections.unmodifiableSet(mSubtypes);
}
+ /**
+ * Sets the timestamp after when this service is expired.
+ *
+ * Note: only number of seconds of {@code expirationTime} is used.
+ *
+ * @hide
+ */
+ public void setExpirationTime(@Nullable Instant expirationTime) {
+ if (expirationTime == null) {
+ mExpirationTime = null;
+ } else {
+ mExpirationTime = Instant.ofEpochSecond(expirationTime.getEpochSecond());
+ }
+ }
+
+ /**
+ * Returns the timestamp after when this service is expired or {@code null} if it's unknown.
+ *
+ * A service is considered expired if any of its DNS record is expired.
+ *
+ * Clients that are depending on the refreshness of the service information should not continue
+ * use this service after the returned timestamp. Instead, clients may re-send queries for the
+ * service to get updated the service information.
+ *
+ * @hide
+ */
+ // @FlaggedApi(NsdManager.Flags.NSD_CUSTOM_TTL_ENABLED)
+ @Nullable
+ public Instant getExpirationTime() {
+ return mExpirationTime;
+ }
+
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
@@ -499,7 +535,8 @@
.append(", hostAddresses: ").append(TextUtils.join(", ", mHostAddresses))
.append(", hostname: ").append(mHostname)
.append(", port: ").append(mPort)
- .append(", network: ").append(mNetwork);
+ .append(", network: ").append(mNetwork)
+ .append(", expirationTime: ").append(mExpirationTime);
byte[] txtRecord = getTxtRecord();
sb.append(", txtRecord: ").append(new String(txtRecord, StandardCharsets.UTF_8));
@@ -539,6 +576,7 @@
InetAddressUtils.parcelInetAddress(dest, address, flags);
}
dest.writeString(mHostname);
+ dest.writeLong(mExpirationTime != null ? mExpirationTime.getEpochSecond() : -1);
}
/** Implement the Parcelable interface */
@@ -569,6 +607,8 @@
info.mHostAddresses.add(InetAddressUtils.unparcelInetAddress(in));
}
info.mHostname = in.readString();
+ final long seconds = in.readLong();
+ info.setExpirationTime(seconds < 0 ? null : Instant.ofEpochSecond(seconds));
return info;
}
diff --git a/framework/Android.bp b/framework/Android.bp
index a351683..52f2c7c 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
diff --git a/nearby/service/Android.bp b/nearby/service/Android.bp
index d34fd83..749113d 100644
--- a/nearby/service/Android.bp
+++ b/nearby/service/Android.bp
@@ -43,6 +43,7 @@
],
static_libs: [
"androidx.core_core",
+ "android.hardware.bluetooth.finder-V1-java",
"guava",
"libprotobuf-java-lite",
"modules-utils-build",
diff --git a/nearby/service/java/com/android/server/nearby/managers/BluetoothFinderManager.java b/nearby/service/java/com/android/server/nearby/managers/BluetoothFinderManager.java
index 63ff516..365b099 100644
--- a/nearby/service/java/com/android/server/nearby/managers/BluetoothFinderManager.java
+++ b/nearby/service/java/com/android/server/nearby/managers/BluetoothFinderManager.java
@@ -16,26 +16,151 @@
package com.android.server.nearby.managers;
+import static com.android.server.nearby.NearbyService.TAG;
+
+import android.annotation.TargetApi;
+import android.hardware.bluetooth.finder.Eid;
+import android.hardware.bluetooth.finder.IBluetoothFinder;
import android.nearby.PoweredOffFindingEphemeralId;
+import android.os.Build;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.os.ServiceManager;
+import android.os.ServiceSpecificException;
+import android.util.Log;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.modules.utils.build.SdkLevel;
import java.util.List;
/** Connects to {@link IBluetoothFinder} HAL and invokes its API. */
-// A placeholder implementation until the HAL API can be used.
+@TargetApi(Build.VERSION_CODES.TIRAMISU)
public class BluetoothFinderManager {
- private boolean mPoweredOffFindingModeEnabled = false;
+ private static final String HAL_INSTANCE_NAME = IBluetoothFinder.DESCRIPTOR + "/default";
- /** An empty implementation of the corresponding HAL API call. */
- public void sendEids(List<PoweredOffFindingEphemeralId> eids) {}
+ private IBluetoothFinder mBluetoothFinder;
+ private IBinder.DeathRecipient mServiceDeathRecipient;
+ private final Object mLock = new Object();
- /** A placeholder implementation of the corresponding HAL API call. */
- public void setPoweredOffFinderMode(boolean enable) {
- mPoweredOffFindingModeEnabled = enable;
+ private boolean initBluetoothFinderHal() {
+ final String methodStr = "initBluetoothFinderHal";
+ if (!SdkLevel.isAtLeastV()) return false;
+ synchronized (mLock) {
+ if (mBluetoothFinder != null) {
+ Log.i(TAG, "Bluetooth Finder HAL is already initialized");
+ return true;
+ }
+ try {
+ mBluetoothFinder = getServiceMockable();
+ if (mBluetoothFinder == null) {
+ Log.e(TAG, "Unable to obtain IBluetoothFinder");
+ return false;
+ }
+ Log.i(TAG, "Obtained IBluetoothFinder. Local ver: " + IBluetoothFinder.VERSION
+ + ", Remote ver: " + mBluetoothFinder.getInterfaceVersion());
+
+ IBinder serviceBinder = getServiceBinderMockable();
+ if (serviceBinder == null) {
+ Log.e(TAG, "Unable to obtain the service binder for IBluetoothFinder");
+ return false;
+ }
+ mServiceDeathRecipient = new BluetoothFinderDeathRecipient();
+ serviceBinder.linkToDeath(mServiceDeathRecipient, /* flags= */ 0);
+
+ Log.i(TAG, "Bluetooth Finder HAL initialization was successful");
+ return true;
+ } catch (RemoteException e) {
+ handleRemoteException(e, methodStr);
+ } catch (Exception e) {
+ Log.e(TAG, methodStr + " encountered an exception: " + e);
+ }
+ return false;
+ }
}
- /** A placeholder implementation of the corresponding HAL API call. */
+ @VisibleForTesting
+ protected IBluetoothFinder getServiceMockable() {
+ return IBluetoothFinder.Stub.asInterface(
+ ServiceManager.waitForDeclaredService(HAL_INSTANCE_NAME));
+ }
+
+ @VisibleForTesting
+ protected IBinder getServiceBinderMockable() {
+ return mBluetoothFinder.asBinder();
+ }
+
+ private class BluetoothFinderDeathRecipient implements IBinder.DeathRecipient {
+ @Override
+ public void binderDied() {
+ Log.e(TAG, "BluetoothFinder service died.");
+ synchronized (mLock) {
+ mBluetoothFinder = null;
+ }
+ }
+ }
+
+ /** See comments for {@link IBluetoothFinder#sendEids(Eid[])} */
+ public void sendEids(List<PoweredOffFindingEphemeralId> eids) {
+ final String methodStr = "sendEids";
+ if (!checkHalAndLogFailure(methodStr)) return;
+ Eid[] eidArray = eids.stream().map(
+ ephmeralId -> {
+ Eid eid = new Eid();
+ eid.bytes = ephmeralId.bytes;
+ return eid;
+ }).toArray(Eid[]::new);
+ try {
+ mBluetoothFinder.sendEids(eidArray);
+ } catch (RemoteException e) {
+ handleRemoteException(e, methodStr);
+ } catch (ServiceSpecificException e) {
+ handleServiceSpecificException(e, methodStr);
+ }
+ }
+
+ /** See comments for {@link IBluetoothFinder#setPoweredOffFinderMode(boolean)} */
+ public void setPoweredOffFinderMode(boolean enable) {
+ final String methodStr = "setPoweredOffMode";
+ if (!checkHalAndLogFailure(methodStr)) return;
+ try {
+ mBluetoothFinder.setPoweredOffFinderMode(enable);
+ } catch (RemoteException e) {
+ handleRemoteException(e, methodStr);
+ } catch (ServiceSpecificException e) {
+ handleServiceSpecificException(e, methodStr);
+ }
+ }
+
+ /** See comments for {@link IBluetoothFinder#getPoweredOffFinderMode()} */
public boolean getPoweredOffFinderMode() {
- return mPoweredOffFindingModeEnabled;
+ final String methodStr = "getPoweredOffMode";
+ if (!checkHalAndLogFailure(methodStr)) return false;
+ try {
+ return mBluetoothFinder.getPoweredOffFinderMode();
+ } catch (RemoteException e) {
+ handleRemoteException(e, methodStr);
+ } catch (ServiceSpecificException e) {
+ handleServiceSpecificException(e, methodStr);
+ }
+ return false;
+ }
+
+ private boolean checkHalAndLogFailure(String methodStr) {
+ if ((mBluetoothFinder == null) && !initBluetoothFinderHal()) {
+ Log.e(TAG, "Unable to call " + methodStr + " because IBluetoothFinder is null.");
+ return false;
+ }
+ return true;
+ }
+
+ private void handleRemoteException(RemoteException e, String methodStr) {
+ mBluetoothFinder = null;
+ Log.e(TAG, methodStr + " failed with remote exception: " + e);
+ }
+
+ private void handleServiceSpecificException(ServiceSpecificException e, String methodStr) {
+ Log.e(TAG, methodStr + " failed with service-specific exception: " + e);
}
}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/managers/BluetoothFinderManagerTest.java b/nearby/tests/unit/src/com/android/server/nearby/managers/BluetoothFinderManagerTest.java
new file mode 100644
index 0000000..671b5c5
--- /dev/null
+++ b/nearby/tests/unit/src/com/android/server/nearby/managers/BluetoothFinderManagerTest.java
@@ -0,0 +1,186 @@
+/*
+ * 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.nearby.managers;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.anyBoolean;
+import static org.mockito.Mockito.anyInt;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.hardware.bluetooth.finder.Eid;
+import android.hardware.bluetooth.finder.IBluetoothFinder;
+import android.nearby.PoweredOffFindingEphemeralId;
+import android.os.IBinder;
+import android.os.IBinder.DeathRecipient;
+import android.os.RemoteException;
+import android.os.ServiceSpecificException;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.util.List;
+
+public class BluetoothFinderManagerTest {
+ private BluetoothFinderManager mBluetoothFinderManager;
+ private boolean mGetServiceCalled = false;
+
+ @Mock private IBluetoothFinder mIBluetoothFinderMock;
+ @Mock private IBinder mServiceBinderMock;
+
+ private ArgumentCaptor<DeathRecipient> mDeathRecipientCaptor =
+ ArgumentCaptor.forClass(DeathRecipient.class);
+
+ private ArgumentCaptor<Eid[]> mEidArrayCaptor = ArgumentCaptor.forClass(Eid[].class);
+
+ private class BluetoothFinderManagerSpy extends BluetoothFinderManager {
+ @Override
+ protected IBluetoothFinder getServiceMockable() {
+ mGetServiceCalled = true;
+ return mIBluetoothFinderMock;
+ }
+
+ @Override
+ protected IBinder getServiceBinderMockable() {
+ return mServiceBinderMock;
+ }
+ }
+
+ @Before
+ public void setup() {
+ MockitoAnnotations.initMocks(this);
+ mBluetoothFinderManager = new BluetoothFinderManagerSpy();
+ }
+
+ @Test
+ public void testSendEids() throws Exception {
+ byte[] eidBytes1 = {
+ (byte) 0xe1, (byte) 0xde, (byte) 0x1d, (byte) 0xe1, (byte) 0xde, (byte) 0x1d,
+ (byte) 0xe1, (byte) 0xde, (byte) 0x1d, (byte) 0xe1, (byte) 0xde, (byte) 0x1d,
+ (byte) 0xe1, (byte) 0xde, (byte) 0x1d, (byte) 0xe1, (byte) 0xde, (byte) 0x1d,
+ (byte) 0xe1, (byte) 0xde
+ };
+ byte[] eidBytes2 = {
+ (byte) 0xf2, (byte) 0xef, (byte) 0x2e, (byte) 0xf2, (byte) 0xef, (byte) 0x2e,
+ (byte) 0xf2, (byte) 0xef, (byte) 0x2e, (byte) 0xf2, (byte) 0xef, (byte) 0x2e,
+ (byte) 0xf2, (byte) 0xef, (byte) 0x2e, (byte) 0xf2, (byte) 0xef, (byte) 0x2e,
+ (byte) 0xf2, (byte) 0xef
+ };
+ PoweredOffFindingEphemeralId ephemeralId1 = new PoweredOffFindingEphemeralId();
+ PoweredOffFindingEphemeralId ephemeralId2 = new PoweredOffFindingEphemeralId();
+ ephemeralId1.bytes = eidBytes1;
+ ephemeralId2.bytes = eidBytes2;
+
+ mBluetoothFinderManager.sendEids(List.of(ephemeralId1, ephemeralId2));
+
+ verify(mIBluetoothFinderMock).sendEids(mEidArrayCaptor.capture());
+ assertThat(mEidArrayCaptor.getValue()[0].bytes).isEqualTo(eidBytes1);
+ assertThat(mEidArrayCaptor.getValue()[1].bytes).isEqualTo(eidBytes2);
+ }
+
+ @Test
+ public void testSendEids_remoteException() throws Exception {
+ doThrow(new RemoteException())
+ .when(mIBluetoothFinderMock).sendEids(any());
+ mBluetoothFinderManager.sendEids(List.of());
+
+ // Verify that we get the service again following a RemoteException.
+ mGetServiceCalled = false;
+ mBluetoothFinderManager.sendEids(List.of());
+ assertThat(mGetServiceCalled).isTrue();
+ }
+
+ @Test
+ public void testSendEids_serviceSpecificException() throws Exception {
+ doThrow(new ServiceSpecificException(1))
+ .when(mIBluetoothFinderMock).sendEids(any());
+ mBluetoothFinderManager.sendEids(List.of());
+ }
+
+ @Test
+ public void testSetPoweredOffFinderMode() throws Exception {
+ mBluetoothFinderManager.setPoweredOffFinderMode(true);
+ verify(mIBluetoothFinderMock).setPoweredOffFinderMode(true);
+
+ mBluetoothFinderManager.setPoweredOffFinderMode(false);
+ verify(mIBluetoothFinderMock).setPoweredOffFinderMode(false);
+ }
+
+ @Test
+ public void testSetPoweredOffFinderMode_remoteException() throws Exception {
+ doThrow(new RemoteException())
+ .when(mIBluetoothFinderMock).setPoweredOffFinderMode(anyBoolean());
+ mBluetoothFinderManager.setPoweredOffFinderMode(true);
+
+ // Verify that we get the service again following a RemoteException.
+ mGetServiceCalled = false;
+ mBluetoothFinderManager.setPoweredOffFinderMode(true);
+ assertThat(mGetServiceCalled).isTrue();
+ }
+
+ @Test
+ public void testSetPoweredOffFinderMode_serviceSpecificException() throws Exception {
+ doThrow(new ServiceSpecificException(1))
+ .when(mIBluetoothFinderMock).setPoweredOffFinderMode(anyBoolean());
+ mBluetoothFinderManager.setPoweredOffFinderMode(true);
+ }
+
+ @Test
+ public void testGetPoweredOffFinderMode() throws Exception {
+ when(mIBluetoothFinderMock.getPoweredOffFinderMode()).thenReturn(true);
+ assertThat(mBluetoothFinderManager.getPoweredOffFinderMode()).isTrue();
+
+ when(mIBluetoothFinderMock.getPoweredOffFinderMode()).thenReturn(false);
+ assertThat(mBluetoothFinderManager.getPoweredOffFinderMode()).isFalse();
+ }
+
+ @Test
+ public void testGetPoweredOffFinderMode_remoteException() throws Exception {
+ when(mIBluetoothFinderMock.getPoweredOffFinderMode()).thenThrow(new RemoteException());
+ assertThat(mBluetoothFinderManager.getPoweredOffFinderMode()).isFalse();
+
+ // Verify that we get the service again following a RemoteException.
+ mGetServiceCalled = false;
+ assertThat(mBluetoothFinderManager.getPoweredOffFinderMode()).isFalse();
+ assertThat(mGetServiceCalled).isTrue();
+ }
+
+ @Test
+ public void testGetPoweredOffFinderMode_serviceSpecificException() throws Exception {
+ when(mIBluetoothFinderMock.getPoweredOffFinderMode())
+ .thenThrow(new ServiceSpecificException(1));
+ assertThat(mBluetoothFinderManager.getPoweredOffFinderMode()).isFalse();
+ }
+
+ @Test
+ public void testDeathRecipient() throws Exception {
+ mBluetoothFinderManager.setPoweredOffFinderMode(true);
+ verify(mServiceBinderMock).linkToDeath(mDeathRecipientCaptor.capture(), anyInt());
+ mDeathRecipientCaptor.getValue().binderDied();
+
+ // Verify that we get the service again following a binder death.
+ mGetServiceCalled = false;
+ mBluetoothFinderManager.setPoweredOffFinderMode(true);
+ assertThat(mGetServiceCalled).isTrue();
+ }
+}
diff --git a/netbpfload/NetBpfLoad.cpp b/netbpfload/NetBpfLoad.cpp
index 2bfaee4..ed7d048 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) {
@@ -367,6 +369,13 @@
if (createSysFsBpfSubDir(location.prefix)) return 1;
}
+ // Note: there's no actual src dir for fs_bpf_loader .o's,
+ // so it is not listed in 'locations[].prefix'.
+ // This is because this is primarily meant for triggering genfscon rules,
+ // and as such this will likely always be the case.
+ // Thus we need to manually create the /sys/fs/bpf/loader subdirectory.
+ if (createSysFsBpfSubDir("loader")) return 1;
+
// Load all ELF objects, create programs and maps, and pin them
for (const auto& location : locations) {
if (loadAllElfObjects(location) != 0) {
diff --git a/service-t/src/com/android/server/NsdService.java b/service-t/src/com/android/server/NsdService.java
index 9ba49d2..cfb1a33 100644
--- a/service-t/src/com/android/server/NsdService.java
+++ b/service-t/src/com/android/server/NsdService.java
@@ -28,6 +28,7 @@
import static android.net.nsd.NsdManager.RESOLVE_SERVICE_SUCCEEDED;
import static android.net.nsd.NsdManager.SUBTYPE_LABEL_REGEX;
import static android.net.nsd.NsdManager.TYPE_REGEX;
+import static android.os.Process.SYSTEM_UID;
import static android.provider.DeviceConfig.NAMESPACE_TETHERING;
import static com.android.modules.utils.build.SdkLevel.isAtLeastU;
@@ -92,6 +93,7 @@
import com.android.modules.utils.build.SdkLevel;
import com.android.net.module.util.CollectionUtils;
import com.android.net.module.util.DeviceConfigUtils;
+import com.android.net.module.util.HandlerUtils;
import com.android.net.module.util.InetAddressUtils;
import com.android.net.module.util.PermissionUtils;
import com.android.net.module.util.SharedLog;
@@ -115,6 +117,7 @@
import java.net.NetworkInterface;
import java.net.SocketException;
import java.net.UnknownHostException;
+import java.time.Duration;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
@@ -738,6 +741,33 @@
return new ArraySet<>(subtypeMap.values());
}
+ private boolean checkTtl(
+ @Nullable Duration ttl, @NonNull ClientInfo clientInfo) {
+ if (ttl == null) {
+ return true;
+ }
+
+ final long ttlSeconds = ttl.toSeconds();
+ final int uid = clientInfo.getUid();
+
+ // Allows Thread module in the system_server to register TTL that is smaller than
+ // 30 seconds
+ final long minTtlSeconds = uid == SYSTEM_UID ? 0 : NsdManager.TTL_SECONDS_MIN;
+
+ // Allows Thread module in the system_server to register TTL that is larger than
+ // 10 hours
+ final long maxTtlSeconds =
+ uid == SYSTEM_UID ? 0xffffffffL : NsdManager.TTL_SECONDS_MAX;
+
+ if (ttlSeconds < minTtlSeconds || ttlSeconds > maxTtlSeconds) {
+ mServiceLogs.e("ttlSeconds exceeds allowed range (value = "
+ + ttlSeconds + ", allowedRange = [" + minTtlSeconds
+ + ", " + maxTtlSeconds + " ])");
+ return false;
+ }
+ return true;
+ }
+
@Override
public boolean processMessage(Message msg) {
final ClientInfo clientInfo;
@@ -964,11 +994,19 @@
break;
}
+ if (!checkTtl(advertisingRequest.getTtl(), clientInfo)) {
+ clientInfo.onRegisterServiceFailedImmediately(clientRequestId,
+ NsdManager.FAILURE_BAD_PARAMETERS, false /* isLegacy */);
+ break;
+ }
+
serviceInfo.setSubtypes(subtypes);
maybeStartMonitoringSockets();
final MdnsAdvertisingOptions mdnsAdvertisingOptions =
- MdnsAdvertisingOptions.newBuilder().setIsOnlyUpdate(
- isUpdateOnly).build();
+ MdnsAdvertisingOptions.newBuilder()
+ .setIsOnlyUpdate(isUpdateOnly)
+ .setTtl(advertisingRequest.getTtl())
+ .build();
mAdvertiser.addOrUpdateService(transactionId, serviceInfo,
mdnsAdvertisingOptions, clientInfo.mUid);
storeAdvertiserRequestMap(clientRequestId, transactionId, clientInfo,
@@ -1511,6 +1549,7 @@
network == null ? INetd.LOCAL_NET_ID : network.netId,
serviceInfo.getInterfaceIndex());
servInfo.setSubtypes(dedupSubtypeLabels(serviceInfo.getSubtypes()));
+ servInfo.setExpirationTime(serviceInfo.getExpirationTime());
return servInfo;
}
@@ -2510,6 +2549,14 @@
pw.increaseIndent();
mServiceLogs.reverseDump(pw);
pw.decreaseIndent();
+
+ //Dump DiscoveryManager
+ pw.println();
+ pw.println("DiscoveryManager:");
+ pw.increaseIndent();
+ HandlerUtils.runWithScissorsForDump(
+ mNsdStateMachine.getHandler(), () -> mMdnsDiscoveryManager.dump(pw), 10_000);
+ pw.decreaseIndent();
}
private abstract static class ClientRequest {
@@ -2671,6 +2718,10 @@
return sb.toString();
}
+ public int getUid() {
+ return mUid;
+ }
+
private boolean isPreSClient() {
return mIsPreSClient;
}
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsAdvertiser.java b/service-t/src/com/android/server/connectivity/mdns/MdnsAdvertiser.java
index 0b60572..c162bcc 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsAdvertiser.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsAdvertiser.java
@@ -449,7 +449,8 @@
mPendingRegistrations.put(id, registration);
for (int i = 0; i < mAdvertisers.size(); i++) {
try {
- mAdvertisers.valueAt(i).addService(id, registration.getServiceInfo());
+ mAdvertisers.valueAt(i).addService(id, registration.getServiceInfo(),
+ registration.getAdvertisingOptions());
} catch (NameConflictException e) {
mSharedLog.wtf("Name conflict adding services that should have unique names",
e);
@@ -515,7 +516,7 @@
final Registration registration = mPendingRegistrations.valueAt(i);
try {
advertiser.addService(mPendingRegistrations.keyAt(i),
- registration.getServiceInfo());
+ registration.getServiceInfo(), registration.getAdvertisingOptions());
} catch (NameConflictException e) {
mSharedLog.wtf("Name conflict adding services that should have unique names",
e);
@@ -587,15 +588,17 @@
@NonNull
private NsdServiceInfo mServiceInfo;
final int mClientUid;
+ private final MdnsAdvertisingOptions mAdvertisingOptions;
int mConflictDuringProbingCount;
int mConflictAfterProbingCount;
-
- private Registration(@NonNull NsdServiceInfo serviceInfo, int clientUid) {
+ private Registration(@NonNull NsdServiceInfo serviceInfo, int clientUid,
+ @NonNull MdnsAdvertisingOptions advertisingOptions) {
this.mOriginalServiceName = serviceInfo.getServiceName();
this.mOriginalHostname = serviceInfo.getHostname();
this.mServiceInfo = serviceInfo;
this.mClientUid = clientUid;
+ this.mAdvertisingOptions = advertisingOptions;
}
/** Check if the new {@link NsdServiceInfo} doesn't update any data other than subtypes. */
@@ -697,6 +700,11 @@
public NsdServiceInfo getServiceInfo() {
return mServiceInfo;
}
+
+ @NonNull
+ public MdnsAdvertisingOptions getAdvertisingOptions() {
+ return mAdvertisingOptions;
+ }
}
/**
@@ -855,7 +863,7 @@
}
mSharedLog.i("Adding service " + service + " with ID " + id + " and subtypes "
+ subtypes + " advertisingOptions " + advertisingOptions);
- registration = new Registration(service, clientUid);
+ registration = new Registration(service, clientUid, advertisingOptions);
final BiPredicate<Network, InterfaceAdvertiserRequest> checkConflictFilter;
if (network == null) {
// If registering on all networks, no advertiser must have conflicts
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsAdvertisingOptions.java b/service-t/src/com/android/server/connectivity/mdns/MdnsAdvertisingOptions.java
index e7a6ca7..a81d1e4 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsAdvertisingOptions.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsAdvertisingOptions.java
@@ -16,6 +16,11 @@
package com.android.server.connectivity.mdns;
+import android.annotation.Nullable;
+
+import java.time.Duration;
+import java.util.Objects;
+
/**
* API configuration parameters for advertising the mDNS service.
*
@@ -27,13 +32,15 @@
private static MdnsAdvertisingOptions sDefaultOptions;
private final boolean mIsOnlyUpdate;
+ @Nullable
+ private final Duration mTtl;
/**
* Parcelable constructs for a {@link MdnsAdvertisingOptions}.
*/
- MdnsAdvertisingOptions(
- boolean isOnlyUpdate) {
+ MdnsAdvertisingOptions(boolean isOnlyUpdate, @Nullable Duration ttl) {
this.mIsOnlyUpdate = isOnlyUpdate;
+ this.mTtl = ttl;
}
/**
@@ -60,9 +67,36 @@
return mIsOnlyUpdate;
}
+ /**
+ * Returns the TTL for all records in a service.
+ */
+ @Nullable
+ public Duration getTtl() {
+ return mTtl;
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (this == other) {
+ return true;
+ } else if (!(other instanceof MdnsAdvertisingOptions)) {
+ return false;
+ } else {
+ final MdnsAdvertisingOptions otherOptions = (MdnsAdvertisingOptions) other;
+ return mIsOnlyUpdate == otherOptions.mIsOnlyUpdate
+ && Objects.equals(mTtl, otherOptions.mTtl);
+ }
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mIsOnlyUpdate, mTtl);
+ }
+
@Override
public String toString() {
- return "MdnsAdvertisingOptions{" + "mIsOnlyUpdate=" + mIsOnlyUpdate + '}';
+ return "MdnsAdvertisingOptions{" + "mIsOnlyUpdate=" + mIsOnlyUpdate + ", mTtl=" + mTtl
+ + '}';
}
/**
@@ -70,6 +104,8 @@
*/
public static final class Builder {
private boolean mIsOnlyUpdate = false;
+ @Nullable
+ private Duration mTtl;
private Builder() {
}
@@ -83,10 +119,18 @@
}
/**
+ * Sets the TTL duration for all records of the service.
+ */
+ public Builder setTtl(@Nullable Duration ttl) {
+ this.mTtl = ttl;
+ return this;
+ }
+
+ /**
* Builds a {@link MdnsAdvertisingOptions} with the arguments supplied to this builder.
*/
public MdnsAdvertisingOptions build() {
- return new MdnsAdvertisingOptions(mIsOnlyUpdate);
+ return new MdnsAdvertisingOptions(mIsOnlyUpdate, mTtl);
}
}
}
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsDiscoveryManager.java b/service-t/src/com/android/server/connectivity/mdns/MdnsDiscoveryManager.java
index 1d6039c..21b7069 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsDiscoveryManager.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsDiscoveryManager.java
@@ -34,6 +34,7 @@
import com.android.server.connectivity.mdns.util.MdnsUtils;
import java.io.IOException;
+import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
@@ -363,4 +364,18 @@
executorProvider.newServiceTypeClientSchedulerExecutor(), socketKey,
sharedLog.forSubComponent(tag), looper, serviceCache);
}
+
+ /**
+ * Dump DiscoveryManager state.
+ */
+ public void dump(PrintWriter pw) {
+ discoveryExecutor.checkAndRunOnHandlerThread(() -> {
+ pw.println();
+ // Dump ServiceTypeClients
+ for (MdnsServiceTypeClient serviceTypeClient
+ : perSocketServiceTypeClients.getAllMdnsServiceTypeClient()) {
+ serviceTypeClient.dump(pw);
+ }
+ });
+ }
}
\ No newline at end of file
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsInterfaceAdvertiser.java b/service-t/src/com/android/server/connectivity/mdns/MdnsInterfaceAdvertiser.java
index aa51c41..c2363c0 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsInterfaceAdvertiser.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsInterfaceAdvertiser.java
@@ -258,8 +258,10 @@
*
* @throws NameConflictException There is already a service being advertised with that name.
*/
- public void addService(int id, NsdServiceInfo service) throws NameConflictException {
- final int replacedExitingService = mRecordRepository.addService(id, service);
+ public void addService(int id, NsdServiceInfo service,
+ @NonNull MdnsAdvertisingOptions advertisingOptions) throws NameConflictException {
+ final int replacedExitingService =
+ mRecordRepository.addService(id, service, advertisingOptions.getTtl());
// Cancel announcements for the existing service. This only happens for exiting services
// (so cancelling exiting announcements), as per RecordRepository.addService.
if (replacedExitingService >= 0) {
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 ed0bde2..ac64c3a 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsRecordRepository.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsRecordRepository.java
@@ -45,6 +45,7 @@
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.NetworkInterface;
+import java.time.Duration;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
@@ -75,9 +76,9 @@
// TTL for records with a host name as the resource record's name (e.g., A, AAAA, HINFO) or a
// host name contained within the resource record's rdata (e.g., SRV, reverse mapping PTR
// record)
- private static final long NAME_RECORDS_TTL_MILLIS = TimeUnit.SECONDS.toMillis(120);
+ private static final long DEFAULT_NAME_RECORDS_TTL_MILLIS = TimeUnit.SECONDS.toMillis(120);
// TTL for other records
- private static final long NON_NAME_RECORDS_TTL_MILLIS = TimeUnit.MINUTES.toMillis(75);
+ private static final long DEFAULT_NON_NAME_RECORDS_TTL_MILLIS = TimeUnit.MINUTES.toMillis(75);
// Top-level domain for link-local queries, as per RFC6762 3.
private static final String LOCAL_TLD = "local";
@@ -193,6 +194,9 @@
*/
private boolean isProbing;
+ @Nullable
+ private Duration ttl;
+
/**
* Create a ServiceRegistration with only update the subType.
*/
@@ -200,16 +204,32 @@
NsdServiceInfo newServiceInfo = new NsdServiceInfo(serviceInfo);
newServiceInfo.setSubtypes(newSubtypes);
return new ServiceRegistration(srvRecord.record.getServiceHost(), newServiceInfo,
- repliedServiceCount, sentPacketCount, exiting, isProbing);
+ repliedServiceCount, sentPacketCount, exiting, isProbing, ttl);
}
/**
* Create a ServiceRegistration for dns-sd service registration (RFC6763).
*/
ServiceRegistration(@NonNull String[] deviceHostname, @NonNull NsdServiceInfo serviceInfo,
- int repliedServiceCount, int sentPacketCount, boolean exiting, boolean isProbing) {
+ int repliedServiceCount, int sentPacketCount, boolean exiting, boolean isProbing,
+ @Nullable Duration ttl) {
this.serviceInfo = serviceInfo;
+ final long nonNameRecordsTtlMillis;
+ final long nameRecordsTtlMillis;
+
+ // When custom TTL is specified, all records of the service will use the custom TTL.
+ // This is typically useful for SRP (Service Registration Protocol:
+ // https://datatracker.ietf.org/doc/html/draft-ietf-dnssd-srp-24) Advertising Proxy
+ // where all records in a single SRP are required the same TTL.
+ if (ttl != null) {
+ nonNameRecordsTtlMillis = ttl.toMillis();
+ nameRecordsTtlMillis = ttl.toMillis();
+ } else {
+ nonNameRecordsTtlMillis = DEFAULT_NON_NAME_RECORDS_TTL_MILLIS;
+ nameRecordsTtlMillis = DEFAULT_NAME_RECORDS_TTL_MILLIS;
+ }
+
final boolean hasService = !TextUtils.isEmpty(serviceInfo.getServiceType());
final boolean hasCustomHost = !TextUtils.isEmpty(serviceInfo.getHostname());
final String[] hostname =
@@ -229,7 +249,7 @@
serviceType,
0L /* receiptTimeMillis */,
false /* cacheFlush */,
- NON_NAME_RECORDS_TTL_MILLIS,
+ nonNameRecordsTtlMillis,
serviceName),
true /* sharedName */));
for (String subtype : serviceInfo.getSubtypes()) {
@@ -239,7 +259,7 @@
MdnsUtils.constructFullSubtype(serviceType, subtype),
0L /* receiptTimeMillis */,
false /* cacheFlush */,
- NON_NAME_RECORDS_TTL_MILLIS,
+ nonNameRecordsTtlMillis,
serviceName),
true /* sharedName */));
}
@@ -249,7 +269,7 @@
new MdnsServiceRecord(serviceName,
0L /* receiptTimeMillis */,
true /* cacheFlush */,
- NAME_RECORDS_TTL_MILLIS,
+ nameRecordsTtlMillis,
0 /* servicePriority */, 0 /* serviceWeight */,
serviceInfo.getPort(),
hostname),
@@ -261,7 +281,7 @@
0L /* receiptTimeMillis */,
// Service name is verified unique after probing
true /* cacheFlush */,
- NON_NAME_RECORDS_TTL_MILLIS,
+ nonNameRecordsTtlMillis,
attrsToTextEntries(serviceInfo.getAttributes())),
false /* sharedName */);
@@ -275,7 +295,7 @@
DNS_SD_SERVICE_TYPE,
0L /* receiptTimeMillis */,
false /* cacheFlush */,
- NON_NAME_RECORDS_TTL_MILLIS,
+ nonNameRecordsTtlMillis,
serviceType),
true /* sharedName */));
} else {
@@ -292,7 +312,7 @@
new MdnsInetAddressRecord(hostname,
0L /* receiptTimeMillis */,
true /* cacheFlush */,
- NAME_RECORDS_TTL_MILLIS,
+ nameRecordsTtlMillis,
address),
false /* sharedName */));
}
@@ -315,9 +335,9 @@
* @param serviceInfo Service to advertise
*/
ServiceRegistration(@NonNull String[] deviceHostname, @NonNull NsdServiceInfo serviceInfo,
- int repliedServiceCount, int sentPacketCount) {
+ int repliedServiceCount, int sentPacketCount, @Nullable Duration ttl) {
this(deviceHostname, serviceInfo,repliedServiceCount, sentPacketCount,
- false /* exiting */, true /* isProbing */);
+ false /* exiting */, true /* isProbing */, ttl);
}
void setProbing(boolean probing) {
@@ -339,7 +359,7 @@
revDnsAddr,
0L /* receiptTimeMillis */,
true /* cacheFlush */,
- NAME_RECORDS_TTL_MILLIS,
+ DEFAULT_NAME_RECORDS_TTL_MILLIS,
mDeviceHostname),
false /* sharedName */));
@@ -349,7 +369,7 @@
mDeviceHostname,
0L /* receiptTimeMillis */,
true /* cacheFlush */,
- NAME_RECORDS_TTL_MILLIS,
+ DEFAULT_NAME_RECORDS_TTL_MILLIS,
addr.getAddress()),
false /* sharedName */));
}
@@ -378,11 +398,13 @@
* This may remove/replace any existing service that used the name added but is exiting.
* @param serviceId A unique service ID.
* @param serviceInfo Service info to add.
+ * @param ttl the TTL duration for all records of {@code serviceInfo} or {@code null}
* @return If the added service replaced another with a matching name (which was exiting), the
* ID of the replaced service.
* @throws NameConflictException There is already a (non-exiting) service using the name.
*/
- public int addService(int serviceId, NsdServiceInfo serviceInfo) throws NameConflictException {
+ public int addService(int serviceId, NsdServiceInfo serviceInfo, @Nullable Duration ttl)
+ throws NameConflictException {
if (mServices.contains(serviceId)) {
throw new IllegalArgumentException(
"Service ID must not be reused across registrations: " + serviceId);
@@ -397,7 +419,7 @@
final ServiceRegistration registration = new ServiceRegistration(
mDeviceHostname, serviceInfo, NO_PACKET /* repliedServiceCount */,
- NO_PACKET /* sentPacketCount */);
+ NO_PACKET /* sentPacketCount */, ttl);
mServices.put(serviceId, registration);
// Remove existing exiting service
@@ -776,7 +798,7 @@
true /* cacheFlush */,
// TODO: RFC6762 6.1: "In general, the TTL given for an NSEC record SHOULD
// be the same as the TTL that the record would have had, had it existed."
- NAME_RECORDS_TTL_MILLIS,
+ DEFAULT_NAME_RECORDS_TTL_MILLIS,
question.getName(),
new int[] { question.getType() });
additionalAnswerInfo.add(
@@ -1211,7 +1233,7 @@
if (existing == null) return null;
final ServiceRegistration newService = new ServiceRegistration(mDeviceHostname, newInfo,
- existing.repliedServiceCount, existing.sentPacketCount);
+ existing.repliedServiceCount, existing.sentPacketCount, existing.ttl);
mServices.put(serviceId, newService);
return makeProbingInfo(serviceId, newService);
}
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsServiceInfo.java b/service-t/src/com/android/server/connectivity/mdns/MdnsServiceInfo.java
index 78df6df..f60a95e 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsServiceInfo.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsServiceInfo.java
@@ -28,6 +28,7 @@
import com.android.net.module.util.ByteUtils;
import java.nio.charset.Charset;
+import java.time.Instant;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
@@ -62,7 +63,8 @@
source.createStringArrayList(),
source.createTypedArrayList(TextEntry.CREATOR),
source.readInt(),
- source.readParcelable(null));
+ source.readParcelable(Network.class.getClassLoader()),
+ Instant.ofEpochSecond(source.readLong()));
}
@Override
@@ -89,6 +91,9 @@
@Nullable
private final Network network;
+ @NonNull
+ private final Instant expirationTime;
+
/** Constructs a {@link MdnsServiceInfo} object with default values. */
public MdnsServiceInfo(
String serviceInstanceName,
@@ -110,7 +115,8 @@
textStrings,
/* textEntries= */ null,
/* interfaceIndex= */ INTERFACE_INDEX_UNSPECIFIED,
- /* network= */ null);
+ /* network= */ null,
+ /* expirationTime= */ Instant.MAX);
}
/** Constructs a {@link MdnsServiceInfo} object with default values. */
@@ -135,7 +141,8 @@
textStrings,
textEntries,
/* interfaceIndex= */ INTERFACE_INDEX_UNSPECIFIED,
- /* network= */ null);
+ /* network= */ null,
+ /* expirationTime= */ Instant.MAX);
}
/**
@@ -165,7 +172,8 @@
textStrings,
textEntries,
interfaceIndex,
- /* network= */ null);
+ /* network= */ null,
+ /* expirationTime= */ Instant.MAX);
}
/**
@@ -184,7 +192,8 @@
@Nullable List<String> textStrings,
@Nullable List<TextEntry> textEntries,
int interfaceIndex,
- @Nullable Network network) {
+ @Nullable Network network,
+ @NonNull Instant expirationTime) {
this.serviceInstanceName = serviceInstanceName;
this.serviceType = serviceType;
this.subtypes = new ArrayList<>();
@@ -217,6 +226,7 @@
this.attributes = Collections.unmodifiableMap(attributes);
this.interfaceIndex = interfaceIndex;
this.network = network;
+ this.expirationTime = Instant.ofEpochSecond(expirationTime.getEpochSecond());
}
private static List<TextEntry> parseTextStrings(List<String> textStrings) {
@@ -314,6 +324,17 @@
}
/**
+ * Returns the timestamp after when this service is expired or {@code null} if the expiration
+ * time is unknown.
+ *
+ * A service is considered expired if any of its DNS record is expired.
+ */
+ @NonNull
+ public Instant getExpirationTime() {
+ return expirationTime;
+ }
+
+ /**
* Returns attribute value for {@code key} as a UTF-8 string. It's the caller who must make sure
* that the value of {@code key} is indeed a UTF-8 string. {@code null} will be returned if no
* attribute value exists for {@code key}.
@@ -364,6 +385,7 @@
out.writeTypedList(textEntries);
out.writeInt(interfaceIndex);
out.writeParcelable(network, 0);
+ out.writeLong(expirationTime.getEpochSecond());
}
@Override
@@ -377,7 +399,8 @@
+ ", interfaceIndex: " + interfaceIndex
+ ", network: " + network
+ ", textStrings: " + textStrings
- + ", textEntries: " + textEntries;
+ + ", textEntries: " + textEntries
+ + ", expirationTime: " + expirationTime;
}
@@ -496,4 +519,4 @@
out.writeByteArray(value);
}
}
-}
\ No newline at end of file
+}
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 4cb88b4..8f41b94 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsServiceTypeClient.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsServiceTypeClient.java
@@ -35,8 +35,10 @@
import com.android.net.module.util.SharedLog;
import com.android.server.connectivity.mdns.util.MdnsUtils;
+import java.io.PrintWriter;
import java.net.Inet4Address;
import java.net.Inet6Address;
+import java.time.Instant;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
@@ -140,8 +142,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;
}
@@ -309,6 +310,7 @@
textStrings = response.getTextRecord().getStrings();
textEntries = response.getTextRecord().getEntries();
}
+ Instant now = Instant.now();
// TODO: Throw an error message if response doesn't have Inet6 or Inet4 address.
return new MdnsServiceInfo(
serviceInstanceName,
@@ -321,7 +323,8 @@
textStrings,
textEntries,
response.getInterfaceIndex(),
- response.getNetwork());
+ response.getNetwork(),
+ now.plusMillis(response.getMinRemainingTtl(now.toEpochMilli())));
}
/**
@@ -388,8 +391,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);
}
@@ -627,6 +629,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) {
@@ -643,6 +649,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;
@@ -743,4 +760,13 @@
args.sessionId, timeToNextTasksWithBackoffInMs));
return timeToNextTasksWithBackoffInMs;
}
+
+ /**
+ * Dump ServiceTypeClient state.
+ */
+ public void dump(PrintWriter pw) {
+ ensureRunningOnHandlerThread(handler);
+ pw.println("ServiceTypeClient: Type{" + serviceType + "} " + socketKey + " with "
+ + listeners.size() + " listeners.");
+ }
}
\ No newline at end of file
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/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/staticlibs/Android.bp b/staticlibs/Android.bp
index a398dc9..f7b42a6 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/tests/common/java/android/net/NetworkCapabilitiesTest.java b/tests/common/java/android/net/NetworkCapabilitiesTest.java
index 3124b1b..6eb56c7b 100644
--- a/tests/common/java/android/net/NetworkCapabilitiesTest.java
+++ b/tests/common/java/android/net/NetworkCapabilitiesTest.java
@@ -61,7 +61,6 @@
import static android.net.NetworkCapabilities.TRANSPORT_WIFI;
import static android.net.NetworkCapabilities.TRANSPORT_WIFI_AWARE;
import static android.os.Process.INVALID_UID;
-
import static com.android.modules.utils.build.SdkLevel.isAtLeastS;
import static com.android.modules.utils.build.SdkLevel.isAtLeastT;
import static com.android.modules.utils.build.SdkLevel.isAtLeastV;
@@ -69,7 +68,6 @@
import static com.android.testutils.MiscAsserts.assertEmpty;
import static com.android.testutils.MiscAsserts.assertThrows;
import static com.android.testutils.ParcelUtils.assertParcelingIsLossless;
-
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
@@ -83,25 +81,22 @@
import android.net.wifi.aware.PeerHandle;
import android.net.wifi.aware.WifiAwareNetworkSpecifier;
import android.os.Build;
-import android.test.suitebuilder.annotation.SmallTest;
import android.util.ArraySet;
import android.util.Range;
-
+import androidx.test.filters.SmallTest;
import com.android.testutils.CompatUtil;
import com.android.testutils.ConnectivityModuleTest;
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;
-import org.mockito.Mockito;
-
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Set;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mockito;
@SmallTest
@RunWith(DevSdkIgnoreRunner.class)
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/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/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/android/net/nsd/AdvertisingRequestTest.kt b/tests/unit/java/android/net/nsd/AdvertisingRequestTest.kt
new file mode 100644
index 0000000..332f2a3
--- /dev/null
+++ b/tests/unit/java/android/net/nsd/AdvertisingRequestTest.kt
@@ -0,0 +1,106 @@
+/*
+ * 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.nsd
+
+import android.net.nsd.AdvertisingRequest.NSD_ADVERTISING_UPDATE_ONLY
+import android.net.nsd.NsdManager.PROTOCOL_DNS_SD
+import android.os.Build
+import androidx.test.filters.SmallTest
+import com.android.testutils.ConnectivityModuleTest
+import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo
+import com.android.testutils.DevSdkIgnoreRunner
+import com.android.testutils.parcelingRoundTrip
+import java.time.Duration
+import kotlin.test.assertEquals
+import kotlin.test.assertNotEquals
+import kotlin.test.assertNull
+import org.junit.Test
+import org.junit.runner.RunWith
+
+// TODO: move this class to CTS tests when AdvertisingRequest is made public
+/** Unit tests for {@link AdvertisingRequest}. */
+@IgnoreUpTo(Build.VERSION_CODES.S_V2)
+@RunWith(DevSdkIgnoreRunner::class)
+@SmallTest
+@ConnectivityModuleTest
+class AdvertisingRequestTest {
+ @Test
+ fun testParcelingIsLossLess() {
+ val info = NsdServiceInfo().apply {
+ serviceType = "_ipp._tcp"
+ }
+ val beforeParcel = AdvertisingRequest.Builder(info, PROTOCOL_DNS_SD)
+ .setAdvertisingConfig(NSD_ADVERTISING_UPDATE_ONLY)
+ .setTtl(Duration.ofSeconds(30L))
+ .build()
+
+ val afterParcel = parcelingRoundTrip(beforeParcel)
+
+ assertEquals(beforeParcel.serviceInfo.serviceType, afterParcel.serviceInfo.serviceType)
+ assertEquals(beforeParcel.advertisingConfig, afterParcel.advertisingConfig)
+ }
+
+@Test
+fun testBuilder_setNullTtl_success() {
+ val info = NsdServiceInfo().apply {
+ serviceType = "_ipp._tcp"
+ }
+ val request = AdvertisingRequest.Builder(info, PROTOCOL_DNS_SD)
+ .setTtl(null)
+ .build()
+
+ assertNull(request.ttl)
+}
+
+ @Test
+ fun testBuilder_setPropertiesSuccess() {
+ val info = NsdServiceInfo().apply {
+ serviceType = "_ipp._tcp"
+ }
+ val request = AdvertisingRequest.Builder(info, PROTOCOL_DNS_SD)
+ .setAdvertisingConfig(NSD_ADVERTISING_UPDATE_ONLY)
+ .setTtl(Duration.ofSeconds(100L))
+ .build()
+
+ assertEquals("_ipp._tcp", request.serviceInfo.serviceType)
+ assertEquals(PROTOCOL_DNS_SD, request.protocolType)
+ assertEquals(NSD_ADVERTISING_UPDATE_ONLY, request.advertisingConfig)
+ assertEquals(Duration.ofSeconds(100L), request.ttl)
+ }
+
+ @Test
+ fun testEquality() {
+ val info = NsdServiceInfo().apply {
+ serviceType = "_ipp._tcp"
+ }
+ val request1 = AdvertisingRequest.Builder(info, PROTOCOL_DNS_SD).build()
+ val request2 = AdvertisingRequest.Builder(info, PROTOCOL_DNS_SD).build()
+ val request3 = AdvertisingRequest.Builder(info, PROTOCOL_DNS_SD)
+ .setAdvertisingConfig(NSD_ADVERTISING_UPDATE_ONLY)
+ .setTtl(Duration.ofSeconds(120L))
+ .build()
+ val request4 = AdvertisingRequest.Builder(info, PROTOCOL_DNS_SD)
+ .setAdvertisingConfig(NSD_ADVERTISING_UPDATE_ONLY)
+ .setTtl(Duration.ofSeconds(120L))
+ .build()
+
+ assertEquals(request1, request2)
+ assertEquals(request3, request4)
+ assertNotEquals(request1, request3)
+ assertNotEquals(request2, request4)
+ }
+}
diff --git a/tests/unit/java/android/net/nsd/NsdManagerTest.java b/tests/unit/java/android/net/nsd/NsdManagerTest.java
index 951675c..76a649e 100644
--- a/tests/unit/java/android/net/nsd/NsdManagerTest.java
+++ b/tests/unit/java/android/net/nsd/NsdManagerTest.java
@@ -23,6 +23,7 @@
import static org.junit.Assert.fail;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.anyInt;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
@@ -54,6 +55,7 @@
import java.net.InetAddress;
import java.util.List;
+import java.time.Duration;
@DevSdkIgnoreRunner.MonitorThreadLeak
@RunWith(DevSdkIgnoreRunner.class)
@@ -224,6 +226,23 @@
verify(listener, timeout(mTimeoutMs).times(1)).onServiceRegistered(request);
}
+ @Test
+ public void testRegisterServiceWithCustomTtl() throws Exception {
+ final NsdManager manager = mManager;
+ final NsdServiceInfo info = new NsdServiceInfo("another_name2", "another_type2");
+ info.setPort(2203);
+ final AdvertisingRequest request = new AdvertisingRequest.Builder(info, PROTOCOL)
+ .setTtl(Duration.ofSeconds(30)).build();
+ final NsdManager.RegistrationListener listener = mock(
+ NsdManager.RegistrationListener.class);
+
+ manager.registerService(request, Runnable::run, listener);
+
+ AdvertisingRequest capturedRequest = getAdvertisingRequest(
+ req -> verify(mServiceConn).registerService(anyInt(), req.capture()));
+ assertEquals(request, capturedRequest);
+ }
+
private void doTestRegisterService() throws Exception {
NsdManager manager = mManager;
@@ -501,4 +520,12 @@
verifier.accept(captor);
return captor.getValue();
}
+
+ AdvertisingRequest getAdvertisingRequest(
+ ThrowingConsumer<ArgumentCaptor<AdvertisingRequest>> verifier) throws Exception {
+ final ArgumentCaptor<AdvertisingRequest> captor =
+ ArgumentCaptor.forClass(AdvertisingRequest.class);
+ verifier.accept(captor);
+ return captor.getValue();
+ }
}
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/NsdServiceTest.java b/tests/unit/java/com/android/server/NsdServiceTest.java
index 624855e..881de56 100644
--- a/tests/unit/java/com/android/server/NsdServiceTest.java
+++ b/tests/unit/java/com/android/server/NsdServiceTest.java
@@ -83,6 +83,7 @@
import android.net.mdns.aidl.IMDnsEventListener;
import android.net.mdns.aidl.RegistrationInfo;
import android.net.mdns.aidl.ResolutionInfo;
+import android.net.nsd.AdvertisingRequest;
import android.net.nsd.INsdManagerCallback;
import android.net.nsd.INsdServiceConnector;
import android.net.nsd.MDnsManager;
@@ -101,6 +102,7 @@
import android.os.IBinder;
import android.os.Looper;
import android.os.Message;
+import android.os.Process;
import android.os.RemoteException;
import android.util.Pair;
@@ -110,6 +112,7 @@
import com.android.metrics.NetworkNsdReportedMetrics;
import com.android.server.NsdService.Dependencies;
import com.android.server.connectivity.mdns.MdnsAdvertiser;
+import com.android.server.connectivity.mdns.MdnsAdvertisingOptions;
import com.android.server.connectivity.mdns.MdnsDiscoveryManager;
import com.android.server.connectivity.mdns.MdnsInterfaceSocket;
import com.android.server.connectivity.mdns.MdnsSearchOptions;
@@ -137,6 +140,8 @@
import java.net.InetAddress;
import java.net.UnknownHostException;
+import java.time.Duration;
+import java.time.Instant;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedList;
@@ -971,7 +976,8 @@
List.of() /* textStrings */,
List.of() /* textEntries */,
1234,
- network);
+ network,
+ Instant.MAX /* expirationTime */);
// Callbacks for query sent.
listener.onDiscoveryQuerySent(Collections.emptyList(), 1 /* transactionId */);
@@ -1001,7 +1007,8 @@
List.of() /* textStrings */,
List.of() /* textEntries */,
1234,
- network);
+ network,
+ Instant.MAX /* expirationTime */);
// Verify onServiceUpdated callback.
listener.onServiceUpdated(updatedServiceInfo);
@@ -1133,7 +1140,8 @@
List.of(), /* textStrings */
List.of(), /* textEntries */
1234, /* interfaceIndex */
- network);
+ network,
+ Instant.MAX /* expirationTime */);
// Verify onServiceNameDiscovered callback
listener.onServiceNameDiscovered(foundInfo, false /* isServiceFromCache */);
@@ -1154,7 +1162,8 @@
null, /* textStrings */
null, /* textEntries */
1234, /* interfaceIndex */
- network);
+ network,
+ Instant.MAX /* expirationTime */);
// Verify onServiceNameRemoved callback
listener.onServiceNameRemoved(removedInfo);
verify(discListener, timeout(TIMEOUT_MS)).onServiceLost(argThat(info ->
@@ -1276,7 +1285,8 @@
List.of(MdnsServiceInfo.TextEntry.fromBytes(new byte[]{
'k', 'e', 'y', '=', (byte) 0xFF, (byte) 0xFE})) /* textEntries */,
1234,
- network);
+ network,
+ Instant.ofEpochSecond(1000_000L) /* expirationTime */);
// Verify onServiceFound callback
doReturn(TEST_TIME_MS + 10L).when(mClock).elapsedRealtime();
@@ -1301,6 +1311,7 @@
assertTrue(info.getHostAddresses().stream().anyMatch(
address -> address.equals(parseNumericAddress("2001:db8::2"))));
assertEquals(network, info.getNetwork());
+ assertEquals(Instant.ofEpochSecond(1000_000L), info.getExpirationTime());
// Verify the listener has been unregistered.
verify(mDiscoveryManager, timeout(TIMEOUT_MS))
@@ -1518,6 +1529,82 @@
}
@Test
+ public void testAdvertiseCustomTtl_validTtl_success() {
+ runValidTtlAdvertisingTest(30L);
+ runValidTtlAdvertisingTest(10 * 3600L);
+ }
+
+ @Test
+ public void testAdvertiseCustomTtl_ttlSmallerThan30SecondsButClientIsSystemServer_success() {
+ when(mDeps.getCallingUid()).thenReturn(Process.SYSTEM_UID);
+
+ runValidTtlAdvertisingTest(29L);
+ }
+
+ @Test
+ public void testAdvertiseCustomTtl_ttlLargerThan10HoursButClientIsSystemServer_success() {
+ when(mDeps.getCallingUid()).thenReturn(Process.SYSTEM_UID);
+
+ runValidTtlAdvertisingTest(10 * 3600L + 1);
+ runValidTtlAdvertisingTest(0xffffffffL);
+ }
+
+ private void runValidTtlAdvertisingTest(long validTtlSeconds) {
+ setMdnsAdvertiserEnabled();
+
+ final NsdManager client = connectClient(mService);
+ final RegistrationListener regListener = mock(RegistrationListener.class);
+ final ArgumentCaptor<MdnsAdvertiser.AdvertiserCallback> cbCaptor =
+ ArgumentCaptor.forClass(MdnsAdvertiser.AdvertiserCallback.class);
+ verify(mDeps).makeMdnsAdvertiser(any(), any(), cbCaptor.capture(), any(), any(), any());
+
+ final NsdServiceInfo regInfo = new NsdServiceInfo("Service custom TTL", SERVICE_TYPE);
+ regInfo.setPort(1234);
+ final AdvertisingRequest request =
+ new AdvertisingRequest.Builder(regInfo, NsdManager.PROTOCOL_DNS_SD)
+ .setTtl(Duration.ofSeconds(validTtlSeconds)).build();
+
+ client.registerService(request, Runnable::run, regListener);
+ waitForIdle();
+
+ final ArgumentCaptor<Integer> idCaptor = ArgumentCaptor.forClass(Integer.class);
+ final MdnsAdvertisingOptions expectedAdverstingOptions =
+ MdnsAdvertisingOptions.newBuilder().setTtl(request.getTtl()).build();
+ verify(mAdvertiser).addOrUpdateService(idCaptor.capture(), any(),
+ eq(expectedAdverstingOptions), anyInt());
+
+ // Verify onServiceRegistered callback
+ final MdnsAdvertiser.AdvertiserCallback cb = cbCaptor.getValue();
+ final int regId = idCaptor.getValue();
+ cb.onRegisterServiceSucceeded(regId, regInfo);
+
+ verify(regListener, timeout(TIMEOUT_MS)).onServiceRegistered(
+ argThat(info -> matches(info, new NsdServiceInfo(regInfo.getServiceName(), null))));
+ }
+
+ @Test
+ public void testAdvertiseCustomTtl_invalidTtl_FailsWithBadParameters() {
+ setMdnsAdvertiserEnabled();
+ final long invalidTtlSeconds = 29L;
+ final NsdManager client = connectClient(mService);
+ final RegistrationListener regListener = mock(RegistrationListener.class);
+ final ArgumentCaptor<MdnsAdvertiser.AdvertiserCallback> cbCaptor =
+ ArgumentCaptor.forClass(MdnsAdvertiser.AdvertiserCallback.class);
+ verify(mDeps).makeMdnsAdvertiser(any(), any(), cbCaptor.capture(), any(), any(), any());
+
+ final NsdServiceInfo regInfo = new NsdServiceInfo("Service custom TTL", SERVICE_TYPE);
+ regInfo.setPort(1234);
+ final AdvertisingRequest request =
+ new AdvertisingRequest.Builder(regInfo, NsdManager.PROTOCOL_DNS_SD)
+ .setTtl(Duration.ofSeconds(invalidTtlSeconds)).build();
+ client.registerService(request, Runnable::run, regListener);
+ waitForIdle();
+
+ verify(regListener, timeout(TIMEOUT_MS))
+ .onRegistrationFailed(any(), eq(FAILURE_BAD_PARAMETERS));
+ }
+
+ @Test
public void testStopServiceResolutionWithMdnsDiscoveryManager() {
setMdnsDiscoveryManagerEnabled();
diff --git a/tests/unit/java/com/android/server/connectivity/AutomaticOnOffKeepaliveTrackerTest.java b/tests/unit/java/com/android/server/connectivity/AutomaticOnOffKeepaliveTrackerTest.java
index 6cc301d..c53feee 100644
--- a/tests/unit/java/com/android/server/connectivity/AutomaticOnOffKeepaliveTrackerTest.java
+++ b/tests/unit/java/com/android/server/connectivity/AutomaticOnOffKeepaliveTrackerTest.java
@@ -20,10 +20,8 @@
import static android.net.ConnectivityManager.TYPE_MOBILE;
import static android.net.NetworkAgent.CMD_STOP_SOCKET_KEEPALIVE;
import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR;
-
import static com.android.server.connectivity.AutomaticOnOffKeepaliveTracker.METRICS_COLLECTION_DURATION_MS;
import static com.android.testutils.HandlerUtils.visibleOnHandlerThread;
-
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
@@ -71,29 +69,16 @@
import android.os.Message;
import android.os.SystemClock;
import android.telephony.SubscriptionManager;
-import android.test.suitebuilder.annotation.SmallTest;
import android.util.Log;
-
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
-
+import androidx.test.filters.SmallTest;
import com.android.internal.util.IndentingPrintWriter;
import com.android.server.connectivity.AutomaticOnOffKeepaliveTracker.AutomaticOnOffKeepalive;
import com.android.server.connectivity.KeepaliveTracker.KeepaliveInfo;
import com.android.testutils.DevSdkIgnoreRule;
import com.android.testutils.DevSdkIgnoreRunner;
import com.android.testutils.HandlerUtils;
-
-import libcore.util.HexEncoding;
-
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.ArgumentCaptor;
-import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
-
import java.io.FileDescriptor;
import java.io.StringWriter;
import java.net.Inet4Address;
@@ -103,6 +88,14 @@
import java.nio.ByteOrder;
import java.util.ArrayList;
import java.util.List;
+import libcore.util.HexEncoding;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
@RunWith(DevSdkIgnoreRunner.class)
@SmallTest
diff --git a/tests/unit/java/com/android/server/connectivity/IpConnectivityEventBuilderTest.java b/tests/unit/java/com/android/server/connectivity/IpConnectivityEventBuilderTest.java
index 52b05aa..ab1e467 100644
--- a/tests/unit/java/com/android/server/connectivity/IpConnectivityEventBuilderTest.java
+++ b/tests/unit/java/com/android/server/connectivity/IpConnectivityEventBuilderTest.java
@@ -26,7 +26,6 @@
import static com.android.server.connectivity.metrics.nano.IpConnectivityLogClass.IpConnectivityLog;
import static com.android.server.connectivity.metrics.nano.IpConnectivityLogClass.MULTIPLE;
import static com.android.server.connectivity.metrics.nano.IpConnectivityLogClass.WIFI;
-
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.fail;
@@ -43,17 +42,14 @@
import android.net.metrics.ValidationProbeEvent;
import android.net.metrics.WakeupStats;
import android.os.Build;
-import android.test.suitebuilder.annotation.SmallTest;
-
+import androidx.test.filters.SmallTest;
import com.android.server.connectivity.metrics.nano.IpConnectivityLogClass.IpConnectivityEvent;
import com.android.testutils.DevSdkIgnoreRule;
import com.android.testutils.DevSdkIgnoreRunner;
-
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
import java.util.Arrays;
import java.util.List;
+import org.junit.Test;
+import org.junit.runner.RunWith;
// TODO: instead of comparing textpb to textpb, parse textpb and compare proto to proto.
@RunWith(DevSdkIgnoreRunner.class)
diff --git a/tests/unit/java/com/android/server/connectivity/IpConnectivityMetricsTest.java b/tests/unit/java/com/android/server/connectivity/IpConnectivityMetricsTest.java
index 5881a8e..91626d2 100644
--- a/tests/unit/java/com/android/server/connectivity/IpConnectivityMetricsTest.java
+++ b/tests/unit/java/com/android/server/connectivity/IpConnectivityMetricsTest.java
@@ -18,7 +18,6 @@
import static android.net.metrics.INetdEventListener.EVENT_GETADDRINFO;
import static android.net.metrics.INetdEventListener.EVENT_GETHOSTBYNAME;
-
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.fail;
@@ -50,14 +49,14 @@
import android.os.Parcelable;
import android.os.SystemClock;
import android.system.OsConstants;
-import android.test.suitebuilder.annotation.SmallTest;
import android.util.Base64;
-
+import androidx.test.filters.SmallTest;
import com.android.internal.util.BitUtils;
import com.android.server.connectivity.metrics.nano.IpConnectivityLogClass;
import com.android.testutils.DevSdkIgnoreRule;
import com.android.testutils.DevSdkIgnoreRunner;
-
+import java.io.PrintWriter;
+import java.io.StringWriter;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -65,9 +64,6 @@
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
-import java.io.PrintWriter;
-import java.io.StringWriter;
-
@RunWith(DevSdkIgnoreRunner.class)
@SmallTest
@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
diff --git a/tests/unit/java/com/android/server/connectivity/NetdEventListenerServiceTest.java b/tests/unit/java/com/android/server/connectivity/NetdEventListenerServiceTest.java
index d667662..89e2a51 100644
--- a/tests/unit/java/com/android/server/connectivity/NetdEventListenerServiceTest.java
+++ b/tests/unit/java/com/android/server/connectivity/NetdEventListenerServiceTest.java
@@ -18,9 +18,7 @@
import static android.net.metrics.INetdEventListener.EVENT_GETADDRINFO;
import static android.net.metrics.INetdEventListener.EVENT_GETHOSTBYNAME;
-
import static com.android.testutils.MiscAsserts.assertStringContains;
-
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.fail;
import static org.mockito.ArgumentMatchers.any;
@@ -34,27 +32,23 @@
import android.net.NetworkCapabilities;
import android.os.Build;
import android.system.OsConstants;
-import android.test.suitebuilder.annotation.SmallTest;
import android.util.Base64;
-
+import androidx.test.filters.SmallTest;
import com.android.server.connectivity.metrics.nano.IpConnectivityLogClass.IpConnectivityEvent;
import com.android.server.connectivity.metrics.nano.IpConnectivityLogClass.IpConnectivityLog;
import com.android.testutils.DevSdkIgnoreRule;
import com.android.testutils.DevSdkIgnoreRunner;
-
-import libcore.util.EmptyArray;
-
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.ArgumentCaptor;
-
import java.io.FileOutputStream;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
+import libcore.util.EmptyArray;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
@RunWith(DevSdkIgnoreRunner.class)
@SmallTest
diff --git a/tests/unit/java/com/android/server/connectivity/mdns/MdnsAdvertiserTest.kt b/tests/unit/java/com/android/server/connectivity/mdns/MdnsAdvertiserTest.kt
index f753c93..b8ebf0f 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/MdnsAdvertiserTest.kt
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsAdvertiserTest.kt
@@ -38,6 +38,7 @@
import com.android.testutils.DevSdkIgnoreRunner
import com.android.testutils.waitForIdle
import java.net.NetworkInterface
+import java.time.Duration
import java.util.Objects
import java.util.concurrent.CompletableFuture
import java.util.concurrent.TimeUnit
@@ -313,9 +314,9 @@
eq(thread.looper), any(), intAdvCbCaptor2.capture(), eq(TEST_HOSTNAME), any(), any()
)
verify(mockInterfaceAdvertiser1).addService(
- anyInt(), eq(ALL_NETWORKS_SERVICE_SUBTYPE))
+ anyInt(), eq(ALL_NETWORKS_SERVICE_SUBTYPE), any())
verify(mockInterfaceAdvertiser2).addService(
- anyInt(), eq(ALL_NETWORKS_SERVICE_SUBTYPE))
+ anyInt(), eq(ALL_NETWORKS_SERVICE_SUBTYPE), any())
doReturn(false).`when`(mockInterfaceAdvertiser1).isProbing(SERVICE_ID_1)
postSync { intAdvCbCaptor1.value.onServiceProbingSucceeded(
@@ -489,15 +490,15 @@
eq(thread.looper), any(), intAdvCbCaptor.capture(), eq(TEST_HOSTNAME), any(), any()
)
verify(mockInterfaceAdvertiser1).addService(eq(SERVICE_ID_1),
- argThat { it.matches(SERVICE_1) })
+ argThat { it.matches(SERVICE_1) }, any())
verify(mockInterfaceAdvertiser1).addService(eq(SERVICE_ID_2),
- argThat { it.matches(expectedRenamed) })
+ argThat { it.matches(expectedRenamed) }, any())
verify(mockInterfaceAdvertiser1).addService(eq(LONG_SERVICE_ID_1),
- argThat { it.matches(LONG_SERVICE_1) })
+ argThat { it.matches(LONG_SERVICE_1) }, any())
verify(mockInterfaceAdvertiser1).addService(eq(LONG_SERVICE_ID_2),
- argThat { it.matches(expectedLongRenamed) })
+ argThat { it.matches(expectedLongRenamed) }, any())
verify(mockInterfaceAdvertiser1).addService(eq(CASE_INSENSITIVE_TEST_SERVICE_ID),
- argThat { it.matches(expectedCaseInsensitiveRenamed) })
+ argThat { it.matches(expectedCaseInsensitiveRenamed) }, any())
doReturn(false).`when`(mockInterfaceAdvertiser1).isProbing(SERVICE_ID_1)
postSync { intAdvCbCaptor.value.onServiceProbingSucceeded(
@@ -532,7 +533,7 @@
postSync { socketCb.onSocketCreated(TEST_SOCKETKEY_1, mockSocket1, listOf(TEST_LINKADDR)) }
verify(mockInterfaceAdvertiser1).addService(eq(SERVICE_ID_1),
- argThat { it.matches(ALL_NETWORKS_SERVICE) })
+ argThat { it.matches(ALL_NETWORKS_SERVICE) }, any())
val updateOptions = MdnsAdvertisingOptions.newBuilder().setIsOnlyUpdate(true).build()
@@ -554,7 +555,24 @@
// Newly created MdnsInterfaceAdvertiser will get addService() call.
postSync { socketCb.onSocketCreated(TEST_SOCKETKEY_2, mockSocket2, listOf(TEST_LINKADDR2)) }
verify(mockInterfaceAdvertiser2).addService(eq(SERVICE_ID_1),
- argThat { it.matches(ALL_NETWORKS_SERVICE_SUBTYPE) })
+ argThat { it.matches(ALL_NETWORKS_SERVICE_SUBTYPE) }, any())
+ }
+
+ @Test
+ fun testAddOrUpdateService_customTtl_registeredSuccess() {
+ val advertiser = MdnsAdvertiser(
+ thread.looper, socketProvider, cb, mockDeps, sharedlog, flags, context)
+ val updateOptions =
+ MdnsAdvertisingOptions.newBuilder().setTtl(Duration.ofSeconds(30)).build()
+
+ postSync { advertiser.addOrUpdateService(SERVICE_ID_1, ALL_NETWORKS_SERVICE,
+ updateOptions, TEST_CLIENT_UID_1) }
+
+ val socketCbCaptor = ArgumentCaptor.forClass(SocketCallback::class.java)
+ verify(socketProvider).requestSocket(eq(null), socketCbCaptor.capture())
+ val socketCb = socketCbCaptor.value
+ postSync { socketCb.onSocketCreated(TEST_SOCKETKEY_1, mockSocket1, listOf(TEST_LINKADDR)) }
+ verify(mockInterfaceAdvertiser1).addService(eq(SERVICE_ID_1), any(), eq(updateOptions))
}
@Test
diff --git a/tests/unit/java/com/android/server/connectivity/mdns/MdnsInterfaceAdvertiserTest.kt b/tests/unit/java/com/android/server/connectivity/mdns/MdnsInterfaceAdvertiserTest.kt
index 0637ad1..28608bb 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/MdnsInterfaceAdvertiserTest.kt
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsInterfaceAdvertiserTest.kt
@@ -132,7 +132,7 @@
knownServices.add(inv.getArgument(0))
-1
- }.`when`(repository).addService(anyInt(), any())
+ }.`when`(repository).addService(anyInt(), any(), any())
doAnswer { inv ->
knownServices.remove(inv.getArgument(0))
null
@@ -403,9 +403,10 @@
@Test
fun testReplaceExitingService() {
doReturn(TEST_SERVICE_ID_DUPLICATE).`when`(repository)
- .addService(eq(TEST_SERVICE_ID_DUPLICATE), any())
- advertiser.addService(TEST_SERVICE_ID_DUPLICATE, TEST_SERVICE_1_SUBTYPE)
- verify(repository).addService(eq(TEST_SERVICE_ID_DUPLICATE), any())
+ .addService(eq(TEST_SERVICE_ID_DUPLICATE), any(), any())
+ advertiser.addService(TEST_SERVICE_ID_DUPLICATE, TEST_SERVICE_1_SUBTYPE,
+ MdnsAdvertisingOptions.getDefaultOptions())
+ verify(repository).addService(eq(TEST_SERVICE_ID_DUPLICATE), any(), any())
verify(announcer).stop(TEST_SERVICE_ID_DUPLICATE)
verify(prober).startProbing(any())
}
@@ -413,7 +414,7 @@
@Test
fun testUpdateExistingService() {
doReturn(TEST_SERVICE_ID_DUPLICATE).`when`(repository)
- .addService(eq(TEST_SERVICE_ID_DUPLICATE), any())
+ .addService(eq(TEST_SERVICE_ID_DUPLICATE), any(), any())
val subTypes = setOf("_sub")
advertiser.updateService(TEST_SERVICE_ID_DUPLICATE, subTypes)
verify(repository).updateService(eq(TEST_SERVICE_ID_DUPLICATE), any())
@@ -427,8 +428,8 @@
doReturn(serviceId).`when`(testProbingInfo).serviceId
doReturn(testProbingInfo).`when`(repository).setServiceProbing(serviceId)
- advertiser.addService(serviceId, serviceInfo)
- verify(repository).addService(serviceId, serviceInfo)
+ advertiser.addService(serviceId, serviceInfo, MdnsAdvertisingOptions.getDefaultOptions())
+ verify(repository).addService(serviceId, serviceInfo, null /* ttl */)
verify(prober).startProbing(testProbingInfo)
// Simulate probing success: continues to announcing
diff --git a/tests/unit/java/com/android/server/connectivity/mdns/MdnsRecordRepositoryTest.kt b/tests/unit/java/com/android/server/connectivity/mdns/MdnsRecordRepositoryTest.kt
index fd8d98b..8d1dff6 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/MdnsRecordRepositoryTest.kt
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsRecordRepositoryTest.kt
@@ -145,7 +145,8 @@
fun testAddServiceAndProbe() {
val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags())
assertEquals(0, repository.servicesCount)
- assertEquals(-1, repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_1))
+ assertEquals(-1,
+ repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_1, null /* ttl */))
assertEquals(1, repository.servicesCount)
val probingInfo = repository.setServiceProbing(TEST_SERVICE_ID_1)
@@ -180,10 +181,10 @@
val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags())
repository.initWithService(TEST_SERVICE_ID_1, TEST_SERVICE_1)
assertFailsWith(NameConflictException::class) {
- repository.addService(TEST_SERVICE_ID_2, TEST_SERVICE_1)
+ repository.addService(TEST_SERVICE_ID_2, TEST_SERVICE_1, null /* ttl */)
}
assertFailsWith(NameConflictException::class) {
- repository.addService(TEST_SERVICE_ID_3, TEST_SERVICE_3)
+ repository.addService(TEST_SERVICE_ID_3, TEST_SERVICE_3, null /* ttl */)
}
}
@@ -224,9 +225,9 @@
@Test
fun testInvalidReuseOfServiceId() {
val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags())
- repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_1)
+ repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_1, null /* ttl */)
assertFailsWith(IllegalArgumentException::class) {
- repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_2)
+ repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_2, null /* ttl */)
}
}
@@ -235,7 +236,7 @@
val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags())
assertFalse(repository.hasActiveService(TEST_SERVICE_ID_1))
- repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_1)
+ repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_1, null /* ttl */)
assertTrue(repository.hasActiveService(TEST_SERVICE_ID_1))
val probingInfo = repository.setServiceProbing(TEST_SERVICE_ID_1)
@@ -327,7 +328,7 @@
repository.exitService(TEST_SERVICE_ID_1)
assertEquals(TEST_SERVICE_ID_1,
- repository.addService(TEST_SERVICE_ID_2, TEST_SERVICE_1))
+ repository.addService(TEST_SERVICE_ID_2, TEST_SERVICE_1, null /* ttl */))
assertEquals(1, repository.servicesCount)
repository.removeService(TEST_SERVICE_ID_2)
@@ -824,7 +825,7 @@
repository.initWithService(
TEST_CUSTOM_HOST_ID_1, TEST_CUSTOM_HOST_1, subtypes = setOf(),
listOf(LinkAddress(parseNumericAddress("192.0.2.111"), 24)))
- repository.addService(TEST_CUSTOM_HOST_ID_2, TEST_CUSTOM_HOST_2)
+ repository.addService(TEST_CUSTOM_HOST_ID_2, TEST_CUSTOM_HOST_2, null /* ttl */)
val src = InetSocketAddress(parseNumericAddress("fe80::123"), 5353)
val query = makeQuery(TYPE_AAAA to TEST_CUSTOM_HOST_1_NAME)
@@ -890,7 +891,8 @@
repository.initWithService(
TEST_CUSTOM_HOST_ID_1, TEST_CUSTOM_HOST_1, subtypes = setOf(),
listOf(LinkAddress(parseNumericAddress("192.0.2.111"), 24)))
- repository.addService(TEST_SERVICE_CUSTOM_HOST_ID_1, TEST_SERVICE_CUSTOM_HOST_1)
+ repository.addService(
+ TEST_SERVICE_CUSTOM_HOST_ID_1, TEST_SERVICE_CUSTOM_HOST_1, null /* ttl */)
repository.removeService(TEST_CUSTOM_HOST_ID_1)
repository.removeService(TEST_SERVICE_CUSTOM_HOST_ID_1)
@@ -989,8 +991,8 @@
@Test
fun testGetConflictingServices() {
val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags())
- repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_1)
- repository.addService(TEST_SERVICE_ID_2, TEST_SERVICE_2)
+ repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_1, null /* ttl */)
+ repository.addService(TEST_SERVICE_ID_2, TEST_SERVICE_2, null /* ttl */)
val packet = MdnsPacket(
0 /* flags */,
@@ -1020,8 +1022,8 @@
@Test
fun testGetConflictingServicesCaseInsensitive() {
val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags())
- repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_1)
- repository.addService(TEST_SERVICE_ID_2, TEST_SERVICE_2)
+ repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_1, null /* ttl */)
+ repository.addService(TEST_SERVICE_ID_2, TEST_SERVICE_2, null /* ttl */)
val packet = MdnsPacket(
0 /* flags */,
@@ -1050,8 +1052,8 @@
@Test
fun testGetConflictingServices_customHosts_differentAddresses() {
val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags())
- repository.addService(TEST_CUSTOM_HOST_ID_1, TEST_CUSTOM_HOST_1)
- repository.addService(TEST_CUSTOM_HOST_ID_2, TEST_CUSTOM_HOST_2)
+ repository.addService(TEST_CUSTOM_HOST_ID_1, TEST_CUSTOM_HOST_1, null /* ttl */)
+ repository.addService(TEST_CUSTOM_HOST_ID_2, TEST_CUSTOM_HOST_2, null /* ttl */)
val packet = MdnsPacket(
0, /* flags */
@@ -1074,8 +1076,8 @@
@Test
fun testGetConflictingServices_customHosts_moreAddressesThanUs_conflict() {
val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags())
- repository.addService(TEST_CUSTOM_HOST_ID_1, TEST_CUSTOM_HOST_1)
- repository.addService(TEST_CUSTOM_HOST_ID_2, TEST_CUSTOM_HOST_2)
+ repository.addService(TEST_CUSTOM_HOST_ID_1, TEST_CUSTOM_HOST_1, null /* ttl */)
+ repository.addService(TEST_CUSTOM_HOST_ID_2, TEST_CUSTOM_HOST_2, null /* ttl */)
val packet = MdnsPacket(
0, /* flags */
@@ -1101,8 +1103,8 @@
@Test
fun testGetConflictingServices_customHostsReplyHasFewerAddressesThanUs_noConflict() {
val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags())
- repository.addService(TEST_CUSTOM_HOST_ID_1, TEST_CUSTOM_HOST_1)
- repository.addService(TEST_CUSTOM_HOST_ID_2, TEST_CUSTOM_HOST_2)
+ repository.addService(TEST_CUSTOM_HOST_ID_1, TEST_CUSTOM_HOST_1, null /* ttl */)
+ repository.addService(TEST_CUSTOM_HOST_ID_2, TEST_CUSTOM_HOST_2, null /* ttl */)
val packet = MdnsPacket(
0, /* flags */
@@ -1122,8 +1124,8 @@
@Test
fun testGetConflictingServices_customHostsReplyHasIdenticalHosts_noConflict() {
val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags())
- repository.addService(TEST_CUSTOM_HOST_ID_1, TEST_CUSTOM_HOST_1)
- repository.addService(TEST_CUSTOM_HOST_ID_2, TEST_CUSTOM_HOST_2)
+ repository.addService(TEST_CUSTOM_HOST_ID_1, TEST_CUSTOM_HOST_1, null /* ttl */)
+ repository.addService(TEST_CUSTOM_HOST_ID_2, TEST_CUSTOM_HOST_2, null /* ttl */)
val packet = MdnsPacket(
0, /* flags */
@@ -1147,8 +1149,8 @@
@Test
fun testGetConflictingServices_customHostsCaseInsensitiveReplyHasIdenticalHosts_noConflict() {
val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags())
- repository.addService(TEST_CUSTOM_HOST_ID_1, TEST_CUSTOM_HOST_1)
- repository.addService(TEST_CUSTOM_HOST_ID_2, TEST_CUSTOM_HOST_2)
+ repository.addService(TEST_CUSTOM_HOST_ID_1, TEST_CUSTOM_HOST_1, null /* ttl */)
+ repository.addService(TEST_CUSTOM_HOST_ID_2, TEST_CUSTOM_HOST_2, null /* ttl */)
val packet = MdnsPacket(
0, /* flags */
@@ -1171,8 +1173,8 @@
@Test
fun testGetConflictingServices_IdenticalService() {
val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags())
- repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_1)
- repository.addService(TEST_SERVICE_ID_2, TEST_SERVICE_2)
+ repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_1, null /* ttl */)
+ repository.addService(TEST_SERVICE_ID_2, TEST_SERVICE_2, null /* ttl */)
val otherTtlMillis = 1234L
val packet = MdnsPacket(
@@ -1200,8 +1202,8 @@
@Test
fun testGetConflictingServicesCaseInsensitive_IdenticalService() {
val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags())
- repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_1)
- repository.addService(TEST_SERVICE_ID_2, TEST_SERVICE_2)
+ repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_1, null /* ttl */)
+ repository.addService(TEST_SERVICE_ID_2, TEST_SERVICE_2, null /* ttl */)
val otherTtlMillis = 1234L
val packet = MdnsPacket(
@@ -1256,7 +1258,8 @@
makeFlags(includeInetAddressesInProbing = true))
repository.updateAddresses(TEST_ADDRESSES)
assertEquals(0, repository.servicesCount)
- assertEquals(-1, repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_1))
+ assertEquals(-1,
+ repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_1, null /* ttl */))
assertEquals(1, repository.servicesCount)
val probingInfo = repository.setServiceProbing(TEST_SERVICE_ID_1)
@@ -1689,7 +1692,7 @@
serviceId: Int,
serviceInfo: NsdServiceInfo
): AnnouncementInfo {
- addService(serviceId, serviceInfo)
+ addService(serviceId, serviceInfo, null /* ttl */)
val probingInfo = setServiceProbing(serviceId)
assertNotNull(probingInfo)
return onProbingSucceeded(probingInfo)
diff --git a/tests/unit/java/com/android/server/connectivity/mdns/MdnsServiceInfoTest.java b/tests/unit/java/com/android/server/connectivity/mdns/MdnsServiceInfoTest.java
index e7d7a98..8740e80 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/MdnsServiceInfoTest.java
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsServiceInfoTest.java
@@ -35,6 +35,7 @@
import org.junit.Test;
import org.junit.runner.RunWith;
+import java.time.Instant;
import java.util.List;
import java.util.Map;
@@ -202,7 +203,8 @@
List.of(),
/* textEntries= */ null,
/* interfaceIndex= */ 20,
- network);
+ network,
+ Instant.MAX /* expirationTime */);
assertEquals(network, info2.getNetwork());
}
@@ -225,7 +227,8 @@
MdnsServiceInfo.TextEntry.fromString("mn=Google Nest Hub Max"),
MdnsServiceInfo.TextEntry.fromString("test=")),
20 /* interfaceIndex */,
- new Network(123));
+ new Network(123),
+ Instant.MAX /* expirationTime */);
beforeParcel.writeToParcel(parcel, 0);
parcel.setDataPosition(0);
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/thread/flags/Android.bp b/thread/flags/Android.bp
deleted file mode 100644
index 15f58a9..0000000
--- a/thread/flags/Android.bp
+++ /dev/null
@@ -1,23 +0,0 @@
-//
-// 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.
-//
-
-aconfig_declarations {
- name: "com.android.net.thread.flags-aconfig",
- package: "com.android.net.thread.flags",
- container: "system",
- srcs: ["thread_base.aconfig"],
- visibility: ["//packages/modules/Connectivity:__subpackages__"],
-}
diff --git a/thread/service/java/com/android/server/thread/ThreadNetworkControllerService.java b/thread/service/java/com/android/server/thread/ThreadNetworkControllerService.java
index 56dd056..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;
@@ -70,6 +69,7 @@
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;
@@ -108,6 +108,7 @@
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;
@@ -123,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;
@@ -1001,11 +1003,6 @@
}
}
- private boolean isMulticastForwardingEnabled() {
- return !(mUpstreamMulticastRoutingConfig.getForwardingMode() == FORWARD_NONE
- && mDownstreamMulticastRoutingConfig.getForwardingMode() == FORWARD_NONE);
- }
-
private void sendLocalNetworkConfig() {
if (mNetworkAgent == null) {
return;
@@ -1015,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 {
@@ -1248,7 +1217,6 @@
onInterfaceStateChanged(newState.isInterfaceUp);
onDeviceRoleChanged(newState.deviceRole, listenerId);
onPartitionIdChanged(newState.partitionId, listenerId);
- onMulticastForwardingStateChanged(newState.multicastForwardingEnabled);
mState = newState;
ActiveOperationalDataset newActiveDataset;
@@ -1357,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..c1cf0a0 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: [
@@ -41,6 +42,7 @@
"guava",
"guava-android-testlib",
"net-tests-utils",
+ "ThreadNetworkTestUtils",
"truth",
],
libs: [
diff --git a/thread/tests/cts/src/android/net/thread/cts/ThreadNetworkControllerTest.java b/thread/tests/cts/src/android/net/thread/cts/ThreadNetworkControllerTest.java
index 3bec36b..36ce4d5 100644
--- a/thread/tests/cts/src/android/net/thread/cts/ThreadNetworkControllerTest.java
+++ b/thread/tests/cts/src/android/net/thread/cts/ThreadNetworkControllerTest.java
@@ -17,7 +17,6 @@
package android.net.thread.cts;
import static android.Manifest.permission.ACCESS_NETWORK_STATE;
-import static android.Manifest.permission.MANAGE_TEST_NETWORKS;
import static android.net.thread.ThreadNetworkController.DEVICE_ROLE_CHILD;
import static android.net.thread.ThreadNetworkController.DEVICE_ROLE_LEADER;
import static android.net.thread.ThreadNetworkController.DEVICE_ROLE_ROUTER;
@@ -33,7 +32,6 @@
import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
-import static com.android.testutils.TestNetworkTrackerKt.initTestNetwork;
import static com.android.testutils.TestPermissionUtil.runAsShell;
import static com.google.common.truth.Truth.assertThat;
@@ -48,7 +46,6 @@
import android.content.Context;
import android.net.ConnectivityManager;
-import android.net.LinkAddress;
import android.net.Network;
import android.net.NetworkCapabilities;
import android.net.NetworkRequest;
@@ -62,7 +59,9 @@
import android.net.thread.ThreadNetworkController.StateCallback;
import android.net.thread.ThreadNetworkException;
import android.net.thread.ThreadNetworkManager;
+import android.net.thread.utils.TapTestNetworkTracker;
import android.os.Build;
+import android.os.HandlerThread;
import android.os.OutcomeReceiver;
import androidx.annotation.NonNull;
@@ -74,7 +73,6 @@
import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
import com.android.testutils.DevSdkIgnoreRunner;
import com.android.testutils.FunctionalUtils.ThrowingRunnable;
-import com.android.testutils.TestNetworkTracker;
import org.junit.After;
import org.junit.Before;
@@ -110,7 +108,7 @@
private static final int NETWORK_CALLBACK_TIMEOUT_MILLIS = 10 * 1000;
private static final int CALLBACK_TIMEOUT_MILLIS = 1_000;
private static final int ENABLED_TIMEOUT_MILLIS = 2_000;
- private static final int SERVICE_DISCOVERY_TIMEOUT_MILLIS = 10 * 1000;
+ private static final int SERVICE_DISCOVERY_TIMEOUT_MILLIS = 30_000;
private static final String MESHCOP_SERVICE_TYPE = "_meshcop._udp";
private static final String THREAD_NETWORK_PRIVILEGED =
"android.permission.THREAD_NETWORK_PRIVILEGED";
@@ -123,12 +121,11 @@
private NsdManager mNsdManager;
private Set<String> mGrantedPermissions;
+ private HandlerThread mHandlerThread;
+ private TapTestNetworkTracker mTestNetworkTracker;
@Before
public void setUp() throws Exception {
-
- mGrantedPermissions = new HashSet<String>();
- mExecutor = Executors.newSingleThreadExecutor();
ThreadNetworkManager manager = mContext.getSystemService(ThreadNetworkManager.class);
if (manager != null) {
mController = manager.getAllThreadNetworkControllers().get(0);
@@ -138,20 +135,23 @@
// tests if a feature is not available.
assumeNotNull(mController);
- setEnabledAndWait(mController, true);
-
+ mGrantedPermissions = new HashSet<String>();
+ mExecutor = Executors.newSingleThreadExecutor();
mNsdManager = mContext.getSystemService(NsdManager.class);
+ mHandlerThread = new HandlerThread(this.getClass().getSimpleName());
+ mHandlerThread.start();
+
+ setEnabledAndWait(mController, true);
}
@After
public void tearDown() throws Exception {
- if (mController != null) {
- grantPermissions(THREAD_NETWORK_PRIVILEGED);
- CompletableFuture<Void> future = new CompletableFuture<>();
- mController.leave(mExecutor, future::complete);
- future.get(LEAVE_TIMEOUT_MILLIS, MILLISECONDS);
+ if (mController == null) {
+ return;
}
dropAllPermissions();
+ leaveAndWait(mController);
+ tearDownTestNetwork();
}
@Test
@@ -829,7 +829,7 @@
@Test
public void meshcopService_threadEnabledButNotJoined_discoveredButNoNetwork() throws Exception {
- TestNetworkTracker testNetwork = setUpTestNetwork();
+ setUpTestNetwork();
setEnabledAndWait(mController, true);
leaveAndWait(mController);
@@ -845,13 +845,11 @@
assertThat(txtMap.get("rv")).isNotNull();
assertThat(txtMap.get("tv")).isNotNull();
assertThat(txtMap.get("sb")).isNotNull();
-
- tearDownTestNetwork(testNetwork);
}
@Test
public void meshcopService_joinedNetwork_discoveredHasNetwork() throws Exception {
- TestNetworkTracker testNetwork = setUpTestNetwork();
+ setUpTestNetwork();
String networkName = "TestNet" + new Random().nextInt(10_000);
joinRandomizedDatasetAndWait(mController, networkName);
@@ -872,27 +870,26 @@
assertThat(txtMap.get("tv")).isNotNull();
assertThat(txtMap.get("sb")).isNotNull();
assertThat(txtMap.get("id").length).isEqualTo(16);
-
- tearDownTestNetwork(testNetwork);
}
@Test
public void meshcopService_threadDisabled_notDiscovered() throws Exception {
- TestNetworkTracker testNetwork = setUpTestNetwork();
+ setUpTestNetwork();
CompletableFuture<NsdServiceInfo> serviceLostFuture = new CompletableFuture<>();
NsdManager.DiscoveryListener listener =
discoverForServiceLost(MESHCOP_SERVICE_TYPE, serviceLostFuture);
setEnabledAndWait(mController, false);
-
try {
- serviceLostFuture.get(10_000, MILLISECONDS);
+ serviceLostFuture.get(SERVICE_DISCOVERY_TIMEOUT_MILLIS, MILLISECONDS);
+ } catch (InterruptedException | ExecutionException | TimeoutException ignored) {
+ // It's fine if the service lost event didn't show up. The service may not ever be
+ // advertised.
} finally {
mNsdManager.stopServiceDiscovery(listener);
}
- assertThrows(TimeoutException.class, () -> discoverService(MESHCOP_SERVICE_TYPE));
- tearDownTestNetwork(testNetwork);
+ assertThrows(TimeoutException.class, () -> discoverService(MESHCOP_SERVICE_TYPE));
}
private static void dropAllPermissions() {
@@ -1163,14 +1160,17 @@
}
}
- TestNetworkTracker setUpTestNetwork() {
- return runAsShell(
- MANAGE_TEST_NETWORKS,
- () -> initTestNetwork(mContext, new LinkAddress("2001:db8:123::/64"), 10_000));
+ private void setUpTestNetwork() {
+ assertThat(mTestNetworkTracker).isNull();
+ mTestNetworkTracker = new TapTestNetworkTracker(mContext, mHandlerThread.getLooper());
}
- void tearDownTestNetwork(TestNetworkTracker testNetwork) {
- runAsShell(MANAGE_TEST_NETWORKS, () -> testNetwork.teardown());
+ private void tearDownTestNetwork() throws InterruptedException {
+ if (mTestNetworkTracker != null) {
+ mTestNetworkTracker.tearDown();
+ }
+ mHandlerThread.quitSafely();
+ mHandlerThread.join();
}
private static class DefaultDiscoveryListener implements NsdManager.DiscoveryListener {
diff --git a/thread/tests/integration/src/android/net/thread/BorderRoutingTest.java b/thread/tests/integration/src/android/net/thread/BorderRoutingTest.java
index 7554610..e8ef346 100644
--- a/thread/tests/integration/src/android/net/thread/BorderRoutingTest.java
+++ b/thread/tests/integration/src/android/net/thread/BorderRoutingTest.java
@@ -296,6 +296,16 @@
}
@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));
@@ -364,9 +374,13 @@
subscribeMulticastAddressAndWait(ftd2, GROUP_ADDR_SCOPE_4);
mInfraDevice.sendEchoRequest(GROUP_ADDR_SCOPE_5);
- mInfraDevice.sendEchoRequest(GROUP_ADDR_SCOPE_4);
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()));
}
@@ -395,11 +409,13 @@
startFtdChild(ftd2);
subscribeMulticastAddressAndWait(ftd2, GROUP_ADDR_SCOPE_5);
- // Send the request twice as the order of replies from ftd1 and ftd2 are not guaranteed
- mInfraDevice.sendEchoRequest(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()));
}
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 4948c22..60a5f2b 100644
--- a/thread/tests/unit/src/com/android/server/thread/ThreadNetworkControllerServiceTest.java
+++ b/thread/tests/unit/src/com/android/server/thread/ThreadNetworkControllerServiceTest.java
@@ -16,7 +16,6 @@
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;
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 66%
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..49b002a 100644
--- a/thread/tests/unit/src/android/net/thread/ThreadPersistentSettingsTest.java
+++ b/thread/tests/unit/src/com/android/server/thread/ThreadPersistentSettingsTest.java
@@ -17,24 +17,26 @@
package com.android.server.thread;
import static com.android.server.thread.ThreadPersistentSettings.THREAD_ENABLED;
-
import static com.google.common.truth.Truth.assertThat;
-
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.filters.SmallTest;
import androidx.test.runner.AndroidJUnit4;
-
+import com.android.connectivity.resources.R;
+import com.android.server.connectivity.ConnectivityResources;
+import java.io.ByteArrayOutputStream;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
@@ -42,25 +44,27 @@
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
-import java.io.ByteArrayOutputStream;
-import java.io.FileInputStream;
-import java.io.FileOutputStream;
-
/** Unit tests for {@link ThreadPersistentSettings}. */
@RunWith(AndroidJUnit4.class)
@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 +74,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 +117,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/thread/tests/utils/Android.bp b/thread/tests/utils/Android.bp
new file mode 100644
index 0000000..24e9bb9
--- /dev/null
+++ b/thread/tests/utils/Android.bp
@@ -0,0 +1,37 @@
+//
+// 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 {
+ default_team: "trendy_team_fwk_thread_network",
+ default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+java_library {
+ name: "ThreadNetworkTestUtils",
+ min_sdk_version: "30",
+ static_libs: [
+ "compatibility-device-util-axt",
+ "net-tests-utils",
+ "net-utils-device-common",
+ "net-utils-device-common-bpf",
+ ],
+ srcs: [
+ "src/**/*.java",
+ ],
+ defaults: [
+ "framework-connectivity-test-defaults",
+ ],
+}
diff --git a/thread/tests/utils/src/android/net/thread/utils/TapTestNetworkTracker.java b/thread/tests/utils/src/android/net/thread/utils/TapTestNetworkTracker.java
new file mode 100644
index 0000000..43f177d
--- /dev/null
+++ b/thread/tests/utils/src/android/net/thread/utils/TapTestNetworkTracker.java
@@ -0,0 +1,185 @@
+/*
+ * 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.thread.utils;
+
+import static android.Manifest.permission.MANAGE_TEST_NETWORKS;
+import static android.net.InetAddresses.parseNumericAddress;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_TRUSTED;
+import static android.net.NetworkCapabilities.TRANSPORT_TEST;
+import static android.system.OsConstants.AF_INET6;
+import static android.system.OsConstants.IPPROTO_UDP;
+import static android.system.OsConstants.SOCK_DGRAM;
+
+import static com.android.testutils.RecorderCallback.CallbackEntry.LINK_PROPERTIES_CHANGED;
+import static com.android.testutils.TestPermissionUtil.runAsShell;
+
+import android.content.Context;
+import android.net.ConnectivityManager;
+import android.net.LinkAddress;
+import android.net.LinkProperties;
+import android.net.Network;
+import android.net.NetworkAgentConfig;
+import android.net.NetworkCapabilities;
+import android.net.NetworkRequest;
+import android.net.TestNetworkInterface;
+import android.net.TestNetworkManager;
+import android.net.TestNetworkSpecifier;
+import android.os.Looper;
+import android.system.ErrnoException;
+import android.system.Os;
+
+import com.android.compatibility.common.util.PollingCheck;
+import com.android.testutils.TestableNetworkAgent;
+import com.android.testutils.TestableNetworkCallback;
+
+import java.io.FileDescriptor;
+import java.io.IOException;
+import java.net.InterfaceAddress;
+import java.net.NetworkInterface;
+import java.net.SocketException;
+import java.time.Duration;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/** A class that can create/destroy a test network based on TAP interface. */
+public final class TapTestNetworkTracker {
+ private static final Duration TIMEOUT = Duration.ofSeconds(2);
+ private final Context mContext;
+ private final Looper mLooper;
+ private TestNetworkInterface mInterface;
+ private TestableNetworkAgent mAgent;
+ private final TestableNetworkCallback mNetworkCallback;
+ private final ConnectivityManager mConnectivityManager;
+
+ /**
+ * Constructs a {@link TapTestNetworkTracker}.
+ *
+ * <p>It creates a TAP interface (e.g. testtap0) and registers a test network using that
+ * interface. It also requests the test network by {@link ConnectivityManager#requestNetwork} so
+ * the test network won't be automatically turned down by {@link
+ * com.android.server.ConnectivityService}.
+ */
+ public TapTestNetworkTracker(Context context, Looper looper) {
+ mContext = context;
+ mLooper = looper;
+ mConnectivityManager = mContext.getSystemService(ConnectivityManager.class);
+ mNetworkCallback = new TestableNetworkCallback();
+ runAsShell(MANAGE_TEST_NETWORKS, this::setUpTestNetwork);
+ }
+
+ /** Tears down the test network. */
+ public void tearDown() {
+ runAsShell(MANAGE_TEST_NETWORKS, this::tearDownTestNetwork);
+ }
+
+ /** Returns the interface name of the test network. */
+ public String getInterfaceName() {
+ return mInterface.getInterfaceName();
+ }
+
+ private void setUpTestNetwork() throws Exception {
+ mInterface = mContext.getSystemService(TestNetworkManager.class).createTapInterface();
+
+ mConnectivityManager.requestNetwork(newNetworkRequest(), mNetworkCallback);
+
+ LinkProperties lp = new LinkProperties();
+ lp.setInterfaceName(getInterfaceName());
+ mAgent =
+ new TestableNetworkAgent(
+ mContext,
+ mLooper,
+ newNetworkCapabilities(),
+ lp,
+ new NetworkAgentConfig.Builder().build());
+ final Network network = mAgent.register();
+ mAgent.markConnected();
+
+ PollingCheck.check(
+ "No usable address on interface",
+ TIMEOUT.toMillis(),
+ () -> hasUsableAddress(network, getInterfaceName()));
+
+ lp.setLinkAddresses(makeLinkAddresses());
+ mAgent.sendLinkProperties(lp);
+ mNetworkCallback.eventuallyExpect(
+ LINK_PROPERTIES_CHANGED,
+ TIMEOUT.toMillis(),
+ l -> !l.getLp().getAddresses().isEmpty());
+ }
+
+ private void tearDownTestNetwork() throws IOException {
+ mConnectivityManager.unregisterNetworkCallback(mNetworkCallback);
+ mAgent.unregister();
+ mInterface.getFileDescriptor().close();
+ mAgent.waitForIdle(TIMEOUT.toMillis());
+ }
+
+ private NetworkRequest newNetworkRequest() {
+ return new NetworkRequest.Builder()
+ .removeCapability(NET_CAPABILITY_TRUSTED)
+ .addTransportType(TRANSPORT_TEST)
+ .setNetworkSpecifier(new TestNetworkSpecifier(getInterfaceName()))
+ .build();
+ }
+
+ private NetworkCapabilities newNetworkCapabilities() {
+ return new NetworkCapabilities()
+ .removeCapability(NET_CAPABILITY_TRUSTED)
+ .addTransportType(TRANSPORT_TEST)
+ .setNetworkSpecifier(new TestNetworkSpecifier(getInterfaceName()));
+ }
+
+ private List<LinkAddress> makeLinkAddresses() {
+ List<LinkAddress> linkAddresses = new ArrayList<>();
+ List<InterfaceAddress> interfaceAddresses = Collections.emptyList();
+
+ try {
+ interfaceAddresses =
+ NetworkInterface.getByName(getInterfaceName()).getInterfaceAddresses();
+ } catch (SocketException ignored) {
+ // Ignore failures when getting the addresses.
+ }
+
+ for (InterfaceAddress address : interfaceAddresses) {
+ linkAddresses.add(
+ new LinkAddress(address.getAddress(), address.getNetworkPrefixLength()));
+ }
+
+ return linkAddresses;
+ }
+
+ private static boolean hasUsableAddress(Network network, String interfaceName) {
+ try {
+ if (NetworkInterface.getByName(interfaceName).getInterfaceAddresses().isEmpty()) {
+ return false;
+ }
+ } catch (SocketException e) {
+ return false;
+ }
+ // Check if the link-local address can be used. Address flags are not available without
+ // elevated permissions, so check that bindSocket works.
+ try {
+ FileDescriptor sock = Os.socket(AF_INET6, SOCK_DGRAM, IPPROTO_UDP);
+ network.bindSocket(sock);
+ Os.connect(sock, parseNumericAddress("ff02::fb%" + interfaceName), 12345);
+ Os.close(sock);
+ } catch (ErrnoException | IOException e) {
+ return false;
+ }
+ return true;
+ }
+}