sabato 16 gennaio 2016

Implementing bluetooh LE on android

Intro

Recently I found myself working on this really simple application while testing bluetooth le implementation on android for a particular and obscure device. Only after I've already started to work on the application I've found out that the device I was trying to communicate with was actually using bluetooth 2.0, because of this you will find the code to actually handle both LE and 2.0 bluetooth technology.
Of course all the information you need is available on android developers webpage, but you can find here an already working example (even tough it's incomplete, I only had few hours to work on this).

Code

Here the code of the main activity, useful info can be found in the comments.


import butterknife.Bind;
import butterknife.ButterKnife;

public class MainActivity extends AppCompatActivity {

    //
    // View & View control
    //

    @Bind(R.id.container)
    LinearLayout container;
    @Bind(R.id.supported_text)
    TextView supportedText;
    @Bind(R.id.list)
    ListView listView;

    DeviceArrayAdapter devicesArrayAdapter;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        ButterKnife.bind(this);
        Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
        setSupportActionBar(toolbar);

        checkPhoneReq();
        setupLayout();

        //for classic scan
        // Register the BroadcastReceiver
        IntentFilter filter = new IntentFilter(BluetoothDevice.ACTION_FOUND);
        registerReceiver(mReceiver, filter); // Don't forget to unregister during onDestroy
    }

    void setupLayout() {
        devices = new ArrayList<>();
        devicesArrayAdapter = new DeviceArrayAdapter(this, R.layout.list_item);
        listView.setAdapter(devicesArrayAdapter);
        listView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
            @Override
            public void onItemClick(AdapterView parent, View view, int position, long id) {
                //start details activity
                BluetoothDevice device = devicesArrayAdapter.getItem(position);
                startDetailsActivity(device);
            }
        });
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        unregisterReceiver(mReceiver);
    }

    private class DeviceArrayAdapter extends ArrayAdapter {
        Context context;
        int res;

        public DeviceArrayAdapter(Context context, int resource) {
            super(context, resource, devices);
//            this.devices = devices;
            this.context = context;
            res = resource;
        }

        @Override
        public View getView(int position, View convertView, ViewGroup parent) {
            LayoutInflater inflater = LayoutInflater.from(context);
            View row = inflater.inflate(res, parent, false);
            BluetoothDevice device = devices.get(position);
            TextView tv = (TextView) row.findViewById(R.id.label1);

            tv.setText("TYPE: " + device.getType() + " CLASS: " + device.getBluetoothClass());
            tv = (TextView) row.findViewById(R.id.label2);

            tv.setText("NAME: " + device.getName() + " STATE: " + device.getBondState());
            //STATE 10 = none 11 = bounding 12= bounded
            tv = (TextView) row.findViewById(R.id.label3);
            tv.setText(device.getAddress());
            return row;
        }
    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        // Inflate the menu; this adds items to the action bar if it is present.
        getMenuInflater().inflate(R.menu.menu_main, menu);
        return true;
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        // Handle action bar item clicks here. The action bar will
        // automatically handle clicks on the Home/Up button, so long
        // as you specify a parent activity in AndroidManifest.xml.
        int id = item.getItemId();

        if (id == R.id.action_settings) {
            log("Not implemented");
            return true;
        }
        if (id == R.id.action_scan) {
            if (!mScanning) {
                log("Start scanning for devices");
                scanForLeDevices(true);
            } else {
                log("Stopping");
                scanForLeDevices(false);
            }
            return true;
        }
        if (id == R.id.action_scan_classic) {
            log("Start scanning for devices (Classic)");
            scanForDevicesClassic();
            return true;
        }
        if (id == R.id.action_query) {
            queryPaired();
        }
        if (id == R.id.action_clear) {
            clearList();
        }
        return super.onOptionsItemSelected(item);
    }

    void log(String s) {
        Snackbar.make(container, s, Snackbar.LENGTH_LONG)
                .setAction("Action", null).show();
    }

    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        if (requestCode == REQUEST_ENABLE_BT) {
            // Make sure the request was successful
            if (resultCode == RESULT_OK) {
                //...
                log("Bluetooth enabled.");
            }
            if (requestCode == RESULT_CANCELED) {
                log("Bluetooth must be enabled.");
            }
        }
    }

    //
    // Behaviour
    //

    BluetoothAdapter mBluetoothAdapter;
    BluetoothLeScanner mBluetoothScanner; //new
    private final int REQUEST_ENABLE_BT = 1;
    private boolean mScanning;
    private Handler mHandler;
    // Stops scanning after 10 seconds.
    private static final long SCAN_PERIOD = 10000;
    ArrayList devices;

    void clearList(){
        devices.removeAll(devices);
        devicesArrayAdapter.notifyDataSetChanged();
    }

    void checkPhoneReq() {
        // Use this check to determine whether BLE is supported on the device. Then
        // you can selectively disable BLE-related features.
        if (!getPackageManager().hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE)) {
            log("Bluetooth LE not supported");
            supportedText.setText("not supported");
        }

        // Initializes Bluetooth adapter.
        final BluetoothManager bluetoothManager =
                (BluetoothManager) getSystemService(Context.BLUETOOTH_SERVICE);
        mBluetoothAdapter = bluetoothManager.getAdapter(); //Req min API 18

        // Ensures Bluetooth is available on the device and it is enabled. If not,
        // displays a dialog requesting user permission to enable Bluetooth.
        if (mBluetoothAdapter == null || !mBluetoothAdapter.isEnabled()) {
            //error prompting the user to go to Settings to enable Bluetooth:
            Intent enableBtIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
            startActivityForResult(enableBtIntent, REQUEST_ENABLE_BT);
        }
    }

    //Battery-Intensive operation
    //Guidelines:
    // As soon as you find your device, stop.
    // Never in a loop
    // Set a time limit
    //Need 2 implementation if support is needed for < API 21
    private void scanForLeDevices(boolean enable) {
        int apiVersion = android.os.Build.VERSION.SDK_INT;
        mHandler = new Handler(Looper.getMainLooper());
        if (apiVersion > android.os.Build.VERSION_CODES.KITKAT) {
            useScanner(enable);
        } else {
            useScannerOld(enable);
        }
    }

    void useScanner(boolean enable) {
        if (enable) {
            mBluetoothScanner = mBluetoothAdapter.getBluetoothLeScanner();
            // scan for devices but only for a specified time
            mHandler.postDelayed(new Runnable() {
                @Override
                public void run() {
                    mScanning = false;
                    if (mBluetoothScanner != null) {
                        mBluetoothScanner.stopScan(mScanCallback);
                        log("Scan ended after 10s");
                    }
                }
            }, SCAN_PERIOD);

            mBluetoothScanner.startScan(mScanCallback);

        } else {
            mScanning = false;
            if (mBluetoothScanner != null)
                mBluetoothScanner.stopScan(mScanCallback);
        }
    }

    //for kitkat and below
    void useScannerOld(boolean enable) {
        // targetting kitkat or bellow
        if (enable) {
            mHandler.postDelayed(new Runnable() {
                @Override
                public void run() {
                    mScanning = false;
                    mBluetoothAdapter.stopLeScan(mLeScanCallback);
                }
            }, SCAN_PERIOD);

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

    //DEVICE SCAN CALLBACK

    //FOR NEW DEVICES
    ScanCallback mScanCallback = new ScanCallback() {
        @Override
        public void onScanResult(int callbackType, ScanResult result) {
            BluetoothDevice device = result.getDevice();
            addDeviceToList(device);
        }

        @Override
        public void onScanFailed(int errorCode) {
            super.onScanFailed(errorCode);
            log("Scan failed with code: " + errorCode);
        }
    };

    //FOR OLD DEVICES
    BluetoothAdapter.LeScanCallback mLeScanCallback = new BluetoothAdapter.LeScanCallback() {
        @Override
        public void onLeScan(final BluetoothDevice device, int rssi, byte[] scanRecord) {
            runOnUiThread(new Runnable() {
                @Override
                public void run() {
                    addDeviceToList(device);
                }
            });
        }
    };

    //Adds the device to a List Adapter
    void addDeviceToList(BluetoothDevice device) {
        Log.v("test", "Found something.");
        devices.add(device);
        devicesArrayAdapter.notifyDataSetChanged();
    }

    ///
    void queryPaired() {
        Set deviceSet = mBluetoothAdapter.getBondedDevices();
        if (deviceSet.size() > 0) {
            for (BluetoothDevice device : deviceSet) {
                addDeviceToList(device);
            }
        }
    }

    ////

    // Create a BroadcastReceiver for ACTION_FOUND
    private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
        public void onReceive(Context context, Intent intent) {
            String action = intent.getAction();
            // When discovery finds a device
            if (BluetoothDevice.ACTION_FOUND.equals(action)) {
                // Get the BluetoothDevice object from the Intent
                BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
                // Add the name and address to an array adapter to show in a ListView
                addDeviceToList(device);
            }
        }
    };

    void scanForDevicesClassic() {
        /*
        The process is asynchronous and the method will immediately return with
        a boolean indicating whether discovery has successfully started.
        The discovery process usually involves an inquiry scan of about 12 seconds,
        followed by a page scan of each found device to retrieve its Bluetooth name.
         */
        mBluetoothAdapter.startDiscovery();
    }


    public final static String EXTRA_BTDEVICE = "btdevice";

    //on list click
    void startDetailsActivity(BluetoothDevice device){
        Intent i = new Intent(this, DetailsActivity.class);
        i.putExtra(EXTRA_BTDEVICE, device);
        startActivity(i);
    }
}


Download

Source code on git