Session 1: Android Basics

Objectives

  • Learn about and use the following Android components:
    • Activities
    • Intents
    • Broadcast Receivers
  • Use some UI elements: Button, RecyclerView (for displaying lists)
  • Understand the activities' lifecycle
  • Communicate between your components
  • Create an application that uses these components and scans for Bluetooth devices

Prerequisites

  1. Setup your development environment: Setup page
  2. Follow the information from the First Project tutorial to learn how to configure your app and understand the project's structure

Bluetooth Scanning App

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:

  • A Button for triggering a classic Bluetooth scan
  • A Button for triggering a BLE scan
    • Because of the system's limitations, we cannot perform both at the same time, so you should not allow that in the UI as well
    • A classic BT scan (the terminology in the documentation and API is “discovery”) finishes after ˜1 minute, but for the BLE scan you should explicitly turn it off. We will go into more details about BLE in the third Android session.
  • Show the results in a RecyclerView list

UI

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:

  • you start the app
  • you force close the app
  • you rotate the screen
  • you switch to another app (aka you put the app in background) and then come back to it (put it in foreground)

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.

Intents and Broadcast Receivers

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?

  • The “events” related to Bluetooth are sent by the system as intents. You register a BroadcastReceiver that receives all the ones you specify in an IntentFilter.
  • You need to send the results back to the UI (your Activity) to be displayed in the list. You can explicitly send them bundled in intents and your activity receives them using another BroadcastReceiver.
    • use the LocalBroadcastManager in this case because it is just communication inside your app (see more explanations here

Unregister your broadcast receivers in the onStop() or onDestroy() callbacks. Also stop the current scan, if any.

Bluetooth

Permissions

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
    ...

One star angry reviews from users: “The app tracks my location, uninstall!”

  • the ACCESS_COARSE_LOCATION permission is needed for Bluetooth scanning since Android 6, the reason is provided here
  • if you build an app that uses BLE, you need to clearly inform the user why this is needed, in a custom dialog that prompts them to enable it.

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();
            }
        }
    }
 

Interaction with the system

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:

  • start a classic Bluetooth scan
    • check if bluetooth is supported, notify the user if not
    • check if bluetooth is enabled, prompt the user to enable it if it's not
    • create an IntentFilter with the actions you need, we recommend: BluetoothDevice.ACTION_FOUND and BluetoothAdapter.ACTION_DISCOVERY_FINISHED
    • register a BroadcastReceiver object you implemented for handling those actions
    • use the BluetoothAdapter's startDiscovery method
  • start a BLE scan
    • check if bluetooth le is supported
    • check if bluetooth is enabled, prompt the user to enable it if it's not
    • start a scan using BluetoothLeScanner's startScan method
  • stop a classic scan
    • use the BluetoothAdapter's cancelDiscovery method
  • stop a BLE scan
    • use the BluetoothLeScanner's stopScan method
  • cleanup
    • cancel ongoing scans
    • unregister the broadcast receiver

For 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");
        }
}

Resources

fss/sessions/session1-android.txt · Last modified: 2019/06/27 15:18 by adriana.draghici
CC Attribution-Share Alike 3.0 Unported
www.chimeric.de Valid CSS Driven by DokuWiki do yourself a favour and use a real browser - get firefox!! Recent changes RSS feed Valid XHTML 1.0