22

I use a LeScanCallback (can not use the newer scan methods because I'm developing for api 18. Not that it matters, since the android 5.0+ apis don't offer this functionality either) to detect when a nearby BLE device is detected:

private BluetoothAdapter.LeScanCallback bleCallback = new BluetoothAdapter.LeScanCallback() {

    @Override
    public void onLeScan(BluetoothDevice bluetoothDevice, int i, byte[] bytes) {
        discoveredDevices.add(bluetoothDevice);
    }
};

I am not pairing or connecting with the devices because that's not required, I simply want to see which devices are nearby.

I'm trying to make a service that, every 5 mins or so, calls a webserver to update which devices are nearby at that moment.

Tricky part is that the android device will be moving, so a bluetooth device that is nearby right now, might not be in 5 mins. In that case I need to remove it from discoveredDevices.

Ideally, I would like to receive a callback when a bluetooth device was in range before, but is not anymore. This callback doesn't exist though.

(I'm aware of the android.bluetooth.device.action.ACL_CONNECTED and android.bluetooth.device.action.ACL_DISCONNECTED broadcasts, but those are for when you connect to a bluetooth device, which I don't want.)

An option is to do a fresh scan every 5 mins, but you can't tell when all nearby devices have been discovered, so you would have to do a timed scan, e.g. scan for 5 seconds and then send the collected data to the webservice.
This sounds dirty and risky because you can never know for sure all nearby devices were discovered within the allotted time, so I would very much like to avoid doing it like that.

Is there another way to do this?


Edit
Some devices continuously report discovery of nearby bluetooth devices, even if they were already discovered before. If that functionality was universal I could solve my problem, however this is device specific.

My phone's bluetooth adapter for example only discovers nearby devices once. Some other devices I have tested with do continuously report the same nearby devices, but not all devices do, so I can't rely on that unfortunately.

Tim
  • 41,901
  • 18
  • 127
  • 145
  • Never scan on a loop, and set a time limit on your scan. A device that was previously available may have moved out of range, and continuing to scan drains the battery.User needs to connect manually... – Gopi Krishna Oct 09 '15 at 10:45
  • @GopiKrishna I'm not sure I understand what you're talking about. I don't want to scan with a time limit so I'm asking for other options – Tim Oct 09 '15 at 10:48
  • i had developed what you require, not the most clean way maybe, but didn't find better options. It can be done if you pair scans and corresponding device lists, considering if device 'a' is within proximity in the first and second scans, insert it into the final list being sent to the server, discard the ones that are not common to lists of both scans – Pararth Oct 21 '15 at 05:33
  • meaning if you need to send the data to server every 5 mins, can make it 2 scans in 5mins and one final list for the common devices from the two, this way it can be quite accurate – Pararth Oct 21 '15 at 05:35
  • @user2450263 I don't see the benefit of using a second list. Anyway that still requires a scan with a fixed time limit, no? – Tim Oct 21 '15 at 07:24
  • yes, you will need 2 scans to detect if a ble device is in range or not, because if the device is 'not-in-range' then there is no way to communicate anyway... so irony in circles with only one scan – Pararth Oct 21 '15 at 07:29
  • i didn't understand your edit in the question, what is the meaning of devices report nearby devices once or continuously ? don't think reporting -once or continuously- is possible without scanning... – Pararth Oct 21 '15 at 07:33
  • @user2450263 I'm not communicating with the devices, only reporting the presence of them. What you do with 2 scans can be done with 1 scan, by simply clearing the list of nearby devices after each scan. What I mean by "reporting" is the callback firing when a device has been scanned. – Tim Oct 21 '15 at 07:34
  • ok, i'm talking about reporting the devices which are nearby for a certain period of time.. that if done with 1 scan is just sending the device list to the server, with 2 scans in the same duration, if a few devices have moved out of range, they need not be included in what is sent to the server..that is one way to find devices not in range- from the mobile end.. otherwise the server can figure it out by comparing data it receives – Pararth Oct 21 '15 at 07:48

2 Answers2

19

This sounds dirty and risky because you can never know for sure all nearby devices were discovered within the allotted time, so I would very much like to avoid doing it like that.

That sounds like a reasonable assumption, but it's wrong.

Bluetooth low energy works in a particular way and BLE devices have some limits. For instance, they have a fixed range of possible advertising frequencies, ranging from 20 milliseconds to 10.24 seconds, in steps of 0.625 milliseconds. See here and here for more detailed information.

This means that it can take at most 10.24 seconds before a device will broadcast a new advertisement package. BLE devices generally, if not always, provide a way for their owner to adjust their advertising frequency, so the frequency can of course vary.

In cases where you are periodically collecting data about nearby devices, like yours, it is fine to use a scan with a fixed time limit, save that data somewhere, restart the scan, collect new data, compare with old data --> get results.

For example, if a device was found in scan 1 but not in scan 2, you can conclude that the device was in range, but is not anymore.
Same goes for the other way around: if a device was found in scan 4 but not in scan 3, it is a newly discovered device.
Finally, if a device was found in scan 5, was not found in scan 6, but was again found in scan 7, it is rediscovered and can be handled as such if need be.


Because I'm answering my own question here, I'll add the code that I used to implement this.

I have the scanning done in a background service, and communicate to other parts of the app using BroadcastReceivers. Asset is a custom class of mine that holds some data. DataManager is a custom class of mine that - how did you guess it - manages data.

public class BLEDiscoveryService extends Service {

    // Broadcast identifiers.
    public static final String EVENT_NEW_ASSET = "EVENT_NEW_ASSET ";
    public static final String EVENT_LOST_ASSET = "EVENT_LOST_ASSET ";

    private static Handler handler;
    private static final int BLE_SCAN_TIMEOUT = 11000; // 11 seconds

    // Lists to keep track of current and previous detected devices.
    // Used to determine which are in range and which are not anymore.
    private List<Asset> previouslyDiscoveredAssets;
    private List<Asset> currentlyDiscoveredAssets;

    private BluetoothAdapter bluetoothAdapter;

    private BluetoothAdapter.LeScanCallback BLECallback = new BluetoothAdapter.LeScanCallback() {

        @Override
        public void onLeScan(BluetoothDevice bluetoothDevice, int i, byte[] bytes) {

            Asset asset = DataManager.getAssetForMACAddress(bluetoothDevice.getAddress());
            handleDiscoveredAsset(asset);
        }
    };

    @Override
    public void onCreate() {
        super.onCreate();

        BluetoothManager manager = (BluetoothManager) getSystemService(BLUETOOTH_SERVICE);
        bluetoothAdapter = manager.getAdapter();

        previouslyDiscoveredAssets = new ArrayList<>();
        currentlyDiscoveredAssets = new ArrayList<>();

        handler = new Handler();
    }

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        // Start scanning.
        startBLEScan();

        // After a period of time, stop the current scan and start a new one.
        // This is used to detect when assets are not in range anymore.
        handler.postDelayed(new Runnable() {
            @Override
            public void run() {
                performRepeatingTask();

                // Repeat.
                handler.postDelayed(this, BLE_SCAN_TIMEOUT);
            }
        }, BLE_SCAN_TIMEOUT);

        // Service is not restarted if it gets terminated.
        return Service.START_NOT_STICKY;
    }

    @Override
    public IBinder onBind(Intent intent) {
        return null;
    }

    @Override
    public void onDestroy() {
        handler.removeCallbacksAndMessages(null);
        stopBLEScan();

        super.onDestroy();
    }

    private void startBLEScan() {
        bluetoothAdapter.startLeScan(BLECallback);
    }

    private void stopBLEScan() {
        bluetoothAdapter.stopLeScan(BLECallback);
    }

    private void handleDiscoveredAsset(Asset asset) {
        currentlyDiscoveredAssets.add(asset);

        // Notify observers that we have a new asset discovered, but only if it was not
        // discovered previously.
        if (currentlyDiscoveredAssets.contains(asset) &&
                !previouslyDiscoveredAssets.contains(asset)) {
            notifyObserversOfNewAsset(asset);
        }
    }

    private void performRepeatingTask() {
        // Check if a previously discovered asset is not discovered this scan round,
        // meaning it's not in range anymore.
        for (Asset asset : previouslyDiscoveredAssets) {
            if (!currentlyDiscoveredAssets.contains(asset)) {
                notifyObserversOfLostAsset(asset);
            }
        }

        // Update lists for a new round of scanning.
        previouslyDiscoveredAssets.clear();
        previouslyDiscoveredAssets.addAll(currentlyDiscoveredAssets);
        currentlyDiscoveredAssets.clear();

        // Reset the scan.
        stopBLEScan();
        startBLEScan();
    }

    private void notifyObserversOfNewAsset(Asset asset) {
        Intent intent = new Intent();
        intent.putExtra("macAddress", asset.MAC_address);
        intent.setAction(EVENT_NEW_ASSET);

        sendBroadcast(intent);
    }

    private void notifyObserversOfLostAsset(Asset asset) {
        Intent intent = new Intent();
        intent.putExtra("macAddress", asset.MAC_address);
        intent.setAction(EVENT_LOST_ASSET);      

        sendBroadcast(intent);
    }
}

This code is not perfect and might even be buggy, but it will at least give you an idea or example of how this can be implemented.

Community
  • 1
  • 1
Tim
  • 41,901
  • 18
  • 127
  • 145
2

I can recommend this approach:

Use Map<BluetoothDevice, Long> structure to store the discovered devices, where Long is the time of detection of the device (can be System.currentTimeMillis() for example).

Then in your service (as far as I understand from the question there will be implemented some kind of repeated task) just extract actual devices based on the time of their detection.

And you are absolutely right, there are no guarantee that all nearby devices were discovered within the allotted time. Especially this is actual for the Android devices. iOS devices in it's turn have another issue - they can change their BluetoothDevice's adress in runtime without apparent external cause. Hope this will help you to save the time during debugging.


Edit

As a result of research of this topic found this discussion on code.google.com

Issue is still open and seems that it is related to the hardware features and can't be fixed programmatically. Moreover, it seems that bug will remains on problem devices even after a system updates. So restarting the scan periodically might be acceptable workaround for this case.

DmitryArc
  • 4,757
  • 2
  • 37
  • 42
  • "just extract actual devices based on the time of their detection" - how would that work? You cannot say *this device is not nearby anymore because it was scanned 5 hours ago* because maybe it still is nearby – Tim Oct 20 '15 at 09:10
  • 1
    if device is still nearby it will be catched by BluetoothAdapter.LeScanCallback and the time of it's detection will be updated – DmitryArc Oct 20 '15 at 09:33
  • This is device specific. My phone's bluetooth adapter for example only reports nearby devices once. Some other devices I have tested with do continuously report the same nearby devices and this would provide a solution, but not all devices do, so we can't rely on that unfortunately – Tim Oct 20 '15 at 09:37
  • ok, it makes all the difference. Could you please say which test device you use? – DmitryArc Oct 20 '15 at 09:57
  • In my experience we used to restart BLE scanning periodically. But we did it in order to workaround another bugs in BLE stack on some concrete devices. So here is the reason, why I haven't face the issue you have described. But I can't consider it to be the best solution. – DmitryArc Oct 20 '15 at 10:19
  • I have a oneplus one with cyanogen 12.1 and android 5.1.1 – Tim Oct 20 '15 at 11:29