1 /* 2 * Copyright (C) 2014 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.settings.util.bluetooth; 18 19 import java.util.ArrayList; 20 21 import android.bluetooth.BluetoothAdapter; 22 import android.bluetooth.BluetoothDevice; 23 import android.content.BroadcastReceiver; 24 import android.content.Context; 25 import android.content.Intent; 26 import android.content.IntentFilter; 27 import android.os.Handler; 28 import android.util.Log; 29 30 /** 31 * Listens for unconfigured or problematic devices to show up on 32 * bluetooth and returns lists of them. Also manages their colors. 33 */ 34 public class BluetoothScanner { 35 private static final String TAG = "BluetoothScanner"; 36 private static final boolean DEBUG = false; 37 38 private static final int FOUND_ON_SCAN = -1; 39 private static final int CONSECUTIVE_MISS_THRESHOLD = 4; 40 private static final int FAILED_SETTING_NAME = CONSECUTIVE_MISS_THRESHOLD + 1; 41 private static final int SCAN_DELAY = 4000; 42 43 private static Receiver sReceiver; 44 45 public static class Device { 46 public BluetoothDevice btDevice; 47 public String address; 48 public String btName; 49 public String name = ""; 50 public LedConfiguration leds; 51 public int consecutiveMisses; 52 // the type of configuration this device needs, or -1 if the device does not 53 // specify a configuration type 54 public int configurationType = 0; 55 56 @Override 57 public String toString() { 58 StringBuilder str = new StringBuilder(); 59 str.append("Device(addr="); 60 str.append(address); 61 str.append(" name=\""); 62 str.append(name); 63 str.append("\" leds="); 64 str.append(leds); 65 str.append("\" configuration_type="); 66 str.append(configurationType); 67 str.append(")"); 68 return str.toString(); 69 } 70 71 public String getNameString() { 72 return String.format("\"%s\" (%s)", this.name, 73 this.leds == null ? "" : this.leds.getNameString()); 74 } 75 76 public boolean setNameString(String str) { 77 this.btName = str; 78 if (str == null || !BluetoothNameUtils.isValidName(str)) { 79 this.name = ""; 80 this.leds = null; 81 return false; 82 } 83 84 this.leds = BluetoothNameUtils.getColorConfiguration(str); 85 this.configurationType = BluetoothNameUtils.getSetupType(str); 86 return true; 87 } 88 89 public boolean hasConfigurationType() { 90 return configurationType != 0; 91 } 92 93 public boolean needsPlaceSetup() { 94 if (!hasConfigurationType() || 95 (configurationType & PairingUtils.TASK_SETUP_PLACE) == 96 PairingUtils.TASK_SETUP_PLACE) { 97 return true; 98 } else { 99 return false; 100 } 101 } 102 103 /** 104 * Note this is not 100% conclusive. There was a brief period when a device 105 * may require network setup, but not specify it. With these devices we can get 106 * more confidence if we check if the device appears in a list of devices 107 * belonging to a Place. If so we can assume this device requires only 108 * network setup. 109 * @return 110 */ 111 public boolean needsNetworkSetup() { 112 if (hasConfigurationType() && 113 (configurationType & PairingUtils.TASK_SETUP_NETWORK) == 114 PairingUtils.TASK_SETUP_NETWORK) { 115 return true; 116 } else { 117 return false; 118 } 119 } 120 } 121 122 public static class Listener { 123 public void onScanningStarted() { 124 } 125 public void onScanningStopped(ArrayList<Device> devices) { 126 } 127 public void onDeviceAdded(Device device) { 128 } 129 public void onDeviceChanged(Device device) { 130 } 131 public void onDeviceRemoved(Device device) { 132 } 133 } 134 135 private BluetoothScanner() { 136 throw new RuntimeException("do not instantiate"); 137 } 138 139 /** 140 * Starts listening. Will call onto listener with any devices we have 141 * cached before this call returns. 142 */ 143 public static void startListening(Context context, Listener listener, 144 BluetoothDeviceCriteria criteria) { 145 if (sReceiver == null) { 146 sReceiver = new Receiver(context.getApplicationContext()); 147 } 148 sReceiver.startListening(listener, criteria); 149 Log.d(TAG, "startListening"); 150 } 151 152 /** 153 * Removes the listener now, so there will be no more callbacks, but 154 * leaves the scan running for 20 seconds to keep the cache warm just 155 * in case it's needed again. 156 */ 157 public static void stopListening(Listener listener) { 158 Log.d(TAG, "stopListening sReceiver=" + sReceiver); 159 if (sReceiver != null) { 160 sReceiver.stopListening(listener); 161 } 162 } 163 164 /** 165 * Initiates a scan right now. 166 */ 167 public static void scanNow() { 168 if (sReceiver != null) { 169 sReceiver.scanNow(); 170 } 171 } 172 173 public static void stopNow() { 174 if (sReceiver != null) { 175 sReceiver.stopNow(); 176 } 177 } 178 179 public static void removeDevice(Device device) { 180 removeDevice(device.address); 181 } 182 183 public static void removeDevice(String btAddress) { 184 if (sReceiver != null) { 185 sReceiver.removeDevice(btAddress); 186 } 187 } 188 189 private static class ClientRecord { 190 public Listener listener; 191 public ArrayList<Device> devices; 192 public BluetoothDeviceCriteria matcher; 193 194 public ClientRecord(Listener listener, BluetoothDeviceCriteria matcher) { 195 this.listener = listener; 196 devices = new ArrayList<Device>(); 197 this.matcher = matcher; 198 } 199 } 200 201 private static class Receiver extends BroadcastReceiver { 202 private final Handler mHandler = new Handler(); 203 // TODO mListenerLock should probably now protect mClients 204 private final ArrayList<ClientRecord> mClients = new ArrayList<ClientRecord>(); 205 private final ArrayList<Device> mPresentDevices = new ArrayList<Device>(); 206 private Context mContext; 207 private BluetoothAdapter mBtAdapter; 208 private static boolean mKeepScanning; 209 private boolean mRegistered = false; 210 private Object mListenerLock = new Object(); 211 212 public Receiver(Context context) { 213 mContext = context; 214 215 // Bluetooth 216 mBtAdapter = BluetoothAdapter.getDefaultAdapter(); 217 } 218 219 /** 220 * @param listener 221 * @param matcher Pattern matcher to determine whether this listener 222 * will be notified about changes in status of a discovered device. Note 223 * that the matcher is only run against the device when the device is 224 * first discovered, not each time it appears in scan results. Device 225 * properties are assumed to be stable. 226 */ 227 public void startListening(Listener listener, BluetoothDeviceCriteria matcher) { 228 int size = 0; 229 ClientRecord newClient = new ClientRecord(listener, matcher); 230 synchronized (mListenerLock) { 231 for (int ptr = mClients.size() - 1; ptr > -1; ptr--) { 232 if (mClients.get(ptr).listener == listener) { 233 throw new RuntimeException("Listener already registered: " + listener); 234 } 235 } 236 237 // Save this listener in the list 238 mClients.add(newClient); 239 size = mClients.size(); 240 241 } 242 // Register for broadcasts when a device is discovered 243 // and broadcasts when discovery has finished 244 if (size == 1) { 245 mPresentDevices.clear(); 246 IntentFilter filter = new IntentFilter(BluetoothDevice.ACTION_FOUND); 247 filter.addAction(BluetoothAdapter.ACTION_DISCOVERY_FINISHED); 248 mContext.registerReceiver(this, filter); 249 mRegistered = true; 250 } 251 252 // Keep retrying until we say stop 253 mKeepScanning = true; 254 255 // Call back with the ones we have already 256 final int N = mPresentDevices.size(); 257 for (int i=0; i<N; i++) { 258 Device target = mPresentDevices.get(i); 259 if (newClient.matcher.isMatchingDevice(target.btDevice)) { 260 newClient.devices.add(target); 261 newClient.listener.onDeviceAdded(target); 262 } 263 } 264 265 // If we have a pending stop, cancel that. 266 mHandler.removeCallbacks(mStopTask); 267 268 // If there is a pending scan, we'll do one now, so we can scan any 269 // pending ones. 270 mHandler.removeCallbacks(mScanTask); 271 272 scanNow(); 273 } 274 275 public void stopListening(Listener listener) { 276 int size = 0; 277 synchronized (mListenerLock) { 278 for (int ptr = mClients.size() - 1; ptr > -1; ptr--) { 279 ClientRecord client = mClients.get(ptr); 280 if (client.listener == listener) { 281 mClients.remove(ptr); 282 break; 283 } 284 } 285 size = mClients.size(); 286 } 287 if (size == 0) { 288 mHandler.removeCallbacks(mStopTask); 289 mHandler.postDelayed(mStopTask, 20 * 1000 /* ms */); 290 } 291 } 292 293 public void scanNow() { 294 // If we're already discovering, stop it. 295 if (mBtAdapter.isDiscovering()) { 296 mBtAdapter.cancelDiscovery(); 297 } 298 299 sendScanningStarted(); 300 301 // Request discover from BluetoothAdapter 302 mBtAdapter.startDiscovery(); 303 } 304 305 public void stopNow() { 306 int size = 0; 307 synchronized (mListenerLock) { 308 size = mClients.size(); 309 } 310 if (size == 0) { 311 Log.d(TAG, "mStopTask.run()"); 312 313 // cancel any pending scans 314 mHandler.removeCallbacks(mScanTask); 315 316 // If there is a pending stop, cancel it 317 mHandler.removeCallbacks(mStopTask); 318 319 // Make sure we're not doing discovery anymore 320 if (mBtAdapter != null) { 321 mBtAdapter.cancelDiscovery(); 322 } 323 324 // shut down discovery and prevent it from restarting 325 mKeepScanning = false; 326 327 // if the Bluetooth adapter is enabled, we're listening for discovery events and 328 // should stop 329 if (BluetoothAdapter.getDefaultAdapter().isEnabled() && mRegistered) { 330 mContext.unregisterReceiver(Receiver.this); 331 mRegistered = false; 332 } 333 } 334 } 335 336 public void removeDevice(String btAddress) { 337 int count = mPresentDevices.size(); 338 for (int i = 0; i < count; i++) { 339 Device d = mPresentDevices.get(i); 340 if (btAddress.equals(d.address)) { 341 mPresentDevices.remove(d); 342 break; 343 } 344 } 345 346 for (int ptr = mClients.size() - 1; ptr > -1; ptr--) { 347 ClientRecord client = mClients.get(ptr); 348 for (int devPtr = client.devices.size() - 1; devPtr > -1; devPtr--) { 349 Device d = client.devices.get(devPtr); 350 if (btAddress.equals(d.address)) { 351 client.devices.remove(devPtr); 352 break; 353 } 354 } 355 } 356 } 357 358 private final Runnable mStopTask = new Runnable() { 359 @Override 360 public void run() { 361 synchronized (mListenerLock) { 362 if (mClients.size() != 0) { 363 throw new RuntimeException("mStopTask running with mListeners.size=" 364 + mClients.size()); 365 } 366 } 367 stopNow(); 368 } 369 }; 370 371 private final Runnable mScanTask = new Runnable() { 372 @Override 373 public void run() { 374 // If there is a pending scan request, cancel it 375 mHandler.removeCallbacks(mScanTask); 376 377 scanNow(); 378 } 379 }; 380 381 @Override 382 public void onReceive(Context context, Intent intent) { 383 final String action = intent.getAction(); 384 385 if (BluetoothDevice.ACTION_FOUND.equals(action)) { 386 387 // When discovery finds a device 388 389 // Get the BluetoothDevice object from the Intent 390 BluetoothDevice btDevice = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); 391 final String address = btDevice.getAddress(); 392 String name = btDevice.getName(); 393 394 if (DEBUG) { 395 Log.d(TAG, "Device found, address: " + address + " name: \"" + name + "\""); 396 } 397 398 if (address == null || name == null) { 399 return; 400 } 401 402 // Older Bluetooth stacks may append a null character to a device name 403 if (name.endsWith("\0")) { 404 name = name.substring(0, name.length() - 1); 405 } 406 407 // See if this is a device we already know about 408 Device device = null; 409 final int N = mPresentDevices.size(); 410 for (int i=0; i<N; i++) { 411 final Device d = mPresentDevices.get(i); 412 if (address.equals(d.address)) { 413 device = d; 414 break; 415 } 416 } 417 418 if (device == null) { 419 if (DEBUG) { 420 Log.d(TAG, "Device is a new device."); 421 } 422 // New device. 423 device = new Device(); 424 device.btDevice = btDevice; 425 device.address = address; 426 device.consecutiveMisses = -1; 427 428 device.setNameString(name); 429 // Save it 430 mPresentDevices.add(device); 431 432 // Tell the listeners 433 sendDeviceAdded(device); 434 } else { 435 if (DEBUG) { 436 Log.d(TAG, "Device is an existing device."); 437 } 438 // Existing device: update miss count. 439 device.consecutiveMisses = FOUND_ON_SCAN; 440 if (device.btName == name 441 || (device.btName != null && device.btName.equals(name))) { 442 // Name hasn't changed 443 return; 444 } else { 445 device.setNameString(name); 446 sendDeviceChanged(device); 447 // If we can't parse it properly, treat it as a delete 448 // when we iterate through them again. 449 } 450 } 451 } else if (BluetoothAdapter.ACTION_DISCOVERY_FINISHED.equals(action)) { 452 // Clear any devices that have disappeared since the last scan completed 453 final int N = mPresentDevices.size(); 454 for (int i=N-1; i>=0; i--) { 455 Device device = mPresentDevices.get(i); 456 if (device.consecutiveMisses < 0) { 457 // -1 means found on this scan, raise to 0 for next time 458 if (DEBUG) Log.d(TAG, device.address + " -- Found"); 459 device.consecutiveMisses = 0; 460 461 } else if (device.consecutiveMisses >= CONSECUTIVE_MISS_THRESHOLD) { 462 // Too many failures 463 if (DEBUG) Log.d(TAG, device.address + " -- Removing"); 464 mPresentDevices.remove(i); 465 sendDeviceRemoved(device); 466 467 } else { 468 // Didn't see it this time, but not ready to delete it yet 469 device.consecutiveMisses++; 470 if (DEBUG) { 471 Log.d(TAG, device.address + " -- Missed consecutiveMisses=" 472 + device.consecutiveMisses); 473 } 474 } 475 } 476 477 // Show status when scanning is completed. 478 sendScanningStopped(); 479 480 if (mKeepScanning) { 481 // Try again in SCAN_DELAY ms. 482 mHandler.postDelayed(mScanTask, SCAN_DELAY); 483 } 484 } 485 } 486 487 private void sendScanningStarted() { 488 synchronized (mListenerLock) { 489 final int N = mClients.size(); 490 for (int i = 0; i < N; i++) { 491 mClients.get(i).listener.onScanningStarted(); 492 } 493 } 494 } 495 496 private void sendScanningStopped() { 497 synchronized (mListenerLock) { 498 final int N = mClients.size(); 499 // Loop backwards through the list in case a client wants to 500 // remove its listener in this callback. 501 for (int i = N - 1; i >= 0; --i) { 502 ClientRecord client = mClients.get(i); 503 client.listener.onScanningStopped(client.devices); 504 } 505 } 506 } 507 508 private void sendDeviceAdded(Device device) { 509 synchronized (mListenerLock) { 510 for (int ptr = mClients.size() - 1; ptr > -1; ptr--) { 511 ClientRecord client = mClients.get(ptr); 512 if (client.matcher.isMatchingDevice(device.btDevice)) { 513 client.devices.add(device); 514 client.listener.onDeviceAdded(device); 515 } 516 } 517 } 518 } 519 520 private void sendDeviceChanged(Device device) { 521 synchronized (mListenerLock) { 522 final int N = mClients.size(); 523 for (int i = 0; i < N; i++) { 524 ClientRecord client = mClients.get(i); 525 for (int ptr = client.devices.size() - 1; ptr > -1; ptr--) { 526 Device d = client.devices.get(ptr); 527 if (d.btDevice.getAddress().equals(device.btDevice.getAddress())) { 528 client.listener.onDeviceChanged(device); 529 break; 530 } 531 } 532 } 533 } 534 } 535 536 private void sendDeviceRemoved(Device device) { 537 synchronized (mListenerLock) { 538 for (int ptr = mClients.size() - 1; ptr > -1; ptr--) { 539 ClientRecord client = mClients.get(ptr); 540 for (int devPtr = client.devices.size() - 1; devPtr > -1; devPtr--) { 541 Device d = client.devices.get(devPtr); 542 if (d.btDevice.getAddress().equals(device.btDevice.getAddress())) { 543 client.devices.remove(devPtr); 544 client.listener.onDeviceRemoved(device); 545 break; 546 } 547 } 548 } 549 } 550 } 551 } 552 } 553