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