diff options
Diffstat (limited to 'PrintServiceStubs/src/com/android/printservicestubs/servicediscovery')
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; + } + +} |