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 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
     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 
     72         public String getNameString() {
     73             return String.format("\"%s\" (%s)", this.name,
     74                     this.leds == null ? "" : this.leds.getNameString());
     75         }
     76 
     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 
     90         public boolean hasConfigurationType() {
     91             return configurationType != 0;
     92         }
     93     }
     94 
     95     public static class Listener {
     96         public void onScanningStarted() {
     97         }
     98         public void onScanningStopped(ArrayList<Device> devices) {
     99         }
    100         public void onDeviceAdded(Device device) {
    101         }
    102         public void onDeviceChanged(Device device) {
    103         }
    104         public void onDeviceRemoved(Device device) {
    105         }
    106     }
    107 
    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      */
    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      */
    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      */
    141     public static void scanNow() {
    142         if (sReceiver != null) {
    143             sReceiver.scanNow();
    144         }
    145     }
    146 
    147     public static void stopNow() {
    148         if (sReceiver != null) {
    149             sReceiver.stopNow();
    150         }
    151     }
    152 
    153     public static void removeDevice(Device device) {
    154         removeDevice(device.address);
    155     }
    156 
    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 
    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 
    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          */
    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 
    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 
    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 
    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 
    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
    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 
    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 
    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 
    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 
    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 
    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