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