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));
+    }
+}