diff --git a/MPDroid/src/main/java/com/cafbit/multicasttest/NetThread.java b/MPDroid/src/main/java/com/cafbit/multicasttest/NetThread.java new file mode 100644 index 0000000000..5739b5544f --- /dev/null +++ b/MPDroid/src/main/java/com/cafbit/multicasttest/NetThread.java @@ -0,0 +1,138 @@ +/* + * Copyright 2011 David Simmons + * http://cafbit.com/entry/testing_multicast_support_on_android + * + * 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.cafbit.multicasttest; + +import java.io.IOException; +import java.net.DatagramPacket; +import java.net.InetAddress; +import java.net.MulticastSocket; +import java.net.NetworkInterface; +import java.util.Set; + +import com.cafbit.netlib.NetUtil; +import com.cafbit.netlib.dns.DNSMessage; + +import android.content.Context; +import android.net.wifi.WifiManager.MulticastLock; +import android.util.Log; + +/** + * This thread runs in the background while the user has our + * program in the foreground, and handles sending mDNS queries + * and processing incoming mDNS packets. + * @author simmons + */ +public class NetThread extends Thread { + + private static final String TAG = "NetThread"; + + // the standard mDNS multicast address and port number + private static final byte[] MDNS_ADDR = + new byte[] {(byte) 224,(byte) 0,(byte) 0,(byte) 251}; + private static final int MDNS_PORT = 5353; + + private static final int BUFFER_SIZE = 4096; + + private NetworkInterface networkInterface; + private InetAddress groupAddress; + private MulticastSocket multicastSocket = null; + private NetUtil netUtil; + private String servicename; + + /** + * Construct the network thread. + * @param activity + * @param servicename Name of service to search for + */ + public NetThread(Context activity, String servicename) { + super("net"); + this.servicename = servicename; + netUtil = new NetUtil(activity); + } + + /** + * Open a multicast socket on the mDNS address and port. + * @throws IOException + */ + private void openSocket() throws IOException { + multicastSocket = new MulticastSocket(MDNS_PORT); + multicastSocket.setTimeToLive(2); + multicastSocket.setSoTimeout(5000); + multicastSocket.setReuseAddress(true); + multicastSocket.setNetworkInterface(networkInterface); + multicastSocket.joinGroup(groupAddress); + } + + /** + */ + @Override + public void run() { + Log.v(TAG, "starting network thread"); + + Set localAddresses = NetUtil.getLocalAddresses(); + MulticastLock multicastLock = null; + + try { + networkInterface = netUtil.getFirstWifiOrEthernetInterface(); + if (networkInterface == null) { + throw new IOException("Your WiFi is not enabled."); + } + groupAddress = InetAddress.getByAddress(MDNS_ADDR); + + multicastLock = netUtil.getWifiManager().createMulticastLock("unmote"); + multicastLock.acquire(); + Log.v(TAG, "acquired multicast lock: "+multicastLock); + + openSocket(); + Log.v(TAG, "opensocket returned"); + + query(servicename); + Log.v(TAG, "sent query"); + + // set up the buffer for incoming packets + byte[] responseBuffer = new byte[BUFFER_SIZE]; + DatagramPacket response = new DatagramPacket(responseBuffer, BUFFER_SIZE); + multicastSocket.receive(response); + Log.v(TAG, "received response"+response.getSocketAddress()); + multicastSocket.close(); + } catch (IOException e1) { + Log.v(TAG, "send mdns query failed "+e1.toString()); + return; + } + + if(multicastSocket != null) + multicastSocket.close(); + + // release the multicast lock + if(multicastLock != null) + multicastLock.release(); + + Log.v(TAG, "stopping network thread"); + } + + /** + * Transmit an mDNS query on the local network. + * @param servicename + * @throws IOException + */ + private void query(String servicename) throws IOException { + byte[] requestData = (new DNSMessage(servicename)).serialize(); + DatagramPacket request = + new DatagramPacket(requestData, requestData.length, InetAddress.getByAddress(MDNS_ADDR), MDNS_PORT); + multicastSocket.send(request); + } +} diff --git a/MPDroid/src/main/java/com/cafbit/multicasttest/Util.java b/MPDroid/src/main/java/com/cafbit/multicasttest/Util.java new file mode 100644 index 0000000000..4c70e06fb4 --- /dev/null +++ b/MPDroid/src/main/java/com/cafbit/multicasttest/Util.java @@ -0,0 +1,63 @@ +/* + * Copyright 2011 David Simmons + * http://cafbit.com/entry/testing_multicast_support_on_android + * + * 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.cafbit.multicasttest; + +/** + * Various mundate utility methods. + * @author simmons + */ +public class Util { + + public static String hexDump(byte[] bytes) { + return hexDump(bytes, 0, bytes.length); + } + + public static String hexDump(byte[] bytes, int offset, int length) { + StringBuilder sb = new StringBuilder(); + for (int i=0; i 16) { rowSize = 16; } + byte[] row = new byte[rowSize]; + System.arraycopy(bytes, offset+i, row, 0, rowSize); + hexDumpRow(sb, row, i); + } + return sb.toString(); + } + + private static void hexDumpRow(StringBuilder sb, byte[] bytes, int offset) { + sb.append(String.format("%04X: ",offset)); + for (int i=0; i<16; i++) { + if (bytes.length > i) { + sb.append(String.format("%02X ",bytes[i])); + } else { + sb.append(" "); + } + } + for (int i=0; i<16; i++) { + if (bytes.length > i) { + char c = '.'; + int v = (int)bytes[i]; + if ((v > 0x20) && (v < 0x7F)) { + c = (char)v; + } + sb.append(c); + } + } + sb.append('\n'); + } + +} diff --git a/MPDroid/src/main/java/com/cafbit/netlib/NetUtil.java b/MPDroid/src/main/java/com/cafbit/netlib/NetUtil.java new file mode 100644 index 0000000000..1c1f17d6d2 --- /dev/null +++ b/MPDroid/src/main/java/com/cafbit/netlib/NetUtil.java @@ -0,0 +1,243 @@ +/* + * Copyright 2011 David Simmons + * http://cafbit.com/entry/testing_multicast_support_on_android + * + * 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.cafbit.netlib; + +import java.net.InetAddress; +import java.net.NetworkInterface; +import java.net.SocketException; +import java.net.UnknownHostException; +import java.util.ArrayList; +import java.util.Enumeration; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import android.content.Context; +import android.net.wifi.WifiManager; +import android.util.Log; + +/** + * Various Android network utility methods + * @author simmons + */ +public class NetUtil { + + private static final String TAG = "NetLib"; + private WifiManager wifiManager; + + public static class NetInfoException extends Exception { + private static final long serialVersionUID = 5543786811674326615L; + public NetInfoException() {} + public NetInfoException(String message) { + super(message); + } + public NetInfoException(Throwable e) { + super(e); + } + public NetInfoException(String message, Throwable e) { + super(message, e); + } + } + + public static class InterfaceInfo { + private NetworkInterface networkInterface; + private List addresses; + private int flags = 0; + public static final int NET_ETHERNET = 1<<2; + public static final int NET_LOCALHOST = 1<<0; + public static final int NET_OTHER = 1<<3; + public static final int NET_WIFI = 1<<1; + + public InterfaceInfo(NetworkInterface networkInterface, List addresses, int flags) { + this.networkInterface = networkInterface; + this.addresses = addresses; + this.flags = flags; + } + + public NetworkInterface getNetworkInterface() { + return networkInterface; + } + public List getAddresses() { + return addresses; + } + public int getFlags() { + return flags; + } + public boolean isLocalhost() { + return ((flags & NET_LOCALHOST) != 0); + } + public boolean isWifi() { + return ((flags & NET_WIFI) != 0); + } + public boolean isEthernet() { + return ((flags & NET_ETHERNET) != 0); + } + + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("interface "+networkInterface+" :"); + if ((flags & NET_LOCALHOST)!=0) { sb.append(" localhost"); } + if ((flags & NET_WIFI)!=0) { sb.append(" wifi"); } + if ((flags & NET_ETHERNET)!=0) { sb.append(" ethernet"); } + sb.append("\n"); + for (InetAddress address : addresses) { + sb.append(" addr "+address.toString()+"\n"); + } + return sb.toString(); + } + + } + + public NetUtil(Context context) { + wifiManager = (WifiManager) context.getSystemService(Context.WIFI_SERVICE); + } + + public WifiManager getWifiManager() { + return wifiManager; + } + + public static Set getLocalAddresses() { + Set addresses = new HashSet(); + + Enumeration networkInterfaces; + try { + networkInterfaces = NetworkInterface.getNetworkInterfaces(); + } catch (SocketException e) { + Log.v(TAG, "getNetworkInterfaces(): "+e.getMessage(), e); + return null; + } + + while (networkInterfaces.hasMoreElements()) { + NetworkInterface networkInterface = networkInterfaces.nextElement(); + Enumeration addressEnum = networkInterface.getInetAddresses(); + while (addressEnum.hasMoreElements()) { + addresses.add(addressEnum.nextElement()); + } + } + + return addresses; + } + + public List getNetworkInformation() throws NetInfoException { + List interfaceList = new ArrayList(); + + InetAddress wifiAddress = null; + InetAddress reversedWifiAddress = null; + if (wifiManager.isWifiEnabled()) { + // get the ip address of the wifi interface + int rawAddress = wifiManager.getConnectionInfo().getIpAddress(); + try { + wifiAddress = InetAddress.getByAddress(new byte[] { + (byte) ((rawAddress >> 0) & 0xFF), + (byte) ((rawAddress >> 8) & 0xFF), + (byte) ((rawAddress >> 16) & 0xFF), + (byte) ((rawAddress >> 24) & 0xFF), + }); + // It's unclear how to interpret the byte order + // of the WifiInfo.getIpAddress() int value, so + // we also compare with the reverse order. The + // result is probably consistent with ByteOrder.nativeOrder(), + // but we don't know for certain since there's no documentation. + reversedWifiAddress = InetAddress.getByAddress(new byte[] { + (byte) ((rawAddress >> 24) & 0xFF), + (byte) ((rawAddress >> 16) & 0xFF), + (byte) ((rawAddress >> 8) & 0xFF), + (byte) ((rawAddress >> 0) & 0xFF), + }); + } catch (UnknownHostException e) { + throw new NetInfoException("problem retreiving wifi ip address", e); + } + } + + InetAddress localhost; + try { + localhost = InetAddress.getLocalHost(); + } catch (Exception e) { + throw new NetInfoException("cannot determine the localhost address", e); + } + + // get a list of all network interfaces + Enumeration networkInterfaces; + try { + networkInterfaces = NetworkInterface.getNetworkInterfaces(); + } catch (SocketException e) { + throw new NetInfoException("problem getting net interfaces", e); + } + + // find the wifi network interface based on the ip address + while (networkInterfaces.hasMoreElements()) { + NetworkInterface networkInterface = networkInterfaces.nextElement(); + int flags = 0; + Enumeration addressEnum = networkInterface.getInetAddresses(); + List addresses = new ArrayList(); + while (addressEnum.hasMoreElements()) { + InetAddress address = addressEnum.nextElement(); + + // check for localhost + if (address.equals(localhost)) { + flags |= InterfaceInfo.NET_LOCALHOST; + } + + // check for wifi + if ( (wifiAddress != null) && + (reversedWifiAddress != null) && + (address.equals(wifiAddress) || address.equals(reversedWifiAddress)) + ) { + flags |= InterfaceInfo.NET_WIFI; + } + + addresses.add(address); + } + + // assume an eth* interface that isn't wifi is wired ethernet. + if (((flags & InterfaceInfo.NET_WIFI)==0) && networkInterface.getName().startsWith("eth")) { + flags |= InterfaceInfo.NET_ETHERNET; + } + + interfaceList.add(new InterfaceInfo(networkInterface, addresses, flags)); + } + return interfaceList; + } + + public NetworkInterface getFirstWifiInterface() { + try { + for (InterfaceInfo ii : getNetworkInformation()) { + if (ii.isWifi()) { + return ii.getNetworkInterface(); + } + } + } catch (NetInfoException e) { + Log.w(TAG, "cannot find a wifi interface"); + } + return null; + } + + public NetworkInterface getFirstWifiOrEthernetInterface() { + try { + for (InterfaceInfo ii : getNetworkInformation()) { + if (ii.isWifi() || ii.isEthernet()) { + return ii.getNetworkInterface(); + } + } + } catch (NetInfoException e) { + Log.w(TAG, "cannot find a wifi/ethernet interface"); + } + return null; + } + + +} diff --git a/MPDroid/src/main/java/com/cafbit/netlib/dns/DNSAnswer.java b/MPDroid/src/main/java/com/cafbit/netlib/dns/DNSAnswer.java new file mode 100644 index 0000000000..28dbc67b48 --- /dev/null +++ b/MPDroid/src/main/java/com/cafbit/netlib/dns/DNSAnswer.java @@ -0,0 +1,110 @@ +/* + * Copyright 2011 David Simmons + * http://cafbit.com/entry/testing_multicast_support_on_android + * + * 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.cafbit.netlib.dns; + +import java.net.InetAddress; +import java.net.UnknownHostException; + +/** + * This class represents a DNS "answer" component. + * @author simmons + */ +public class DNSAnswer extends DNSComponent { + + public String name; + public Type type; + public int ttl; + public byte[] rdata; + public String rdataString; + + public DNSAnswer(DNSBuffer buffer) { + parse(buffer); + } + + @Override + public int length() { + // TODO: implement + return 0; + } + + @Override + public void serialize(DNSBuffer buffer) { + // TODO: implement + } + + private void parse(DNSBuffer buffer) { + name = buffer.readName(); + type = Type.getType(buffer.readShort()); + + // the most significant bit of the rrclass is special + // in Multicast DNS -- it is used as a "cache flush" bit, + // and only the least significant 15 bits should be used + // as the class. + // see: + // http://tools.ietf.org/html/draft-cheshire-dnsext-multicastdns-05 + // section 11.3 + int aclass = buffer.readShortAsInt(); + //boolean cacheFlush = ((aclass & 0x8000) != 0); + aclass = aclass & 0x7FFF; + if (aclass != 1) { + throw new DNSException("only class IN supported. (got "+aclass+")"); + } + + ttl = buffer.readInteger(); + rdata = buffer.readRdata(); + + if (type.equals(Type.A) || type.equals(Type.AAAA)) { + try { + rdataString = InetAddress.getByAddress(rdata).toString(); + } catch (UnknownHostException e) { + throw new DNSException("problem parsing rdata"); + } + } else if (type.equals(Type.TXT)) { + rdataString = ""; + for (int i=0; i>>6) & 0x03); + if (hiBits == 3) { + // handle compressed names + short compressionOffset = + (short) ((short)((lengthByte & 0x3F) << 8) | readByte()); + pushOffset(start+compressionOffset); + return readLabel(); + } else if (hiBits > 0) { + throw new DNSException("unknown label compression format"); + } + int length = (int) lengthByte; + if ((length == 0) && (! offsetStack.isEmpty())) { + // TODO: it turns out that the compression scheme is not a stack! clean this up... + while (! offsetStack.isEmpty()) { + popOffset(); + } + return null; + } + + if (length > 63) { + throw new DNSException("label length > 63"); + } else if (length == 0) { + return null; + } + return readString(length); + } + + public String readName() { + StringBuilder sb = new StringBuilder(); + boolean needDot = false; + String label; + while ((label = readLabel()) != null) { + if (needDot) { + sb.append('.'); + } else { + needDot = true; + } + sb.append(label); + } + return sb.toString(); + } + + public byte[] readRdata() { + int length = (int) readShort(); + byte[] rdata = readBytes(length); + return rdata; + } + + public void rewind(int amount) { + offset = offset - amount; + } + + // write methods + + public void writeByte(byte b) { + bytes[offset++] = b; + } + + public void writeBytes(byte[] ba) { + System.arraycopy(ba, 0, bytes, offset, ba.length); + offset += ba.length; + } + + public void writeShort(short s) { + bytes[offset++] = (byte)((s>>>8) & 0xFF); + bytes[offset++] = (byte)(s & 0xFF); + } + + public void writeInteger(int i) { + bytes[offset++] = (byte)((i>>>24) & 0xFF); + bytes[offset++] = (byte)((i>>>16) & 0xFF); + bytes[offset++] = (byte)((i>>>8) & 0xFF); + bytes[offset++] = (byte)(i & 0xFF); + } + + public void writeShort(int i) { + writeShort((short) i); + } + + public void writeString(String string) { + byte[] stringBytes = stringToBytes(string); + System.arraycopy(stringBytes, 0, bytes, offset, stringBytes.length); + offset += stringBytes.length; + } + + public void writeLabel(String label) { + if (label.length() > 63) { + throw new DNSException("label length > 63"); + } + writeByte((byte) lengthInBytes(label)); + writeString(label); + } + + public void writeName(String name) { + String[] labels = nameToLabels(name); + for (int i=0; i offsetStack = new Stack(); + + private void pushOffset(int newOffset) { + offsetStack.push(offset); + offset = newOffset; + } + + private void popOffset() { + offset = offsetStack.pop(); + } + + // private static utility methods + + private static String[] nameToLabels(String name) { + return name.split("\\."); + } + + // uncomment if needed + /* + private static String labelsToName(String[] labels) { + StringBuilder sb = new StringBuilder(); + for (int i=0; i questions = new LinkedList(); + public LinkedList answers = new LinkedList(); + + /** + * Construct a DNS host query + */ + public DNSMessage(String hostname) { + messageId = nextMessageId++; + questions.add(new DNSQuestion(DNSQuestion.Type.ANY, hostname)); + } + + /** + * Parse the supplied packet as a DNS message. + */ + public DNSMessage(byte[] packet) { + parse(packet, 0, packet.length); + } + + /** + * Parse the supplied packet as a DNS message. + */ + public DNSMessage(byte[] packet, int offset, int length) { + parse(packet, offset, length); + } + + public int length() { + int length = 12; // header length + for (DNSQuestion q : questions) { + length += q.length(); + } + for (DNSAnswer a : answers) { + length += a.length(); + } + return length; + } + + public byte[] serialize() { + DNSBuffer buffer = new DNSBuffer(length()); + + // header + buffer.writeShort(messageId); + buffer.writeShort(0); // flags + buffer.writeShort(questions.size()); // qdcount + buffer.writeShort(answers.size()); // ancount + buffer.writeShort(0); // nscount + buffer.writeShort(0); // arcount + + // questions + for (DNSQuestion question : questions) { + question.serialize(buffer); + } + + // answers + for (DNSAnswer answer : answers) { + answer.serialize(buffer); + } + + return buffer.bytes; + } + + private void parse(byte[] packet, int offset, int length) { + DNSBuffer buffer = new DNSBuffer(packet, offset, length); + + // header + messageId = buffer.readShort(); + buffer.readShort(); // flags + int qdcount = buffer.readShort(); + int ancount = buffer.readShort(); + buffer.readShort(); // nscount + buffer.readShort(); // arcount + + // questions + questions.clear(); + for (int i=0; i> answersByName = new TreeMap>(); + for (DNSAnswer a : answers) { + List list; + if (answersByName.containsKey(a.name)) { + list = answersByName.get(a.name); + } else { + list = new LinkedList(); + answersByName.put(a.name, list); + } + list.add(a); + } + for (Map.Entry> entry : answersByName.entrySet()) { + sb.append(entry.getKey()+"\n"); + for (DNSAnswer a : entry.getValue()){ + sb.append(" "+a.type.toString()+" "+a.getRdataString()+"\n"); + } + } + + return sb.toString(); + } + +} diff --git a/MPDroid/src/main/java/com/cafbit/netlib/dns/DNSQuestion.java b/MPDroid/src/main/java/com/cafbit/netlib/dns/DNSQuestion.java new file mode 100644 index 0000000000..610715e2a5 --- /dev/null +++ b/MPDroid/src/main/java/com/cafbit/netlib/dns/DNSQuestion.java @@ -0,0 +1,73 @@ +/* + * Copyright 2011 David Simmons + * http://cafbit.com/entry/testing_multicast_support_on_android + * + * 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.cafbit.netlib.dns; + +/** + * This class represents a DNS "question" component. + * @author simmons + */ +public class DNSQuestion extends DNSComponent { + + public Type type; + public String name; + + public DNSQuestion(Type type, String name) { + this.type = type; + this.name = name; + } + + public DNSQuestion(DNSBuffer buffer) { + parse(buffer); + } + + /** + * Return the expected byte length of this question. + */ + public int length() { + int length = DNSBuffer.nameByteLength(name); + length += 5; // zero-terminating length byte, qtype short, qclass short + return length; + } + + /** + * Render this DNS question into a byte buffer + */ + public void serialize(DNSBuffer buffer) { + buffer.checkRemaining(length()); + buffer.writeName(name); // qname + buffer.writeShort(type.qtype); // qtype + buffer.writeShort(1); // qclass (IN) + } + + /** + * Parse a question from the byte buffer + * @param buffer + */ + private void parse(DNSBuffer buffer) { + name = buffer.readName(); + type = Type.getType(buffer.readShort()); + int qclass = buffer.readShort(); + if (qclass != 1) { + throw new DNSException("only class IN supported. (got "+qclass+")"); + } + } + + public String toString() { + return type.toString()+"? "+name; + } + +} diff --git a/MPDroid/src/main/java/com/example/android/nsdchat/NsdHelper.java b/MPDroid/src/main/java/com/example/android/nsdchat/NsdHelper.java new file mode 100644 index 0000000000..de39364477 --- /dev/null +++ b/MPDroid/src/main/java/com/example/android/nsdchat/NsdHelper.java @@ -0,0 +1,144 @@ +/* + * Copyright (C) 2012 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. + */ + +// Adapted from NsdHelper.java in the NsdChat demo application by +// Patrick Wood + +package com.example.android.nsdchat; + +import android.app.Activity; +import android.content.Context; +import android.net.nsd.NsdServiceInfo; +import android.net.nsd.NsdManager; +import android.os.Bundle; +import android.os.Handler; +import android.os.Message; +import android.util.Log; +import android.widget.ArrayAdapter; + +import java.util.concurrent.ConcurrentLinkedQueue; + +public class NsdHelper { + + private NsdManager mNsdManager; + private NsdManager.ResolveListener mResolveListener; + private NsdManager.DiscoveryListener mDiscoveryListener; + private Handler mUpdateHandler; + private ConcurrentLinkedQueuemQueue; + + private String mservice_type = "_workstation._tcp."; + + private static final String TAG = "NsdHelper"; + + public NsdHelper(Context context, Handler handler) { + mUpdateHandler = handler; + mQueue = new ConcurrentLinkedQueue(); + mNsdManager = (NsdManager) context.getSystemService(Context.NSD_SERVICE); + } + + public void initializeNsd() { + initializeResolveListener(); + } + + private void initializeDiscoveryListener() { + mDiscoveryListener = new NsdManager.DiscoveryListener() { + + @Override + public void onDiscoveryStarted(String regType) { + Log.d(TAG, "Service discovery started: " + regType); + } + + @Override + public void onServiceFound(NsdServiceInfo service) { + Log.d(TAG, "Service discovery success: " + service); + if(mQueue.isEmpty()) + mNsdManager.resolveService(service, mResolveListener); + // head of queue is currently resolving service + mQueue.add(service); + } + + @Override + public void onServiceLost(NsdServiceInfo service) { + Log.e(TAG, "service lost" + service); + } + + @Override + public void onDiscoveryStopped(String serviceType) { + Log.i(TAG, "Discovery stopped: " + serviceType); + } + + @Override + public void onStartDiscoveryFailed(String serviceType, int errorCode) { + Log.e(TAG, "Discovery failed: Error code:" + errorCode); + mDiscoveryListener = null; + } + + @Override + public void onStopDiscoveryFailed(String serviceType, int errorCode) { + Log.e(TAG, "Discovery failed: Error code:" + errorCode); + } + }; + } + + private void initializeResolveListener() { + mResolveListener = new NsdManager.ResolveListener() { + + @Override + public void onResolveFailed(NsdServiceInfo serviceInfo, int errorCode) { + Log.e(TAG, "Resolve failed: Error code:" + errorCode); + } + + @Override + public void onServiceResolved(NsdServiceInfo service) { + Log.e(TAG, "Resolve Succeeded. " + service); + + Bundle messageBundle = new Bundle(); + messageBundle.putString("name", service.getServiceName()); + messageBundle.putByteArray("address", service.getHost().getAddress()); + messageBundle.putInt("port", service.getPort()); + Message message = new Message(); + message.setData(messageBundle); + mUpdateHandler.sendMessage(message); + + // pop active resolution + mQueue.poll(); + if(!mQueue.isEmpty()) + mNsdManager.resolveService(mQueue.peek(), mResolveListener); + } + }; + } + + public void discoverServices(String Service) { + stopDiscovery(); // Cancel any existing discovery request + initializeDiscoveryListener(); + if(Service != null) + mservice_type = Service; + mNsdManager.discoverServices(mservice_type, NsdManager.PROTOCOL_DNS_SD, mDiscoveryListener); + } + + public void stopDiscovery() { + if (mDiscoveryListener != null) { + try { + mNsdManager.stopServiceDiscovery(mDiscoveryListener); + } finally { + } + mDiscoveryListener = null; + } + } + + public void tearDown() { + } +} diff --git a/MPDroid/src/main/java/com/namelessdev/mpdroid/ConnectionSettings.java b/MPDroid/src/main/java/com/namelessdev/mpdroid/ConnectionSettings.java index 63524a12d5..7aa6f4a72b 100644 --- a/MPDroid/src/main/java/com/namelessdev/mpdroid/ConnectionSettings.java +++ b/MPDroid/src/main/java/com/namelessdev/mpdroid/ConnectionSettings.java @@ -16,28 +16,135 @@ package com.namelessdev.mpdroid; +import com.cafbit.multicasttest.NetThread; +import com.example.android.nsdchat.NsdHelper; + +import android.content.Context; import android.content.Intent; +import android.os.Build; import android.os.Bundle; +import android.os.Handler; +import android.os.Message; import android.preference.CheckBoxPreference; import android.preference.EditTextPreference; +import android.preference.ListPreference; +import android.preference.Preference; import android.preference.PreferenceActivity; import android.preference.PreferenceCategory; import android.preference.PreferenceScreen; import android.text.InputType; +import android.util.Log; import android.view.Menu; import android.view.MenuItem; +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.util.ArrayList; + @SuppressWarnings("deprecation") public class ConnectionSettings extends PreferenceActivity { public static final int MAIN = 0; + private static final String TAG = "ConnectionSettings"; + private static final String KEY_CONNECTION_CATEGORY = "connectionCategory"; + private NsdHelper mNsdHelper = null; + private EditTextPreference prefHost; + private EditTextPreference prefPort; + + /** + * This method is the Preference for getting MPD server info via NSD + * + * @param context The current context. + * @param keyPrefix The Wi-Fi Set Service ID. + * @return The host name Preference. + */ + private ListPreference getMPDServer(final Context context, final String keyPrefix) { + // try to prevent the Nsd stack from returning IPv6 addresses + java.lang.System.setProperty("java.net.preferIPv6Addresses", "false"); + java.lang.System.setProperty("java.net.preferIPv4Stack", "true"); + + final ArrayList entries = new ArrayList(); + final ArrayList entriesValues = new ArrayList(); + + //entries.add("localhost:6600"); + //entriesValues.add("127.0.0.1:5500"); + + Preference.OnPreferenceChangeListener mChangeListener = new Preference.OnPreferenceChangeListener() { + public boolean onPreferenceChange(Preference preference, Object newValue) { + Log.d(TAG, "set new value to "+newValue); + String splitstr[] = newValue.toString().split(":"); + prefHost.setText(splitstr[0]); + prefPort.setText(splitstr[1]); + return true; + } + }; + + final ListPreference prefMPDServer = new ListPreference(context); + prefMPDServer.setTitle("Searching for MPD Servers"); + prefMPDServer.setSummary(""); + prefMPDServer.setDialogTitle("NO MPD Servers Found"); + prefMPDServer.setEntries(entries.toArray(new CharSequence[]{})); + prefMPDServer.setEntryValues(entriesValues.toArray(new CharSequence[]{})); + prefMPDServer.setKey(keyPrefix + "nsdhostandport"); + + prefMPDServer.setOnPreferenceChangeListener(mChangeListener); + + Handler mUpdateHandler = new Handler() { + @Override + public void handleMessage(Message msg) { + try { + InetAddress serviceaddress = InetAddress.getByAddress(msg.getData().getByteArray("address")); + Integer serviceport = msg.getData().getInt("port"); + String servicename = msg.getData().getString("name") + ":" + serviceport.toString(); + Log.d(TAG, "found mpd server "+servicename+" at "+serviceaddress); + for (int i = 0; i < entries.size(); ) { + if (servicename.equals(entries.get(i))) { + entries.remove(i); + entriesValues.remove(i); + } + else { + i++; + } + } + prefMPDServer.setTitle("Found MPD Servers on Local Network"); + prefMPDServer.setSummary("Touch to see MPD servers"); + prefMPDServer.setDialogTitle("MPD Servers"); + entries.add(servicename); + entriesValues.add(serviceaddress.getHostAddress() + ":" + serviceport.toString()); + prefMPDServer.setEntries(entries.toArray(new CharSequence[]{})); + prefMPDServer.setEntryValues(entriesValues.toArray(new CharSequence[]{})); + } catch (UnknownHostException e) { + Log.d(TAG, "resolving mpd server failed: "+e); + } + } + }; + + mNsdHelper = new NsdHelper(context, mUpdateHandler); + mNsdHelper.initializeNsd(); + mNsdHelper.discoverServices("_mpd._tcp"); + + // This sends a low-level DNS query to the mDNS daemon on the Android device. + // Unfortunately, the Android NsdManager doesn't seem to wake up properly + // (at least not up through 8.0.0), so a low-level query to the system's + // mDNS daemon is in order. + final NetThread mnetThread = new NetThread(context, "_mpd._tcp.local"); + mnetThread.start(); + + return prefMPDServer; + } + private void createDynamicSettings(final String keyPrefix, final PreferenceCategory toCategory) { - final EditTextPreference prefHost = new EditTextPreference(this); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + final ListPreference prefMPDServer = getMPDServer(this, keyPrefix); + toCategory.addPreference(prefMPDServer); + } + + prefHost = new EditTextPreference(this); prefHost.getEditText().setInputType( InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_URI); prefHost.setDialogTitle(R.string.host); @@ -47,7 +154,7 @@ private void createDynamicSettings(final String keyPrefix, prefHost.setKey(keyPrefix + "hostname"); toCategory.addPreference(prefHost); - final EditTextPreference prefPort = new EditTextPreference(this); + prefPort = new EditTextPreference(this); prefPort.getEditText().setInputType(InputType.TYPE_CLASS_NUMBER); prefPort.setDialogTitle(R.string.port); prefPort.setTitle(R.string.port); @@ -128,6 +235,16 @@ protected void onCreate(final Bundle savedInstanceState) { } } + @Override + protected void onDestroy() { + if (mNsdHelper != null) { + mNsdHelper.stopDiscovery(); + mNsdHelper.tearDown(); + mNsdHelper = null; + } + super.onDestroy(); + } + @Override public boolean onCreateOptionsMenu(final Menu menu) { final boolean result = super.onCreateOptionsMenu(menu);