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