Merge "Add MdnsPacketRepeater"
diff --git a/service/mdns/com/android/server/connectivity/mdns/MdnsAdvertiser.java b/service/mdns/com/android/server/connectivity/mdns/MdnsAdvertiser.java
new file mode 100644
index 0000000..dee78fd
--- /dev/null
+++ b/service/mdns/com/android/server/connectivity/mdns/MdnsAdvertiser.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.connectivity.mdns;
+
+import android.util.Log;
+
+/**
+ * MdnsAdvertiser manages advertising services per {@link com.android.server.NsdService} requests.
+ *
+ * TODO: implement
+ */
+public class MdnsAdvertiser {
+ private static final String TAG = MdnsAdvertiser.class.getSimpleName();
+ public static final boolean DBG = Log.isLoggable(TAG, Log.DEBUG);
+}
diff --git a/service/mdns/com/android/server/connectivity/mdns/MdnsPacket.java b/service/mdns/com/android/server/connectivity/mdns/MdnsPacket.java
new file mode 100644
index 0000000..eae084a
--- /dev/null
+++ b/service/mdns/com/android/server/connectivity/mdns/MdnsPacket.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.connectivity.mdns;
+
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * A class holding data that can be included in a mDNS packet.
+ */
+public class MdnsPacket {
+ public final int flags;
+ public final List<MdnsRecord> questions;
+ public final List<MdnsRecord> answers;
+ public final List<MdnsRecord> authorityRecords;
+ public final List<MdnsRecord> additionalRecords;
+
+ MdnsPacket(int flags,
+ List<MdnsRecord> questions,
+ List<MdnsRecord> answers,
+ List<MdnsRecord> authorityRecords,
+ List<MdnsRecord> additionalRecords) {
+ this.flags = flags;
+ this.questions = Collections.unmodifiableList(questions);
+ this.answers = Collections.unmodifiableList(answers);
+ this.authorityRecords = Collections.unmodifiableList(authorityRecords);
+ this.additionalRecords = Collections.unmodifiableList(additionalRecords);
+ }
+}
diff --git a/service/mdns/com/android/server/connectivity/mdns/MdnsPacketRepeater.java b/service/mdns/com/android/server/connectivity/mdns/MdnsPacketRepeater.java
new file mode 100644
index 0000000..015dbd8
--- /dev/null
+++ b/service/mdns/com/android/server/connectivity/mdns/MdnsPacketRepeater.java
@@ -0,0 +1,179 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.connectivity.mdns;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import android.util.Log;
+
+import java.io.IOException;
+import java.net.SocketAddress;
+
+/**
+ * A class used to send several packets at given time intervals.
+ * @param <T> The type of the request providing packet repeating parameters.
+ */
+public abstract class MdnsPacketRepeater<T extends MdnsPacketRepeater.Request> {
+ private static final boolean DBG = MdnsAdvertiser.DBG;
+ @NonNull
+ private final MdnsReplySender mReplySender;
+ @NonNull
+ protected final Handler mHandler;
+ @Nullable
+ private final PacketRepeaterCallback<T> mCb;
+
+ /**
+ * Status callback from {@link MdnsPacketRepeater}.
+ *
+ * Callbacks are called on the {@link MdnsPacketRepeater} handler thread.
+ * @param <T> The type of the request providing packet repeating parameters.
+ */
+ public interface PacketRepeaterCallback<T extends MdnsPacketRepeater.Request> {
+ /**
+ * Called when a packet was sent.
+ */
+ default void onSent(int index, @NonNull T info) {}
+
+ /**
+ * Called when the {@link MdnsPacketRepeater} is done sending packets.
+ */
+ default void onFinished(@NonNull T info) {}
+ }
+
+ /**
+ * A request to repeat packets.
+ *
+ * All methods are called in the looper thread.
+ */
+ public interface Request {
+ /**
+ * Get a packet to send for one iteration.
+ */
+ @NonNull
+ MdnsPacket getPacket(int index);
+
+ /**
+ * Get a set of destinations for the packet for one iteration.
+ */
+ @NonNull
+ Iterable<SocketAddress> getDestinations(int index);
+
+ /**
+ * Get the delay in milliseconds until the next packet transmission.
+ */
+ long getDelayMs(int nextIndex);
+
+ /**
+ * Get the number of packets that should be sent.
+ */
+ int getNumSends();
+ }
+
+ /**
+ * Get the logging tag to use.
+ */
+ @NonNull
+ protected abstract String getTag();
+
+ private final class ProbeHandler extends Handler {
+ ProbeHandler(@NonNull Looper looper) {
+ super(looper);
+ }
+
+ @Override
+ public void handleMessage(@NonNull Message msg) {
+ final int index = msg.arg1;
+ final T request = (T) msg.obj;
+
+ if (index >= request.getNumSends()) {
+ if (mCb != null) {
+ mCb.onFinished(request);
+ }
+ return;
+ }
+
+ final MdnsPacket packet = request.getPacket(index);
+ final Iterable<SocketAddress> destinations = request.getDestinations(index);
+ if (DBG) {
+ Log.v(getTag(), "Sending packets to " + destinations + " for iteration "
+ + index + " out of " + request.getNumSends());
+ }
+ for (SocketAddress destination : destinations) {
+ try {
+ mReplySender.sendNow(packet, destination);
+ } catch (IOException e) {
+ Log.e(getTag(), "Error sending packet to " + destination, e);
+ }
+ }
+
+ int nextIndex = index + 1;
+ // No need to go through the last handler loop if there's no callback to call
+ if (nextIndex < request.getNumSends() || mCb != null) {
+ // TODO: consider using AlarmManager / WakeupMessage to avoid missing sending during
+ // deep sleep; but this would affect battery life, and discovered services are
+ // likely not to be available since the device is in deep sleep anyway.
+ final long delay = request.getDelayMs(nextIndex);
+ sendMessageDelayed(obtainMessage(msg.what, nextIndex, 0, request), delay);
+ if (DBG) Log.v(getTag(), "Scheduled next packet in " + delay + "ms");
+ }
+
+ // Call onSent after scheduling the next run, to allow the callback to cancel it
+ if (mCb != null) {
+ mCb.onSent(index, request);
+ }
+ }
+ }
+
+ protected MdnsPacketRepeater(@NonNull Looper looper, @NonNull MdnsReplySender replySender,
+ @Nullable PacketRepeaterCallback<T> cb) {
+ mHandler = new ProbeHandler(looper);
+ mReplySender = replySender;
+ mCb = cb;
+ }
+
+ protected void startSending(int id, @NonNull T request, long initialDelayMs) {
+ if (DBG) {
+ Log.v(getTag(), "Starting send with id " + id + ", request "
+ + request.getClass().getSimpleName() + ", delay " + initialDelayMs);
+ }
+ mHandler.sendMessageDelayed(mHandler.obtainMessage(id, 0, 0, request), initialDelayMs);
+ }
+
+ /**
+ * Stop sending the packets for the specified ID
+ * @return true if probing was in progress, false if this was a no-op
+ */
+ public boolean stop(int id) {
+ if (mHandler.getLooper().getThread() != Thread.currentThread()) {
+ throw new IllegalStateException("stop can only be called from the looper thread");
+ }
+ // Since this is run on the looper thread, messages cannot be currently processing and are
+ // all in the handler queue; unless this method is called from a message, but the current
+ // message cannot be cancelled.
+ if (mHandler.hasMessages(id)) {
+ if (DBG) {
+ Log.v(getTag(), "Stopping send on id " + id);
+ }
+ mHandler.removeMessages(id);
+ return true;
+ }
+ return false;
+ }
+}
diff --git a/service/mdns/com/android/server/connectivity/mdns/MdnsPacketWriter.java b/service/mdns/com/android/server/connectivity/mdns/MdnsPacketWriter.java
index 611787f..1f22fa9 100644
--- a/service/mdns/com/android/server/connectivity/mdns/MdnsPacketWriter.java
+++ b/service/mdns/com/android/server/connectivity/mdns/MdnsPacketWriter.java
@@ -29,7 +29,7 @@
public class MdnsPacketWriter {
private static final int MDNS_POINTER_MASK = 0xC000;
private final byte[] data;
- private final Map<Integer, String[]> labelDictionary;
+ private final Map<Integer, String[]> labelDictionary = new HashMap<>();
private int pos = 0;
private int savedWritePos = -1;
@@ -44,7 +44,15 @@
}
data = new byte[maxSize];
- labelDictionary = new HashMap<>();
+ }
+
+ /**
+ * Constructs a writer for a new packet.
+ *
+ * @param buffer The buffer to write to.
+ */
+ public MdnsPacketWriter(byte[] buffer) {
+ data = buffer;
}
/** Returns the current write position. */
diff --git a/service/mdns/com/android/server/connectivity/mdns/MdnsReplySender.java b/service/mdns/com/android/server/connectivity/mdns/MdnsReplySender.java
new file mode 100644
index 0000000..2acd789
--- /dev/null
+++ b/service/mdns/com/android/server/connectivity/mdns/MdnsReplySender.java
@@ -0,0 +1,88 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.connectivity.mdns;
+
+import android.annotation.NonNull;
+import android.os.Looper;
+
+import java.io.IOException;
+import java.net.DatagramPacket;
+import java.net.MulticastSocket;
+import java.net.SocketAddress;
+
+/**
+ * A class that handles sending mDNS replies to a {@link MulticastSocket}, possibly queueing them
+ * to be sent after some delay.
+ *
+ * TODO: implement sending after a delay, combining queued replies and duplicate answer suppression
+ */
+public class MdnsReplySender {
+ @NonNull
+ private final MulticastSocket mSocket;
+ @NonNull
+ private final Looper mLooper;
+ @NonNull
+ private final byte[] mPacketCreationBuffer;
+
+ public MdnsReplySender(@NonNull Looper looper,
+ @NonNull MulticastSocket socket, @NonNull byte[] packetCreationBuffer) {
+ mLooper = looper;
+ mSocket = socket;
+ mPacketCreationBuffer = packetCreationBuffer;
+ }
+
+ /**
+ * Send a packet immediately.
+ *
+ * Must be called on the looper thread used by the {@link MdnsReplySender}.
+ */
+ public void sendNow(@NonNull MdnsPacket packet, @NonNull SocketAddress destination)
+ throws IOException {
+ if (Thread.currentThread() != mLooper.getThread()) {
+ throw new IllegalStateException("sendNow must be called in the handler thread");
+ }
+
+ // TODO: support packets over size (send in multiple packets with TC bit set)
+ final MdnsPacketWriter writer = new MdnsPacketWriter(mPacketCreationBuffer);
+
+ writer.writeUInt16(0); // Transaction ID (advertisement: 0)
+ writer.writeUInt16(packet.flags); // Response, authoritative (rfc6762 18.4)
+ writer.writeUInt16(packet.questions.size()); // questions count
+ writer.writeUInt16(packet.answers.size()); // answers count
+ writer.writeUInt16(packet.authorityRecords.size()); // authority entries count
+ writer.writeUInt16(packet.additionalRecords.size()); // additional records count
+
+ for (MdnsRecord record : packet.questions) {
+ record.write(writer, 0L);
+ }
+ for (MdnsRecord record : packet.answers) {
+ record.write(writer, 0L);
+ }
+ for (MdnsRecord record : packet.authorityRecords) {
+ record.write(writer, 0L);
+ }
+ for (MdnsRecord record : packet.additionalRecords) {
+ record.write(writer, 0L);
+ }
+
+ final int len = writer.getWritePosition();
+ final byte[] outBuffer = new byte[len];
+ System.arraycopy(mPacketCreationBuffer, 0, outBuffer, 0, len);
+
+ mSocket.send(new DatagramPacket(outBuffer, 0, len, destination));
+ }
+}