In this session we will create an app that scans for both classic and low-energy Bluetooth devices. It should look like in the following image (or prettier if you have the time) and it must have the following functionalities:
You will need an Activity, which is a special Android class responsible for the “screens” of your app. It renders the layout components defined in your project's res/ folder in xml format. As an Android dev you will work with other UI related components, such as Fragments or Dialogs, but we will not focus on them in this session.
What is important to know about them is their lifecycle and the callbacks called by the system when its state changes. The image from the documentation describes them. As a first step to understand them, we ask you as a first exercise, to override in your app all these methods, add a Log and observe in the Logcat panel of your IDE what happens when:
For the app you should use a simple layout, e.g. a LinearLayout and edit it using the panels provided by the IDE. The following code snippet shows a LinearLayout with a TextView and a Button.
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" tools:context=".MainActivity"> <TextView android:id="@+id/textView" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_margin="8dp" android:padding="8dp" android:text="@string/hello_fitbit" /> <!-- We saved the strings into a string.xml resources file --> <Button android:id="@+id/scan_button" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginStart="8dp" <!-- This value can be added to a dimens or styles resources file --> android:layout_marginTop="8dp" android:layout_marginEnd="8dp" android:text="@string/scan_devices" /> <!-- This attribute, and the others, can be also modified programmatically --> </LinearLayout>
The Activity's code must load this layout in its onCreate callback. In the following example we also add a click listener to the button.
@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); Log.i(TAG, "onCreate"); initViews(); } private void initViews() { scanButton = findViewById(R.id.scan_button); scanButton.setOnClickListener(new ScanButtonClickListener()); // you can also use an anonymous class that implements the OnClickListener interface }
For the RecyclerView which will display the list of devices discovered by the scan, we also need to create an adapter for its data. A easier example than the one from the documentation is this one.
The following wiki page provides detailed explanations about the role of Intents and Broadcast Receivers as well as security good practices related to them. It was recently created by two Fitbit developers for a master programme course: Intents.
Using intents you can communicate with another component of your app (e.g. a second activity) or with another app installed in the system (e.g. start a browser to display an url).
BroadcastReceivers are classes that receive intents sent by the system, other apps or your own app. The recommended way of working with them is to register and unregister them dynamically, in this way you can control its lifetime and correlate it with your activity's lifecycle.
Why use intents and broadcast receivers in this session app?
In order to use the Bluetooth API, your app needs to declare certain permissions in the manifest, like in the following snippet.
<manifest xmlns:tools="http://schemas.android.com/tools" package="com.fitbit.firstsession" xmlns:android="http://schemas.android.com/apk/res/android"> <uses-permission android:name="android.permission.BLUETOOTH"/> <uses-permission android:name="android.permission.BLUETOOTH_ADMIN"/> <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/> <application ...
Since Android 6 permissions can be granted at runtime. In the case of the ACCESS_COARSE_LOCATION permission needed for Bluetooth scanning, the system does not automatically prompt you for it when starting a scan, you need to implicitly check if it is granted and inform the user if it is not.
private boolean checkAndRequestPermissionForScanning() { if (ContextCompat.checkSelfPermission(MainActivity.this, Manifest.permission.ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED) { if (ActivityCompat.shouldShowRequestPermissionRationale( MainActivity.this, Manifest.permission.ACCESS_COARSE_LOCATION)) { Toast.makeText(MainActivity.this, "Cannot scan without granting location permission.", Toast.LENGTH_SHORT).show(); } else { ActivityCompat.requestPermissions(MainActivity.this, new String[]{Manifest.permission.ACCESS_COARSE_LOCATION}, REQUEST_PERMISSION_BLUETOOTH); // <--- a number defined by you in your activity } return false; } return true; }
Start the scan only if the user granted the permission, and you are informed by of this action in a callback in your activity
@Override public void onRequestPermissionsResult(int requestCode, @NonNull String permissions[], @NonNull int[] grantResults) { if (requestCode == REQUEST_PERMISSION_BLUETOOTH) { if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { Toast.makeText(this, "Permission granted, starting BT scan", Toast.LENGTH_SHORT).show(); // start scan, etc } else { Toast.makeText(this, "Permission not granted, cannot scan for BT devices", Toast.LENGTH_SHORT).show(); } } }
The most relevant object for working with Bluetooth is the BluetoothAdapter. The Developer's Guide provides examples for all the operations you can do with Bluetooth: discover, become discoverable, connect to a device, get the list of paired devices, be informed about changes in connectivity.
We recommend to not include the bluetooth logic in your activity. You should separate it into dedicated classes and call their methods. For this app, you need a BluetoothController class that should be able to do the following:
BluetoothDevice.ACTION_FOUND
and BluetoothAdapter.ACTION_DISCOVERY_FINISHED
startDiscovery
methodstartScan
methodcancelDiscovery
methodstopScan
methodFor Bluetooth LE the scanning is performed differently, not being needed to register for intents, only providing a callback implementation to the scanning method. The Bluetooth LE Guide uses the deprecated method, we recommend using the newer API, like in the following snippets:
// for starting a scan: bluetoothAdapter.getBluetoothLeScanner().startScan(bleScanCallback); //implementation of a callback: class LeScanCallback extends ScanCallback { @Override public void onScanResult(int callbackType, ScanResult result) { super.onScanResult(callbackType, result); BluetoothDevice device = result.getDevice(); // do stuff! } @Override public void onScanFailed(int errorCode) { super.onScanFailed(errorCode); Log.w(TAG, "BLE scan failed"); } }