aboutsummaryrefslogtreecommitdiff
path: root/PrintServiceStubs/src/com/android/printservicestubs/servicediscovery
diff options
context:
space:
mode:
Diffstat (limited to 'PrintServiceStubs/src/com/android/printservicestubs/servicediscovery')
-rw-r--r--PrintServiceStubs/src/com/android/printservicestubs/servicediscovery/DiscoveryListener.java39
-rw-r--r--PrintServiceStubs/src/com/android/printservicestubs/servicediscovery/IDiscovery.java30
-rw-r--r--PrintServiceStubs/src/com/android/printservicestubs/servicediscovery/NetworkDevice.java299
-rw-r--r--PrintServiceStubs/src/com/android/printservicestubs/servicediscovery/NetworkDiscovery.java335
-rw-r--r--PrintServiceStubs/src/com/android/printservicestubs/servicediscovery/NetworkUtils.java290
-rw-r--r--PrintServiceStubs/src/com/android/printservicestubs/servicediscovery/ServiceParser.java35
-rw-r--r--PrintServiceStubs/src/com/android/printservicestubs/servicediscovery/mdns/BonjourException.java33
-rw-r--r--PrintServiceStubs/src/com/android/printservicestubs/servicediscovery/mdns/BonjourParser.java147
-rw-r--r--PrintServiceStubs/src/com/android/printservicestubs/servicediscovery/mdns/BonjourServiceParser.java31
-rw-r--r--PrintServiceStubs/src/com/android/printservicestubs/servicediscovery/mdns/DnsException.java30
-rw-r--r--PrintServiceStubs/src/com/android/printservicestubs/servicediscovery/mdns/DnsPacket.java472
-rw-r--r--PrintServiceStubs/src/com/android/printservicestubs/servicediscovery/mdns/DnsParser.java221
-rw-r--r--PrintServiceStubs/src/com/android/printservicestubs/servicediscovery/mdns/DnsSdException.java29
-rw-r--r--PrintServiceStubs/src/com/android/printservicestubs/servicediscovery/mdns/DnsSdParser.java166
-rw-r--r--PrintServiceStubs/src/com/android/printservicestubs/servicediscovery/mdns/DnsService.java68
-rw-r--r--PrintServiceStubs/src/com/android/printservicestubs/servicediscovery/mdns/MDNSUtils.java104
-rw-r--r--PrintServiceStubs/src/com/android/printservicestubs/servicediscovery/mdns/MDnsDiscovery.java207
17 files changed, 2536 insertions, 0 deletions
diff --git a/PrintServiceStubs/src/com/android/printservicestubs/servicediscovery/DiscoveryListener.java b/PrintServiceStubs/src/com/android/printservicestubs/servicediscovery/DiscoveryListener.java
new file mode 100644
index 0000000..ffd53ee
--- /dev/null
+++ b/PrintServiceStubs/src/com/android/printservicestubs/servicediscovery/DiscoveryListener.java
@@ -0,0 +1,39 @@
+/*
+ * (c) Copyright 2016 Mopria Alliance, Inc.
+ * (c) Copyright 2016 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.printservicestubs.servicediscovery;
+
+import android.annotation.NonNull;
+
+/**
+ * Listener for discovered network devices
+ */
+public interface DiscoveryListener {
+ /**
+ * Called when a device was removed.
+ *
+ * @param networkDevice The device that was removed
+ */
+ void onDeviceRemoved(@NonNull NetworkDevice networkDevice);
+
+ /**
+ * Called when a device was found.
+ *
+ * @param networkDevice The device that was found
+ */
+ void onDeviceFound(@NonNull NetworkDevice networkDevice);
+}
diff --git a/PrintServiceStubs/src/com/android/printservicestubs/servicediscovery/IDiscovery.java b/PrintServiceStubs/src/com/android/printservicestubs/servicediscovery/IDiscovery.java
new file mode 100644
index 0000000..f295a87
--- /dev/null
+++ b/PrintServiceStubs/src/com/android/printservicestubs/servicediscovery/IDiscovery.java
@@ -0,0 +1,30 @@
+/*
+ * (c) Copyright 2016 Mopria Alliance, Inc.
+ * (c) Copyright 2016 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.printservicestubs.servicediscovery;
+
+import java.net.DatagramPacket;
+import java.net.UnknownHostException;
+import java.util.ArrayList;
+
+public interface IDiscovery {
+ void clear();
+ DatagramPacket[] createQueryPackets() throws UnknownHostException;
+ ArrayList<ServiceParser> parseResponse(DatagramPacket reply);
+ int getPort();
+ boolean isFallback();
+}
diff --git a/PrintServiceStubs/src/com/android/printservicestubs/servicediscovery/NetworkDevice.java b/PrintServiceStubs/src/com/android/printservicestubs/servicediscovery/NetworkDevice.java
new file mode 100644
index 0000000..42a7aba
--- /dev/null
+++ b/PrintServiceStubs/src/com/android/printservicestubs/servicediscovery/NetworkDevice.java
@@ -0,0 +1,299 @@
+/*
+ * (c) Copyright 2016 Mopria Alliance, Inc.
+ * (c) Copyright 2016 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.printservicestubs.servicediscovery;
+
+import android.os.Bundle;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.text.TextUtils;
+
+import com.android.printservicestubs.servicediscovery.mdns.BonjourParser;
+
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * There is no public constructor. Instances are either returned by the printer
+ * discovery provided by the print system, read from parcels or loaded from
+ * shared preferences.
+ */
+@SuppressWarnings("unused")
+public final class NetworkDevice implements Parcelable {
+
+ public enum DiscoveryMethod {
+ MDNS_DISCOVERY,
+ SNMP_DISCOVERY,
+ OTHER_DISCOVERY,
+ }
+
+ private static final String TAG = "NetworkDiscovery";
+
+ /*
+ * Keep these declarations ordered alphabetically by field hostname. This helps
+ * to keep readFromParcel and writeToParcel up-to-date.
+ *
+ * The fields have package-level visibility because they must be
+ * accessible by the "friend" class EPrintersDatabase.
+ */
+ private final InetAddress inetAddress;
+ private final String model;
+ private final String hostname;
+
+ private final String bonjourName;
+ private final String bonjourDomainName;
+ private final DiscoveryMethod discoveryMethod;
+ private final String mBonjourService;
+ private final int mPort;
+ private final String mDeviceIdentifier;
+
+ private Bundle bonjourData = new Bundle();
+
+ private final List<NetworkDevice> mOtherInstances = new ArrayList<>();
+
+ public NetworkDevice(ServiceParser parser) throws IllegalArgumentException {
+ InetAddress inetAddress = parser.getAddress();
+ if (inetAddress == null) {
+ throw new IllegalArgumentException("inetAddress can not be null");
+ }
+ this.inetAddress = parser.getAddress();
+ this.model = parser.getModel();
+ this.hostname = parser.getHostname();
+ this.mPort = parser.getPort();
+ this.mDeviceIdentifier = parser.getDeviceIdentifier();
+
+ if (parser instanceof BonjourParser) {
+ BonjourParser bonjourParser = (BonjourParser)parser;
+ this.bonjourName = bonjourParser.getBonjourName();
+ this.bonjourDomainName = bonjourParser.getHostname();
+ this.mBonjourService = bonjourParser.getBonjourServiceName();
+ } else {
+ this.bonjourName = null;
+ this.bonjourDomainName = null;
+ this.mBonjourService = parser.getServiceName();
+ }
+ bonjourData.putAll(parser.getAllAttributes());
+
+ discoveryMethod = parser.getDiscoveryMethod();
+ }
+
+ private NetworkDevice(Parcel in) throws UnknownHostException {
+ int inetAddrSize = in.readInt();
+
+ if (inetAddrSize > 0) {
+ byte[] addr = new byte[inetAddrSize];
+ in.readByteArray(addr);
+ this.inetAddress = InetAddress.getByAddress(addr);
+ } else {
+ this.inetAddress = null;
+ }
+ this.model = in.readString();
+ this.hostname = in.readString();
+ this.mPort = in.readInt();
+ this.mDeviceIdentifier = in.readString();
+ this.bonjourName = in.readString();
+ this.bonjourDomainName = in.readString();
+ this.mBonjourService = in.readString();
+ this.discoveryMethod = DiscoveryMethod.values()[in.readInt()];
+ this.bonjourData = in.readBundle(Bundle.class.getClassLoader());
+ in.readTypedList(this.mOtherInstances, NetworkDevice.CREATOR);
+ }
+
+ public String deviceInfo() {
+ return "ip: " + this.inetAddress + " model: " + this.model + " hostname: " + this.hostname
+ + " bonjourName: " + this.bonjourName + " bonjourDomainName: " + this.bonjourDomainName;
+ }
+
+ /* (non-Javadoc)
+ * @see java.lang.Object#equals(java.lang.Object)
+ */
+ @Override
+ public boolean equals(Object obj) {
+ do {
+ // coverity[UNREACHABLE]
+ if (!(obj instanceof NetworkDevice)) continue;
+ NetworkDevice other = (NetworkDevice) obj;
+ // coverity[UNREACHABLE]
+ if (!this.getInetAddress().equals(other.getInetAddress())) continue;
+ // coverity[UNREACHABLE]
+ if (!TextUtils.equals(this.bonjourDomainName, other.bonjourDomainName)) continue;
+ // coverity[UNREACHABLE]
+ if (!TextUtils.equals(this.mBonjourService, other.mBonjourService)) continue;
+ // coverity[UNREACHABLE]
+ if (!TextUtils.equals(this.mDeviceIdentifier, other.mDeviceIdentifier)) continue;
+ return true;
+ } while (false);
+ return false;
+ }
+
+ public String getDeviceIdentifier() {
+ return mDeviceIdentifier;
+ }
+
+ @Override
+ public int hashCode() {
+ final int prime = 31;
+ int hashCode = 1;
+ hashCode = hashCode * prime + this.getInetAddress().hashCode();
+ hashCode = hashCode * prime + ((bonjourDomainName != null) ? bonjourDomainName.hashCode() : 0);
+ hashCode = hashCode * prime + ((mBonjourService != null) ? mBonjourService.hashCode() : 0);
+ hashCode = hashCode * prime + ((mDeviceIdentifier != null) ? mDeviceIdentifier.hashCode() : 0);
+ return hashCode;
+ }
+
+ public ArrayList<NetworkDevice> getAllDiscoveryInstances() {
+ ArrayList<NetworkDevice> instances = new ArrayList<>(mOtherInstances.size() + 1);
+ instances.add(this);
+ instances.addAll(mOtherInstances);
+ return instances;
+ }
+
+ public void addDiscoveryInstance(NetworkDevice device) {
+ if ((device != null)
+ && !equals(device) && !mOtherInstances.contains(device)) {
+ mOtherInstances.add(device);
+ }
+ }
+
+ public String getBonjourService() {
+ return this.mBonjourService;
+ }
+
+ public DiscoveryMethod getDiscoveryMethod() {
+ return discoveryMethod;
+ }
+
+ /**
+ * @return the InetAddress
+ */
+ public InetAddress getInetAddress() {
+ return this.inetAddress;
+ }
+
+ /**
+ * @return the printer model hostname, e.g. "HP Officejet 6500 E709n"
+ */
+ public String getModel() {
+ return this.model;
+ }
+
+ /**
+ * @return the hostname of the printer in the network
+ */
+ public String getHostname() {
+ return this.hostname;
+ }
+
+ /**
+ * @return the bonjour hostname of the printer in the network
+ */
+ public String getBonjourName() {
+ return this.bonjourName;
+ }
+
+ public int getPort() {
+ return mPort;
+ }
+
+ /**
+ * @return the bonjour domain hostname of the printer in the network
+ */
+ public String getBonjourDomainName() {
+ return this.bonjourDomainName;
+ }
+
+ public Bundle getTxtAttributes() {
+ return new Bundle(bonjourData);
+ }
+
+ public Bundle getTxtAttributes(String serviceName) {
+ List<NetworkDevice> instances = getAllDiscoveryInstances();
+ for (NetworkDevice instance : instances) {
+ if (TextUtils.equals(serviceName, instance.getBonjourService())) return instance.getTxtAttributes();
+ }
+ return new Bundle();
+ }
+
+
+ /**
+ * Report the nature of this NetworkDevice's contents
+ *
+ * @return a bitmask indicating the set of special object types marshalled
+ * by the NetworkDevice.
+ * @see Parcelable#describeContents()
+ */
+
+ public int describeContents() {
+ return 0;
+ }
+
+ /**
+ * Writes the NetworkDevice contents to a Parcel, typically in order for it to be
+ * passed through an IBinder connection.
+ *
+ * @param parcel The parcel to copy this NetworkDevice to.
+ * @param flags Additional flags about how the object should be written. May
+ * be 0 or {@link Parcelable#PARCELABLE_WRITE_RETURN_VALUE
+ * PARCELABLE_WRITE_RETURN_VALUE}.
+ * @see Parcelable#writeToParcel(Parcel, int)
+ */
+ @Override
+ public void writeToParcel(Parcel parcel, int flags) {
+ /*
+ * In spite of what Coverity thinks, InetAddress.getAddress() can NOT
+ * return null. Never.
+ */
+ if (this.inetAddress != null) {
+ byte[] address = this.inetAddress.getAddress();
+ parcel.writeInt(address.length);
+ parcel.writeByteArray(address);
+ } else {
+ parcel.writeInt(0);
+ }
+ parcel.writeString(this.model);
+ parcel.writeString(this.hostname);
+ parcel.writeInt(this.mPort);
+ parcel.writeString(this.mDeviceIdentifier);
+ parcel.writeString(this.bonjourName);
+ parcel.writeString(this.bonjourDomainName);
+ parcel.writeString(this.mBonjourService);
+ parcel.writeInt(this.discoveryMethod.ordinal());
+ parcel.writeBundle(this.bonjourData);
+ parcel.writeTypedList(this.mOtherInstances);
+ }
+
+ /**
+ * Reads Printers from Parcels.
+ */
+ public static final Creator<NetworkDevice> CREATOR = new Creator<NetworkDevice>() {
+ @Override
+ public NetworkDevice createFromParcel(Parcel in) {
+ try {
+ return new NetworkDevice(in);
+ } catch (UnknownHostException ignored) {
+ }
+ return null;
+ }
+
+ @Override
+ public NetworkDevice[] newArray(int size) {
+ return new NetworkDevice[size];
+ }
+ };
+}
diff --git a/PrintServiceStubs/src/com/android/printservicestubs/servicediscovery/NetworkDiscovery.java b/PrintServiceStubs/src/com/android/printservicestubs/servicediscovery/NetworkDiscovery.java
new file mode 100644
index 0000000..b7b1c6b
--- /dev/null
+++ b/PrintServiceStubs/src/com/android/printservicestubs/servicediscovery/NetworkDiscovery.java
@@ -0,0 +1,335 @@
+/*
+ * (c) Copyright 2016 Mopria Alliance, Inc.
+ * (c) Copyright 2016 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.printservicestubs.servicediscovery;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.Context;
+import com.android.internal.util.Preconditions;
+import com.android.printservicestubs.R;
+import com.android.printservicestubs.servicediscovery.mdns.MDnsDiscovery;
+
+import java.io.IOException;
+import java.net.DatagramPacket;
+import java.net.MulticastSocket;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+
+/**
+ * Discovers devices on the network and notifies the listeners about those devices.
+ */
+public class NetworkDiscovery {
+ /**
+ * Currently active clients of the discovery
+ */
+ private static final @NonNull ArrayList<DiscoveryListener> sListeners = new ArrayList<>();
+
+ /**
+ * If the network discovery is running, this stores the instance. There can always be at most
+ * once instance running.
+ */
+ private static @Nullable NetworkDiscovery sInstance = null;
+
+ /**
+ * Method used for discovering printers
+ */
+ private final @NonNull MDnsDiscovery mDiscoveryMethod;
+
+ /**
+ * List of discovered printers sorted by device identifier
+ */
+ private final @NonNull LinkedHashMap<String, NetworkDevice> mDiscoveredPrinters;
+
+ /**
+ * Socket used to discover devices (both query and listen).
+ */
+ private @NonNull MulticastSocket mSocket;
+
+ /**
+ * Thread sending the broadcasts that should make the device announce them self
+ */
+ private @NonNull QueryThread mQueryThread;
+
+ /**
+ * Thread that receives the announcements of new devices and processes them
+ */
+ private @NonNull ListenerThread mListenerThread;
+
+ /**
+ * Register a new listener for network devices. If this is the first listener, this starts a new
+ * discovery session.
+ *
+ * @param listener Listener to register.
+ * @param context Context the listener is running in.
+ *
+ * @throws IOException If the discovery session could not be started
+ */
+ public static void onListenerAdded(@NonNull DiscoveryListener listener,
+ @NonNull Context context) throws IOException {
+ listener = Preconditions.checkNotNull(listener, "listener");
+ context = Preconditions.checkNotNull(context, "context");
+
+ synchronized (sListeners) {
+ sListeners.add(listener);
+
+ if (sInstance == null) {
+ sInstance = new NetworkDiscovery(context);
+ } else {
+ sInstance.onListenerAdded(listener);
+ }
+ }
+ }
+
+ /**
+ * Remove a previously registered listener for network devices. If this is the last listener,
+ * the discovery session is terminated.
+ *
+ * @param listener The listener to remove
+ *
+ * @throws InterruptedException If the thread was interrupted while waiting for the session to
+ * finish.
+ */
+ public static void removeDiscoveryListener(@NonNull DiscoveryListener listener)
+ throws InterruptedException {
+ listener = Preconditions.checkNotNull(listener, "listener");
+
+ synchronized (sListeners) {
+ sListeners.remove(listener);
+
+ if (sListeners.isEmpty()) {
+ sInstance.close();
+ sInstance = null;
+ }
+ }
+ }
+
+ /**
+ * Create and start a new network discovery session.
+ *
+ * @param context The context requesting the start of the session.
+ *
+ * @throws IOException If the discovery session could not be started
+ */
+ private NetworkDiscovery(@NonNull Context context) throws IOException {
+ mDiscoveredPrinters = new LinkedHashMap<>();
+
+ mDiscoveryMethod = new MDnsDiscovery(
+ context.getResources().getStringArray(R.array.mdns_services));
+
+ mSocket = NetworkUtils.createMulticastSocket(context, null);
+ mSocket.setBroadcast(true);
+ mSocket.setReuseAddress(true);
+ mSocket.setSoTimeout(0);
+
+ mListenerThread = new ListenerThread();
+ mQueryThread = new QueryThread();
+
+ mListenerThread.start();
+ mQueryThread.start();
+ }
+
+ /**
+ * If a new listener was added while the session was already running, announce all already found
+ * devices to the new listener.
+ *
+ * @param listener The listener that was just added.
+ */
+ private void onListenerAdded(@NonNull DiscoveryListener listener) {
+ synchronized (mDiscoveredPrinters) {
+ for (NetworkDevice device : mDiscoveredPrinters.values()) {
+ listener.onDeviceFound(device);
+ }
+ }
+ }
+
+ /**
+ * Clean up discovery session.
+ *
+ * @throws InterruptedException If the current thread was interrupted while the session was
+ * cleaned up.
+ */
+ private void close() throws InterruptedException {
+ // Closing the socket causes IOExceptions on operations on this socket. This in turn will
+ // end the threads.
+ mSocket.close();
+
+ mListenerThread.join();
+ mQueryThread.join();
+ }
+
+ /**
+ * Notify all currently registered listeners that a new device was removed.
+ *
+ * @param networkDevice The device that was removed
+ */
+ private void fireDeviceRemoved(@NonNull NetworkDevice networkDevice) {
+ synchronized (sListeners) {
+ final int numListeners = sListeners.size();
+ for (int i = 0; i < numListeners; i++) {
+ sListeners.get(i).onDeviceRemoved(networkDevice);
+ }
+ }
+ }
+
+ /**
+ * Notify all currently registered listeners that a new device was found.
+ *
+ * @param networkDevice The device that was found
+ */
+ private void fireDeviceFound(@NonNull NetworkDevice networkDevice) {
+ synchronized (sListeners) {
+ final int numListeners = sListeners.size();
+ for (int i = 0; i < numListeners; i++) {
+ sListeners.get(i).onDeviceFound(networkDevice);
+ }
+ }
+ }
+
+ /**
+ * Thread receiving and processing the packets that announce network devices
+ */
+ private class ListenerThread extends Thread {
+ private static final int BUFFER_LENGTH = 4 * 1024;
+
+ @Override
+ public void run() {
+ while (true) {
+ byte[] buffer = new byte[BUFFER_LENGTH];
+ DatagramPacket packet = new DatagramPacket(buffer, buffer.length);
+
+ try {
+ mSocket.receive(packet);
+
+ if (packet.getPort() != mDiscoveryMethod.getPort()) {
+ continue;
+ }
+
+ ArrayList<ServiceParser> serviceParsers = mDiscoveryMethod
+ .parseResponse(packet);
+
+ final int numParsers = serviceParsers.size();
+ for (int i = 0; i < numParsers; i++) {
+ ServiceParser parser = serviceParsers.get(i);
+ NetworkDevice device = new NetworkDevice(parser);
+ String key = device.getDeviceIdentifier();
+ NetworkDevice discoveredNetworkDevice = mDiscoveredPrinters.get(key);
+
+ if (discoveredNetworkDevice != null) {
+ if(!device.getInetAddress()
+ .equals(discoveredNetworkDevice.getInetAddress())) {
+ fireDeviceRemoved(discoveredNetworkDevice);
+ } else {
+ discoveredNetworkDevice.addDiscoveryInstance(device);
+ device = discoveredNetworkDevice;
+ }
+ } else {
+ synchronized (mDiscoveredPrinters) {
+ mDiscoveredPrinters.put(key, device);
+ }
+ }
+
+ fireDeviceFound(device);
+ }
+ } catch (IOException e) {
+ // Socket got closed
+ break;
+ }
+ }
+ }
+ }
+
+ /**
+ * Thread that sends out the packages that make the devices announce them self. The announcement
+ * are the received by the {@link ListenerThread listener thread}.
+ */
+ private class QueryThread extends Thread {
+ private static final int MAX_ACTIVE_QUERIES = 10;
+ private static final int MAX_DELAY_SECONDS = 60;
+
+ private boolean mUseFallback = false;
+ private boolean mIsActiveDiscovery = true;
+ private int mQueriesSent = 0;
+
+ /**
+ * @return the next wait interval, in milliseconds, using an exponential backoff algorithm.
+ */
+ private int getQueryDelayInMillis() {
+ int delayInSeconds = 1;
+
+ int first = 1;
+ int second = 1;
+ int index;
+
+ if (mQueriesSent > MAX_ACTIVE_QUERIES) {
+ delayInSeconds = MAX_DELAY_SECONDS;
+ } else {
+
+ for (index = 1; index < mQueriesSent; index++) {
+ if (index <= 1) {
+ delayInSeconds = index;
+ } else {
+ delayInSeconds = first + second;
+ first = second;
+ second = delayInSeconds;
+ }
+ }
+ }
+
+ if (delayInSeconds >= MAX_DELAY_SECONDS) {
+ delayInSeconds = MAX_DELAY_SECONDS;
+ if (mIsActiveDiscovery) {
+ mIsActiveDiscovery = false;
+ }
+ }
+
+ return delayInSeconds * 1000;
+ }
+
+ @Override
+ public void run() {
+ mQueriesSent = (mIsActiveDiscovery ? 0 : MAX_ACTIVE_QUERIES);
+ mUseFallback = false;
+
+ while (true) {
+ try {
+ ArrayList<DatagramPacket> datagramList = new ArrayList<>();
+
+ if (!mDiscoveryMethod.isFallback() || mUseFallback) {
+ Collections.addAll(datagramList, mDiscoveryMethod.createQueryPackets());
+ }
+
+ for (DatagramPacket packet : datagramList) {
+ mSocket.send(packet);
+ }
+
+ mQueriesSent++;
+
+ Thread.sleep(getQueryDelayInMillis());
+
+ if (!mUseFallback) {
+ mUseFallback = mDiscoveredPrinters.isEmpty();
+ }
+ } catch (Exception e) {
+ // Socket got closed
+ break;
+ }
+ }
+ }
+ }
+}
diff --git a/PrintServiceStubs/src/com/android/printservicestubs/servicediscovery/NetworkUtils.java b/PrintServiceStubs/src/com/android/printservicestubs/servicediscovery/NetworkUtils.java
new file mode 100644
index 0000000..8cf0ce6
--- /dev/null
+++ b/PrintServiceStubs/src/com/android/printservicestubs/servicediscovery/NetworkUtils.java
@@ -0,0 +1,290 @@
+/*
+ * (c) Copyright 2016 Mopria Alliance, Inc.
+ * (c) Copyright 2016 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.printservicestubs.servicediscovery;
+
+import android.annotation.SuppressLint;
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.net.ConnectivityManager;
+import android.net.DhcpInfo;
+import android.net.NetworkInfo;
+import android.net.wifi.WifiInfo;
+import android.net.wifi.WifiManager;
+import android.os.Build;
+import android.text.TextUtils;
+import android.util.Log;
+
+import java.io.IOException;
+import java.net.InetAddress;
+import java.net.InterfaceAddress;
+import java.net.MulticastSocket;
+import java.net.NetworkInterface;
+import java.net.SocketException;
+import java.net.UnknownHostException;
+import java.util.List;
+
+/**
+ * Common class to perform network-related tests. For example:
+ * <ul>
+ * <li>isConnected: returns true if network connectivity exists
+ * <li>isMobileNetworkConnected: returns true if Mobile Network is active
+ * <li>isWiFiConnected: returns true if WiFi is active
+ * <li>[... Your network-related test here]
+ * </ul>
+ */
+@SuppressWarnings("unused")
+class NetworkUtils {
+ public static final String TAG = NetworkUtils.class.getSimpleName();
+
+ private static final int MULTICAST_TTL = 255;
+ private static boolean mIsDebuggable = false;
+
+ // Need a fake SSID for ethernet. Create one that is longer than 32 characters
+ // as SSID must be 0-32 chars long and we don't want anything that could really
+ // be returned by a wireless SSID. This one is 35 characters.
+ // Should match the value in shared/NetworkUtilities.
+ @SuppressWarnings("FieldCanBeLocal")
+ private static String ETHERNET_SSID = "Ethernet901234567890123456789012345";
+ private static String ETHERNET_NETIFC = "eth0";
+
+ public static boolean isWifiConnected(Context context) {
+ boolean connected = false;
+ WifiManager wifiManager = (WifiManager) context.getSystemService(Context.WIFI_SERVICE);
+ WifiInfo info = wifiManager.getConnectionInfo();
+ if (info != null) {
+ if ((info.getSSID() != null) && (info.getIpAddress() != 0)) {
+ connected = true;
+ }
+ }
+ return connected;
+ }
+
+ public static boolean isConnectedToEthernet(Context context) {
+ ConnectivityManager connMgr = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
+ NetworkInfo ethInfo = null;
+ if (Build.VERSION.SDK_INT > Build.VERSION_CODES.HONEYCOMB_MR2)
+ ethInfo = getEthernetInfo(connMgr);
+ return ethInfo != null && ethInfo.isConnected();
+ }
+
+ public static boolean isConnectedToWifiOrEthernet(Context context) {
+ return isWifiConnected(context) || isConnectedToEthernet(context);
+ }
+
+ /**
+ * Returns an instance of the ConnectivityManager for handling management of
+ * network connections.
+ *
+ * @param context Context running discovery
+ * @return an instance of the ConnectivityManager for handling management of network connections
+ */
+ private static ConnectivityManager getConnectivityManager(Context context) {
+ return (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
+ }
+
+ @TargetApi(Build.VERSION_CODES.HONEYCOMB_MR2)
+ private static NetworkInfo getEthernetInfo(ConnectivityManager connMgr) {
+ //noinspection deprecation
+ return connMgr.getNetworkInfo(ConnectivityManager.TYPE_ETHERNET);
+ }
+
+ @SuppressLint("NewApi")
+ public static InetAddress getEthernetBroadcast() {
+ NetworkInterface netIf;
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.GINGERBREAD)
+ return null;
+
+ try {
+ netIf = NetworkInterface.getByName(ETHERNET_NETIFC);
+ } catch (SocketException e) {
+ netIf = null;
+ }
+ if (netIf == null)
+ return null;
+
+ InetAddress broadcastAddress = null;
+ List<InterfaceAddress> addresses = netIf.getInterfaceAddresses();
+ if ((addresses != null) && !addresses.isEmpty()) {
+
+ for (InterfaceAddress address : addresses) {
+ InetAddress bAddr = address.getBroadcast();
+ if (bAddr != null) {
+ broadcastAddress = bAddr;
+ }
+ }
+ }
+ return broadcastAddress;
+ }
+
+ /**
+ * @return If there is Wi-Fi and DHCP info returns Broadcast address
+ * else returns null
+ * @throws UnknownHostException
+ */
+ public static InetAddress getBroadcastAddress(Context context) throws UnknownHostException {
+ if (connectedToEthernet(context)) {
+ return getEthernetBroadcast();
+ } else {
+ WifiManager wifiManager = (WifiManager) context.getSystemService(Context.WIFI_SERVICE);
+ WifiInfo wifiInfo = wifiManager.getConnectionInfo();
+ DhcpInfo dhcpInfo = wifiManager.getDhcpInfo();
+
+ if ((wifiInfo == null) || (dhcpInfo == null)) {
+ return null;
+ }
+ int hostAddr = wifiInfo.getIpAddress();
+ int broadcastAddress = (hostAddr & dhcpInfo.netmask) | ~dhcpInfo.netmask;
+ byte[] broadcastAddressBytes = {
+ (byte) (broadcastAddress & 0xFF),
+ (byte) ((broadcastAddress >> 8) & 0xFF),
+ (byte) ((broadcastAddress >> 16) & 0xFF),
+ (byte) ((broadcastAddress >> 24) & 0xFF)};
+
+ return InetAddress.getByAddress(broadcastAddressBytes);
+ }
+ }
+
+ public static NetworkInterface getNetworkIFC(Context context, String networkIFC) throws IOException {
+ NetworkInterface netIf = null;
+
+ if (!TextUtils.isEmpty(networkIFC)) {
+ netIf = NetworkInterface.getByName(networkIFC);
+ } else if (connectedToEthernet(context)) {
+ netIf = NetworkInterface.getByName(ETHERNET_NETIFC);
+ } else {
+ WifiManager wifiManager = (WifiManager) context.getSystemService(Context.WIFI_SERVICE);
+ WifiInfo wifiInfo = wifiManager.getConnectionInfo();
+ if (wifiInfo != null) {
+ int intaddr = wifiInfo.getIpAddress();
+ byte[] byteaddr = new byte[]{
+ (byte) (intaddr & 0xff),
+ (byte) (intaddr >> 8 & 0xff),
+ (byte) (intaddr >> 16 & 0xff),
+ (byte) (intaddr >> 24 & 0xff)};
+ InetAddress addr = InetAddress.getByAddress(byteaddr);
+ netIf = NetworkInterface.getByInetAddress(addr);
+ }
+ }
+ return netIf;
+ }
+
+ /**
+ * Ensures that the caller receives an usable multicast socket.
+ * In case the network configuration does not have a default gateway
+ * set, the multicast socket might not work. This is the case when
+ * the device is connected to a Wireless Direct printer. In that
+ * cases, force a network interface into the socket.
+ *
+ * @return A ready-to-use multicast socket.
+ */
+ public static MulticastSocket createMulticastSocket(Context context, String networkIFC)
+ throws IOException {
+
+ NetworkInterface netIf = getNetworkIFC(context, networkIFC);
+ MulticastSocket multicastSocket = new MulticastSocket();
+
+ if (netIf != null) {
+ multicastSocket.setNetworkInterface(netIf);
+ }
+
+ multicastSocket.setTimeToLive(MULTICAST_TTL);
+ return multicastSocket;
+ }
+
+ public static MulticastSocket createMulticastSocket(Context context)
+ throws IOException {
+ return createMulticastSocket(context, null);
+ }
+
+
+ /**
+ * When Mobile Network is active, all data traffic will use this connection
+ * by default. Should not coexist with other connections.
+ *
+ * @param context Context running discovery
+ * @return true if Mobile Network is active
+ */
+ public static boolean isMobileNetworkConnected(Context context) {
+ NetworkInfo netInfo = getConnectivityManager(context).getActiveNetworkInfo();
+
+ return (netInfo != null) && (netInfo.getType() == ConnectivityManager.TYPE_MOBILE);
+ }
+
+ public static boolean connectedToEthernet(Context context) {
+ ConnectivityManager connMgr = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
+// NetworkInfo ethInfo = connMgr.getNetworkInfo(ConnectivityManager.TYPE_ETHERNET);
+// ethInfo = connMgr.getNetworkInfo(ConnectivityManager.TYPE_ETHERNET);
+ NetworkInfo ethInfo = null;
+ if (Build.VERSION.SDK_INT > Build.VERSION_CODES.HONEYCOMB_MR2)
+ ethInfo = getEthernetInfo(connMgr);
+ return ethInfo != null && ethInfo.isConnectedOrConnecting();
+ }
+
+ public static boolean isWirelessDirect(Context context) {
+ ConnectivityManager connManager =
+ (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
+ NetworkInfo netInfo = connManager.getActiveNetworkInfo();
+
+ if (netInfo != null && netInfo.isConnected() && (netInfo.getType() == ConnectivityManager.TYPE_WIFI)) {
+ WifiManager wifiManager =
+ (WifiManager) context.getSystemService(Context.WIFI_SERVICE);
+ DhcpInfo dhcpInfo = wifiManager.getDhcpInfo();
+
+ if ((dhcpInfo != null) && (dhcpInfo.gateway == 0)) {
+ if (mIsDebuggable) Log.d(TAG, "isWirelessDirect: probably wireless direct.");
+ return true;
+ }
+ }
+ return false;
+ }
+
+ public static boolean isConnectedAndNotWirelessDirect(Context context) {
+ ConnectivityManager connManager =
+ (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
+ NetworkInfo netInfo = connManager.getActiveNetworkInfo();
+
+ if (netInfo != null && netInfo.isConnected()) {
+ if (netInfo.getType() == ConnectivityManager.TYPE_WIFI) {
+ WifiManager wifiManager = (WifiManager) context.getSystemService(Context.WIFI_SERVICE);
+ DhcpInfo dhcpInfo = wifiManager.getDhcpInfo();
+
+ if ((dhcpInfo != null) && (dhcpInfo.gateway == 0)) {
+ if (mIsDebuggable)
+ Log.d(TAG, "isConnectedButNotWirelessDirect: probably wireless direct.");
+ return false;
+ }
+ }
+ return true;
+ }
+ return false;
+ }
+
+
+ public static String getCurrentSSID(Context context) {
+ String currentSSID = null;
+ if (isWifiConnected(context)) {
+ WifiManager wifiManager = (WifiManager) context.getSystemService(Context.WIFI_SERVICE);
+ WifiInfo wifiInfo = wifiManager.getConnectionInfo();
+ currentSSID = (wifiInfo != null) ? wifiInfo.getSSID() : null;
+ } else if (isConnectedToEthernet(context)) {
+ currentSSID = ETHERNET_SSID;
+ }
+ return currentSSID;
+ }
+
+}
diff --git a/PrintServiceStubs/src/com/android/printservicestubs/servicediscovery/ServiceParser.java b/PrintServiceStubs/src/com/android/printservicestubs/servicediscovery/ServiceParser.java
new file mode 100644
index 0000000..f08b48d
--- /dev/null
+++ b/PrintServiceStubs/src/com/android/printservicestubs/servicediscovery/ServiceParser.java
@@ -0,0 +1,35 @@
+/*
+ * (c) Copyright 2016 Mopria Alliance, Inc.
+ * (c) Copyright 2016 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.printservicestubs.servicediscovery;
+
+import android.os.Bundle;
+
+import java.net.InetAddress;
+
+public interface ServiceParser {
+ NetworkDevice.DiscoveryMethod getDiscoveryMethod();
+ int getPort();
+ String getDeviceIdentifier();
+ String getHostname();
+ InetAddress getAddress();
+ String getModel();
+ String getServiceName();
+ String getAttribute(String key) throws Exception;
+ Bundle getAllAttributes();
+
+}
diff --git a/PrintServiceStubs/src/com/android/printservicestubs/servicediscovery/mdns/BonjourException.java b/PrintServiceStubs/src/com/android/printservicestubs/servicediscovery/mdns/BonjourException.java
new file mode 100644
index 0000000..f6cb447
--- /dev/null
+++ b/PrintServiceStubs/src/com/android/printservicestubs/servicediscovery/mdns/BonjourException.java
@@ -0,0 +1,33 @@
+/*
+ * (c) Copyright 2016 Mopria Alliance, Inc.
+ * (c) Copyright 2016 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.printservicestubs.servicediscovery.mdns;
+
+@SuppressWarnings({"serial", "unused"})
+class BonjourException extends DnsException {
+
+ public BonjourException(String detailString) {
+ super(detailString);
+ }
+
+ public BonjourException(String detailMessage, Throwable cause) {
+ super(detailMessage, cause);
+ }
+}
diff --git a/PrintServiceStubs/src/com/android/printservicestubs/servicediscovery/mdns/BonjourParser.java b/PrintServiceStubs/src/com/android/printservicestubs/servicediscovery/mdns/BonjourParser.java
new file mode 100644
index 0000000..78b91b5
--- /dev/null
+++ b/PrintServiceStubs/src/com/android/printservicestubs/servicediscovery/mdns/BonjourParser.java
@@ -0,0 +1,147 @@
+/*
+ * (c) Copyright 2016 Mopria Alliance, Inc.
+ * (c) Copyright 2016 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.printservicestubs.servicediscovery.mdns;
+
+import android.os.Bundle;
+import android.text.TextUtils;
+
+import com.android.printservicestubs.servicediscovery.NetworkDevice;
+
+import java.io.UnsupportedEncodingException;
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.util.Map;
+import java.util.Set;
+
+public class BonjourParser implements BonjourServiceParser {
+
+ public static final String VALUE_ENCODING = "UTF-8";
+ private static final int IPV4_LENGTH = 4;
+
+ private DnsService service;
+
+ public BonjourParser(DnsService service) {
+ this.service = service;
+ }
+
+ @Override
+ public NetworkDevice.DiscoveryMethod getDiscoveryMethod() {
+ return NetworkDevice.DiscoveryMethod.MDNS_DISCOVERY;
+ }
+
+ @Override
+ public int getPort() {
+ return service.getPort();
+ }
+
+ @Override
+ public String getDeviceIdentifier() {
+ return getHostname();
+ }
+
+ @Override
+ public String getBonjourName() {
+ return this.service.getName().getLabels()[0];
+ }
+
+ @Override
+ public String getHostname() {
+ return this.service.getHostname().getLabels()[0];
+ }
+
+ @Override
+ public InetAddress getAddress() {
+ byte[] address = this.getFirstIpv4Address();
+
+ try {
+ return InetAddress.getByAddress(address);
+ } catch (UnknownHostException exc) {
+ return null;
+ }
+ }
+
+ private byte[] getFirstIpv4Address() {
+ byte[][] addresses = this.service.getAddresses();
+
+ for (byte[] address : addresses) {
+ if (IPV4_LENGTH == address.length) {
+ return address;
+ }
+ }
+ return addresses[0];
+ }
+
+ @Override
+ public String getModel() {
+ String model = null;
+ try {
+ model = this.getAttribute(MDNSUtils.ATTRIBUTE__TY);
+ } catch (BonjourException ignored) {
+ }
+ return model;
+ }
+
+ @Override
+ public String getServiceName() {
+ return this.service.getName().toString();
+ }
+
+ @Override
+ public String getBonjourServiceName() {
+ String name = this.service.getName().toString();
+ return name.substring(name.indexOf('.') + 1, name.lastIndexOf('.'));
+ }
+
+ @Override
+ public String getAttribute(String key) throws BonjourException {
+ byte[] value = this.service.getAttributes().get(key);
+
+ if (value == null) {
+ return null;
+ }
+ try {
+ return new String(value, VALUE_ENCODING);
+ } catch (UnsupportedEncodingException exc) {
+ throw new BonjourException("Unsupported encoding to read attribute value: "
+ + VALUE_ENCODING, exc);
+ }
+ }
+
+ @Override
+ public Bundle getAllAttributes() {
+ Bundle attribtues = new Bundle();
+ Set<Map.Entry<String, byte[]>> attributeSet = this.service.getAttributes().entrySet();
+ for (Map.Entry<String, byte[]> entry : attributeSet) {
+ if (entry == null) {
+ continue;
+ }
+ String key = entry.getKey();
+ byte[] bytes = entry.getValue();
+ if (TextUtils.isEmpty(key)) {
+ continue;
+ }
+ String value;
+ try {
+ value = new String(bytes, VALUE_ENCODING);
+ attribtues.putString(key, value);
+ } catch (UnsupportedEncodingException ignored) {
+ }
+ }
+ return attribtues;
+ }
+}
diff --git a/PrintServiceStubs/src/com/android/printservicestubs/servicediscovery/mdns/BonjourServiceParser.java b/PrintServiceStubs/src/com/android/printservicestubs/servicediscovery/mdns/BonjourServiceParser.java
new file mode 100644
index 0000000..5cd03f0
--- /dev/null
+++ b/PrintServiceStubs/src/com/android/printservicestubs/servicediscovery/mdns/BonjourServiceParser.java
@@ -0,0 +1,31 @@
+/*
+ * (c) Copyright 2016 Mopria Alliance, Inc.
+ * (c) Copyright 2016 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.printservicestubs.servicediscovery.mdns;
+
+import android.os.Bundle;
+
+import com.android.printservicestubs.servicediscovery.ServiceParser;
+
+@SuppressWarnings("unused")
+interface BonjourServiceParser extends ServiceParser {
+
+ String getBonjourName();
+ String getBonjourServiceName();
+ String getAttribute(String key) throws Exception;
+ Bundle getAllAttributes();
+}
diff --git a/PrintServiceStubs/src/com/android/printservicestubs/servicediscovery/mdns/DnsException.java b/PrintServiceStubs/src/com/android/printservicestubs/servicediscovery/mdns/DnsException.java
new file mode 100644
index 0000000..d23da0d
--- /dev/null
+++ b/PrintServiceStubs/src/com/android/printservicestubs/servicediscovery/mdns/DnsException.java
@@ -0,0 +1,30 @@
+/*
+ * (c) Copyright 2016 Mopria Alliance, Inc.
+ * (c) Copyright 2016 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.printservicestubs.servicediscovery.mdns;
+
+@SuppressWarnings("serial")
+class DnsException extends Exception {
+
+ public DnsException(String detailMessage) {
+ super(detailMessage);
+ }
+
+ public DnsException(String detailMessage, Throwable cause) {
+ super(detailMessage, cause);
+ }
+}
diff --git a/PrintServiceStubs/src/com/android/printservicestubs/servicediscovery/mdns/DnsPacket.java b/PrintServiceStubs/src/com/android/printservicestubs/servicediscovery/mdns/DnsPacket.java
new file mode 100644
index 0000000..cc58ed9
--- /dev/null
+++ b/PrintServiceStubs/src/com/android/printservicestubs/servicediscovery/mdns/DnsPacket.java
@@ -0,0 +1,472 @@
+/*
+ * (c) Copyright 2016 Mopria Alliance, Inc.
+ * (c) Copyright 2016 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.printservicestubs.servicediscovery.mdns;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Locale;
+
+@SuppressWarnings("unused")
+class DnsPacket {
+
+ public enum ResourceType {
+ UNKNOWN(-1),
+
+ // IPv4 address
+ A(1),
+ NS(2),
+ MD(3),
+ MF(4),
+
+ // Canonical name
+ CNAME(5),
+ SOA(6),
+ MB(7),
+ MG(8),
+ MR(9),
+ NULL(10),
+ WKS(11),
+
+ // Domain name pointer
+ PTR(12),
+ HINFO(13),
+ MINFO(14),
+ MX(15),
+
+ // Arbitrary text string
+ TXT(16),
+
+ // IPv6 address (Thomson)
+ AAAA(28),
+
+ // Service (RFC 2782)
+ SRV(33),
+
+ // OPT pseudo-RR
+ OPT(41),
+
+ // Question-only types
+ AXFR(252),
+ MAILB(253),
+ MAILA(254),
+ STAR(255);
+
+ private int code;
+
+ ResourceType(int code) {
+ this.code = code;
+ }
+
+ public static ResourceType valueOf(int code) {
+ for (ResourceType rt : values()) {
+ if (rt.code == code) {
+ return rt;
+ }
+ }
+ return UNKNOWN;
+ }
+ }
+
+ // The most significant bit of the RRCLASS fields can be used either as
+ // a request for unicast responses (in question entries) or as an
+ // indicator that the resource is unique and all other entries in the
+ // cache should be flushed (in response entries). For more details, see
+ // draft on Multicast DNS - Cheshire.
+ private static final int RRCLASS_MASK = 0x00007FFF;
+
+ private int id;
+ private int flags;
+ private Question[] questions;
+ private Entry[] answers;
+ private Entry[] authorities;
+ private Entry[] additionals;
+
+ public DnsPacket(int id, int flags, Question[] questions, Entry[] answers, Entry[] authorities,
+ Entry[] additionals) {
+ this.id = id;
+ this.flags = flags;
+ this.questions = questions;
+ this.answers = answers;
+ this.authorities = authorities;
+ this.additionals = additionals;
+ }
+
+ public int getId() {
+ return this.id;
+ }
+
+ public int getFlags() {
+ return this.flags;
+ }
+
+ public Question[] getQuestions() {
+ return this.questions;
+ }
+
+ public Entry[] getAnswers() {
+ return this.answers;
+ }
+
+ public Entry[] getAuthorities() {
+ return this.authorities;
+ }
+
+ public Entry[] getAdditionals() {
+ return this.additionals;
+ }
+
+
+ public interface Name {
+
+ String[] getLabels();
+
+ byte[] getBytes();
+ }
+
+
+ public static class CompressedName implements Name {
+ private NameSection[] sections;
+ private String[] labels;
+ private byte[] bytes;
+ private String string;
+
+ public CompressedName(NameSection[] sections) {
+ this.sections = sections;
+ }
+
+ public int getSizeInBytes() {
+ int size = 0;
+
+ for (NameSection section : this.sections) {
+ size += section.getSizeInBytes();
+ }
+ return size;
+ }
+
+ public String[] getLabels() {
+ if (this.labels == null) {
+ List<String> labelList = new ArrayList<>();
+
+ for (NameSection section : this.sections) {
+ labelList.addAll(Arrays.asList(section.getLabels()));
+ }
+ this.labels = labelList.toArray(new String[labelList.size()]);
+ }
+ return this.labels;
+ }
+
+ public byte[] getBytes() {
+ if (this.bytes == null) {
+ ByteArrayOutputStream labelBytes = new ByteArrayOutputStream();
+
+ for (NameSection section : this.sections) {
+ try {
+ labelBytes.write(section.getBytes());
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+ this.bytes = labelBytes.toByteArray();
+ }
+ return this.bytes;
+ }
+
+ @Override
+ public String toString() {
+ if (this.string == null) {
+ StringBuilder builder = new StringBuilder();
+
+ for (int i = 0; i < this.sections.length; i++) {
+ String sectionString = this.sections[i].toString();
+
+ if ((i > 0) && (sectionString.length() > 0)) {
+ builder.append(".");
+ }
+ builder.append(sectionString);
+ }
+ this.string = builder.toString();
+ }
+ return this.string;
+ }
+
+ @Override
+ public boolean equals(Object thatObject) {
+ return this == thatObject || thatObject instanceof CompressedName && this.toString().equals(thatObject.toString());
+ }
+
+ @Override
+ public int hashCode() {
+ return this.toString().hashCode();
+ }
+ }
+
+
+ public interface NameSection extends Name {
+
+ int getSizeInBytes();
+
+ boolean isEmpty();
+
+ boolean isPointer();
+ }
+
+
+ public static class NameLabel implements NameSection {
+ private String string;
+
+ public NameLabel(String string) {
+ this.string = string;
+ }
+
+ public int getSizeInBytes() {
+ return this.string.length() + 1;
+ }
+
+ public String[] getLabels() {
+ return new String[]{this.string};
+ }
+
+ public byte[] getBytes() {
+ // coverity[FB.DM_DEFAULT_ENCODING]
+ return this.string.getBytes();
+ }
+
+ public boolean isEmpty() {
+ return this.string.length() == 0;
+ }
+
+ public boolean isPointer() {
+ return false;
+ }
+
+ @Override
+ public String toString() {
+ return this.string;
+ }
+ }
+
+
+ public static class NamePointer implements NameSection {
+ private Name pointedName;
+
+ public NamePointer(Name pointedName) {
+ this.pointedName = pointedName;
+ }
+
+ public int getSizeInBytes() {
+ return 2;
+ }
+
+ public String[] getLabels() {
+ return this.pointedName.getLabels();
+ }
+
+ public byte[] getBytes() {
+ return this.pointedName.getBytes();
+ }
+
+ public boolean isEmpty() {
+ return false;
+ }
+
+ public boolean isPointer() {
+ return true;
+ }
+
+ @Override
+ public String toString() {
+ return this.pointedName.toString();
+ }
+ }
+
+
+ public static abstract class BasicEntry {
+ private static final String FORMAT = "DNS basic entry [name=%s; type=%s; class=%d]";
+
+ private Name name;
+ private ResourceType type;
+ private int clazz;
+ private boolean flag;
+
+ protected BasicEntry(ResourceType type) {
+ this.type = type;
+ }
+
+ protected BasicEntry(Name name, ResourceType type, int clazz) {
+ this.name = name;
+ this.type = type;
+ this.clazz = (clazz & RRCLASS_MASK);
+ this.flag = ((clazz & ~RRCLASS_MASK) != 0);
+ }
+
+ public Name getName() {
+ return this.name;
+ }
+
+ public ResourceType getType() {
+ return this.type;
+ }
+
+ public int getClazz() {
+ return this.clazz;
+ }
+
+ public boolean isUnique() {
+ return this.flag;
+ }
+
+ public boolean isUnicastQuestion() {
+ return this.flag;
+ }
+
+ @Override
+ public String toString() {
+ return String.format(Locale.US, FORMAT, this.name, this.type, this.clazz);
+ }
+ }
+
+
+ public static class Question extends BasicEntry {
+
+ public Question(Name name, ResourceType type, int clazz) {
+ super(name, type, clazz);
+ }
+ }
+
+
+ public static abstract class Entry extends BasicEntry {
+ private int ttl;
+
+ public Entry(ResourceType type) {
+ super(type);
+ }
+
+ public Entry(Name name, ResourceType type, int clazz, int ttl) {
+ super(name, type, clazz);
+ this.ttl = ttl;
+ }
+
+ public int getTtl() {
+ return this.ttl;
+ }
+ }
+
+
+ public static class GenericEntry extends Entry {
+ private byte[] data;
+
+ public GenericEntry(Name name, ResourceType type, int clazz, int ttl, byte[] data) {
+ super(name, type, clazz, ttl);
+ this.data = data;
+ }
+
+ public byte[] getData() {
+ return this.data;
+ }
+ }
+
+
+ public static class Address extends Entry {
+ private byte[] address;
+
+ public Address(Name name, ResourceType type, int clazz, int ttl, byte[] bytes) {
+ super(name, type, clazz, ttl);
+ this.address = bytes;
+ }
+
+ public byte[] getAddress() {
+ return this.address;
+ }
+ }
+
+
+ public static class Ptr extends Entry {
+ private Name pointedName;
+
+ public Ptr(Name name, ResourceType type, int clazz, int ttl, Name pointedName) {
+ super(name, type, clazz, ttl);
+ this.pointedName = pointedName;
+ }
+
+ public Name getPointedName() {
+ return this.pointedName;
+ }
+ }
+
+ public static class Opt extends Entry {
+ private int size;
+
+ public Opt(ResourceType type, int size) {
+ super(type);
+ this.size = size;
+ }
+
+ public int getPayloadSize() {
+ return this.size;
+ }
+ }
+
+ public static class Txt extends Entry {
+ private byte[] text;
+
+ public Txt(Name name, ResourceType type, int clazz, int ttl, byte[] bytes) {
+ super(name, type, clazz, ttl);
+ this.text = bytes;
+ }
+
+ public byte[] getText() {
+ return this.text;
+ }
+ }
+
+
+ public static class Srv extends Entry {
+ private final int priority;
+ private final int weight;
+ private final int port;
+ private final Name target;
+
+ public Srv(Name name, ResourceType type, int clazz, int ttl, int priority, int weight,
+ int port, Name target) {
+ super(name, type, clazz, ttl);
+ this.priority = priority;
+ this.weight = weight;
+ this.port = port;
+ this.target = target;
+ }
+
+ public int getPriority() {
+ return this.priority;
+ }
+
+ public int getWeight() {
+ return this.weight;
+ }
+
+ public int getPort() {
+ return this.port;
+ }
+
+ public Name getTarget() {
+ return this.target;
+ }
+ }
+}
diff --git a/PrintServiceStubs/src/com/android/printservicestubs/servicediscovery/mdns/DnsParser.java b/PrintServiceStubs/src/com/android/printservicestubs/servicediscovery/mdns/DnsParser.java
new file mode 100644
index 0000000..7d80ca4
--- /dev/null
+++ b/PrintServiceStubs/src/com/android/printservicestubs/servicediscovery/mdns/DnsParser.java
@@ -0,0 +1,221 @@
+/*
+ * (c) Copyright 2016 Mopria Alliance, Inc.
+ * (c) Copyright 2016 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.printservicestubs.servicediscovery.mdns;
+
+import java.io.UnsupportedEncodingException;
+import java.net.DatagramPacket;
+import java.util.ArrayList;
+
+class DnsParser {
+
+ private static final int MINIMUM_PACKET_SIZE = 12;
+ private static final int BYTE_LENGTH = 8;
+ private static final int INT16_LENGTH = 16;
+ private static final int INT32_LENGTH = 32;
+ private static final int BYTE_MASK = 0x000000FF;
+ private static final int NAME_POINTER_MASK = 0xFFFFFFC0;
+ private static final String NAME_ENCODING = "UTF-8";
+
+ private byte[] data;
+ private int length;
+ private int offset;
+
+ public DnsPacket parse(byte[] packet) throws DnsException {
+ this.data = packet;
+ this.length = packet.length;
+ this.offset = 0;
+ if ((this.length - this.offset) < MINIMUM_PACKET_SIZE) {
+ throw new DnsException("Invalid mDNS packet: insufficient data.");
+ }
+ int id = this.parseUInt16();
+ int flags = this.parseUInt16();
+ int nQuestions = this.parseUInt16();
+ int nAnswers = this.parseUInt16();
+ int nAuthorities = this.parseUInt16();
+ int nAdditionals = this.parseUInt16();
+ DnsPacket.Question[] questions = this.parseQuestions(nQuestions);
+ DnsPacket.Entry[] answers = this.parseEntries(nAnswers);
+ DnsPacket.Entry[] authorities = this.parseEntries(nAuthorities);
+ DnsPacket.Entry[] additionals = this.parseEntries(nAdditionals);
+
+ return new DnsPacket(id, flags, questions, answers, authorities, additionals);
+ }
+
+ public DnsPacket parse(DatagramPacket packet) throws DnsException {
+ return parse(packet.getData());
+ }
+
+ private DnsPacket.Question[] parseQuestions(int nQuestions) throws DnsException {
+ if (nQuestions > 0) {
+ ArrayList<DnsPacket.Question> buffer = new ArrayList<>(nQuestions);
+
+ for (int i = 0; i < nQuestions; i++) {
+ buffer.add(this.parseQuestion());
+ }
+ return buffer.toArray(new DnsPacket.Question[buffer.size()]);
+ }
+ return new DnsPacket.Question[0];
+ }
+
+ private DnsPacket.Entry[] parseEntries(int nEntries) throws DnsException {
+ ArrayList<DnsPacket.Entry> entries = new ArrayList<>(nEntries);
+
+ for (int i = 0; i < nEntries; i++) {
+ entries.add(this.parseEntry());
+ }
+ return entries.toArray(new DnsPacket.Entry[entries.size()]);
+ }
+
+ private DnsPacket.Question parseQuestion() throws DnsException {
+ DnsPacket.Name name = this.parseName();
+ int type = this.parseUInt16();
+ int clazz = this.parseUInt16();
+ DnsPacket.ResourceType resourceType = DnsPacket.ResourceType.valueOf(type);
+
+ return new DnsPacket.Question(name, resourceType, clazz);
+ }
+
+ private DnsPacket.Entry parseEntry() throws DnsException {
+ DnsPacket.Name name = this.parseName();
+ int type = this.parseUInt16();
+ int clazz = this.parseUInt16();
+ int ttl = this.parseInt32();
+ int len = this.parseUInt16();
+ DnsPacket.ResourceType resourceType = DnsPacket.ResourceType.valueOf(type);
+
+ switch (resourceType) {
+ case A:
+ case AAAA:
+ return new DnsPacket.Address(name, resourceType, clazz, ttl, this.parseBytes(len));
+ case CNAME:
+ case PTR:
+ return new DnsPacket.Ptr(name, resourceType, clazz, ttl, this.parseName());
+ case TXT:
+ return new DnsPacket.Txt(name, resourceType, clazz, ttl, this.parseBytes(len));
+ case SRV:
+ int priority = this.parseUInt16();
+ int weight = this.parseUInt16();
+ int port = this.parseUInt16();
+ DnsPacket.Name target = this.parseName();
+
+ return new DnsPacket.Srv(name, resourceType, clazz, ttl, priority, weight,
+ port, target);
+ case OPT:
+ return new DnsPacket.Opt(resourceType, clazz);
+ default:
+ return new DnsPacket.GenericEntry(name, resourceType, clazz, ttl,
+ this.parseBytes(len));
+ }
+ }
+
+ private int parseUInt8() throws DnsException {
+ return this.data[this.offset++] & BYTE_MASK;
+ }
+
+ private int parseUInt16() throws DnsException {
+ return this.parseInt(INT16_LENGTH);
+ }
+
+ private int parseInt32() throws DnsException {
+ return this.parseInt(INT32_LENGTH);
+ }
+
+ private int parseInt(int nBits) throws DnsException {
+ int nBytes = nBits / BYTE_LENGTH;
+ int intNumber = 0;
+
+ if ((this.length - this.offset) < nBytes) {
+ throw new DnsException("Failed to read an int field: insufficient data.");
+ }
+ for (int i = 0; i < nBytes; i++) {
+ int shiftBits = (nBytes - i - 1) * BYTE_LENGTH;
+
+ intNumber |= (this.parseUInt8() << shiftBits);
+ }
+ return intNumber;
+ }
+
+ private byte[] parseBytes(int len) throws DnsException {
+ if ((this.length - this.offset) < len) {
+ throw new DnsException("Failed to read byte array: insufficient data.");
+ }
+ byte[] bytes = new byte[len];
+
+ System.arraycopy(this.data, this.offset, bytes, 0, len);
+ this.offset += len;
+ return bytes;
+ }
+
+ private DnsPacket.CompressedName parseName() throws DnsException {
+ if ((this.length - this.offset) < 1) {
+ throw new DnsException("Failed to read a name: insufficient data.");
+ }
+ DnsPacket.CompressedName name = this.readName(this.offset);
+
+ this.offset += name.getSizeInBytes();
+ return name;
+ }
+
+ private DnsPacket.CompressedName readName(int nameDataOffset) throws DnsException {
+ int dataOffset = nameDataOffset;
+ ArrayList<DnsPacket.NameSection> sectionList = new ArrayList<>();
+ DnsPacket.NameSection section;
+
+ do {
+ section = this.readNameSection(dataOffset);
+ sectionList.add(section);
+ dataOffset += section.getSizeInBytes();
+ } while (!section.isEmpty() && !section.isPointer());
+ return new DnsPacket.CompressedName(sectionList.toArray(new DnsPacket.NameSection[sectionList.size()]));
+ }
+
+ private DnsPacket.NameSection readNameSection(int dataOffset) throws DnsException {
+ byte labelLength = this.data[dataOffset];
+
+ if ((labelLength & NAME_POINTER_MASK) == NAME_POINTER_MASK) {
+ return this.readNamePointer(dataOffset);
+ } else if ((labelLength & NAME_POINTER_MASK) == 0) {
+ return this.readNameLabel(dataOffset, labelLength);
+ }
+ return this.readNameLabel(dataOffset, labelLength);
+ }
+
+ private DnsPacket.NameLabel readNameLabel(int dataOffset, int labelLength) throws DnsException {
+ byte[] labelBuffer = new byte[labelLength];
+
+ System.arraycopy(this.data, dataOffset + 1, labelBuffer, 0, labelLength);
+ try {
+ return new DnsPacket.NameLabel(new String(labelBuffer, NAME_ENCODING));
+ } catch (UnsupportedEncodingException exc) {
+ throw new DnsException("Unsupported encoding to parse DNS name: " + NAME_ENCODING, exc);
+ }
+ }
+
+ private DnsPacket.NamePointer readNamePointer(int dataOffset) throws DnsException {
+ int nameOffset = this.readNameOffset(dataOffset);
+
+ return new DnsPacket.NamePointer(this.readName(nameOffset));
+ }
+
+ private int readNameOffset(int dataOffset) {
+ int first = (this.data[dataOffset] & ~NAME_POINTER_MASK);
+ int second = this.data[dataOffset + 1] & BYTE_MASK;
+
+ return ((first << BYTE_LENGTH) | second);
+ }
+}
diff --git a/PrintServiceStubs/src/com/android/printservicestubs/servicediscovery/mdns/DnsSdException.java b/PrintServiceStubs/src/com/android/printservicestubs/servicediscovery/mdns/DnsSdException.java
new file mode 100644
index 0000000..a13c208
--- /dev/null
+++ b/PrintServiceStubs/src/com/android/printservicestubs/servicediscovery/mdns/DnsSdException.java
@@ -0,0 +1,29 @@
+/*
+ * (c) Copyright 2016 Mopria Alliance, Inc.
+ * (c) Copyright 2016 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.printservicestubs.servicediscovery.mdns;
+
+@SuppressWarnings("serial")
+class DnsSdException extends DnsException {
+
+ public DnsSdException(String detailString) {
+ super(detailString);
+ }
+}
diff --git a/PrintServiceStubs/src/com/android/printservicestubs/servicediscovery/mdns/DnsSdParser.java b/PrintServiceStubs/src/com/android/printservicestubs/servicediscovery/mdns/DnsSdParser.java
new file mode 100644
index 0000000..6319315
--- /dev/null
+++ b/PrintServiceStubs/src/com/android/printservicestubs/servicediscovery/mdns/DnsSdParser.java
@@ -0,0 +1,166 @@
+/*
+ * (c) Copyright 2016 Mopria Alliance, Inc.
+ * (c) Copyright 2016 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.printservicestubs.servicediscovery.mdns;
+
+import android.util.Log;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Map;
+
+class DnsSdParser {
+ private static final String TAG = DnsSdParser.class.getSimpleName();
+
+ private static final char SEPARATOR = '=';
+
+ private DnsPacket packet;
+
+ public DnsService[] parse(DnsPacket aPacket) throws DnsException {
+ this.packet = aPacket;
+ return this.parseServices();
+ }
+
+ private DnsService[] parseServices() throws DnsException {
+ ArrayList<DnsService> serviceList = new ArrayList<>();
+
+ for (DnsPacket.Entry answerEntry : this.packet.getAnswers()) {
+ if (DnsPacket.ResourceType.PTR == answerEntry.getType()) {
+ DnsPacket.Ptr ptr = (DnsPacket.Ptr) answerEntry;
+
+ try {
+ serviceList.add(this.buildService(ptr));
+ } catch (DnsSdException ignore) {
+ }
+ }
+ }
+ return serviceList.toArray(new DnsService[serviceList.size()]);
+ }
+
+ private DnsService buildService(DnsPacket.Ptr ptr) throws DnsSdException {
+ DnsPacket.Name serviceNameSuffix = ptr.getName();
+ DnsPacket.Name serviceName = ptr.getPointedName();
+ DnsPacket.Srv srvEntry = this.findSrv(serviceName);
+ DnsPacket.Txt txtEntry = this.findTxt(serviceName);
+ DnsPacket.Name hostname = srvEntry.getTarget();
+ DnsPacket.Address[] addressEntries = this.findAddresses(hostname);
+ int port = srvEntry.getPort();
+ Map<String, byte[]> attributes = parseAttributes(txtEntry.getText());
+ byte[][] addresses = new byte[addressEntries.length][];
+
+ for (int i = 0; i < addressEntries.length; i++) {
+ addresses[i] = addressEntries[i].getAddress();
+ }
+ return new DnsService(serviceName, serviceNameSuffix, hostname, addresses, port, attributes);
+ }
+
+ private DnsPacket.Srv findSrv(DnsPacket.Name serviceName) throws DnsSdException {
+ for (DnsPacket.Entry additionalEntry : this.packet.getAdditionals()) {
+ if (DnsPacket.ResourceType.SRV == additionalEntry.getType()) {
+ DnsPacket.Srv srv = (DnsPacket.Srv) additionalEntry;
+
+ if (srv.getName().equals(serviceName)) {
+ return srv;
+ }
+ }
+ }
+ throw new DnsSdException("Service does not contain correspondent srv entry.");
+ }
+
+ private DnsPacket.Txt findTxt(DnsPacket.Name serviceName) throws DnsSdException {
+ for (DnsPacket.Entry additionalEntry : this.packet.getAdditionals()) {
+ if (DnsPacket.ResourceType.TXT == additionalEntry.getType()) {
+ DnsPacket.Txt txt = (DnsPacket.Txt) additionalEntry;
+
+ if (txt.getName().equals(serviceName)) {
+ return txt;
+ }
+ }
+ }
+ throw new DnsSdException("Service does not contain correspondent txt entry.");
+ }
+
+ private DnsPacket.Address[] findAddresses(DnsPacket.Name hostname) throws DnsSdException {
+ ArrayList<DnsPacket.Address> addressEntries = new ArrayList<>();
+
+ for (DnsPacket.Entry additionalEntry : this.packet.getAdditionals()) {
+ if ((DnsPacket.ResourceType.A == additionalEntry.getType())
+ || (DnsPacket.ResourceType.AAAA == additionalEntry.getType())) {
+ DnsPacket.Address address = (DnsPacket.Address) additionalEntry;
+
+ if (address.getName().equals(hostname)) {
+ addressEntries.add(address);
+ }
+ }
+ }
+ if (addressEntries.isEmpty()) {
+ throw new DnsSdException("Service does not contain correspondent address entry.");
+ }
+ return addressEntries.toArray(new DnsPacket.Address[addressEntries.size()]);
+ }
+
+ private static Map<String, byte[]> parseAttributes(byte[] txtData) {
+ Map<String, byte[]> attributes = new HashMap<>();
+ int offset = 0;
+
+ while (offset < txtData.length) {
+ int attrLength = txtData[offset++];
+ if (attrLength < 0) {
+ attrLength += 256;
+ }
+ int sepIndex;
+ int keyLength;
+ String key;
+ byte[] value = null;
+
+ if ((attrLength < 0) || ((offset + attrLength) > txtData.length)) {
+ return attributes;
+ }
+ sepIndex = findSeparator(txtData, offset, attrLength);
+ keyLength = (sepIndex > 0) ? (sepIndex - offset) : attrLength;
+ if (keyLength == 0) {
+ return attributes;
+ }
+ try {
+ key = new String(txtData, offset, keyLength, "US-ASCII");
+ } catch (Exception e) {
+ Log.e(TAG, "Exception: parsing attribute: " + e);
+ e.printStackTrace();
+ continue;
+ }
+
+ if (sepIndex > 0) {
+ int valueLength = (attrLength - keyLength - 1);
+
+ value = new byte[valueLength];
+ System.arraycopy(txtData, sepIndex + 1, value, 0, valueLength);
+ }
+ attributes.put(key, value);
+ offset += attrLength;
+ }
+ return attributes;
+ }
+
+ private static int findSeparator(byte[] txtData, int offset, int length) {
+ for (int i = offset; i < (offset + length); i++) {
+ if (txtData[i] == (byte) SEPARATOR) {
+ return i;
+ }
+ }
+ return -1;
+ }
+}
diff --git a/PrintServiceStubs/src/com/android/printservicestubs/servicediscovery/mdns/DnsService.java b/PrintServiceStubs/src/com/android/printservicestubs/servicediscovery/mdns/DnsService.java
new file mode 100644
index 0000000..4b2b588
--- /dev/null
+++ b/PrintServiceStubs/src/com/android/printservicestubs/servicediscovery/mdns/DnsService.java
@@ -0,0 +1,68 @@
+/*
+ * (c) Copyright 2016 Mopria Alliance, Inc.
+ * (c) Copyright 2016 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.printservicestubs.servicediscovery.mdns;
+
+import java.util.Map;
+
+class DnsService {
+ private DnsPacket.Name name;
+ private DnsPacket.Name serviceNameSuffix;
+ private DnsPacket.Name hostname;
+ private byte[][] addresses;
+ private int port;
+ private Map<String, byte[]> attributes;
+
+ public DnsService(DnsPacket.Name name, DnsPacket.Name serviceNameSuffix, DnsPacket.Name hostname, byte[][] addresses, int port,
+ Map<String, byte[]> attributes) {
+ this.name = name;
+ this.serviceNameSuffix = serviceNameSuffix;
+ this.hostname = hostname;
+ this.addresses = addresses;
+ this.port = port;
+ this.attributes = attributes;
+ }
+
+ public DnsPacket.Name getName() {
+ return this.name;
+ }
+
+ public DnsPacket.Name getNameSuffix() {
+ return this.serviceNameSuffix;
+ }
+
+ public DnsPacket.Name getHostname() {
+ return this.hostname;
+ }
+
+ public byte[][] getAddresses() {
+ return addresses;
+ }
+
+ public int getPort() {
+ return port;
+ }
+
+ public Map<String, byte[]> getAttributes() {
+ return attributes;
+ }
+
+ @Override
+ public String toString() {
+ return this.getName().toString();
+ }
+}
diff --git a/PrintServiceStubs/src/com/android/printservicestubs/servicediscovery/mdns/MDNSUtils.java b/PrintServiceStubs/src/com/android/printservicestubs/servicediscovery/mdns/MDNSUtils.java
new file mode 100644
index 0000000..81db89f
--- /dev/null
+++ b/PrintServiceStubs/src/com/android/printservicestubs/servicediscovery/mdns/MDNSUtils.java
@@ -0,0 +1,104 @@
+/*
+ * (c) Copyright 2016 Mopria Alliance, Inc.
+ * (c) Copyright 2016 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.printservicestubs.servicediscovery.mdns;
+
+import android.annotation.NonNull;
+import android.os.Bundle;
+
+import com.android.internal.util.Preconditions;
+import com.android.printservicestubs.servicediscovery.NetworkDevice;
+
+import java.util.ArrayList;
+import java.util.Locale;
+import java.util.Set;
+
+/**
+ * Utils for dealing with mDNS attributes
+ */
+public class MDNSUtils {
+ public static final String ATTRIBUTE__TY = "ty";
+ public static final String ATTRIBUTE__PRODUCT = "product";
+ public static final String ATTRIBUTE__USB_MFG = "usb_MFG";
+ public static final String ATTRIBUTE__MFG = "mfg";
+
+ /**
+ * Check if the device has any of a set of vendor names.
+ *
+ * @param networkDevice The device
+ * @param vendorNames The vendors
+ *
+ * @return true iff the has any of the set of vendor names
+ */
+ public static boolean isVendorPrinter(@NonNull NetworkDevice networkDevice,
+ @NonNull Set<String> vendorNames) {
+ networkDevice = Preconditions.checkNotNull(networkDevice);
+ vendorNames = Preconditions.checkCollectionElementsNotNull(vendorNames, "vendorNames");
+
+ ArrayList<NetworkDevice> instances = networkDevice.getAllDiscoveryInstances();
+
+ final int numInstances = instances.size();
+ for (int i = 0; i < numInstances; i++) {
+ Bundle attributes = instances.get(i).getTxtAttributes();
+
+ String product = attributes.getString(ATTRIBUTE__PRODUCT);
+ String ty = attributes.getString(ATTRIBUTE__TY);
+ String usbMfg = attributes.getString(ATTRIBUTE__USB_MFG);
+ String mfg = attributes.getString(ATTRIBUTE__MFG);
+
+ if (containsVendor(product, vendorNames) || containsVendor(ty, vendorNames) ||
+ containsVendor(usbMfg, vendorNames) || containsVendor(mfg, vendorNames)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Check if the attribute matches any of the vendor names, ignoreing capitalization.
+ *
+ * @param attr The attribute
+ * @param vendorNames The vendor names
+ *
+ * @return true iff the attribute matches any of the vendor names
+ */
+ private static boolean containsVendor(String attr, @NonNull Set<String> vendorNames) {
+ if (attr == null) {
+ return false;
+ }
+
+ for (String name : vendorNames) {
+ if (containsString(attr, name) ||
+ containsString(attr.toLowerCase(Locale.US), name.toLowerCase(Locale.US)) ||
+ containsString(attr.toUpperCase(Locale.US), name.toUpperCase(Locale.US)))
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Check if a string in another string
+ *
+ * @param container The string that contains the string
+ * @param contained The string that is contained
+ *
+ * @return true if the string is contained in the other.
+ */
+ private static boolean containsString(@NonNull String container, @NonNull String contained) {
+ return container.equalsIgnoreCase(contained) || container.contains(contained + " ");
+ }
+}
diff --git a/PrintServiceStubs/src/com/android/printservicestubs/servicediscovery/mdns/MDnsDiscovery.java b/PrintServiceStubs/src/com/android/printservicestubs/servicediscovery/mdns/MDnsDiscovery.java
new file mode 100644
index 0000000..73b3b5e
--- /dev/null
+++ b/PrintServiceStubs/src/com/android/printservicestubs/servicediscovery/mdns/MDnsDiscovery.java
@@ -0,0 +1,207 @@
+/*
+ * (c) Copyright 2016 Mopria Alliance, Inc.
+ * (c) Copyright 2016 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.printservicestubs.servicediscovery.mdns;
+
+import android.text.TextUtils;
+import android.util.Pair;
+
+import com.android.printservicestubs.servicediscovery.IDiscovery;
+import com.android.printservicestubs.servicediscovery.ServiceParser;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.net.DatagramPacket;
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicInteger;
+
+public class MDnsDiscovery implements IDiscovery {
+
+ private static final AtomicInteger mTrasactionID = new AtomicInteger(0);
+ static final String MDNS_GROUP_ADDRESS = "224.0.0.251";
+ static final int MDNS_PORT = 5353;
+
+ private int mQueryCount = 0;
+ private static final int ANSWER_RESET_THRESHOLD = 3; // 4 requests, 3 with answers
+
+ private static final byte[] REQ_TRANSACTION_ID = { 0x00, 0x00 };
+ private static final byte[] REQ_FLAGS = { 0x00, 0x00 };
+ private static final byte[] REQ_NO_ANSWERS = { 0x00, 0x00 };
+ private static final byte[] REQ_NUM_AUTHORITY_RRS = { 0x00, 0x00 };
+ private static final byte[] REQ_NUM_ADDITIONAL_RRS = { 0x00, 0x00 };
+ private static final byte[] REQ_QUESTION__LOCAL = { 0x05, 0x6C, 0x6F, 0x63, 0x61, 0x6C, 0x00 };
+ private static final byte[] REQ_PTR_TYPE = { 0x00, 0x0C };
+ private static final byte[] REQ_IN_CLASS_QU_TRUE = { (byte)0x80, 0x01 };
+ private static final byte[] REQ_IN_CLASS_QU_FALSE = { (byte)0x00, 0x01 };
+ private final HashMap<String, Pair<String,String>> mPrinters = new LinkedHashMap<>();
+
+ private final String[] mServiceList;
+
+ public MDnsDiscovery(String[] serviceList) {
+ mServiceList = serviceList;
+ }
+
+ @Override
+ public void clear() {
+ synchronized (mPrinters) {
+ mPrinters.clear();
+ mQueryCount = 0;
+ }
+ }
+
+ @Override
+ public DatagramPacket[] createQueryPackets() throws UnknownHostException {
+
+
+ InetAddress group = InetAddress.getByName(MDNS_GROUP_ADDRESS);
+
+ List<DatagramPacket> packetList = new ArrayList<>();
+
+ HashMap<String,List<Pair<String,String>>> previouslyFoundPrinters = new HashMap<>(mServiceList.length);
+ for(String serviceName : mServiceList) {
+ previouslyFoundPrinters.put(serviceName, new ArrayList<Pair<String,String>>());
+ }
+
+ synchronized (mPrinters) {
+ mQueryCount++;
+ if(mQueryCount > ANSWER_RESET_THRESHOLD){
+ mQueryCount = 0;
+ mPrinters.clear();
+ }
+ for(Pair<String,String> val : mPrinters.values()) {
+ List<Pair<String,String>> list = previouslyFoundPrinters.get(val.first);
+ list.add(val);
+ }
+ }
+
+
+ for(String serviceName : mServiceList) {
+ List<Pair<String,String>> list = previouslyFoundPrinters.get(serviceName);
+
+ ByteArrayOutputStream mdnsBuffer = new ByteArrayOutputStream();
+ try {
+ mdnsBuffer.write(shortToBytes((short)mTrasactionID.getAndIncrement()));
+ mdnsBuffer.write(REQ_FLAGS);
+
+ mdnsBuffer.write(shortToBytes((short)1));
+ mdnsBuffer.write(shortToBytes((short)list.size()));
+ mdnsBuffer.write(REQ_NUM_AUTHORITY_RRS);
+ mdnsBuffer.write(REQ_NUM_ADDITIONAL_RRS);
+ String[] serviceSplits = serviceName.split("\\.");
+ for(String servicePart : serviceSplits) {
+ // coverity[FB.DM_DEFAULT_ENCODING]
+ byte[] serviceBytes = servicePart.getBytes();
+ mdnsBuffer.write(serviceBytes.length);
+ mdnsBuffer.write(serviceBytes);
+ }
+ mdnsBuffer.write(REQ_QUESTION__LOCAL);
+ mdnsBuffer.write(REQ_PTR_TYPE);
+ mdnsBuffer.write(REQ_IN_CLASS_QU_TRUE);
+ for (Pair<String,String> printerAnswer : list) {
+ addAnswer(new String[] { serviceName }, mdnsBuffer, printerAnswer);
+ }
+
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ byte[] mdnsBytes = mdnsBuffer.toByteArray();
+ packetList.add(new DatagramPacket( mdnsBytes, mdnsBytes.length, group, MDNS_PORT));
+ }
+ return packetList.toArray(new DatagramPacket[packetList.size()]);
+
+
+ }
+
+ private void addAnswer(String[] mdnsServices, ByteArrayOutputStream mdnsBuffer, Pair<String,String> printerAnswer) throws IOException {
+ short questionIndex = (short)(REQ_TRANSACTION_ID.length +
+ REQ_FLAGS.length +
+ REQ_NO_ANSWERS.length +
+ REQ_NO_ANSWERS.length +
+ REQ_NUM_AUTHORITY_RRS.length +
+ REQ_NUM_ADDITIONAL_RRS.length);
+ for(String serviceName : mdnsServices) {
+ if (TextUtils.equals(serviceName, printerAnswer.first)) break;
+ // coverity[FB.DM_DEFAULT_ENCODING]
+ String[] serviceSplits = serviceName.split("\\.");
+ for(String servicePart : serviceSplits) {
+ questionIndex += servicePart.getBytes().length + 1;
+ }
+ questionIndex += REQ_QUESTION__LOCAL.length + REQ_PTR_TYPE.length + REQ_IN_CLASS_QU_FALSE.length;
+ }
+
+ byte[] serviceNamePtr = shortToBytes((short)(0xC000 | questionIndex));
+ mdnsBuffer.write(serviceNamePtr); // Pointer to service name
+ mdnsBuffer.write(REQ_PTR_TYPE); // Type (PTR)
+ mdnsBuffer.write(REQ_IN_CLASS_QU_FALSE); // Class (IN)
+ mdnsBuffer.write(intToBytes(3600)); // TTL (1 hour)
+
+ String bonjourName = printerAnswer.second;
+ byte[] nameLength = shortToBytes((short) (bonjourName.length() + serviceNamePtr.length + 1));
+ mdnsBuffer.write(nameLength); // Data length, includes first byte (which denotes the length of the name) and service name pointer length
+
+ mdnsBuffer.write((byte) bonjourName.length()); // Domain name, includes first byte, which denotes the length of the name, and service name pointer
+ mdnsBuffer.write(bonjourName.getBytes());
+ mdnsBuffer.write(serviceNamePtr);
+ }
+
+ private byte[] shortToBytes(short s) {
+ return new byte[]{(byte)((s & 0xFF00)>>8),(byte)(s & 0x00FF)};
+ }
+
+ private byte[] intToBytes(int i) {
+ return ByteBuffer.allocate(4).order(ByteOrder.BIG_ENDIAN).putInt(i).array();
+ }
+
+ @Override
+ public ArrayList<ServiceParser> parseResponse(DatagramPacket packet) {
+ ArrayList<ServiceParser> networkDevices = new ArrayList<>();
+
+ synchronized (mPrinters) {
+ try {
+ DnsPacket dnsPacket = new DnsParser().parse(packet);
+ DnsService[] services = new DnsSdParser().parse(dnsPacket);
+
+ for (DnsService service : services) {
+ BonjourParser bonjourParser = new BonjourParser(service);
+ mPrinters.put(service.getHostname() + "." + service.getNameSuffix(),
+ Pair.create(bonjourParser.getBonjourServiceName(), bonjourParser.getBonjourName()));
+ networkDevices.add(bonjourParser);
+ }
+ } catch (Exception ignored) {
+ }
+ }
+ return networkDevices;
+ }
+
+ @Override
+ public int getPort() {
+ return MDnsDiscovery.MDNS_PORT;
+ }
+
+ @Override
+ public boolean isFallback() {
+ return false;
+ }
+
+}