1

I am trying to scan BLE devices continuously for a research project. I use LOW_LATENCY mode of BLE scanning. However, after 20 mins or so my UI freezes.

Basically I press button it should start scanning for BLE devices, and when I press the button again, it should stop scanning. I want to scan for at least 2 hours continuously. The following is my code.

 @Override
 protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    toggle = (ToggleButton) findViewById(R.id.toggleButton); // initiate a toggle button
    tv = (TextView) findViewById(R.id.tv);

        EasyPermissions.requestPermissions(this, "need permission", 1001, Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.WRITE_EXTERNAL_STORAGE,Manifest.permission.ACCESS_COARSE_LOCATION,Manifest.permission.BLUETOOTH,Manifest.permission.BLUETOOTH_ADMIN);
     // Check BLE support for the device
    if (!getPackageManager().hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE)) {
        Toast.makeText(getApplicationContext(),"no bluetooth LE functionality", Toast.LENGTH_SHORT).show();
        finish();
    }
    btManager = (BluetoothManager)getSystemService(Context.BLUETOOTH_SERVICE);
    btAdapter = btManager.getAdapter();
    bLEScanner = btAdapter.getBluetoothLeScanner();
    settings = new ScanSettings.Builder()
            .setScanMode(SCAN_MODE_LOW_LATENCY)
            .setCallbackType(ScanSettings.CALLBACK_TYPE_ALL_MATCHES)
            .build();
    toggle.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
        @TargetApi(Build.VERSION_CODES.N)
        public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
            if (isChecked) {
                Toast.makeText(getApplicationContext(), "Scanning started",
                        Toast.LENGTH_SHORT).show();
                // The toggle is enabled
                bFileName = getBluetoothFileName("Bluetooth");
                bLEScanner.startScan(null,settings,bluetoothLeScanCallback);
            } else {
                // The toggle is disabled
                Toast.makeText(getApplicationContext(), "Scanning stopped",
                        Toast.LENGTH_SHORT).show();
                bLEScanner.stopScan(bluetoothLeScanCallback);
            }
        }
    });
}

Scan Callback code

private ScanCallback bluetoothLeScanCallback = new ScanCallback() {
 @Override
 public void onScanResult(int callbackType,  android.bluetooth.le.ScanResult result) {
     byte[] scanRecord = result.getScanRecord().getBytes();
     int startByte = 2;
     boolean patternFound = false;
     while (startByte <= 5)
     {
         if (    ((int) scanRecord[startByte + 2] & 0xff) == 0x02 && //Identifies an iBeacon
                 ((int) scanRecord[startByte + 3] & 0xff) == 0x15)
         { //Identifies correct data length
             patternFound = true;
             break;
         }
         startByte++;
     }

     if (patternFound)
     {
         //Convert to hex String
         byte[] uuidBytes = new byte[16];
         System.arraycopy(scanRecord, startByte + 4, uuidBytes, 0, 16);
         String hexString = bytesToHex(uuidBytes);

         //UUID detection
         String uuid =  hexString.substring(0,8) + "-" +
                 hexString.substring(8,12) + "-" +
                 hexString.substring(12,16) + "-" +
                 hexString.substring(16,20) + "-" +
                 hexString.substring(20,32);

         // major
         final int major = (scanRecord[startByte + 20] & 0xff) * 0x100 + (scanRecord[startByte + 21] & 0xff);

         // minor
         final int minor = (scanRecord[startByte + 22] & 0xff) * 0x100 + (scanRecord[startByte + 23] & 0xff);

         Log.i("BLE","UUID: " +uuid + " nmajor : " +major +" nminor " +minor+" time "+getCompleteDate(System.currentTimeMillis())+" rssi "+result.getRssi());
         StringBuilder bStringBuilder = new StringBuilder();
         long sysTime = System.currentTimeMillis();
         bStringBuilder.append(imei+","+sysTime+","+uuid+","+major+","+minor+","+result.getRssi()+","+getCompleteDate(sysTime)+","+"\n");
         String finalString = bStringBuilder.toString();
         exportBluetoothData(finalString, finalString.length(), "Bluetooth");

     }
 }
     @Override
 public void onScanFailed(int errorCode) {
     super.onScanFailed(errorCode);
     Log.i("BLE", "error");
 }
 };

Will it be helpful to use AsyncTask for this kind of long operation? Any guide would be highly appreciated. Thanks.

Phantômaxx
  • 37,901
  • 21
  • 84
  • 115
Hasala
  • 126
  • 10
  • You should use the `AsyncTask` for this kind of work https://stackoverflow.com/questions/14250989/how-to-use-asynctask-correctly-in-android – Muhammad Hasnat Nov 25 '19 at 11:22
  • What does exportBluetoothData do? Is it taking a long time to execute? – Emil Nov 25 '19 at 20:47
  • 1
    @HasnatHasan I heard *AsyncTask* is not suitable for this kind of long operation. Any alternative that you would suggest? – Hasala Nov 26 '19 at 05:39
  • @Emil It exports scan results to a CSV file in local storage. It doesn't take long time to execute. – Hasala Nov 26 '19 at 05:39
  • Please use the cpu profiling tools in Android studio to see what your app is doing after 20 minutes. – Emil Nov 26 '19 at 09:30

3 Answers3

1

It won't be helpful to use AsyncTask for this kind of long operation. Android 7.0 introduced a BLE scan timeout, where any scan running for 30 minutes or more is effectively stopped automatically: https://stackoverflow.com/a/44488991/2068732

Furthermore, BLE scan drains the battery, so you should avoid running BLE scan for so long.

If your app does require scanning frequently, you might consider stopping and re-starting the BLE scan upon a user action. But bare in mind that Android 7 prevents scan start-stops more than 5 times in 30 seconds.

matdev
  • 4,115
  • 6
  • 35
  • 56
  • Thanks for the explanation. I tried using a **handler** to start and stop scan. I used **postDelayed** method with a 15 seconds delay. Basically, I start the scan, and scan for 15 seconds, then stop the handler callback and restart the scan. Still I experience the UI freeze after some time. Any idea how to fix this? Thanks in advance. – Hasala Nov 26 '19 at 05:34
  • Are you running anything on UI thread ? Are you using the method getActivity.runOnUiThread() ? – matdev Nov 26 '19 at 08:32
  • `AsyncTasks` are deprecated. – IgorGanapolsky Oct 05 '20 at 15:23
1

you should use background thread for this process,because when you are scanning, your ui thread may be blocked that's the reason for ui freezing.make use of task scheduler or any other background process for that.

package com.myapp.services;

import java.util.List;

import android.annotation.TargetApi;
import android.app.Service;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothGatt;
import android.bluetooth.BluetoothGattCallback;
import android.bluetooth.BluetoothGattCharacteristic;
import android.bluetooth.BluetoothGattService;
import android.bluetooth.BluetoothManager;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.SharedPreferences.Editor;
import android.content.pm.PackageManager;
import android.os.Build;
import android.os.Handler;
import android.os.IBinder;
import android.os.Looper;
import android.preference.PreferenceManager;
import android.support.v4.content.LocalBroadcastManager;
import android.text.TextUtils;
import android.widget.Toast;

import com.myapp.R;

/**Both RX and RSSI (Received Signal Strength Indication) are indications of the power level being received 
 * by an antenna
 * The difference between RX and RSSI is that RX is measured in milliWatts (mW) or decibel-milliwatts (dBm) 
 * whereas RSSI is a signal strength percentage—the higher the RSSI number, the stronger the signal
 *
 */

@TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR2)
public class BeaconService extends Service implements BluetoothAdapter.LeScanCallback{
    private static final String TAG = BeaconService.class.getSimpleName();

    private BluetoothGatt btGatt;
    private BluetoothAdapter mBluetoothAdapter;

    @Override
    public void onCreate() {
        super.onCreate();
        writeLine("Automate service created...");
        getBTService();
    }

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        writeLine("Automate service start...");
        if (!isBluetoothSupported()) {
            Toast.makeText(this, R.string.ble_not_supported, Toast.LENGTH_SHORT).show();
            stopSelf();
        }else{
            if(mBluetoothAdapter!=null && mBluetoothAdapter.isEnabled()){
                startBLEscan();                     
            }else{
                stopSelf();
            }
        }
        return START_STICKY;
    }

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

    @Override
    public void onDestroy() {
        writeLine("Automate service destroyed...");
        stopBLEscan();
        super.onDestroy();

        if(btGatt!=null){
            btGatt.disconnect();
            btGatt.close();
            btGatt = null;
        }
    }

    @Override
    public boolean stopService(Intent name) {
        writeLine("Automate service stop...");
        stopSelf();
        return super.stopService(name);
    }

    // Initializes a Bluetooth adapter.  For API level 18 and above, get a reference to
    // BluetoothAdapter through BluetoothManager.
    public BluetoothAdapter getBTService(){
        BluetoothManager btManager = (BluetoothManager) getApplicationContext().getSystemService(Context.BLUETOOTH_SERVICE);
        mBluetoothAdapter = (BluetoothAdapter) btManager.getAdapter();
        return mBluetoothAdapter;
    }

    public boolean isBluetoothSupported() {
        return this.getPackageManager().hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE);
    }

    public void startBLEscan(){
        mBluetoothAdapter.startLeScan(this);
    }

    public void stopBLEscan(){
        mBluetoothAdapter.stopLeScan(this);
    }

    /**
     * 
     * @param enable
     */
    public void scanLeDevice(final boolean enable) {
        if (enable) {
            startBLEscan();
        } else {
            stopBLEscan();
        }
    }

    public static void enableDisableBluetooth(boolean enable){
        BluetoothAdapter bluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
        if (bluetoothAdapter != null) {
            if(enable) {
                bluetoothAdapter.enable();
            }else{
                bluetoothAdapter.disable();
            }
        }
    }

    @Override
    public void onLeScan(final BluetoothDevice device, final int rssi, byte[] scanRecord) {
        if(device!=null && device.getName()!=null){
            //Log.d(TAG + " onLeScan: ", "Name: "+device.getName() + "Address: "+device.getAddress()+ "RSSI: "+rssi);
            if(rssi > -90 && rssi <-1){
                writeLine("Automate service BLE device in range: "+ device.getName()+ " "+rssi);
                if (device.getName().equalsIgnoreCase("NCS_Beacon") || device.getName().equalsIgnoreCase("estimote")) {
                    //This Main looper thread is main for connect gatt, don't remove it
                    // Although you need to pass an appropriate context getApplicationContext(),
                    //Here if you use Looper.getMainLooper() it will stop getting callback and give internal exception fail to register //callback
                    new Handler(getApplicationContext().getMainLooper()).post(new Runnable() {
                        @Override
                        public void run() {
                            btGatt = device.connectGatt(getApplicationContext(), false, bleGattCallback);
                            Log.e(TAG, "onLeScan btGatt value returning from connectGatt "+btGatt);
                        }
                    });
                }
                stopBLEscan();  
            }else{
                //Log.v("Device Scan Activity", device.getAddress()+" "+"BT device is still too far - not connecting");
            }
        }
    }

    BluetoothGattCallback bleGattCallback = new BluetoothGattCallback() {

        @Override
        public void onConnectionStateChange(final BluetoothGatt gatt, int status, int newState) {
            super.onConnectionStateChange(gatt, status, newState);
            writeLine("Automate service connection state: "+ newState);
            if (newState == android.bluetooth.BluetoothProfile.STATE_CONNECTED){
                writeLine("Automate service connection state: STATE_CONNECTED");
                Log.v("BLEService", "BLE Connected now discover services");
                Log.v("BLEService", "BLE Connected");
                new Thread(new Runnable() {
                    @Override
                    public void run() {
                        writeLine("Automate service go for discover services");
                        gatt.discoverServices();
                    }
                }).start();
            }else if (newState == android.bluetooth.BluetoothProfile.STATE_DISCONNECTED){
                writeLine("Automate service connection state: STATE_DISCONNECTED");
                Log.v("BLEService", "BLE Disconnected");
            }
        }

        @Override
        public void onServicesDiscovered(final BluetoothGatt gatt, int status) {
            super.onServicesDiscovered(gatt, status);
            if (status == BluetoothGatt.GATT_SUCCESS) {
                writeLine("Automate service discover service: GATT_SUCCESS");
                Log.v("BLEService", "BLE Services onServicesDiscovered");
                //Get service
                List<BluetoothGattService> services = gatt.getServices();
                writeLine("Automate service discover service imei: " +imei);
            }
        }

        @Override
        public void onCharacteristicRead(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic,  int status) {
            super.onCharacteristicRead(gatt, characteristic, status);
        }

        @Override
        public void onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) {
            super.onCharacteristicChanged(gatt, characteristic);
        }
    };

    private void writeLine(final String message) {
        Handler h = new Handler(getApplicationContext().getMainLooper());
        // Although you need to pass an appropriate context
        h.post(new Runnable() {
            @Override
            public void run() {
                Toast.makeText(getApplicationContext(),message,Toast.LENGTH_SHORT).show();
            }
        });
    }

}

In manifest.xml

<uses-feature
        android:name="android.hardware.bluetooth_le"
        android:required="true" />

    <!-- Permission for bluetooth -->
    <uses-permission android:name="android.permission.BLUETOOTH" />
    <uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />

 <service
            android:name="com.myapp.services.BeaconService"
            android:enabled="true"
            android:exported="false" />
Abraham Baby
  • 104
  • 6
  • Thank you. Can you please share a sample code that would achieve this task? How to stop the background thread when I click stop button? – Hasala Nov 26 '19 at 05:37
  • 1
    I don't understand what you're trying to achieve with posting stuff to handlers. All BLE operations are asynchronous and do not block the thread. Toasts however need to be run from the main thread. – Emil Nov 26 '19 at 09:29
1

I used SCAN_MODE_BALANCED instead of SCAN_MODE_LOW_LATENCY Now it doesn't freeze the thread. Thank you for all your answers.

matdev
  • 4,115
  • 6
  • 35
  • 56
Hasala
  • 126
  • 10
  • Maybe you're in an environment with too many advertisements and your code couldn't keep up with the advertisements. – Emil Nov 27 '19 at 18:42
  • It helped thank you. Had the same issue with Android 12 Samsung devices. – Kirk_hehe Apr 11 '23 at 13:40