Home | History | Annotate | Download | only in bluetooth
      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