martes, septiembre 20, 2016

Trabajando con Bluetooth Low Energy (BLE) en Android

Actualmente estoy desarrollando un app que requiere la comunicación con dispositivos bluetooth low energy. Todo parecía muy sencillo, pero la verdad es que hay que escribir mucho código para hacer que funcione servicios, broadcast receivers, binding a servicios, entre otros.

Mientras voy desarrollando tengo en mi gradle el minSDK de mi teléfono (Nexus 6p - SDK 24) esto con el simple hecho de compilar mas rápido.  La aplicación se espera que trabaje desde Android 4.3 SDK18 que fue cuando se introdujo BLE en Android.

Todo iba muy bien hasta que cambié el gradle a minSDK 18.. encontré que todo mi código estaba hecho para SDK21 donde hay métodos nuevos para escanear BLE devices.

Para ser mas concreto cambian los callBacks a partir del SDK 21. Lo resolví de la siguiente manera

Primero nos creamos dos métodos: scanLeDevice21 y scanLeDevice18.

  
    /**
     * Scan for BLE devices with Android API 21 and up
     *
     * @param enable Enabled scanning
     */
    @RequiresApi(21)
    private void scanLeDevice21(final boolean enable) {

        ScanCallback mLeScanCallback = new ScanCallback() {

            @Override
            public void onScanResult(int callbackType, ScanResult result) {

                super.onScanResult(callbackType, result);

                BluetoothDevice bluetoothDevice = result.getDevice();

                if (!bluetoothDeviceList.contains(bluetoothDevice)) {
                    Log.d("DEVICE", bluetoothDevice.getName() + "[" + bluetoothDevice.getAddress() + "]");
                    bluetoothDeviceArrayAdapter.add(bluetoothDevice);
                    bluetoothDeviceArrayAdapter.notifyDataSetChanged();
                }
            }

            @Override
            public void onBatchScanResults(List results) {
                super.onBatchScanResults(results);

            }

            @Override
            public void onScanFailed(int errorCode) {
                super.onScanFailed(errorCode);
            }
        };

        final BluetoothLeScanner bluetoothLeScanner = mBluetoothAdapter.getBluetoothLeScanner();

        if (enable) {
            // Stops scanning after a pre-defined scan period.
            mHandler.postDelayed(() -> {
                mScanning = false;
                swipeRefreshLayout.setRefreshing(false);
                bluetoothLeScanner.stopScan(mLeScanCallback);
            }, SCAN_PERIOD);

            mScanning = true;
            bluetoothLeScanner.startScan(mLeScanCallback);
        } else {
            mScanning = false;
            bluetoothLeScanner.stopScan(mLeScanCallback);
        }
    }

    /**
     * Scan BLE devices on Android API 18 to 20
     *
     * @param enable Enable scan
     */
    private void scanLeDevice18(boolean enable) {

        BluetoothAdapter.LeScanCallback mLeScanCallback =
                new BluetoothAdapter.LeScanCallback() {
                    @Override
                    public void onLeScan(final BluetoothDevice bluetoothDevice, int rssi,
                                         byte[] scanRecord) {
                        getActivity().runOnUiThread(new Runnable() {
                            @Override
                            public void run() {
                                bluetoothDeviceArrayAdapter.add(bluetoothDevice);
                                bluetoothDeviceArrayAdapter.notifyDataSetChanged();
                            }
                        });
                    }
                };
        if (enable) {
            // Stops scanning after a pre-defined scan period.
            mHandler.postDelayed(() -> {
                mScanning = false;
                mBluetoothAdapter.stopLeScan(mLeScanCallback);
            }, SCAN_PERIOD);

            mScanning = true;
            mBluetoothAdapter.startLeScan(mLeScanCallback);
        } else {
            mScanning = false;
            mBluetoothAdapter.stopLeScan(mLeScanCallback);
        }

    }

Si observamos, el método scanLeDevice21 tiene la anotación @RequiresApi(21) que dice lo siguiente:

"Denotes that the annotated element should only be called on the given API level or higher. This is similar in purpose to the older @TargetApi annotation, but more clearly expresses that this is a requirement on the caller, rather than being used to "suppress" warnings within the method that exceed the minSdkVersion"

Luego, cada vez que necesitamos escanear un dispositivo preguntamos en cuál API estamos y utilizamos el método correspondiente. Por ejemplo, tengo mi lista de dispositivos dentro de un RefreshLayout.

    /**
     * Refresh listener
     */
    private void refreshScan() {
        if (!hasFineLocationPermissions()) {
            swipeRefreshLayout.setRefreshing(false);
            requestFineLocationPermission();
        } else {
            swipeRefreshLayout.setRefreshing(true);
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
                scanLeDevice21(true);
            } else {
                scanLeDevice18(true);
            }
        }
    }
Y ya, eso es todo! con esto tenemos nuestra api funcional para las diferentes versiones de Android.