Merge "[Thread] fix access to mOtDaemon" into main
diff --git a/framework-t/src/android/net/EthernetManager.java b/framework-t/src/android/net/EthernetManager.java
index b8070f0..719f60d 100644
--- a/framework-t/src/android/net/EthernetManager.java
+++ b/framework-t/src/android/net/EthernetManager.java
@@ -642,7 +642,14 @@
}
/**
- * Listen to changes in the state of ethernet.
+ * Register a IntConsumer to be called back on ethernet state changes.
+ *
+ * <p>{@link IntConsumer#accept} with the current ethernet state will be triggered immediately
+ * upon adding a listener. The same callback is invoked on Ethernet state change, i.e. when
+ * calling {@link #setEthernetEnabled}.
+ * <p>The reported state is represented by:
+ * {@link #ETHERNET_STATE_DISABLED}: ethernet is now disabled.
+ * {@link #ETHERNET_STATE_ENABLED}: ethernet is now enabled.
*
* @param executor to run callbacks on.
* @param listener to listen ethernet state changed.
diff --git a/service-t/src/com/android/server/NsdService.java b/service-t/src/com/android/server/NsdService.java
index cfb1a33..aca386f 100644
--- a/service-t/src/com/android/server/NsdService.java
+++ b/service-t/src/com/android/server/NsdService.java
@@ -1851,6 +1851,8 @@
mContext, MdnsFeatureFlags.NSD_UNICAST_REPLY_ENABLED))
.setIsAggressiveQueryModeEnabled(mDeps.isFeatureEnabled(
mContext, MdnsFeatureFlags.NSD_AGGRESSIVE_QUERY_MODE))
+ .setIsQueryWithKnownAnswerEnabled(mDeps.isFeatureEnabled(
+ mContext, MdnsFeatureFlags.NSD_QUERY_WITH_KNOWN_ANSWER))
.setOverrideProvider(flag -> mDeps.isFeatureEnabled(
mContext, FORCE_ENABLE_FLAG_FOR_TEST_PREFIX + flag))
.build();
diff --git a/service-t/src/com/android/server/connectivity/mdns/EnqueueMdnsQueryCallable.java b/service-t/src/com/android/server/connectivity/mdns/EnqueueMdnsQueryCallable.java
index c4d3338..e61555a 100644
--- a/service-t/src/com/android/server/connectivity/mdns/EnqueueMdnsQueryCallable.java
+++ b/service-t/src/com/android/server/connectivity/mdns/EnqueueMdnsQueryCallable.java
@@ -23,6 +23,7 @@
import android.text.TextUtils;
import android.util.Pair;
+import com.android.net.module.util.CollectionUtils;
import com.android.net.module.util.SharedLog;
import com.android.server.connectivity.mdns.util.MdnsUtils;
@@ -63,8 +64,6 @@
@NonNull
private final WeakReference<MdnsSocketClientBase> weakRequestSender;
@NonNull
- private final MdnsPacketWriter packetWriter;
- @NonNull
private final String[] serviceTypeLabels;
@NonNull
private final List<String> subtypes;
@@ -79,11 +78,15 @@
private final MdnsUtils.Clock clock;
@NonNull
private final SharedLog sharedLog;
+ @NonNull
+ private final MdnsServiceTypeClient.Dependencies dependencies;
private final boolean onlyUseIpv6OnIpv6OnlyNetworks;
+ private final byte[] packetCreationBuffer = new byte[1500]; // TODO: use interface MTU
+ @NonNull
+ private final List<MdnsResponse> existingServices;
EnqueueMdnsQueryCallable(
@NonNull MdnsSocketClientBase requestSender,
- @NonNull MdnsPacketWriter packetWriter,
@NonNull String serviceType,
@NonNull Collection<String> subtypes,
boolean expectUnicastResponse,
@@ -93,9 +96,10 @@
boolean sendDiscoveryQueries,
@NonNull Collection<MdnsResponse> servicesToResolve,
@NonNull MdnsUtils.Clock clock,
- @NonNull SharedLog sharedLog) {
+ @NonNull SharedLog sharedLog,
+ @NonNull MdnsServiceTypeClient.Dependencies dependencies,
+ @NonNull Collection<MdnsResponse> existingServices) {
weakRequestSender = new WeakReference<>(requestSender);
- this.packetWriter = packetWriter;
serviceTypeLabels = TextUtils.split(serviceType, "\\.");
this.subtypes = new ArrayList<>(subtypes);
this.expectUnicastResponse = expectUnicastResponse;
@@ -106,6 +110,8 @@
this.servicesToResolve = new ArrayList<>(servicesToResolve);
this.clock = clock;
this.sharedLog = sharedLog;
+ this.dependencies = dependencies;
+ this.existingServices = new ArrayList<>(existingServices);
}
/**
@@ -176,29 +182,52 @@
return Pair.create(INVALID_TRANSACTION_ID, new ArrayList<>());
}
+ // Put the existing ptr records into known-answer section.
+ final List<MdnsRecord> knownAnswers = new ArrayList<>();
+ if (sendDiscoveryQueries) {
+ for (MdnsResponse existingService : existingServices) {
+ for (MdnsPointerRecord ptrRecord : existingService.getPointerRecords()) {
+ // Ignore any PTR records that don't match the current query.
+ if (!CollectionUtils.any(questions,
+ q -> q instanceof MdnsPointerRecord
+ && MdnsUtils.equalsDnsLabelIgnoreDnsCase(
+ q.getName(), ptrRecord.getName()))) {
+ continue;
+ }
+
+ knownAnswers.add(new MdnsPointerRecord(
+ ptrRecord.getName(),
+ ptrRecord.getReceiptTime(),
+ ptrRecord.getCacheFlush(),
+ ptrRecord.getRemainingTTL(now), // Put the remaining ttl.
+ ptrRecord.getPointer()));
+ }
+ }
+ }
+
final MdnsPacket queryPacket = new MdnsPacket(
transactionId,
MdnsConstants.FLAGS_QUERY,
questions,
- Collections.emptyList(), /* answers */
+ knownAnswers,
Collections.emptyList(), /* authorityRecords */
Collections.emptyList() /* additionalRecords */);
- MdnsUtils.writeMdnsPacket(packetWriter, queryPacket);
- sendPacketToIpv4AndIpv6(requestSender, MdnsConstants.MDNS_PORT);
+ sendPacketToIpv4AndIpv6(requestSender, MdnsConstants.MDNS_PORT, queryPacket);
for (Integer emulatorPort : castShellEmulatorMdnsPorts) {
- sendPacketToIpv4AndIpv6(requestSender, emulatorPort);
+ sendPacketToIpv4AndIpv6(requestSender, emulatorPort, queryPacket);
}
return Pair.create(transactionId, subtypes);
- } catch (IOException e) {
+ } catch (Exception e) {
sharedLog.e(String.format("Failed to create mDNS packet for subtype: %s.",
TextUtils.join(",", subtypes)), e);
return Pair.create(INVALID_TRANSACTION_ID, new ArrayList<>());
}
}
- private void sendPacket(MdnsSocketClientBase requestSender, InetSocketAddress address)
- throws IOException {
- DatagramPacket packet = packetWriter.getPacket(address);
+ private void sendPacket(MdnsSocketClientBase requestSender, InetSocketAddress address,
+ MdnsPacket mdnsPacket) throws IOException {
+ final DatagramPacket packet = dependencies.getDatagramPacketFromMdnsPacket(
+ packetCreationBuffer, mdnsPacket, address);
if (expectUnicastResponse) {
// MdnsMultinetworkSocketClient is only available on T+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU
@@ -222,16 +251,17 @@
}
}
- private void sendPacketToIpv4AndIpv6(MdnsSocketClientBase requestSender, int port) {
+ private void sendPacketToIpv4AndIpv6(MdnsSocketClientBase requestSender, int port,
+ MdnsPacket mdnsPacket) {
try {
sendPacket(requestSender,
- new InetSocketAddress(MdnsConstants.getMdnsIPv4Address(), port));
+ new InetSocketAddress(MdnsConstants.getMdnsIPv4Address(), port), mdnsPacket);
} catch (IOException e) {
sharedLog.e("Can't send packet to IPv4", e);
}
try {
sendPacket(requestSender,
- new InetSocketAddress(MdnsConstants.getMdnsIPv6Address(), port));
+ new InetSocketAddress(MdnsConstants.getMdnsIPv6Address(), port), mdnsPacket);
} catch (IOException e) {
sharedLog.e("Can't send packet to IPv6", e);
}
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 21b7069..7b0c738 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsDiscoveryManager.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsDiscoveryManager.java
@@ -362,7 +362,7 @@
return new MdnsServiceTypeClient(
serviceType, socketClient,
executorProvider.newServiceTypeClientSchedulerExecutor(), socketKey,
- sharedLog.forSubComponent(tag), looper, serviceCache);
+ sharedLog.forSubComponent(tag), looper, serviceCache, mdnsFeatureFlags);
}
/**
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsFeatureFlags.java b/service-t/src/com/android/server/connectivity/mdns/MdnsFeatureFlags.java
index 56202fd..f4a08ba 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsFeatureFlags.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsFeatureFlags.java
@@ -62,6 +62,11 @@
*/
public static final String NSD_AGGRESSIVE_QUERY_MODE = "nsd_aggressive_query_mode";
+ /**
+ * A feature flag to control whether the query with known-answer should be enabled.
+ */
+ public static final String NSD_QUERY_WITH_KNOWN_ANSWER = "nsd_query_with_known_answer";
+
// Flag for offload feature
public final boolean mIsMdnsOffloadFeatureEnabled;
@@ -83,6 +88,9 @@
// Flag for aggressive query mode
public final boolean mIsAggressiveQueryModeEnabled;
+ // Flag for query with known-answer
+ public final boolean mIsQueryWithKnownAnswerEnabled;
+
@Nullable
private final FlagOverrideProvider mOverrideProvider;
@@ -126,6 +134,14 @@
}
/**
+ * Indicates whether {@link #NSD_QUERY_WITH_KNOWN_ANSWER} is enabled, including for testing.
+ */
+ public boolean isQueryWithKnownAnswerEnabled() {
+ return mIsQueryWithKnownAnswerEnabled
+ || isForceEnabledForTest(NSD_QUERY_WITH_KNOWN_ANSWER);
+ }
+
+ /**
* The constructor for {@link MdnsFeatureFlags}.
*/
public MdnsFeatureFlags(boolean isOffloadFeatureEnabled,
@@ -135,6 +151,7 @@
boolean isKnownAnswerSuppressionEnabled,
boolean isUnicastReplyEnabled,
boolean isAggressiveQueryModeEnabled,
+ boolean isQueryWithKnownAnswerEnabled,
@Nullable FlagOverrideProvider overrideProvider) {
mIsMdnsOffloadFeatureEnabled = isOffloadFeatureEnabled;
mIncludeInetAddressRecordsInProbing = includeInetAddressRecordsInProbing;
@@ -143,6 +160,7 @@
mIsKnownAnswerSuppressionEnabled = isKnownAnswerSuppressionEnabled;
mIsUnicastReplyEnabled = isUnicastReplyEnabled;
mIsAggressiveQueryModeEnabled = isAggressiveQueryModeEnabled;
+ mIsQueryWithKnownAnswerEnabled = isQueryWithKnownAnswerEnabled;
mOverrideProvider = overrideProvider;
}
@@ -162,6 +180,7 @@
private boolean mIsKnownAnswerSuppressionEnabled;
private boolean mIsUnicastReplyEnabled;
private boolean mIsAggressiveQueryModeEnabled;
+ private boolean mIsQueryWithKnownAnswerEnabled;
private FlagOverrideProvider mOverrideProvider;
/**
@@ -175,6 +194,7 @@
mIsKnownAnswerSuppressionEnabled = false;
mIsUnicastReplyEnabled = true;
mIsAggressiveQueryModeEnabled = false;
+ mIsQueryWithKnownAnswerEnabled = false;
mOverrideProvider = null;
}
@@ -261,6 +281,16 @@
}
/**
+ * Set whether the query with known-answer is enabled.
+ *
+ * @see #NSD_QUERY_WITH_KNOWN_ANSWER
+ */
+ public Builder setIsQueryWithKnownAnswerEnabled(boolean isQueryWithKnownAnswerEnabled) {
+ mIsQueryWithKnownAnswerEnabled = isQueryWithKnownAnswerEnabled;
+ return this;
+ }
+
+ /**
* Builds a {@link MdnsFeatureFlags} with the arguments supplied to this builder.
*/
public MdnsFeatureFlags build() {
@@ -271,6 +301,7 @@
mIsKnownAnswerSuppressionEnabled,
mIsUnicastReplyEnabled,
mIsAggressiveQueryModeEnabled,
+ mIsQueryWithKnownAnswerEnabled,
mOverrideProvider);
}
}
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 8f41b94..bfcd0b4 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsServiceTypeClient.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsServiceTypeClient.java
@@ -35,9 +35,12 @@
import com.android.net.module.util.SharedLog;
import com.android.server.connectivity.mdns.util.MdnsUtils;
+import java.io.IOException;
import java.io.PrintWriter;
+import java.net.DatagramPacket;
import java.net.Inet4Address;
import java.net.Inet6Address;
+import java.net.InetSocketAddress;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Arrays;
@@ -55,7 +58,6 @@
public class MdnsServiceTypeClient {
private static final String TAG = MdnsServiceTypeClient.class.getSimpleName();
- private static final int DEFAULT_MTU = 1500;
@VisibleForTesting
static final int EVENT_START_QUERYTASK = 1;
static final int EVENT_QUERY_RESULT = 2;
@@ -84,6 +86,7 @@
notifyRemovedServiceToListeners(previousResponse, "Service record expired");
}
};
+ @NonNull private final MdnsFeatureFlags featureFlags;
private final ArrayMap<MdnsServiceBrowserListener, ListenerInfo> listeners =
new ArrayMap<>();
private final boolean removeServiceAfterTtlExpires =
@@ -142,7 +145,8 @@
// 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(), needSendDiscoveryQueries(listeners));
+ getAllDiscoverySubtypes(), needSendDiscoveryQueries(listeners),
+ getExistingServices());
executor.submit(queryTask);
break;
}
@@ -221,6 +225,16 @@
public void sendMessage(@NonNull Handler handler, @NonNull Message message) {
handler.sendMessage(message);
}
+
+ /**
+ * Generate a DatagramPacket from given MdnsPacket and InetSocketAddress.
+ */
+ public DatagramPacket getDatagramPacketFromMdnsPacket(@NonNull byte[] packetCreationBuffer,
+ @NonNull MdnsPacket packet, @NonNull InetSocketAddress address) throws IOException {
+ final byte[] queryBuffer =
+ MdnsUtils.createRawDnsPacket(packetCreationBuffer, packet);
+ return new DatagramPacket(queryBuffer, 0, queryBuffer.length, address);
+ }
}
/**
@@ -236,9 +250,10 @@
@NonNull SocketKey socketKey,
@NonNull SharedLog sharedLog,
@NonNull Looper looper,
- @NonNull MdnsServiceCache serviceCache) {
+ @NonNull MdnsServiceCache serviceCache,
+ @NonNull MdnsFeatureFlags featureFlags) {
this(serviceType, socketClient, executor, new Clock(), socketKey, sharedLog, looper,
- new Dependencies(), serviceCache);
+ new Dependencies(), serviceCache, featureFlags);
}
@VisibleForTesting
@@ -251,7 +266,8 @@
@NonNull SharedLog sharedLog,
@NonNull Looper looper,
@NonNull Dependencies dependencies,
- @NonNull MdnsServiceCache serviceCache) {
+ @NonNull MdnsServiceCache serviceCache,
+ @NonNull MdnsFeatureFlags featureFlags) {
this.serviceType = serviceType;
this.socketClient = socketClient;
this.executor = executor;
@@ -265,6 +281,7 @@
this.serviceCache = serviceCache;
this.mdnsQueryScheduler = new MdnsQueryScheduler();
this.cacheKey = new MdnsServiceCache.CacheKey(serviceType, socketKey);
+ this.featureFlags = featureFlags;
}
/**
@@ -327,6 +344,11 @@
now.plusMillis(response.getMinRemainingTtl(now.toEpochMilli())));
}
+ private List<MdnsResponse> getExistingServices() {
+ return featureFlags.isQueryWithKnownAnswerEnabled()
+ ? serviceCache.getCachedServices(cacheKey) : Collections.emptyList();
+ }
+
/**
* Registers {@code listener} for receiving discovery event of mDNS service instances, and
* starts
@@ -391,7 +413,8 @@
final QueryTask queryTask = new QueryTask(
mdnsQueryScheduler.scheduleFirstRun(taskConfig, now,
minRemainingTtl, currentSessionId), servicesToResolve,
- getAllDiscoverySubtypes(), needSendDiscoveryQueries(listeners));
+ getAllDiscoverySubtypes(), needSendDiscoveryQueries(listeners),
+ getExistingServices());
executor.submit(queryTask);
}
@@ -617,11 +640,6 @@
return searchOptions != null && searchOptions.removeExpiredService();
}
- @VisibleForTesting
- MdnsPacketWriter createMdnsPacketWriter() {
- return new MdnsPacketWriter(DEFAULT_MTU);
- }
-
private List<MdnsResponse> makeResponsesForResolve(@NonNull SocketKey socketKey) {
final List<MdnsResponse> resolveResponses = new ArrayList<>();
for (int i = 0; i < listeners.size(); i++) {
@@ -694,14 +712,16 @@
private final List<MdnsResponse> servicesToResolve = new ArrayList<>();
private final List<String> subtypes = new ArrayList<>();
private final boolean sendDiscoveryQueries;
+ private final List<MdnsResponse> existingServices = new ArrayList<>();
QueryTask(@NonNull MdnsQueryScheduler.ScheduledQueryTaskArgs taskArgs,
@NonNull Collection<MdnsResponse> servicesToResolve,
- @NonNull Collection<String> subtypes,
- boolean sendDiscoveryQueries) {
+ @NonNull Collection<String> subtypes, boolean sendDiscoveryQueries,
+ @NonNull Collection<MdnsResponse> existingServices) {
this.taskArgs = taskArgs;
this.servicesToResolve.addAll(servicesToResolve);
this.subtypes.addAll(subtypes);
this.sendDiscoveryQueries = sendDiscoveryQueries;
+ this.existingServices.addAll(existingServices);
}
@Override
@@ -711,7 +731,6 @@
result =
new EnqueueMdnsQueryCallable(
socketClient,
- createMdnsPacketWriter(),
serviceType,
subtypes,
taskArgs.config.expectUnicastResponse,
@@ -721,7 +740,9 @@
sendDiscoveryQueries,
servicesToResolve,
clock,
- sharedLog)
+ sharedLog,
+ dependencies,
+ existingServices)
.call();
} catch (RuntimeException e) {
sharedLog.e(String.format("Failed to run EnqueueMdnsQueryCallable for subtype: %s",
diff --git a/tests/cts/net/src/android/net/cts/NsdManagerTest.kt b/tests/cts/net/src/android/net/cts/NsdManagerTest.kt
index 0daf7fe..61117df 100644
--- a/tests/cts/net/src/android/net/cts/NsdManagerTest.kt
+++ b/tests/cts/net/src/android/net/cts/NsdManagerTest.kt
@@ -1888,6 +1888,64 @@
}
}
+ @Test
+ fun testQueryWhenKnownAnswerSuppressionFlagSet() {
+ // The flag may be removed in the future but known-answer suppression should be enabled by
+ // default in that case. The rule will reset flags automatically on teardown.
+ deviceConfigRule.setConfig(NAMESPACE_TETHERING, "test_nsd_query_with_known_answer", "1")
+
+ // Register service on testNetwork1
+ val discoveryRecord = NsdDiscoveryRecord()
+ val packetReader = TapPacketReader(Handler(handlerThread.looper),
+ testNetwork1.iface.fileDescriptor.fileDescriptor, 1500 /* maxPacketSize */)
+ packetReader.startAsyncForTest()
+ handlerThread.waitForIdle(TIMEOUT_MS)
+
+ nsdManager.discoverServices(serviceType, NsdManager.PROTOCOL_DNS_SD,
+ testNetwork1.network, { it.run() }, discoveryRecord)
+
+ tryTest {
+ discoveryRecord.expectCallback<DiscoveryStarted>()
+ assertNotNull(packetReader.pollForQuery("$serviceType.local", DnsResolver.TYPE_PTR))
+ /*
+ Generated with:
+ scapy.raw(scapy.DNS(rd=0, qr=1, aa=1, qd = None, an =
+ scapy.DNSRR(rrname='_nmt123456789._tcp.local', type='PTR', ttl=120,
+ rdata='NsdTest123456789._nmt123456789._tcp.local'))).hex()
+ */
+ val ptrResponsePayload = HexDump.hexStringToByteArray("0000840000000001000000000d5f6e" +
+ "6d74313233343536373839045f746370056c6f63616c00000c000100000078002b104e736454" +
+ "6573743132333435363738390d5f6e6d74313233343536373839045f746370056c6f63616c00")
+
+ replaceServiceNameAndTypeWithTestSuffix(ptrResponsePayload)
+ packetReader.sendResponse(buildMdnsPacket(ptrResponsePayload))
+
+ val serviceFound = discoveryRecord.expectCallback<ServiceFound>()
+ serviceFound.serviceInfo.let {
+ assertEquals(serviceName, it.serviceName)
+ // Discovered service types have a dot at the end
+ assertEquals("$serviceType.", it.serviceType)
+ assertEquals(testNetwork1.network, it.network)
+ // ServiceFound does not provide port, address or attributes (only information
+ // available in the PTR record is included in that callback, regardless of whether
+ // other records exist).
+ assertEquals(0, it.port)
+ assertEmpty(it.hostAddresses)
+ assertEquals(0, it.attributes.size)
+ }
+
+ // Expect the second query with a known answer
+ val query = packetReader.pollForMdnsPacket { pkt ->
+ pkt.isQueryFor("$serviceType.local", DnsResolver.TYPE_PTR) &&
+ pkt.isReplyFor("$serviceType.local", DnsResolver.TYPE_PTR)
+ }
+ assertNotNull(query)
+ } cleanup {
+ nsdManager.stopServiceDiscovery(discoveryRecord)
+ discoveryRecord.expectCallback<DiscoveryStopped>()
+ }
+ }
+
private fun makeLinkLocalAddressOfOtherDeviceOnPrefix(network: Network): Inet6Address {
val lp = cm.getLinkProperties(network) ?: fail("No LinkProperties for net $network")
// Expect to have a /64 link-local address
diff --git a/tests/unit/java/com/android/server/connectivity/mdns/MdnsServiceTypeClientTests.java b/tests/unit/java/com/android/server/connectivity/mdns/MdnsServiceTypeClientTests.java
index f279c5a..2eb9440 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/MdnsServiceTypeClientTests.java
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsServiceTypeClientTests.java
@@ -38,6 +38,7 @@
import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.doCallRealMethod;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.inOrder;
import static org.mockito.Mockito.never;
@@ -117,8 +118,6 @@
@Mock
private MdnsServiceBrowserListener mockListenerTwo;
@Mock
- private MdnsPacketWriter mockPacketWriter;
- @Mock
private MdnsMultinetworkSocketClient mockSocketClient;
@Mock
private Network mockNetwork;
@@ -145,6 +144,7 @@
private long latestDelayMs = 0;
private Message delayMessage = null;
private Handler realHandler = null;
+ private MdnsFeatureFlags featureFlags = MdnsFeatureFlags.newBuilder().build();
@Before
@SuppressWarnings("DoNotMock")
@@ -162,7 +162,8 @@
expectedIPv6Packets[i] = new DatagramPacket(buf, 0 /* offset */, 5 /* length */,
MdnsConstants.getMdnsIPv6Address(), MdnsConstants.MDNS_PORT);
}
- when(mockPacketWriter.getPacket(IPV4_ADDRESS))
+ when(mockDeps.getDatagramPacketFromMdnsPacket(
+ any(), any(MdnsPacket.class), eq(IPV4_ADDRESS)))
.thenReturn(expectedIPv4Packets[0])
.thenReturn(expectedIPv4Packets[1])
.thenReturn(expectedIPv4Packets[2])
@@ -188,7 +189,8 @@
.thenReturn(expectedIPv4Packets[22])
.thenReturn(expectedIPv4Packets[23]);
- when(mockPacketWriter.getPacket(IPV6_ADDRESS))
+ when(mockDeps.getDatagramPacketFromMdnsPacket(
+ any(), any(MdnsPacket.class), eq(IPV6_ADDRESS)))
.thenReturn(expectedIPv6Packets[0])
.thenReturn(expectedIPv6Packets[1])
.thenReturn(expectedIPv6Packets[2])
@@ -242,22 +244,13 @@
return true;
}).when(mockDeps).sendMessage(any(Handler.class), any(Message.class));
- client = makeMdnsServiceTypeClient(mockPacketWriter);
+ client = makeMdnsServiceTypeClient();
}
- private MdnsServiceTypeClient makeMdnsServiceTypeClient(
- @Nullable MdnsPacketWriter packetWriter) {
+ private MdnsServiceTypeClient makeMdnsServiceTypeClient() {
return new MdnsServiceTypeClient(SERVICE_TYPE, mockSocketClient, currentThreadExecutor,
mockDecoderClock, socketKey, mockSharedLog, thread.getLooper(), mockDeps,
- serviceCache) {
- @Override
- MdnsPacketWriter createMdnsPacketWriter() {
- if (packetWriter == null) {
- return super.createMdnsPacketWriter();
- }
- return packetWriter;
- }
- };
+ serviceCache, featureFlags);
}
@After
@@ -697,11 +690,12 @@
@Test
public void testCombinedSubtypesQueriedWithMultipleListeners() throws Exception {
- client = makeMdnsServiceTypeClient(/* packetWriter= */ null);
final MdnsSearchOptions searchOptions1 = MdnsSearchOptions.newBuilder()
.addSubtype("subtype1").build();
final MdnsSearchOptions searchOptions2 = MdnsSearchOptions.newBuilder()
.addSubtype("subtype2").build();
+ doCallRealMethod().when(mockDeps).getDatagramPacketFromMdnsPacket(
+ any(), any(MdnsPacket.class), any(InetSocketAddress.class));
startSendAndReceive(mockListenerOne, searchOptions1);
currentThreadExecutor.getAndClearLastScheduledRunnable().run();
@@ -1021,7 +1015,6 @@
public void processResponse_searchOptionsEnableServiceRemoval_shouldRemove()
throws Exception {
final String serviceInstanceName = "service-instance-1";
- client = makeMdnsServiceTypeClient(mockPacketWriter);
MdnsSearchOptions searchOptions = MdnsSearchOptions.newBuilder()
.setRemoveExpiredService(true)
.setNumOfQueriesBeforeBackoff(Integer.MAX_VALUE)
@@ -1059,7 +1052,6 @@
public void processResponse_searchOptionsNotEnableServiceRemoval_shouldNotRemove()
throws Exception {
final String serviceInstanceName = "service-instance-1";
- client = makeMdnsServiceTypeClient(mockPacketWriter);
startSendAndReceive(mockListenerOne, MdnsSearchOptions.getDefaultOptions());
Runnable firstMdnsTask = currentThreadExecutor.getAndClearSubmittedRunnable();
@@ -1085,7 +1077,6 @@
throws Exception {
//MdnsConfigsFlagsImpl.removeServiceAfterTtlExpires.override(true);
final String serviceInstanceName = "service-instance-1";
- client = makeMdnsServiceTypeClient(mockPacketWriter);
startSendAndReceive(mockListenerOne, MdnsSearchOptions.getDefaultOptions());
Runnable firstMdnsTask = currentThreadExecutor.getAndClearSubmittedRunnable();
@@ -1200,8 +1191,6 @@
@Test
public void testProcessResponse_Resolve() throws Exception {
- client = makeMdnsServiceTypeClient(/* packetWriter= */ null);
-
final String instanceName = "service-instance";
final String[] hostname = new String[] { "testhost "};
final String ipV4Address = "192.0.2.0";
@@ -1212,6 +1201,9 @@
final MdnsSearchOptions resolveOptions2 = MdnsSearchOptions.newBuilder()
.setResolveInstanceName(instanceName).build();
+ doCallRealMethod().when(mockDeps).getDatagramPacketFromMdnsPacket(
+ any(), any(MdnsPacket.class), any(InetSocketAddress.class));
+
startSendAndReceive(mockListenerOne, resolveOptions1);
startSendAndReceive(mockListenerTwo, resolveOptions2);
// No need to verify order for both listeners; and order is not guaranteed between them
@@ -1316,8 +1308,6 @@
@Test
public void testRenewTxtSrvInResolve() throws Exception {
- client = makeMdnsServiceTypeClient(/* packetWriter= */ null);
-
final String instanceName = "service-instance";
final String[] hostname = new String[] { "testhost "};
final String ipV4Address = "192.0.2.0";
@@ -1326,6 +1316,9 @@
final MdnsSearchOptions resolveOptions = MdnsSearchOptions.newBuilder()
.setResolveInstanceName(instanceName).build();
+ doCallRealMethod().when(mockDeps).getDatagramPacketFromMdnsPacket(
+ any(), any(MdnsPacket.class), any(InetSocketAddress.class));
+
startSendAndReceive(mockListenerOne, resolveOptions);
InOrder inOrder = inOrder(mockListenerOne, mockSocketClient);
@@ -1430,8 +1423,6 @@
@Test
public void testProcessResponse_ResolveExcludesOtherServices() {
- client = makeMdnsServiceTypeClient(/* packetWriter= */ null);
-
final String requestedInstance = "instance1";
final String otherInstance = "instance2";
final String ipV4Address = "192.0.2.0";
@@ -1498,8 +1489,6 @@
@Test
public void testProcessResponse_SubtypeDiscoveryLimitedToSubtype() {
- client = makeMdnsServiceTypeClient(/* packetWriter= */ null);
-
final String matchingInstance = "instance1";
final String subtype = "_subtype";
final String otherInstance = "instance2";
@@ -1586,8 +1575,6 @@
@Test
public void testProcessResponse_SubtypeChange() {
- client = makeMdnsServiceTypeClient(/* packetWriter= */ null);
-
final String matchingInstance = "instance1";
final String subtype = "_subtype";
final String ipV4Address = "192.0.2.0";
@@ -1669,8 +1656,6 @@
@Test
public void testNotifySocketDestroyed() throws Exception {
- client = makeMdnsServiceTypeClient(/* packetWriter= */ null);
-
final String requestedInstance = "instance1";
final String otherInstance = "instance2";
final String ipV4Address = "192.0.2.0";
@@ -1945,6 +1930,138 @@
16 /* scheduledCount */);
}
+ @Test
+ public void testSendQueryWithKnownAnswers() throws Exception {
+ client = new MdnsServiceTypeClient(SERVICE_TYPE, mockSocketClient, currentThreadExecutor,
+ mockDecoderClock, socketKey, mockSharedLog, thread.getLooper(), mockDeps,
+ serviceCache,
+ MdnsFeatureFlags.newBuilder().setIsQueryWithKnownAnswerEnabled(true).build());
+
+ doCallRealMethod().when(mockDeps).getDatagramPacketFromMdnsPacket(
+ any(), any(MdnsPacket.class), any(InetSocketAddress.class));
+
+ startSendAndReceive(mockListenerOne, MdnsSearchOptions.getDefaultOptions());
+ InOrder inOrder = inOrder(mockListenerOne, mockSocketClient);
+
+ final ArgumentCaptor<DatagramPacket> queryCaptor =
+ ArgumentCaptor.forClass(DatagramPacket.class);
+ currentThreadExecutor.getAndClearLastScheduledRunnable().run();
+ // Send twice for IPv4 and IPv6
+ inOrder.verify(mockSocketClient, times(2)).sendPacketRequestingUnicastResponse(
+ queryCaptor.capture(), eq(socketKey), eq(false));
+ verify(mockDeps, times(1)).sendMessage(any(), any(Message.class));
+ assertNotNull(delayMessage);
+
+ final MdnsPacket queryPacket = MdnsPacket.parse(
+ new MdnsPacketReader(queryCaptor.getValue()));
+ assertTrue(hasQuestion(queryPacket, MdnsRecord.TYPE_PTR));
+
+ // Process a response
+ final String serviceName = "service-instance";
+ final String ipV4Address = "192.0.2.0";
+ final String[] subtypeLabels = Stream.concat(Stream.of("_subtype", "_sub"),
+ Arrays.stream(SERVICE_TYPE_LABELS)).toArray(String[]::new);
+ final MdnsPacket packetWithoutSubtype = createResponse(
+ serviceName, ipV4Address, 5353, SERVICE_TYPE_LABELS,
+ Collections.emptyMap() /* textAttributes */, TEST_TTL);
+ final MdnsPointerRecord originalPtr = (MdnsPointerRecord) CollectionUtils.findFirst(
+ packetWithoutSubtype.answers, r -> r instanceof MdnsPointerRecord);
+
+ // Add a subtype PTR record
+ final ArrayList<MdnsRecord> newAnswers = new ArrayList<>(packetWithoutSubtype.answers);
+ newAnswers.add(new MdnsPointerRecord(subtypeLabels, originalPtr.getReceiptTime(),
+ originalPtr.getCacheFlush(), originalPtr.getTtl(), originalPtr.getPointer()));
+ final MdnsPacket packetWithSubtype = new MdnsPacket(
+ packetWithoutSubtype.flags,
+ packetWithoutSubtype.questions,
+ newAnswers,
+ packetWithoutSubtype.authorityRecords,
+ packetWithoutSubtype.additionalRecords);
+ processResponse(packetWithSubtype, socketKey);
+
+ // Expect a query with known answers
+ dispatchMessage();
+ final ArgumentCaptor<DatagramPacket> knownAnswersQueryCaptor =
+ ArgumentCaptor.forClass(DatagramPacket.class);
+ currentThreadExecutor.getAndClearLastScheduledRunnable().run();
+ inOrder.verify(mockSocketClient, times(2)).sendPacketRequestingMulticastResponse(
+ knownAnswersQueryCaptor.capture(), eq(socketKey), eq(false));
+
+ final MdnsPacket knownAnswersQueryPacket = MdnsPacket.parse(
+ new MdnsPacketReader(knownAnswersQueryCaptor.getValue()));
+ assertTrue(hasQuestion(knownAnswersQueryPacket, MdnsRecord.TYPE_PTR, SERVICE_TYPE_LABELS));
+ assertTrue(hasAnswer(knownAnswersQueryPacket, MdnsRecord.TYPE_PTR, SERVICE_TYPE_LABELS));
+ assertFalse(hasAnswer(knownAnswersQueryPacket, MdnsRecord.TYPE_PTR, subtypeLabels));
+ }
+
+ @Test
+ public void testSendQueryWithSubTypeWithKnownAnswers() throws Exception {
+ client = new MdnsServiceTypeClient(SERVICE_TYPE, mockSocketClient, currentThreadExecutor,
+ mockDecoderClock, socketKey, mockSharedLog, thread.getLooper(), mockDeps,
+ serviceCache,
+ MdnsFeatureFlags.newBuilder().setIsQueryWithKnownAnswerEnabled(true).build());
+
+ doCallRealMethod().when(mockDeps).getDatagramPacketFromMdnsPacket(
+ any(), any(MdnsPacket.class), any(InetSocketAddress.class));
+
+ final MdnsSearchOptions options = MdnsSearchOptions.newBuilder()
+ .addSubtype("subtype").build();
+ startSendAndReceive(mockListenerOne, options);
+ InOrder inOrder = inOrder(mockListenerOne, mockSocketClient);
+
+ final ArgumentCaptor<DatagramPacket> queryCaptor =
+ ArgumentCaptor.forClass(DatagramPacket.class);
+ currentThreadExecutor.getAndClearLastScheduledRunnable().run();
+ // Send twice for IPv4 and IPv6
+ inOrder.verify(mockSocketClient, times(2)).sendPacketRequestingUnicastResponse(
+ queryCaptor.capture(), eq(socketKey), eq(false));
+ verify(mockDeps, times(1)).sendMessage(any(), any(Message.class));
+ assertNotNull(delayMessage);
+
+ final MdnsPacket queryPacket = MdnsPacket.parse(
+ new MdnsPacketReader(queryCaptor.getValue()));
+ final String[] subtypeLabels = Stream.concat(Stream.of("_subtype", "_sub"),
+ Arrays.stream(SERVICE_TYPE_LABELS)).toArray(String[]::new);
+ assertTrue(hasQuestion(queryPacket, MdnsRecord.TYPE_PTR, SERVICE_TYPE_LABELS));
+ assertTrue(hasQuestion(queryPacket, MdnsRecord.TYPE_PTR, subtypeLabels));
+
+ // Process a response
+ final String serviceName = "service-instance";
+ final String ipV4Address = "192.0.2.0";
+ final MdnsPacket packetWithoutSubtype = createResponse(
+ serviceName, ipV4Address, 5353, SERVICE_TYPE_LABELS,
+ Collections.emptyMap() /* textAttributes */, TEST_TTL);
+ final MdnsPointerRecord originalPtr = (MdnsPointerRecord) CollectionUtils.findFirst(
+ packetWithoutSubtype.answers, r -> r instanceof MdnsPointerRecord);
+
+ // Add a subtype PTR record
+ final ArrayList<MdnsRecord> newAnswers = new ArrayList<>(packetWithoutSubtype.answers);
+ newAnswers.add(new MdnsPointerRecord(subtypeLabels, originalPtr.getReceiptTime(),
+ originalPtr.getCacheFlush(), originalPtr.getTtl(), originalPtr.getPointer()));
+ final MdnsPacket packetWithSubtype = new MdnsPacket(
+ packetWithoutSubtype.flags,
+ packetWithoutSubtype.questions,
+ newAnswers,
+ packetWithoutSubtype.authorityRecords,
+ packetWithoutSubtype.additionalRecords);
+ processResponse(packetWithSubtype, socketKey);
+
+ // Expect a query with known answers
+ dispatchMessage();
+ final ArgumentCaptor<DatagramPacket> knownAnswersQueryCaptor =
+ ArgumentCaptor.forClass(DatagramPacket.class);
+ currentThreadExecutor.getAndClearLastScheduledRunnable().run();
+ inOrder.verify(mockSocketClient, times(2)).sendPacketRequestingMulticastResponse(
+ knownAnswersQueryCaptor.capture(), eq(socketKey), eq(false));
+
+ final MdnsPacket knownAnswersQueryPacket = MdnsPacket.parse(
+ new MdnsPacketReader(knownAnswersQueryCaptor.getValue()));
+ assertTrue(hasQuestion(knownAnswersQueryPacket, MdnsRecord.TYPE_PTR, SERVICE_TYPE_LABELS));
+ assertTrue(hasQuestion(knownAnswersQueryPacket, MdnsRecord.TYPE_PTR, subtypeLabels));
+ assertTrue(hasAnswer(knownAnswersQueryPacket, MdnsRecord.TYPE_PTR, SERVICE_TYPE_LABELS));
+ assertTrue(hasAnswer(knownAnswersQueryPacket, MdnsRecord.TYPE_PTR, subtypeLabels));
+ }
+
private static MdnsServiceInfo matchServiceName(String name) {
return argThat(info -> info.getServiceInstanceName().equals(name));
}
@@ -2005,6 +2122,12 @@
&& (name == null || Arrays.equals(q.name, name)));
}
+ private static boolean hasAnswer(MdnsPacket packet, int type, @NonNull String[] name) {
+ return packet.answers.stream().anyMatch(q -> {
+ return q.getType() == type && (Arrays.equals(q.name, name));
+ });
+ }
+
// A fake ScheduledExecutorService that keeps tracking the last scheduled Runnable and its delay
// time.
private class FakeExecutor extends ScheduledThreadPoolExecutor {
diff --git a/thread/framework/Android.bp b/thread/framework/Android.bp
index 846253c..f8fe422 100644
--- a/thread/framework/Android.bp
+++ b/thread/framework/Android.bp
@@ -30,3 +30,14 @@
"//packages/modules/Connectivity:__subpackages__",
],
}
+
+filegroup {
+ name: "framework-thread-ot-daemon-shared-aidl-sources",
+ srcs: [
+ "java/android/net/thread/ChannelMaxPower.aidl",
+ ],
+ path: "java",
+ visibility: [
+ "//external/ot-br-posix:__subpackages__",
+ ],
+}
diff --git a/thread/framework/java/android/net/thread/ChannelMaxPower.aidl b/thread/framework/java/android/net/thread/ChannelMaxPower.aidl
new file mode 100644
index 0000000..bcda8a8
--- /dev/null
+++ b/thread/framework/java/android/net/thread/ChannelMaxPower.aidl
@@ -0,0 +1,28 @@
+/*
+ * 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;
+
+ /**
+ * Mapping from a channel to its max power.
+ *
+ * {@hide}
+ */
+parcelable ChannelMaxPower {
+ int channel; // The Thread radio channel.
+ int maxPower; // The max power in the unit of 0.01dBm. Passing INT16_MAX(32767) will
+ // disable the channel.
+}
diff --git a/thread/framework/java/android/net/thread/IThreadNetworkController.aidl b/thread/framework/java/android/net/thread/IThreadNetworkController.aidl
index 485e25d..c5ca557 100644
--- a/thread/framework/java/android/net/thread/IThreadNetworkController.aidl
+++ b/thread/framework/java/android/net/thread/IThreadNetworkController.aidl
@@ -17,6 +17,7 @@
package android.net.thread;
import android.net.thread.ActiveOperationalDataset;
+import android.net.thread.ChannelMaxPower;
import android.net.thread.IActiveOperationalDatasetReceiver;
import android.net.thread.IOperationalDatasetCallback;
import android.net.thread.IOperationReceiver;
@@ -39,6 +40,7 @@
void leave(in IOperationReceiver receiver);
void setTestNetworkAsUpstream(in String testNetworkInterfaceName, in IOperationReceiver receiver);
+ void setChannelMaxPowers(in ChannelMaxPower[] channelMaxPowers, in IOperationReceiver receiver);
int getThreadVersion();
void createRandomizedDataset(String networkName, IActiveOperationalDatasetReceiver receiver);
diff --git a/thread/framework/java/android/net/thread/ThreadNetworkController.java b/thread/framework/java/android/net/thread/ThreadNetworkController.java
index db761a3..8d6b40a 100644
--- a/thread/framework/java/android/net/thread/ThreadNetworkController.java
+++ b/thread/framework/java/android/net/thread/ThreadNetworkController.java
@@ -25,10 +25,12 @@
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.RequiresPermission;
+import android.annotation.Size;
import android.annotation.SystemApi;
import android.os.Binder;
import android.os.OutcomeReceiver;
import android.os.RemoteException;
+import android.util.SparseIntArray;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
@@ -98,6 +100,12 @@
/** Thread standard version 1.3. */
public static final int THREAD_VERSION_1_3 = 4;
+ /** Minimum value of max power in unit of 0.01dBm. @hide */
+ private static final int POWER_LIMITATION_MIN = -32768;
+
+ /** Maximum value of max power in unit of 0.01dBm. @hide */
+ private static final int POWER_LIMITATION_MAX = 32767;
+
/** @hide */
@Retention(RetentionPolicy.SOURCE)
@IntDef({THREAD_VERSION_1_3})
@@ -596,6 +604,98 @@
}
}
+ /**
+ * Sets max power of each channel.
+ *
+ * <p>If not set, the default max power is set by the Thread HAL service or the Thread radio
+ * chip firmware.
+ *
+ * <p>On success, the Pending Dataset is successfully registered and persisted on the Leader and
+ * {@link OutcomeReceiver#onResult} of {@code receiver} will be called; When failed, {@link
+ * OutcomeReceiver#onError} will be called with a specific error:
+ *
+ * <ul>
+ * <li>{@link ThreadNetworkException#ERROR_UNSUPPORTED_OPERATION} the operation is no
+ * supported by the platform.
+ * </ul>
+ *
+ * @param channelMaxPowers SparseIntArray (key: channel, value: max power) consists of channel
+ * and corresponding max power. Valid channel values should be between {@link
+ * ActiveOperationalDataset#CHANNEL_MIN_24_GHZ} and {@link
+ * ActiveOperationalDataset#CHANNEL_MAX_24_GHZ}. The unit of the max power is 0.01dBm. Max
+ * power values should be between INT16_MIN (-32768) and INT16_MAX (32767). If the max power
+ * is set to INT16_MAX, the corresponding channel is not supported.
+ * @param executor the executor to execute {@code receiver}.
+ * @param receiver the receiver to receive the result of this operation.
+ * @throws IllegalArgumentException if the size of {@code channelMaxPowers} is smaller than 1,
+ * or invalid channel or max power is configured.
+ * @hide
+ */
+ @RequiresPermission("android.permission.THREAD_NETWORK_PRIVILEGED")
+ public final void setChannelMaxPowers(
+ @NonNull @Size(min = 1) SparseIntArray channelMaxPowers,
+ @NonNull @CallbackExecutor Executor executor,
+ @NonNull OutcomeReceiver<Void, ThreadNetworkException> receiver) {
+ requireNonNull(channelMaxPowers, "channelMaxPowers cannot be null");
+ requireNonNull(executor, "executor cannot be null");
+ requireNonNull(receiver, "receiver cannot be null");
+
+ if (channelMaxPowers.size() < 1) {
+ throw new IllegalArgumentException("channelMaxPowers cannot be empty");
+ }
+
+ for (int i = 0; i < channelMaxPowers.size(); i++) {
+ int channel = channelMaxPowers.keyAt(i);
+ int maxPower = channelMaxPowers.get(channel);
+
+ if ((channel < ActiveOperationalDataset.CHANNEL_MIN_24_GHZ)
+ || (channel > ActiveOperationalDataset.CHANNEL_MAX_24_GHZ)) {
+ throw new IllegalArgumentException(
+ "Channel "
+ + channel
+ + " exceeds allowed range ["
+ + ActiveOperationalDataset.CHANNEL_MIN_24_GHZ
+ + ", "
+ + ActiveOperationalDataset.CHANNEL_MAX_24_GHZ
+ + "]");
+ }
+
+ if ((maxPower < POWER_LIMITATION_MIN) || (maxPower > POWER_LIMITATION_MAX)) {
+ throw new IllegalArgumentException(
+ "Channel power ({channel: "
+ + channel
+ + ", maxPower: "
+ + maxPower
+ + "}) exceeds allowed range ["
+ + POWER_LIMITATION_MIN
+ + ", "
+ + POWER_LIMITATION_MAX
+ + "]");
+ }
+ }
+
+ try {
+ mControllerService.setChannelMaxPowers(
+ toChannelMaxPowerArray(channelMaxPowers),
+ new OperationReceiverProxy(executor, receiver));
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ private static ChannelMaxPower[] toChannelMaxPowerArray(
+ @NonNull SparseIntArray channelMaxPowers) {
+ final ChannelMaxPower[] powerArray = new ChannelMaxPower[channelMaxPowers.size()];
+
+ for (int i = 0; i < channelMaxPowers.size(); i++) {
+ powerArray[i] = new ChannelMaxPower();
+ powerArray[i].channel = channelMaxPowers.keyAt(i);
+ powerArray[i].maxPower = channelMaxPowers.get(powerArray[i].channel);
+ }
+
+ return powerArray;
+ }
+
private static <T> void propagateError(
Executor executor,
OutcomeReceiver<T, ThreadNetworkException> receiver,
diff --git a/thread/framework/java/android/net/thread/ThreadNetworkException.java b/thread/framework/java/android/net/thread/ThreadNetworkException.java
index 4def0fb..f699c30 100644
--- a/thread/framework/java/android/net/thread/ThreadNetworkException.java
+++ b/thread/framework/java/android/net/thread/ThreadNetworkException.java
@@ -138,8 +138,17 @@
*/
public static final int ERROR_THREAD_DISABLED = 12;
+ /**
+ * The operation failed because it is not supported by the platform. For example, some platforms
+ * may not support setting the target power of each channel. The caller should not retry and may
+ * return an error to the user.
+ *
+ * @hide
+ */
+ public static final int ERROR_UNSUPPORTED_OPERATION = 13;
+
private static final int ERROR_MIN = ERROR_INTERNAL_ERROR;
- private static final int ERROR_MAX = ERROR_THREAD_DISABLED;
+ private static final int ERROR_MAX = ERROR_UNSUPPORTED_OPERATION;
private final int mErrorCode;
diff --git a/thread/service/java/com/android/server/thread/ThreadNetworkControllerService.java b/thread/service/java/com/android/server/thread/ThreadNetworkControllerService.java
index 964aee2..5f4627f 100644
--- a/thread/service/java/com/android/server/thread/ThreadNetworkControllerService.java
+++ b/thread/service/java/com/android/server/thread/ThreadNetworkControllerService.java
@@ -40,6 +40,7 @@
import static android.net.thread.ThreadNetworkException.ERROR_THREAD_DISABLED;
import static android.net.thread.ThreadNetworkException.ERROR_TIMEOUT;
import static android.net.thread.ThreadNetworkException.ERROR_UNSUPPORTED_CHANNEL;
+import static android.net.thread.ThreadNetworkException.ERROR_UNSUPPORTED_OPERATION;
import static android.net.thread.ThreadNetworkManager.DISALLOW_THREAD_NETWORK;
import static android.net.thread.ThreadNetworkManager.PERMISSION_THREAD_NETWORK_PRIVILEGED;
@@ -47,6 +48,7 @@
import static com.android.server.thread.openthread.IOtDaemon.ErrorCode.OT_ERROR_BUSY;
import static com.android.server.thread.openthread.IOtDaemon.ErrorCode.OT_ERROR_FAILED_PRECONDITION;
import static com.android.server.thread.openthread.IOtDaemon.ErrorCode.OT_ERROR_INVALID_STATE;
+import static com.android.server.thread.openthread.IOtDaemon.ErrorCode.OT_ERROR_NOT_IMPLEMENTED;
import static com.android.server.thread.openthread.IOtDaemon.ErrorCode.OT_ERROR_NO_BUFS;
import static com.android.server.thread.openthread.IOtDaemon.ErrorCode.OT_ERROR_PARSE;
import static com.android.server.thread.openthread.IOtDaemon.ErrorCode.OT_ERROR_REASSEMBLY_TIMEOUT;
@@ -86,6 +88,7 @@
import android.net.TestNetworkSpecifier;
import android.net.thread.ActiveOperationalDataset;
import android.net.thread.ActiveOperationalDataset.SecurityPolicy;
+import android.net.thread.ChannelMaxPower;
import android.net.thread.IActiveOperationalDatasetReceiver;
import android.net.thread.IOperationReceiver;
import android.net.thread.IOperationalDatasetCallback;
@@ -919,6 +922,8 @@
return ERROR_ABORTED;
case OT_ERROR_BUSY:
return ERROR_BUSY;
+ case OT_ERROR_NOT_IMPLEMENTED:
+ return ERROR_UNSUPPORTED_OPERATION;
case OT_ERROR_NO_BUFS:
return ERROR_RESOURCE_EXHAUSTED;
case OT_ERROR_PARSE:
@@ -1071,6 +1076,30 @@
}
}
+ @RequiresPermission(PERMISSION_THREAD_NETWORK_PRIVILEGED)
+ public void setChannelMaxPowers(
+ @NonNull ChannelMaxPower[] channelMaxPowers, @NonNull IOperationReceiver receiver) {
+ enforceAllPermissionsGranted(PERMISSION_THREAD_NETWORK_PRIVILEGED);
+
+ mHandler.post(
+ () ->
+ setChannelMaxPowersInternal(
+ channelMaxPowers, new OperationReceiverWrapper(receiver)));
+ }
+
+ private void setChannelMaxPowersInternal(
+ @NonNull ChannelMaxPower[] channelMaxPowers,
+ @NonNull OperationReceiverWrapper receiver) {
+ checkOnHandlerThread();
+
+ try {
+ getOtDaemon().setChannelMaxPowers(channelMaxPowers, newOtStatusReceiver(receiver));
+ } catch (RemoteException | ThreadNetworkException e) {
+ Log.e(TAG, "otDaemon.setChannelMaxPowers failed", e);
+ receiver.onError(ERROR_INTERNAL_ERROR, "Thread stack error");
+ }
+ }
+
private void enableBorderRouting(String infraIfName) {
if (mBorderRouterConfig.isBorderRoutingEnabled
&& infraIfName.equals(mBorderRouterConfig.infraInterfaceName)) {
diff --git a/thread/tests/integration/src/android/net/thread/ThreadNetworkControllerTest.java b/thread/tests/integration/src/android/net/thread/ThreadNetworkControllerTest.java
new file mode 100644
index 0000000..496ec9f
--- /dev/null
+++ b/thread/tests/integration/src/android/net/thread/ThreadNetworkControllerTest.java
@@ -0,0 +1,167 @@
+/*
+ * 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;
+
+import static android.net.thread.ThreadNetworkException.ERROR_UNSUPPORTED_OPERATION;
+
+import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
+
+import static com.android.testutils.TestPermissionUtil.runAsShell;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+
+import android.content.Context;
+import android.net.thread.utils.ThreadFeatureCheckerRule.RequiresThreadFeature;
+import android.os.OutcomeReceiver;
+import android.util.SparseIntArray;
+
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.filters.LargeTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+/** Tests for hide methods of {@link ThreadNetworkController}. */
+@LargeTest
+@RequiresThreadFeature
+@RunWith(AndroidJUnit4.class)
+public class ThreadNetworkControllerTest {
+ private static final int VALID_POWER = 32_767;
+ private static final int INVALID_POWER = 32_768;
+ private static final int VALID_CHANNEL = 20;
+ private static final int INVALID_CHANNEL = 10;
+ private static final String THREAD_NETWORK_PRIVILEGED =
+ "android.permission.THREAD_NETWORK_PRIVILEGED";
+
+ private static final SparseIntArray CHANNEL_MAX_POWERS =
+ new SparseIntArray() {
+ {
+ put(20, 32767);
+ }
+ };
+
+ private final Context mContext = ApplicationProvider.getApplicationContext();
+ private ExecutorService mExecutor;
+ private ThreadNetworkController mController;
+
+ @Before
+ public void setUp() throws Exception {
+ mController =
+ mContext.getSystemService(ThreadNetworkManager.class)
+ .getAllThreadNetworkControllers()
+ .get(0);
+
+ mExecutor = Executors.newSingleThreadExecutor();
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ dropAllPermissions();
+ }
+
+ @Test
+ public void setChannelMaxPowers_withPrivilegedPermission_success() throws Exception {
+ CompletableFuture<Void> powerFuture = new CompletableFuture<>();
+
+ runAsShell(
+ THREAD_NETWORK_PRIVILEGED,
+ () ->
+ mController.setChannelMaxPowers(
+ CHANNEL_MAX_POWERS, mExecutor, newOutcomeReceiver(powerFuture)));
+
+ try {
+ assertThat(powerFuture.get()).isNull();
+ } catch (ExecutionException exception) {
+ ThreadNetworkException thrown = (ThreadNetworkException) exception.getCause();
+ assertThat(thrown.getErrorCode()).isEqualTo(ERROR_UNSUPPORTED_OPERATION);
+ }
+ }
+
+ @Test
+ public void setChannelMaxPowers_withoutPrivilegedPermission_throwsSecurityException()
+ throws Exception {
+ dropAllPermissions();
+
+ assertThrows(
+ SecurityException.class,
+ () -> mController.setChannelMaxPowers(CHANNEL_MAX_POWERS, mExecutor, v -> {}));
+ }
+
+ @Test
+ public void setChannelMaxPowers_emptyChannelMaxPower_throwsIllegalArgumentException() {
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> mController.setChannelMaxPowers(new SparseIntArray(), mExecutor, v -> {}));
+ }
+
+ @Test
+ public void setChannelMaxPowers_invalidChannel_throwsIllegalArgumentException() {
+ final SparseIntArray INVALID_CHANNEL_ARRAY =
+ new SparseIntArray() {
+ {
+ put(INVALID_CHANNEL, VALID_POWER);
+ }
+ };
+
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> mController.setChannelMaxPowers(INVALID_CHANNEL_ARRAY, mExecutor, v -> {}));
+ }
+
+ @Test
+ public void setChannelMaxPowers_invalidPower_throwsIllegalArgumentException() {
+ final SparseIntArray INVALID_POWER_ARRAY =
+ new SparseIntArray() {
+ {
+ put(VALID_CHANNEL, INVALID_POWER);
+ }
+ };
+
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> mController.setChannelMaxPowers(INVALID_POWER_ARRAY, mExecutor, v -> {}));
+ }
+
+ private static void dropAllPermissions() {
+ getInstrumentation().getUiAutomation().dropShellPermissionIdentity();
+ }
+
+ private static <V> OutcomeReceiver<V, ThreadNetworkException> newOutcomeReceiver(
+ CompletableFuture<V> future) {
+ return new OutcomeReceiver<V, ThreadNetworkException>() {
+ @Override
+ public void onResult(V result) {
+ future.complete(result);
+ }
+
+ @Override
+ public void onError(ThreadNetworkException e) {
+ future.completeExceptionally(e);
+ }
+ };
+ }
+}
diff --git a/thread/tests/unit/src/android/net/thread/ThreadNetworkControllerTest.java b/thread/tests/unit/src/android/net/thread/ThreadNetworkControllerTest.java
index 75eb043..ac74372 100644
--- a/thread/tests/unit/src/android/net/thread/ThreadNetworkControllerTest.java
+++ b/thread/tests/unit/src/android/net/thread/ThreadNetworkControllerTest.java
@@ -19,6 +19,7 @@
import static android.net.thread.ThreadNetworkController.DEVICE_ROLE_CHILD;
import static android.net.thread.ThreadNetworkException.ERROR_UNAVAILABLE;
import static android.net.thread.ThreadNetworkException.ERROR_UNSUPPORTED_CHANNEL;
+import static android.net.thread.ThreadNetworkException.ERROR_UNSUPPORTED_OPERATION;
import static android.os.Process.SYSTEM_UID;
import static com.google.common.io.BaseEncoding.base16;
@@ -33,6 +34,7 @@
import android.os.Binder;
import android.os.OutcomeReceiver;
import android.os.Process;
+import android.util.SparseIntArray;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;
@@ -77,6 +79,13 @@
private static final ActiveOperationalDataset DEFAULT_DATASET =
ActiveOperationalDataset.fromThreadTlvs(DEFAULT_DATASET_TLVS);
+ private static final SparseIntArray DEFAULT_CHANNEL_POWERS =
+ new SparseIntArray() {
+ {
+ put(20, 32767);
+ }
+ };
+
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
@@ -111,6 +120,10 @@
return (IOperationReceiver) invocation.getArguments()[1];
}
+ private static IOperationReceiver getSetChannelMaxPowersReceiver(InvocationOnMock invocation) {
+ return (IOperationReceiver) invocation.getArguments()[1];
+ }
+
private static IActiveOperationalDatasetReceiver getCreateDatasetReceiver(
InvocationOnMock invocation) {
return (IActiveOperationalDatasetReceiver) invocation.getArguments()[1];
@@ -361,6 +374,51 @@
}
@Test
+ public void setChannelMaxPowers_callbackIsInvokedWithCallingAppIdentity() throws Exception {
+ setBinderUid(SYSTEM_UID);
+
+ AtomicInteger successCallbackUid = new AtomicInteger(0);
+ AtomicInteger errorCallbackUid = new AtomicInteger(0);
+
+ doAnswer(
+ invoke -> {
+ getSetChannelMaxPowersReceiver(invoke).onSuccess();
+ return null;
+ })
+ .when(mMockService)
+ .setChannelMaxPowers(any(ChannelMaxPower[].class), any(IOperationReceiver.class));
+ mController.setChannelMaxPowers(
+ DEFAULT_CHANNEL_POWERS,
+ Runnable::run,
+ v -> successCallbackUid.set(Binder.getCallingUid()));
+ doAnswer(
+ invoke -> {
+ getSetChannelMaxPowersReceiver(invoke)
+ .onError(ERROR_UNSUPPORTED_OPERATION, "");
+ return null;
+ })
+ .when(mMockService)
+ .setChannelMaxPowers(any(ChannelMaxPower[].class), any(IOperationReceiver.class));
+ mController.setChannelMaxPowers(
+ DEFAULT_CHANNEL_POWERS,
+ Runnable::run,
+ new OutcomeReceiver<>() {
+ @Override
+ public void onResult(Void unused) {}
+
+ @Override
+ public void onError(ThreadNetworkException e) {
+ errorCallbackUid.set(Binder.getCallingUid());
+ }
+ });
+
+ assertThat(successCallbackUid.get()).isNotEqualTo(SYSTEM_UID);
+ assertThat(successCallbackUid.get()).isEqualTo(Process.myUid());
+ assertThat(errorCallbackUid.get()).isNotEqualTo(SYSTEM_UID);
+ assertThat(errorCallbackUid.get()).isEqualTo(Process.myUid());
+ }
+
+ @Test
public void setTestNetworkAsUpstream_callbackIsInvokedWithCallingAppIdentity()
throws Exception {
setBinderUid(SYSTEM_UID);
diff --git a/thread/tests/unit/src/android/net/thread/ThreadNetworkExceptionTest.java b/thread/tests/unit/src/android/net/thread/ThreadNetworkExceptionTest.java
index f62b437..5908c20 100644
--- a/thread/tests/unit/src/android/net/thread/ThreadNetworkExceptionTest.java
+++ b/thread/tests/unit/src/android/net/thread/ThreadNetworkExceptionTest.java
@@ -32,6 +32,6 @@
public void constructor_tooLargeErrorCode_throwsIllegalArgumentException() throws Exception {
// TODO (b/323791003): move this test case to cts/ThreadNetworkExceptionTest when mainline
// CTS is ready.
- assertThrows(IllegalArgumentException.class, () -> new ThreadNetworkException(13, "13"));
+ assertThrows(IllegalArgumentException.class, () -> new ThreadNetworkException(14, "14"));
}
}