Optional Hardware APIs - Android Development Patterns: Best Practices for Professional Developers (2016)

Android Development Patterns: Best Practices for Professional Developers (2016)

Chapter 13. Optional Hardware APIs

Android devices come in many different shapes and sizes. Some also come with extra features or hardware. Not every device comes with every supported feature, but as a developer, you should be looking to provide experiences that will work with the myriad of available devices. Working with Bluetooth, NFC, USB, and other device sensors gives your application greater functionality and usefulness. In this chapter, you learn about how this hardware is implemented into Android and some of the ways that you can leverage device features.

Bluetooth

Bluetooth support in Android has come a considerable way since it was first introduced in API level 5. This form of Bluetooth is known as Bluetooth Classic. Starting with API 18, developers can take advantage of Bluetooth low energy (BLE), or Bluetooth Smart. BLE offers a version of the popular protocol that uses several enhancements to allow it to use less power, enabling both the receiver and the transmitter to save on power. It also brings with it the ability to work with new protocols, such as Eddystone, that allow the use of “beacons” to detect when a device is near and interact with it without pairing.

To take things even further when using Bluetooth with Android devices, the Generic Access Profile portion of the Bluetooth stack has been added in API level 21+.

Bluetooth communication can be broken down into three basic steps:

1. Discovery

2. Exploration

3. Interaction

During the discovery stage, two devices broadcast their availability to one another. When they find each other, the smart devices enter into pairing or information-exchange mode and begin broadcasting a unique address that can be received by other Bluetooth devices that are within the proximity of the Bluetooth radio.

Once the two devices discover one another, they move into the exploration stage. During exploration, a device sends a request to pair with the other one. Depending on the device and current Bluetooth support, a pairing may not be needed to exchange data because data can be passed via an exchanged encryption key.

This now moves the process into the interaction stage. Although this is not strictly required as a security measure, before the devices will move into a fully interactive mode, a device may request a passcode to be entered or a passcode exchanged to confirm that it is connecting to the intended device. Note that with BLE, this is not a standard “pairing mode” because the connections made are casual.

Whether you are working with Bluetooth Classic or BLE, the APIs you will work with are found in the android.bluetooth package. Also, accessing the Bluetooth radio requires user permissions, so you need to add the following to your application manifest XML:

<uses-permission android:name="android.permission.BLUETOOTH"/>
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN"/>

The first permission allows access to the Bluetooth hardware, whereas the second permission allows access to enabling the Bluetooth radio as well as for using it for device discovery.

If you are working with BLE devices and want to filter your app so that only devices that support BLE can download your application from the Google Play store, you can use the following <uses-feature> element in conjunction with the previously mentioned permissions:

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

Enabling Bluetooth

When working with Bluetooth Classic, you need to use the BluetoothAdapter class and the getDefaultAdapter() method to see if Bluetooth is available on the device. If there is an adapter available but not currently enabled, you can start an Intent to turn on Bluetooth. The following snippet demonstrates how this is done:

BluetoothAdapter myBluetooth = BluetoothAdapter.getDefaultAdapter();
if(!myBluetooth.isEnabled()) {
Intent enableIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
startActivityForResult(enableIntent, REQUEST_ENABLE_BT);
}

Enabling the BLE adapter is a similar process, but has the notable difference of using the BluetoothManager to get the adapter instead of just using the BluetoothAdapter class. After you create an adapter, you can check to see if the adapter exists and if it is enabled. The following code snippet shows how this is done:

private BluetoothAdapter myBluetoothAdapter;

final BluetoothManager bluetoothManager =
(BluetoothManager) getSystemService(Context.BLUETOOTH_SERVICE);
myBluetoothAdapter = bluetoothManager.getAdapter();

if (myBluetoothAdapter == null || !myBluetoothAdapter.isEnabled()) {
Intent enableIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
startActivityForResult(enableIntent, REQUEST_ENABLE_BT);
}

Now that Bluetooth is enabled and ready for use, it is time to find nearby devices.

Discovering Devices with Bluetooth

If you have not paired with a device before and you are using Bluetooth Classic, you will want to scan for available devices that you can connect with. This can be done by using the startDiscovery() method, which begins a short scan of nearby devices that are currently available for connection. The following code snippet shows the use of a BroadcastReceiver to fire an Intent for Bluetooth devices that are found during the scan:

// Scanning for Bluetooth Classic
private final BroadcastReceiver myReceiver = new BroadcastReceiver() {
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
if (BluetoothDevice.ACTION_FOUND.equals(action)) {
// a bluetooth device has been found, create an object from the Intent
BluetoothDevice device =
intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
// Display the name and address of the found device
mArrayAdapter.add(device.getName() + "\n" + device.getAddress());
}
}
};

// Register BroadcastReceiver
IntentFilter filter = new IntentFilter(BluetoothDevice.ACTION_FOUND);
registerReceiver(myReceiver, filter);

When you are finished scanning, you should use the cancelDiscovery() method. This allows resources and processor-intensive activities to stop and improve performance. You should also remember to unregister myReciver in the onDestroy() method of your application lifecycle.

If you have already paired with a device, you can save some device resources by getting a list of the previously paired devices and scanning to see if they are available. The following code snippet demonstrates how to retrieve the list of devices:

Set<BluetoothDevice> pairedDevices = myBluetoothAdapter.getBondedDevices();
if(pairedDevices.size() > 0) {
for(BluetoothDevice device : pariedDevices) {
// add found devices to a view
myArrayAdapter.add(device.getName() + "\n" + device.getAddress());
}
}

Because BLE devices can behave differently, there is a different method to use when you are scanning for them. The startLeScan() method scans for devices and then uses a callback to display scan results. The following code snippet shows both how to scan and a sample callback method to display the results:

private BluetoothAdapter myBluetoothAdapter;
private boolean myScanning;
private Handler myHandler;

// Stop scanning after 20 seconds
private static final long SCAN_PERIOD = 20000;

private void scanLeDevice(final boolean enable) {
if (enable) {
// Stops scanning after a pre-defined scan period.
myHandler.postDelayed(new Runnable() {
@Override
public void run() {
myScanning = false;
myBluetoothAdapter.stopLeScan(myLeScanCallback);
}
}, SCAN_PERIOD);

myScanning = true;
myBluetoothAdapter.startLeScan(myLeScanCallback);
} else {
myScanning = false;
myBluetoothAdapter.stopLeScan(myLeScanCallback);
}
}

private LeDeviceListAdapter myLeDeviceListAdapter;

// BLE scan callback
private BluetoothAdapter.LeScanCallback myLeScanCallback =
new BluetoothAdapter.LeScanCallback() {
@Override
public void onLeScan(final BluetoothDevice device, int rssi,
byte[] scanRecord) {
runOnUiThread(new Runnable() {
@Override
public void run() {
myLeDeviceListAdapter.addDevice(device);
myLeDeviceListAdapter.notifyDataSetChanged();
}
});
}
};

Connecting via Bluetooth Classic

With Bluetooth Classic communications, one device needs to be the server. Note that a server can have multiple clients and acts as the go-between for any other connected devices. Clients cannot directly communicate with each other, so the server must forward and manage any data that would be shared between multiple clients.

To establish communication, a socket is opened and data is passed. To make sure that data is being passed to the correct client, you must pass the Universally Unique Identifier (UUID) when creating the socket. After the socket is created, the accept() method is used to listen, and, when it’s finished, the close() method should be called to close the socket. You should be especially careful with the accept() method as it is blocking and therefore must not run on the main thread. The following code demonstrates how to set up the socket and accept communications as a server:

private class AcceptThread extends Thread {
private final BluetoothServerSocket myServerSocket;

public AcceptThread() {
// Create a temp object for use with myServerSocket, because
// myServerSocket is final
BluetoothServerSocket tmp = null;
try {
// MY_UUID is the app UUID string
tmp = mBluetoothAdapter.listenUsingRfcommWithServiceRecord(NAME, MY_UUID);
} catch (IOException e) { }
myServerSocket = tmp;
}

public void run() {
BluetoothSocket socket = null;
// make sure myServerSocket is not null
if (myServerSocket != null) {
// Use loop to keep the socket open for either error or data returned
while (true) {
try {
socket = myServerSocket.accept();
} catch (IOException e) {
break;
}
if (socket != null) {
// use a method to handle returned data in a different thread
manageConnectedSocket(socket);
myServerSocket.close();
break;
}
}
}
}

// This method will close the socket and the thread
public void cancel() {
try {
myServerSocket.close();
} catch (IOException e) { }
}
}

To connect as a client, you need to create an object containing the BluetoothDevice of the server. You then need to pass a matching UUID that will be used to ensure you are communicating with the correct device. Just like communicating as a server, the connect() method is used to establish a connection and either get data or an error. The following sample code snippet shows the code required to connect as a client:

private class ConnectThread extends Thread {
private final BluetoothSocket mySocket;
private final BluetoothDevice myDevice;

public ConnectThread(BluetoothDevice device) {
// Create a temp object for mySocket, because mySocket is final
BluetoothSocket tmp = null;
myDevice = device;

// Get a BluetoothSocket to connect with the BluetoothDevice
try {
// MY_UUID is the app UUID string
tmp = device.createRfcomySocketToServiceRecord(MY_UUID);
} catch (IOException e) { }
mySocket = tmp;
}

public void run() {
// Cancel discovery because it will slow down the connection
mBluetoothAdapter.cancelDiscovery();

try {
// Use the socket to connect or throw an exception
// This method is blocking
mySocket.connect();
} catch (IOException connectException) {
// Unable to connect, close the socket
try {
mySocket.close();
} catch (IOException closeException) { }
return;
}

// use a method to handle returned data in a different thread
manageConnectedSocket(mySocket);
}

// This method will close the socket and the thread
public void cancel() {
try {
mySocket.close();
} catch (IOException e) { }
}
}

Communicating with BLE

As mentioned earlier, BLE makes a slight modification to the exploration and interaction phase of connectivity. Instead of devices being required to be paired or even provide a passcode, devices can be detected and perform a key exchange without interaction. These keys provide an encryption method that can be used to encrypt and decrypt data between the devices without the need of a successful pair between them.

Rather than determine a server and client relationship, you need to connect to the Generic Attribute Profile (GATT) server of the device. This can be done with the connectGatt() method. This method takes a context, a Boolean to determine autoConnect, and a reference to a callback method. This is done as follows:

myBluetoothGatt = device.connectGatt(this, false, myGattCallback);

The callback method may be invoked in a service or other form of logic. An example of the method being used inside of a service follows:

private final BluetoothGattCallback mGattCallback =
new BluetoothGattCallback() {
@Override
public void onConnectionStateChange(BluetoothGatt gatt, int status,
int newState) {
String intentAction;
if (newState == BluetoothProfile.STATE_CONNECTED) {
intentAction = ACTION_GATT_CONNECTED;
myConnectionState = STATE_CONNECTED;
broadcastUpdate(intentAction);
Log.i(TAG, "Connected to GATT server.");
Log.i(TAG, "Attempting to start service discovery:" +
gatt.discoverServices());

} else if (newState == BluetoothProfile.STATE_DISCONNECTED) {
intentAction = ACTION_GATT_DISCONNECTED;
myConnectionState = STATE_DISCONNECTED;
Log.i(TAG, "Disconnected from GATT server.");
broadcastUpdate(intentAction);
}
}

@Override
// New services discovered
public void onServicesDiscovered(BluetoothGatt gatt, int status) {
if (status == BluetoothGatt.GATT_SUCCESS) {
// call an update method to announce service
broadcastUpdate(ACTION_GATT_SERVICES_DISCOVERED);
} else {
Log.w(TAG, "onServicesDiscovered received: " + status);
}
}

@Override
// Result of a characteristic read operation
public void onCharacteristicRead(BluetoothGatt gatt,
BluetoothGattCharacteristic characteristic,
int status) {
if (status == BluetoothGatt.GATT_SUCCESS) {
// call an update method to pass data
broadcastUpdate(ACTION_DATA_AVAILABLE, characteristic);
}
}
};

In the instances where the GATT server is connected, or when data is passed, a method named broadcastUpdate() is called. This method handles the custom logic that you will be processing. The following demonstrates using a StringBuilder to handle the data being passed:

private void broadcastUpdate(final String action) {
final Intent intent = new Intent(action);
sendBroadcast(intent);
}

private void broadcastUpdate(final String action,
final BluetoothGattCharacteristic characteristic) {
final Intent intent = new Intent(action);

// Format data for HEX as this is not a Heart Rate Measurement Profile
final byte[] data = characteristic.getValue();
if (data != null && data.length > 0) {
final StringBuilder stringBuilder = new StringBuilder(data.length);
for(byte byteChar : data)
stringBuilder.append(String.format("%02X ", byteChar));
intent.putExtra(EXTRA_DATA, new String(data) + "\n" +
stringBuilder.toString());
}
sendBroadcast(intent);
}

To handle the data sent through the Intent, you need to have a BroadcastReceiver set up. This receiver picks up more than just device data; it also listens for the state of the GATT server. By listening for events, you can handle for disconnection, connection, working with data, and handling services. The following is sample code for working with these events:

private final BroadcastReceiver mGattUpdateReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
final String action = intent.getAction();
if (BluetoothLeService.ACTION_GATT_CONNECTED.equals(action)) {
myConnected = true;
updateConnectionState(R.string.connected);
invalidateOptionsMenu();
} else if (BluetoothLeService.ACTION_GATT_DISCONNECTED.equals(action)) {
myConnected = false;
updateConnectionState(R.string.disconnected);
invalidateOptionsMenu();
clearUI();
} else if (BluetoothLeService.
ACTION_GATT_SERVICES_DISCOVERED.equals(action)) {
// Update the UI for supported services and characteristics
displayGattServices(mBluetoothLeService.getSupportedGattServices());
} else if (BluetoothLeService.ACTION_DATA_AVAILABLE.equals(action)) {
displayData(intent.getStringExtra(BluetoothLeService.EXTRA_DATA));
}
}
};

With the GATT server connected, you can then loop through BluetoothGattService to find available services and read and write data to and from them. You can also set up a listener for GATT notifications by using your myBluetoothGatt object and using thesetCharacteristicNotification() method to inform the local system that a characteristic value has changed. To inform the remote system, you need to get the BluetoothGattDescriptor for the characteristic and usesetValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE) to set the value. You then use gatt.writeDescriptor to send the value to the remote system. When onDescriptorWrite in BluetoothGattCallback runs, you are then ready to receive updates. After you complete your setup, you can override the onCharacteristicChanged() method to broadcast an update when a GATT notification is available.

When you are finished communicating with a BLE device, use the close() method to release the connection. The following is an example of the close() method in use:

public void close() {
if (myBluetoothGatt == null) {
return;
}
myBluetoothGatt.close();
myBluetoothGatt = null;
}

Near Field Communication

Near Field Communication (NFC) is a passive technology created by NXP Semiconductors that allows “tags” to be used with NFC-capable devices. It is a radio technology that has a very small operational field. This field is generally about 4cm, but can be as much as 10cm, depending on the radio of the device and the size of the tag.

Unlike Bluetooth beacons, NFC tags do not require a power source. This makes them ideal for use in semi-permanent locations and as a medium to automate tasks or distribute relevant information for a set location.

Information is stored in bits of data in NFC Data Exchange Format (NDEF) messages. Each NDEF message will contain at least one NDEF record. A record will contain the following fields:

Image Three-bit type name format (TNF)

Image Variable-length type

Image Variable-length ID (optional)

Image Variable-length payload

The TNF field can contain values that the Android system uses to determine how to handle the information presented in the rest of the NDEF message. The rest of the data is generally contained inside of a physical “tag.” However, using technology similar to Android Beam, a device itself may take the role of a physical tag.

Note that not all NFC tags work with all Android devices. This is due to the format and type of NFC tag used compared to the NFC reader hardware inside of the Android device. As defined by the NFC Forum, there are several types of NFC tags:

Image Type 1: Based on ISO/IEC 14443A, is readable and writeable, can be set to read-only, and has 96 bytes of space but is expandable to 2KB.

Image Type 2: Based on ISO/IEC 14443A, is readable and writeable, can be set to read-only, and has 48 bytes of space but is expandable to 2KB.

Image Type 3: Based on (JIS) X 6319-4, comes preconfigured either as readable and writeable or as read-only, and memory can be up to 1MB.

Image Type 4: Compatible with ISO/IEC 14443, comes preconfigured either as readable and writeable or as read-only, and memory can be up to 32KB.

Image MIFARE Classic: Compatible with ISO/IEC 14443, is readable and writeable, can be set to read-only, and has either 1KB or 4KB of space available.

These are the most common types of tags available; however, there are NFC tags in circulation that do not conform to the standards of the NFC Forum. These tags are not guaranteed to work with all NFC hardware. Depending on the device manufacturer, you may find that some Android devices can read tags that other devices cannot. The MIFARE classic is an example of a tag that may not be read or written to by some Android devices. This may be important to know because it may confuse some users who change devices and find that a set of tags no longer works with their new device.

Working with NFC in your application requires the use of the NFC permission. To add this permission to your application, you need to open your application manifest XML file and add the following to it:

<uses-permission android:name="android.permission.NFC"/>

As an extra step, you can also add a <users-feature /> element to the manifest to have your application filtered by the Google Play store so that devices without NFC cannot download it. This is optional, but may save you from having to deal with upset users. Add the following to your application manifest XML to enable the Google Play filtering:

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

When a tag is scanned by your device, it reads the data stored in the TNF and determines the MIME type or URI of the tag. The internal tag dispatch system is used to determine whether the tag is compatible, empty, or if it should be opened in a specific app. Determining which app to open relies on an Intent being created and then matched against any matching Activities.

If your app should respond to the Intent, you need to filter for one or more of the following Intents:

Image ACTION_NDEF_DISCOVERED

Image ACTION_TECH_DISCOVERED

Image ACTION_TAG_DISCOVERED

ACTION_NDEF_DISCOVERED

To filter for this intent, you can either filter on the MIME type, or on the URI. The following shows a sample of filtering for a MIME type of text/plain:

<intent-filter>
<action android:name="android.nfc.action.NDEF_DISCOVERED"/>
<category android:name="android.intent.category.DEFAULT"/>
<data android:mimeType="text/plain" />
</intent-filter>

Filtering on the URI is similar, but changes out the property of <data> element from android:mimetype to android:scheme with some added properties. The following shows how to filter for a URI of http://www.android.com/index.html:

<intent-filter>
<action android:name="android.nfc.action.NDEF_DISCOVERED"/>
<category android:name="android.intent.category.DEFAULT"/>
<data android:scheme="http"
android:host="www.android.com"
android:pathPrefix="/index.html" />
</intent-filter>

ACTION_TECH_DISCOVERED

When filtering on this Intent, you need to create a resource XML file that contains all the technology types you want to monitor. This ensures that when a tag is scanned, your app only opens when the tag contains the technology your app is expecting to work with. This file should reside in the /res/xml folder of your project. An example of the XML file follows:

<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<tech-list>
<tech>android.nfc.tech.IsoDep</tech>
<tech>android.nfc.tech.NfcA</tech>
<tech>android.nfc.tech.NfcB</tech>
<tech>android.nfc.tech.NfcF</tech>
<tech>android.nfc.tech.NfcV</tech>
<tech>android.nfc.tech.Ndef</tech>
<tech>android.nfc.tech.NdefFormatable</tech>
<tech>android.nfc.tech.MifareClassic</tech>
<tech>android.nfc.tech.MifareUltralight</tech>
</tech-list>
</resources>

To reference your XML technology list, you need to add a <meta-data> tag to your application manifest XML. This will contain the path to your resource list. The following shows an example of the <intent-filter> and <meta-data> elements needed for working withACTION_TECH_DISCOVERED:

<intent-filter>
<action android:name="android.nfc.action.TECH_DISCOVERED"/>
</intent-filter>

<meta-data android:name="android.nfc.action.TECH_DISCOVERED"
android:resource="@xml/nfc_tech_filter" />

Note that the resource path used in the <meta-data> element uses a property with a value of @xml/nfc_tech_filter. This value refers to the file /res/xml/nfc_tech_filter.xml in your project.

ACTION_TAG_DISCOVERED

The final Intent, and perhaps the easiest to implement a filter for, is ACTION_TAG_DISCOVERED. Because you are not filtering for what type of technology or information the tag contains, you can use the following <intent-filter>:

<intent-filter>
<action android:name="android.nfc.action.TAG_DISCOVERED"/>
</intent-filter>

Reading and writing information to NFC tags requires you to define your own protocol stack. The following code demonstrates how to work with the fairly common MIFARE Ultralight tag:

package com.example.android.nfc;

import android.nfc.Tag;
import android.nfc.tech.MifareUltralight;
import android.util.Log;
import java.io.IOException;
import java.nio.charset.Charset;

public class MifareUltralightTagTester {

private static final String TAG =
MifareUltralightTagTester.class.getSimpleName();

// Write to the tag:
public void writeTag(Tag tag, String tagText) {
MifareUltralight ultralight = MifareUltralight.get(tag);
try {
ultralight.connect();
ultralight.writePage(4, "abcd".getBytes(Charset.forName("US-ASCII")));
ultralight.writePage(5, "efgh".getBytes(Charset.forName("US-ASCII")));
ultralight.writePage(6, "ijkl".getBytes(Charset.forName("US-ASCII")));
ultralight.writePage(7, "mnop".getBytes(Charset.forName("US-ASCII")));
} catch (IOException e) {
Log.e(TAG, "IOException while closing MifareUltralight", e);
} finally {
if (ultralight != null) {
try {
ultralight.close();
} catch (IOException e) {
Log.e(TAG, "IOException while closing MifareUltralight", e);
}
}
}
}

// Read the tag:
public String readTag(Tag tag) {
MifareUltralight mifare = MifareUltralight.get(tag);
try {
mifare.connect();
byte[] payload = mifare.readPages(4);
return new String(payload, Charset.forName("US-ASCII"));
} catch (IOException e) {
Log.e(TAG, "IOException while writing MifareUltralight message", e);
} finally {
if (mifare != null) {
try {
mifare.close();
}
catch (IOException e) {
Log.e(TAG, "Error closing tag", e);
}
}
}
return null;
}
}

You may be wondering how this code is expected to function in the instance when you have already defined an Intent to trigger when a tag is near the device. Without a solution to this problem, every time you place a tag near your phone, rather than writing it would constantly read the tag. This is where the Foreground Dispatch System comes into play.

The Foreground Dispatch System allows you to hijack an Intent and stop it from going to where it normally would. It requires you to add a PendingIntent in your application’s onCreate() method as well as to use disableForegroundDispatch() in the onPause()method and enableForegroundDispatch() in the onResume() method. Finally, you must also create a method that will handle the data from the scanned NFC tag.

The following code snippet shows an example of the code needed to work with the Foreground Dispatch System:

@Override
protected void onCreate(Bundle savedInstanceState) {
// your code for the method here

PendingIntent pendingIntent = PendingIntent.getActivity(
this, 0, new Intent(this, getClass())
.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP), 0);

// add an IntentFilter to know what to intercept
IntentFilter ndef = new IntentFilter(NfcAdapter.ACTION_NDEF_DISCOVERED);
try {
// This will catch ALL MIME data types
ndef.addDataType("*/*");
} catch (MalformedMimeTypeException e) {
throw new RuntimeException("fail", e);
}
intentFiltersArray = new IntentFilter[] {ndef };

// the techListsArray is used to create a list of tech you will support
// this is used when enabling Foreground Dispatch
techListsArray = new String[][] { new String[] { NfcF.class.getName() } };
}


@Override
public void onPause() {
super.onPause();
// release to resume default scanning behavior
myAdapter.disableForegroundDispatch(this);
}

@Override
public void onResume() {
super.onResume();
// enable to hijack default scanning behavior
myAdapter.enableForegroundDispatch(this, pendingIntent, intentFiltersArray,
techListsArray);
}

public void onNewIntent(Intent intent) {
Tag tagFromIntent = intent.getParcelableExtra(NfcAdapter.EXTRA_TAG);
// Logic here to handle tagFromIntent
}

Device Sensors

Android provides an API for sensors that device manufacturers may have added to their device. The following is a list of sensors that are supported by Android 5.0:

Image Accelerometer: Hardware

Image Ambient temperature: Hardware

Image Gravity: Software or hardware

Image Gyroscope: Hardware

Image Light: Hardware

Image Linear acceleration: Software or hardware

Image Magnetic field: Hardware

Image Pressure: Hardware

Image Proximity: Hardware

Image Relative humidity: Hardware

Image Rotation vector: Software or hardware

Sensors can be built into the device as sensor hardware, or they may be computed through software calculation. In these instances the values are calculated data taken from other sensors.

Many of these sensors should seem familiar to you, and some have even been leveraged as part of great experiments that have turned into defined standards. For example, the first generation of Cardboard used the magnetic field sensor to determine when an action should be performed. Other sensors are used by the Android system itself without you even realizing it; the proximity sensor is used to turn the screen off when you are taking a phone call.

Note that previous versions of Android do not support all the listed sensors. Note that in some previous versions of Android, an orientation sensor and a temperature sensor were available but have since become deprecated.

Detecting the Available Sensors

Not every sensor that has an API will be included in every device, so you should do your best to offer a fallback solution or to remove options that require the sensor to work.

To see what sensors are available, you should create a SensorManager object. This object will contain all the sensors that are either available or that match a particular set of sensors. The following code snippet shows how to populate the SensorManager object:

// create object
private SensorManager mySensorManager;

// in your onCreate or similar method:
mySensorManager = (SensorManager) getSystemService(Context.SENSOR_SERVICE);

// get all device sensors:
List<Sensor> allSensors = mySensorManager.getSensorList(Sensor.TYPE_ALL);

// get just the proximity sensor(s):
List<Sensor> proxSensors = mySensorManager.getSensorList(Sensor.TYPE_PROXIMITY);

In the previous snippet, it may seem confusing that a list is used for what appears to be a single sensor. A list is used because there may be multiple sensors on the device, and some by specific manufacturers that you may want to use. In this case, you could create a logic check that looks for a specific sensor and vendor before allowing the sensor to be used. The following snippet shows this in action:

// see if the device has a proximity sensor
if (mySensorManager.getDefaultSensor(Sensor.TYPE_PROXIMITY) != null) {
List<Sensor> proxSensors =
mySensorManager.getSensorList(Sensor.TYPE_PROXIMITY);
// loop through the sensors to find a Samsung version 1 sensor
for(int i=0; i<proxSensors.size(); i++) {
if ((proxSensors.get(i).getVendor().contains("Samsung")) &&
(proxSensors.get(i).getVersion() == 1)) {
// Success! set a variable to the sensor
mySensor = proxSensors.get(i);
break;
}
}
}

If you can get away with using another sensor, you can modify the previous snippet by adding an else clause that then does another loop through a secondary sensor to determine availability.


Note

If your app must have a specific sensor available in order to function, you can use <uses-feature> with the sensor information added in your manifest to add filtering to your app via the Google Play store. This helps you avoid bad ratings from users who do not meet the system requirements of your app.


After determining that you have sensors available, you need to work with the data they provide.

Reading Sensor Data

To get started reading data, you want to set up an event listener. This can be done by using the SensorEventListener interface and working with the onAccuracyChanged() and onSensorChanged() methods.

Of these two methods, onAccuracyChanged() provides you with the current accuracy setting of the sensor you are working with. This provides a Sensor object with one of the following constants:

Image SENSOR_STATUS_UNRELIABLE

Image SENSOR_STATUS_ACCURACY_LOW

Image SENSOR_STATUS_ACCURACY_MEDIUM

Image SENSOR_STATUS_ACCURACY_HIGH

To perform your custom logic, you need to override the method and place your specific logic handling inside. The following is a sample snippet:

@Override
public final void onAccuracyChanged(Sensor sensor, int accuracy) {
// Custom logic goes here for sensor accuracy changes
}

The other method, onSensorChanged(), provides you with a SensorEvent object that contains sensor accuracy, a timestamp of data provided, which sensor provided data, and the sensor data. Just like the onAccuracyChanged() method, you will need to override the method to perform your custom logic. The following is a snippet demonstrating the method override:

@Override
public final void onSensorChanged(SensorEvent event) {
// The "event" may return multiple values
// Create variables to contain event values
// Perform custom logic based on sensor values
}

With custom logic set up, you can now use the SensorManager to register and unregister the event listeners in the onResume() and onPause() methods. When you register the event listener, you need to specify which sensor to listen to as well as the speed or sampling rate of the sensor. To register the event listener, use the following snippet of code:

// define the Sensor Manager and Sensor
private SensorManager mySensorManager;
private Sensor mySensor

//... other methods and activity lifecycle methods ...

@Override
protected void onResume() {
super.onResume();
mySensorManager.registerListener(this, mySensor,
SensorManager.SENSOR_DELAY_NORMAL);
}

Notice the use of SENSOR_DELAY_NORMAL for the sensor sampling speed; this has a default value of 200,000 microseconds. You can set your own value in microseconds, or you can use the following values:

Image SENSOR_DELAY_GAME: 20,000-microsecond delay

Image SENSOR_DELAY_UI: 60,000-microsecond delay

Image SENSOR_DELAY_FASTEST: 0-microsecond delay

Some sensors happily take a 0-microsecond delay but will not actually return information at that rate. They offer information back at the fastest available speed, however. You should also keep in mind that using a lower delay value creates an increase on power usage, thus resulting in reduced battery life for the user.

You should unregister sensor listeners when you are finished with them, including when pausing your application. Failure to unregister the sensors in use causes them to continue to collect data and use power. It should also be noted that unless a partial wake lock has been invoked, sensor collection will stop when the screen is turned off. An example of how to unregister the listener in the onPause() event lifecycle is shown next:

// define the SensorManager
private SensorManager mySensorManager;

// ... other methods and activity lifecycle methods ...

@Override
protected void onPause() {
super.onPause();
mySensorManager.unregisterListener(this);
}

Summary

In this chapter, you learned about using Bluetooth with your application. You learned that two standards of Bluetooth are available. Older devices use Bluetooth Classic, and newer devices can leverage the new features of BLE.

You also learned about NFC and the types of tags that can be used. You learned about working with NDEF and TNF records on NFC tags. You also learned how to detect support for NFC in the device and that filtering can be applied to have your application only work with devices that have NFC support. You then learned how to read and write information using the Foreground Dispatch System and how it is leveraged to allow you to intercept triggered Intents. This enables you to read and write data without worrying about other applications taking the focus away from the work you are doing with an NFC tag.

Finally, you learned about working with various device sensors that may be in a device. You learned how to detect sensors that are available as well as how to set up event listeners. You learned how to read data from the sensors by overriding the onAccuracyChanged() andonSensorChanged() methods. Just as you learned that registering the events is important, you also learned about the importance of unregistering sensor event listeners to not only stop collecting data, but to save on wasting device power.