1 /*
2  * Copyright (C) 2017 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.tv.tuner.hdhomerun;
18 
19 import android.support.annotation.NonNull;
20 import android.util.Log;
21 import android.util.Pair;
22 import java.io.IOException;
23 import java.net.DatagramPacket;
24 import java.net.DatagramSocket;
25 import java.net.InetAddress;
26 import java.net.InterfaceAddress;
27 import java.net.NetworkInterface;
28 import java.net.SocketException;
29 import java.nio.ByteBuffer;
30 import java.util.ArrayList;
31 import java.util.Arrays;
32 import java.util.Collections;
33 import java.util.Enumeration;
34 import java.util.Iterator;
35 import java.util.List;
36 
37 /** A class to discover HDHomeRun devices on the network with UDP broadcasting. */
38 class HdHomeRunDiscover {
39     private static final String TAG = "HdHomeRunDiscover";
40     private static final boolean DEBUG = false;
41 
42     private static final int HDHOMERUN_DISCOVER_MAX_SOCK_COUNT = 16;
43     private static final int HDHOMERUN_DISCOVER_RETRY_LIMIT = 2;
44     private static final int HDHOMERUN_DISCOVER_TIMEOUT_MS = 500;
45     private static final int HDHOMERUN_DISCOVER_RECEIVE_WAITE_TIME_MS = 10;
46 
47     private List<HdHomeRunDiscoverSocket> mSockets = new ArrayList<>();
48 
49     /** Creates a discover object. If cannot add a default socket, return {@code null}. */
create()50     static HdHomeRunDiscover create() {
51         HdHomeRunDiscover hdHomeRunDiscover = new HdHomeRunDiscover();
52         // Create a routable socket (always first entry).
53         if (!hdHomeRunDiscover.addSocket(0, 0)) {
54             return null;
55         }
56         return hdHomeRunDiscover;
57     }
58 
59     /** Closes and releases all sockets required by this discover object. */
close()60     void close() {
61         for (HdHomeRunDiscoverSocket discoverSocket : mSockets) {
62             discoverSocket.close();
63         }
64     }
65 
66     /** Finds HDHomeRun devices. */
67     @NonNull
findDevices( int targetIp, int deviceType, int deviceId, int maxCount)68     List<HdHomeRunDiscoverDevice> findDevices(
69             int targetIp, int deviceType, int deviceId, int maxCount) {
70         List<HdHomeRunDiscoverDevice> resultList = new ArrayList<>();
71         resetLocalIpSockets();
72         for (int retry = 0;
73                 retry < HDHOMERUN_DISCOVER_RETRY_LIMIT && resultList.isEmpty();
74                 retry++) {
75             int localIpSent = send(targetIp, deviceType, deviceId);
76             if (localIpSent == 0) {
77                 if (DEBUG) {
78                     Log.d(TAG, "Cannot send to target ip: " + HdHomeRunUtils.getIpString(targetIp));
79                 }
80                 continue;
81             }
82             long timeout = System.currentTimeMillis() + HDHOMERUN_DISCOVER_TIMEOUT_MS * localIpSent;
83             while (System.currentTimeMillis() < timeout) {
84                 HdHomeRunDiscoverDevice result = new HdHomeRunDiscoverDevice();
85                 if (!receive(result)) {
86                     continue;
87                 }
88                 // Filter.
89                 if (deviceType != HdHomeRunUtils.HDHOMERUN_DEVICE_TYPE_WILDCARD
90                         && deviceType != result.mDeviceType) {
91                     continue;
92                 }
93                 if (deviceId != HdHomeRunUtils.HDHOMERUN_DEVICE_ID_WILDCARD
94                         && deviceId != result.mDeviceId) {
95                     continue;
96                 }
97                 if (isObsoleteDevice(deviceId)) {
98                     continue;
99                 }
100                 // Ensure not already in list.
101                 if (resultList.contains(result)) {
102                     continue;
103                 }
104                 // Add to list.
105                 resultList.add(result);
106                 if (resultList.size() >= maxCount) {
107                     break;
108                 }
109             }
110         }
111         return resultList;
112     }
113 
addSocket(int localIp, int subnetMask)114     private boolean addSocket(int localIp, int subnetMask) {
115         for (int i = 1; i < mSockets.size(); i++) {
116             HdHomeRunDiscoverSocket discoverSocket = mSockets.get(i);
117             if ((discoverSocket.mLocalIp == localIp)
118                     && (discoverSocket.mSubnetMask == subnetMask)) {
119                 discoverSocket.mDetected = true;
120                 return true;
121             }
122         }
123         if (mSockets.size() >= HDHOMERUN_DISCOVER_MAX_SOCK_COUNT) {
124             return false;
125         }
126         DatagramSocket socket;
127         try {
128             socket = new DatagramSocket(0, HdHomeRunUtils.intToAddress(localIp));
129             socket.setBroadcast(true);
130         } catch (IOException e) {
131             if (DEBUG) Log.d(TAG, "Cannot create socket: " + HdHomeRunUtils.getIpString(localIp));
132             return false;
133         }
134         // Write socket entry.
135         mSockets.add(new HdHomeRunDiscoverSocket(socket, true, localIp, subnetMask));
136         return true;
137     }
138 
resetLocalIpSockets()139     private void resetLocalIpSockets() {
140         for (int i = 1; i < mSockets.size(); i++) {
141             mSockets.get(i).mDetected = false;
142             mSockets.get(i).mDiscoverPacketSent = false;
143         }
144         List<LocalIpInfo> ipInfoList = getLocalIpInfo(HDHOMERUN_DISCOVER_MAX_SOCK_COUNT);
145         for (LocalIpInfo ipInfo : ipInfoList) {
146             if (DEBUG) {
147                 Log.d(
148                         TAG,
149                         "Add local IP: "
150                                 + HdHomeRunUtils.getIpString(ipInfo.mIpAddress)
151                                 + ", "
152                                 + HdHomeRunUtils.getIpString(ipInfo.mSubnetMask));
153             }
154             addSocket(ipInfo.mIpAddress, ipInfo.mSubnetMask);
155         }
156         Iterator<HdHomeRunDiscoverSocket> iterator = mSockets.iterator();
157         while (iterator.hasNext()) {
158             HdHomeRunDiscoverSocket discoverSocket = iterator.next();
159             if (!discoverSocket.mDetected) {
160                 discoverSocket.close();
161                 iterator.remove();
162             }
163         }
164     }
165 
getLocalIpInfo(int maxCount)166     private List<LocalIpInfo> getLocalIpInfo(int maxCount) {
167         Enumeration<NetworkInterface> interfaces;
168         try {
169             interfaces = NetworkInterface.getNetworkInterfaces();
170         } catch (SocketException e) {
171             return Collections.emptyList();
172         }
173         List<LocalIpInfo> result = new ArrayList<>();
174         while (interfaces.hasMoreElements()) {
175             NetworkInterface networkInterface = interfaces.nextElement();
176             for (InterfaceAddress interfaceAddress : networkInterface.getInterfaceAddresses()) {
177                 InetAddress inetAddress = interfaceAddress.getAddress();
178                 if (!inetAddress.isAnyLocalAddress()
179                         && !inetAddress.isLinkLocalAddress()
180                         && !inetAddress.isLoopbackAddress()
181                         && !inetAddress.isMulticastAddress()) {
182                     LocalIpInfo localIpInfo = new LocalIpInfo();
183                     localIpInfo.mIpAddress = HdHomeRunUtils.addressToInt(inetAddress.getAddress());
184                     localIpInfo.mSubnetMask =
185                             (0x7fffffff >> (31 - interfaceAddress.getNetworkPrefixLength()));
186                     result.add(localIpInfo);
187                     if (result.size() >= maxCount) {
188                         return result;
189                     }
190                 }
191             }
192         }
193         return result;
194     }
195 
send(int targetIp, int deviceType, int deviceId)196     private int send(int targetIp, int deviceType, int deviceId) {
197         return targetIp == 0
198                 ? sendWildcardIp(deviceType, deviceId)
199                 : sendTargetIp(targetIp, deviceType, deviceId);
200     }
201 
sendWildcardIp(int deviceType, int deviceId)202     private int sendWildcardIp(int deviceType, int deviceId) {
203         int localIpSent = 0;
204 
205         // Send subnet broadcast using each local ip socket.
206         // This will work with multiple separate 169.254.x.x interfaces.
207         for (int i = 1; i < mSockets.size(); i++) {
208             HdHomeRunDiscoverSocket discoverSocket = mSockets.get(i);
209             int targetIp = discoverSocket.mLocalIp | ~discoverSocket.mSubnetMask;
210             if (DEBUG) Log.d(TAG, "Send: " + HdHomeRunUtils.getIpString(targetIp));
211             localIpSent += discoverSocket.send(targetIp, deviceType, deviceId) ? 1 : 0;
212         }
213         // If no local ip sockets then fall back to sending a global broadcast letting
214         // the OS choose the interface.
215         if (localIpSent == 0) {
216             if (DEBUG) Log.d(TAG, "Send: " + HdHomeRunUtils.getIpString(0xFFFFFFFF));
217             localIpSent = mSockets.get(0).send(0xFFFFFFFF, deviceType, deviceId) ? 1 : 0;
218         }
219         return localIpSent;
220     }
221 
sendTargetIp(int targetIp, int deviceType, int deviceId)222     private int sendTargetIp(int targetIp, int deviceType, int deviceId) {
223         int localIpSent = 0;
224 
225         // Send targeted packet from any local ip that is in the same subnet.
226         // This will work with multiple separate 169.254.x.x interfaces.
227         for (int i = 1; i < mSockets.size(); i++) {
228             HdHomeRunDiscoverSocket discoverSocket = mSockets.get(i);
229             if (discoverSocket.mSubnetMask == 0) {
230                 continue;
231             }
232             if ((targetIp & discoverSocket.mSubnetMask)
233                     != (discoverSocket.mLocalIp & discoverSocket.mSubnetMask)) {
234                 continue;
235             }
236             localIpSent += discoverSocket.send(targetIp, deviceType, deviceId) ? 1 : 0;
237         }
238         // If target IP does not match a local subnet then fall back to letting the OS choose
239         // the gateway interface.
240         if (localIpSent == 0) {
241             localIpSent = mSockets.get(0).send(targetIp, deviceType, deviceId) ? 1 : 0;
242         }
243         return localIpSent;
244     }
245 
receive(HdHomeRunDiscoverDevice result)246     private boolean receive(HdHomeRunDiscoverDevice result) {
247         for (HdHomeRunDiscoverSocket discoverSocket : mSockets) {
248             if (discoverSocket.mDiscoverPacketSent && discoverSocket.receive(result)) {
249                 return true;
250             }
251         }
252         return false;
253     }
254 
isObsoleteDevice(int deviceId)255     private boolean isObsoleteDevice(int deviceId) {
256         switch (deviceId >> 20) {
257             case 0x100: /* TECH-US/TECH3-US */
258                 return (deviceId < 0x10040000);
259             case 0x120: /* TECH3-EU */
260                 return (deviceId < 0x12030000);
261             case 0x101: /* HDHR-US */
262             case 0x102: /* HDHR-T1-US */
263             case 0x103: /* HDHR3-US */
264             case 0x111: /* HDHR3-DT */
265             case 0x121: /* HDHR-EU */
266             case 0x122: /* HDHR3-EU */
267                 return true;
268             default:
269                 return false;
270         }
271     }
272 
273     static class HdHomeRunDiscoverDevice {
274         int mIpAddress;
275         int mDeviceType;
276         int mDeviceId;
277         int mTunerCount;
278         String mBaseUrl;
279 
280         @Override
equals(Object other)281         public boolean equals(Object other) {
282             if (this == other) {
283                 return true;
284             } else if (other instanceof HdHomeRunDiscoverDevice) {
285                 HdHomeRunDiscoverDevice o = (HdHomeRunDiscoverDevice) other;
286                 return mIpAddress == o.mIpAddress
287                         && mDeviceType == o.mDeviceType
288                         && mDeviceId == o.mDeviceId;
289             }
290             return false;
291         }
292 
293         @Override
hashCode()294         public int hashCode() {
295             int result = mIpAddress;
296             result = 31 * result + mDeviceType;
297             result = 31 * result + mDeviceId;
298             return result;
299         }
300     }
301 
302     private static class HdHomeRunDiscoverSocket {
303         DatagramSocket mSocket;
304         boolean mDetected;
305         boolean mDiscoverPacketSent;
306         int mLocalIp;
307         int mSubnetMask;
308 
HdHomeRunDiscoverSocket( DatagramSocket socket, boolean detected, int localIp, int subnetMask)309         private HdHomeRunDiscoverSocket(
310                 DatagramSocket socket, boolean detected, int localIp, int subnetMask) {
311             mSocket = socket;
312             mDetected = detected;
313             mLocalIp = localIp;
314             mSubnetMask = subnetMask;
315         }
316 
send(int targetIp, int deviceType, int deviceId)317         private boolean send(int targetIp, int deviceType, int deviceId) {
318             byte[] data = new byte[12];
319             ByteBuffer buffer = ByteBuffer.wrap(data);
320             buffer.put(HdHomeRunUtils.HDHOMERUN_TAG_DEVICE_TYPE);
321             buffer.put((byte) 4);
322             buffer.putInt(deviceType);
323             buffer.put(HdHomeRunUtils.HDHOMERUN_TAG_DEVICE_ID);
324             buffer.put((byte) 4);
325             buffer.putInt(deviceId);
326             data = HdHomeRunUtils.sealFrame(data, HdHomeRunUtils.HDHOMERUN_TYPE_DISCOVER_REQUEST);
327             try {
328                 DatagramPacket packet =
329                         new DatagramPacket(
330                                 data,
331                                 data.length,
332                                 HdHomeRunUtils.intToAddress(targetIp),
333                                 HdHomeRunUtils.HDHOMERUN_DISCOVER_UDP_PORT);
334                 mSocket.send(packet);
335                 if (DEBUG) {
336                     Log.d(TAG, "Discover packet sent to: " + HdHomeRunUtils.getIpString(targetIp));
337                 }
338                 mDiscoverPacketSent = true;
339             } catch (IOException e) {
340                 if (DEBUG) {
341                     Log.d(
342                             TAG,
343                             "Cannot send discover packet to socket("
344                                     + HdHomeRunUtils.getIpString(mLocalIp)
345                                     + ")");
346                 }
347                 mDiscoverPacketSent = false;
348             }
349             return mDiscoverPacketSent;
350         }
351 
receive(HdHomeRunDiscoverDevice result)352         private boolean receive(HdHomeRunDiscoverDevice result) {
353             DatagramPacket packet = new DatagramPacket(new byte[3074], 3074);
354             try {
355                 mSocket.setSoTimeout(HDHOMERUN_DISCOVER_RECEIVE_WAITE_TIME_MS);
356                 mSocket.receive(packet);
357                 if (DEBUG) Log.d(TAG, "Received packet, size: " + packet.getLength());
358             } catch (IOException e) {
359                 if (DEBUG) {
360                     Log.d(
361                             TAG,
362                             "Cannot receive from socket("
363                                     + HdHomeRunUtils.getIpString(mLocalIp)
364                                     + ")");
365                 }
366                 return false;
367             }
368 
369             Pair<Short, byte[]> data =
370                     HdHomeRunUtils.openFrame(packet.getData(), packet.getLength());
371             if (data == null
372                     || data.first == null
373                     || data.first != HdHomeRunUtils.HDHOMERUN_TYPE_DISCOVER_REPLY) {
374                 if (DEBUG) Log.d(TAG, "Ill-formed packet: " + Arrays.toString(packet.getData()));
375                 return false;
376             }
377             result.mIpAddress = HdHomeRunUtils.addressToInt(packet.getAddress().getAddress());
378             if (DEBUG) {
379                 Log.d(TAG, "Get Device IP: " + HdHomeRunUtils.getIpString(result.mIpAddress));
380             }
381             ByteBuffer buffer = ByteBuffer.wrap(data.second);
382             while (true) {
383                 Pair<Byte, byte[]> tagAndValue = HdHomeRunUtils.readTaggedValue(buffer);
384                 if (tagAndValue == null) {
385                     break;
386                 }
387                 switch (tagAndValue.first) {
388                     case HdHomeRunUtils.HDHOMERUN_TAG_DEVICE_TYPE:
389                         if (tagAndValue.second.length != 4) {
390                             break;
391                         }
392                         result.mDeviceType = ByteBuffer.wrap(tagAndValue.second).getInt();
393                         if (DEBUG) Log.d(TAG, "Get Device Type: " + result.mDeviceType);
394                         break;
395                     case HdHomeRunUtils.HDHOMERUN_TAG_DEVICE_ID:
396                         if (tagAndValue.second.length != 4) {
397                             break;
398                         }
399                         result.mDeviceId = ByteBuffer.wrap(tagAndValue.second).getInt();
400                         if (DEBUG) Log.d(TAG, "Get Device ID: " + result.mDeviceId);
401                         break;
402                     case HdHomeRunUtils.HDHOMERUN_TAG_TUNER_COUNT:
403                         if (tagAndValue.second.length != 1) {
404                             break;
405                         }
406                         result.mTunerCount = tagAndValue.second[0];
407                         if (DEBUG) Log.d(TAG, "Get Tuner Count: " + result.mTunerCount);
408                         break;
409                     case HdHomeRunUtils.HDHOMERUN_TAG_BASE_URL:
410                         result.mBaseUrl = new String(tagAndValue.second);
411                         if (DEBUG) Log.d(TAG, "Get Base URL: " + result.mBaseUrl);
412                         break;
413                     default:
414                         break;
415                 }
416             }
417             // Fixup for old firmware.
418             if (result.mTunerCount == 0) {
419                 switch (result.mDeviceId >> 20) {
420                     case 0x102:
421                         result.mTunerCount = 1;
422                         break;
423                     case 0x100:
424                     case 0x101:
425                     case 0x121:
426                         result.mTunerCount = 2;
427                         break;
428                     default:
429                         break;
430                 }
431             }
432             return true;
433         }
434 
close()435         private void close() {
436             if (mSocket != null) {
437                 mSocket.close();
438             }
439         }
440     }
441 
442     private static class LocalIpInfo {
443         int mIpAddress;
444         int mSubnetMask;
445     }
446 }
447