Unit 5 - Notes

CSE227 10 min read

Unit 5: Smart Device Communication and Sensing Systems

1. Bluetooth Communication

1.1. Bluetooth Overview

Bluetooth is a wireless technology standard for exchanging data over short distances. Android provides a robust Bluetooth API that allows apps to connect to other devices, transfer data, and interact with Bluetooth-enabled peripherals.

Bluetooth Classic vs. Bluetooth Low Energy (BLE)

Feature Bluetooth Classic (BR/EDR) Bluetooth Low Energy (BLE)
Primary Use Case Continuous, high-throughput data streaming. Short bursts of data, low power consumption.
Power Consumption Higher. Designed for devices that can be recharged frequently. Significantly lower. Ideal for battery-powered IoT devices.
Data Throughput Higher (up to 2-3 Mbps). Lower (around 1 Mbps).
Connection Time Slower (around 100ms). Faster (a few ms).
Topology Point-to-point (piconet). Point-to-point, broadcast, mesh.
Examples Wireless speakers, headphones, car kits. Fitness trackers, heart rate monitors, smart home sensors.

Key Concepts

  • BluetoothAdapter: Represents the local device's Bluetooth radio. It's the entry point for all Bluetooth interaction.
  • BluetoothDevice: Represents a remote Bluetooth device. Used to query information like name, address, and connection state.
  • UUID (Universally Unique Identifier): A standardized 128-bit string ID used to uniquely identify services, characteristics, and descriptors in Bluetooth, especially BLE. For example, the UUID for the Heart Rate Service is 0000180D-0000-1000-8000-00805F9B34FB.
  • Profiles: Specifications for how Bluetooth devices communicate for a specific application. Examples include:
    • A2DP (Advanced Audio Distribution Profile): For streaming audio.
    • GATT (Generic Attribute Profile): The standard for BLE data exchange. It defines a hierarchical data structure of Services and Characteristics.

1.2. Setting Up Bluetooth

Permissions

Your AndroidManifest.xml must include the correct permissions. These have changed significantly since Android 12 (API 31).

For Android 11 (API 30) and below:

XML
<!-- Required for discovery and connection -->
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />

<!-- Required for device discovery -->
<!-- For better location results, use FINE. COARSE is the minimum. -->
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />

For Android 12 (API 31) and above:

The old permissions are split into more granular ones for better user privacy.

XML
<!-- Required to scan for nearby devices -->
<uses-permission android:name="android.permission.BLUETOOTH_SCAN" />

<!-- Required to connect to paired devices -->
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />

<!-- Required to make the device discoverable (act as a server) -->
<uses-permission android:name="android.permission.BLUETOOTH_ADVERTISE" />

Note: If your app needs to derive physical location from Bluetooth beacons, you will still need ACCESS_FINE_LOCATION even on Android 12+.

Getting the BluetoothAdapter

The BluetoothAdapter is the core of Bluetooth operations.

KOTLIN
import android.bluetooth.BluetoothManager
import android.content.Context

// Get the BluetoothManager
val bluetoothManager: BluetoothManager = getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager

// Get the BluetoothAdapter
val bluetoothAdapter: BluetoothAdapter? = bluetoothManager.adapter

if (bluetoothAdapter == null) {
    // Device doesn't support Bluetooth
    // Handle this case appropriately
}

Enabling Bluetooth

You must ensure Bluetooth is enabled before performing any operations.

KOTLIN
// Check if Bluetooth is enabled
if (bluetoothAdapter?.isEnabled == false) {
    // Bluetooth is not enabled, request to enable it
    val enableBtIntent = Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE)
    
    // Use the ActivityResultLauncher for a modern approach
    // Requires BLUETOOTH_CONNECT permission on API 31+
    startActivityForResult(enableBtIntent, REQUEST_ENABLE_BT)
}

1.3. Working with Paired and Available Devices

Querying Paired Devices

Paired (or bonded) devices are those that have previously completed the pairing process with the local device. You can get this list directly without performing discovery.

KOTLIN
// Requires BLUETOOTH_CONNECT permission on API 31+
val pairedDevices: Set<BluetoothDevice>? = bluetoothAdapter?.bondedDevices

if (pairedDevices?.isNotEmpty() == true) {
    Log.d("PairedDevices", "Found ${pairedDevices.size} paired devices.")
    pairedDevices.forEach { device ->
        val deviceName = device.name
        val deviceHardwareAddress = device.address // MAC address
        Log.d("PairedDevices", "Device: $deviceName, Address: $deviceHardwareAddress")
    }
} else {
    Log.d("PairedDevices", "No paired devices found.")
}

Discovering Available Devices (Device Discovery)

Discovery is the process of scanning the local area for other Bluetooth-enabled devices. This is an intensive process and significantly drains the battery.

1. Set up a BroadcastReceiver

This receiver listens for system broadcasts about discovered devices.

KOTLIN
private val receiver = object : BroadcastReceiver() {
    override fun onReceive(context: Context, intent: Intent) {
        when(intent.action) {
            BluetoothDevice.ACTION_FOUND -> {
                // A new device was found
                val device: BluetoothDevice? =
                    intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE)
                
                device?.let {
                    // Requires BLUETOOTH_CONNECT permission for device.name
                    val deviceName = it.name ?: "Unknown Device"
                    val deviceAddress = it.address
                    Log.d("Discovery", "Found device: $deviceName at $deviceAddress")
                    // Add the device to a list/adapter
                }
            }
            BluetoothAdapter.ACTION_DISCOVERY_FINISHED -> {
                // Discovery has finished
                Log.d("Discovery", "Discovery finished.")
            }
        }
    }
}

2. Register and Unregister the Receiver

KOTLIN
// In your Activity or Fragment's onResume or onCreate
val filter = IntentFilter(BluetoothDevice.ACTION_FOUND)
filter.addAction(BluetoothAdapter.ACTION_DISCOVERY_FINISHED)
registerReceiver(receiver, filter)

// In your onPause or onDestroy, ALWAYS unregister the receiver
override fun onDestroy() {
    super.onDestroy()
    unregisterReceiver(receiver)
}

3. Start and Stop Discovery

KOTLIN
// To start discovery (requires BLUETOOTH_SCAN permission on API 31+)
if (bluetoothAdapter.isDiscovering) {
    bluetoothAdapter.cancelDiscovery() // Cancel any ongoing discovery
}
bluetoothAdapter.startDiscovery()
Log.d("Discovery", "Starting discovery...")

// To stop discovery
// You should always call this as soon as you have found the device you need
bluetoothAdapter.cancelDiscovery()

2. Wi-Fi Companion Device Pairing

2.1. Overview

The Companion Device Pairing API simplifies the process of connecting to and configuring nearby devices (like IoT devices, smart watches, or Wi-Fi speakers) over Wi-Fi.

Problem it solves: Traditionally, setting up a new Wi-Fi device involved manually connecting to its temporary Wi-Fi network (an "AP"), opening a browser or app to enter your home Wi-Fi credentials, and then switching back. This is cumbersome and error-prone.

How it works: CompanionDeviceManager presents a system-managed dialog that scans for and lists nearby devices. The user selects a device, and the system handles the Wi-Fi connection process, granting the app special privileges to communicate with the paired device.

2.2. Connecting Devices using CompanionDeviceManager

Step 1: Add Permissions to Manifest

XML
<!-- Allows the app to request companion device associations -->
<uses-permission android:name="android.permission.REQUEST_COMPANION_PROFILE_WATCH" />

<!-- Optional: Allows the app to run in the background when the companion device is nearby -->
<uses-permission android:name="android.permission.REQUEST_COMPANION_RUN_IN_BACKGROUND" />

<!-- Optional: Allows the app to use data in the background -->
<uses-permission android:name="android.permission.REQUEST_COMPANION_USE_DATA_IN_BACKGROUND" />

Step 2: Get CompanionDeviceManager

KOTLIN
val deviceManager: CompanionDeviceManager by lazy {
    getSystemService(Context.COMPANION_DEVICE_SERVICE) as CompanionDeviceManager
}

Step 3: Create an Association Request

You define what kind of devices your app is looking for using device filters.

KOTLIN
// Example filter for Wi-Fi devices with a specific SSID prefix
val wifiDeviceFilter = WifiDeviceFilter.Builder()
    .setNamePattern(Pattern.compile("SmartBulb-.*")) // Regex for SSID
    .build()

// You can also filter by Bluetooth devices
val btDeviceFilter = BluetoothDeviceFilter.Builder()
    .setNamePattern(Pattern.compile("MySmartWatch.*"))
    .build()

// Create the association request
val associationRequest = AssociationRequest.Builder()
    // Add one or more filters. The system will search for devices matching any filter.
    .addDeviceFilter(wifiDeviceFilter)
    .addDeviceFilter(btDeviceFilter)
    // Set whether this is a single-device association
    .setSingleDevice(true)
    .build()

Step 4: Launch the System Dialog

You launch the association dialog and handle the result in a callback.

KOTLIN
// Modern approach using ActivityResultLauncher
private val associationResultLauncher = 
    registerForActivityResult(ActivityResultContracts.StartIntentSenderForResult()) { result ->
    if (result.resultCode == Activity.RESULT_OK) {
        val device: ScanResult? = result.data?.getParcelableExtra(CompanionDeviceManager.EXTRA_DEVICE)
        device?.let {
            // Association successful! The system has paired with the device.
            // You can now proceed to connect and configure it.
            // The MAC address of the device is stored in deviceManager.associations
            val macAddress = device.macAddress
            Log.d("CompanionPairing", "Successfully associated with ${macAddress}")
            // Now you can use WifiManager or other APIs to connect to it.
        }
    } else {
        // Association failed or was cancelled by the user.
        Log.e("CompanionPairing", "Association failed.")
    }
}

// Function to start the association
private fun startAssociation() {
    deviceManager.associate(
        associationRequest,
        object : CompanionDeviceManager.Callback() {
            // Called when a suitable device is found and the confirmation UI is shown to the user.
            override fun onDeviceFound(chooserLauncher: IntentSender) {
                try {
                    val intentSenderRequest = IntentSenderRequest.Builder(chooserLauncher).build()
                    associationResultLauncher.launch(intentSenderRequest)
                } catch (e: IntentSender.SendIntentException) {
                    Log.e("CompanionPairing", "Failed to launch chooser", e)
                }
            }

            // Called when the association fails.
            override fun onFailure(error: CharSequence?) {
                Log.e("CompanionPairing", "Association failed: $error")
            }
        },
        null // Handler (optional)
    )
}

3. Android Sensor Systems

Android devices come equipped with a variety of sensors that measure motion, orientation, and various environmental conditions. The Android sensor framework provides a unified way to access and use these sensors.

3.1. Sensor Framework Core Components

  • SensorManager: The system service that lets you access the device's sensors.
  • Sensor: Represents a specific sensor (e.g., the accelerometer).
  • SensorEvent: A data object created by the system when a sensor reading changes. It contains the new data, sensor type, accuracy, and timestamp.
  • SensorEventListener: An interface with two callback methods (onSensorChanged() and onAccuracyChanged()) that your app must implement to receive sensor data.

3.2. Using a Sensor: A General Guide

Step 1: Get SensorManager and the specific Sensor

KOTLIN
private lateinit var sensorManager: SensorManager
private var lightSensor: Sensor? = null

// In onCreate() or a similar setup method
sensorManager = getSystemService(Context.SENSOR_SERVICE) as SensorManager
lightSensor = sensorManager.getDefaultSensor(Sensor.TYPE_LIGHT)

if (lightSensor == null) {
    // Device does not have a light sensor. Handle this.
    Toast.makeText(this, "Light sensor not available!", Toast.LENGTH_SHORT).show()
}

Step 2: Implement SensorEventListener

KOTLIN
class MySensorActivity : AppCompatActivity(), SensorEventListener {
    
    // ... setup code from Step 1 ...

    override fun onSensorChanged(event: SensorEvent?) {
        // This method is called when sensor values have changed.
        if (event?.sensor?.type == Sensor.TYPE_LIGHT) {
            val lightValue = event.values[0]
            // The light sensor provides a single value in lux.
            Log.d("Sensor", "Light level: $lightValue lux")
            // Update UI or perform logic here
        }
    }

    override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) {
        // Called when the accuracy of a sensor has changed.
        // You might want to log this or notify the user.
        Log.d("Sensor", "Accuracy for ${sensor?.name} changed to: $accuracy")
    }
    
    // ... lifecycle methods from Step 3 ...
}

Step 3: Register and Unregister the Listener (Crucial for Battery Life)

It's vital to register the listener only when your activity is in the foreground and unregister it when it goes into the background.

KOTLIN
override fun onResume() {
    super.onResume()
    // Register the listener when the activity is resumed.
    lightSensor?.let {
        sensorManager.registerListener(this, it, SensorManager.SENSOR_DELAY_NORMAL)
    }
}

override fun onPause() {
    super.onPause()
    // Unregister the listener when the activity is paused to save battery.
    sensorManager.unregisterListener(this)
}

Sensor Delays (Sampling Rate):

  • SENSOR_DELAY_FASTEST: Get sensor data as fast as possible.
  • SENSOR_DELAY_GAME: Rate suitable for games.
  • SENSOR_DELAY_UI: Rate suitable for screen orientation changes.
  • SENSOR_DELAY_NORMAL: Default rate, suitable for normal UI changes.

3.3. Motion Sensors

These sensors measure acceleration forces and rotational forces along three axes.

  • Accelerometer (Sensor.TYPE_ACCELEROMETER)

    • Measures: Acceleration force in m/s² along the x, y, and z axes, including the force of gravity.
    • event.values[0]: Acceleration on x-axis.
    • event.values[1]: Acceleration on y-axis.
    • event.values[2]: Acceleration on z-axis.
    • Use Cases: Shake detection, screen orientation.
  • Gyroscope (Sensor.TYPE_GYROSCOPE)

    • Measures: Rate of rotation in rad/s around the x, y, and z axes.
    • event.values[0]: Rate of rotation around x-axis.
    • event.values[1]: Rate of rotation around y-axis.
    • event.values[2]: Rate of rotation around z-axis.
    • Use Cases: 360°/VR apps, advanced motion detection, image stabilization.
  • Linear Acceleration (Sensor.TYPE_LINEAR_ACCELERATION)

    • Measures: Acceleration force in m/s² excluding gravity. It's a software-based (fused) sensor.
    • Acceleration = Gravity + Linear Acceleration
    • Use Cases: Tracking user movement and velocity.
  • Rotation Vector (Sensor.TYPE_ROTATION_VECTOR)

    • Measures: The orientation of the device as a combination of an angle and an axis (represented as a quaternion). This is the preferred way to get device orientation.
    • event.values[0]: x * sin(θ/2)
    • event.values[1]: y * sin(θ/2)
    • event.values[2]: z * sin(θ/2)
    • event.values[3]: cos(θ/2) (optional)
    • Use Cases: Augmented reality, compasses, games.

3.4. Position Sensors

These sensors determine the physical position or location of a device.

  • Geomagnetic Field Sensor (Magnetometer) (Sensor.TYPE_MAGNETIC_FIELD)

    • Measures: The ambient geomagnetic field for all three axes in micro-Tesla (μT).
    • Use Cases: Used in combination with the accelerometer to create a compass. SensorManager.getRotationMatrix() and SensorManager.getOrientation() are helper methods for this.
  • Proximity Sensor (Sensor.TYPE_PROXIMITY)

    • Measures: The distance of an object from the device's screen, usually in centimeters. Most proximity sensors are binary, reporting either a "near" value (e.g., 0 cm) or a "far" value (e.g., 5 cm or more).
    • event.values[0]: Distance in cm.
    • Use Cases: Turning off the screen when you hold the phone to your ear during a call.

3.5. Environment Sensors

These sensors measure various environmental parameters.

  • Light Sensor (Sensor.TYPE_LIGHT)

    • Measures: The ambient light level in lux (lx).
    • event.values[0]: Light level in lx.
    • Use Cases: Automatic screen brightness adjustment.
  • Pressure Sensor (Barometer) (Sensor.TYPE_PRESSURE)

    • Measures: Ambient air pressure in hectopascals (hPa) or millibars (mbar).
    • event.values[0]: Pressure in hPa.
    • Use Cases: Weather forecasting, estimating altitude changes.
  • Temperature Sensor (Sensor.TYPE_AMBIENT_TEMPERATURE)

    • Measures: The ambient room temperature in degrees Celsius (°C).
    • event.values[0]: Temperature in °C.
    • Note: This is less common than the device/CPU temperature sensor (TYPE_TEMPERATURE).
  • Humidity Sensor (Sensor.TYPE_RELATIVE_HUMIDITY)

    • Measures: The relative ambient humidity as a percentage (%).
    • event.values[0]: Humidity in %.
    • Use Cases: Providing local weather/environment conditions.