Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add new CompanionDeviceManager Sample #68

Merged
merged 12 commits into from
Aug 2, 2023
2 changes: 2 additions & 0 deletions samples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ Sample demonstrating how to make incoming call notifications and in call notific
Demonstrates displaying processed pixel data directly from the camera sensor
- [Color Contrast](accessibility/src/main/java/com/example/platform/accessibility/ColorContrast.kt):
This sample demonstrates the importance of proper color contrast and how to
- [Companion Device Manager Sample](connectivity/bluetooth/companion/src/main/java/com/example/platform/connectivity/bluetooth/cdm/CompanionDeviceManagerSample.kt):
This samples shows how to use the CDM to pair and connect with BLE devices
- [Connect to a GATT server](connectivity/bluetooth/ble/src/main/java/com/example/platform/connectivity/bluetooth/ble/ConnectGATTSample.kt):
Shows how to connect to a GATT server hosted by the BLE device and perform simple operations
- [ConstraintLayout - 1. Centering Views](user-interface/constraintlayout/src/main/java/com/example/platform/ui/constraintlayout/ConstraintLayout.kt):
Expand Down
5 changes: 4 additions & 1 deletion samples/connectivity/bluetooth/ble/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@

This module contains samples related to Bluetooth BLE APIs

- [Find devices](src/main/java/com/example/platform/connectivity/bluetooth/ble/FindDevicesSample.kt)
- [Find BLE devices](src/main/java/com/example/platform/connectivity/bluetooth/ble/FindBLEDevicesSample.kt)
- [Find BLE devices using a PendingIntent](src/main/java/com/example/platform/connectivity/bluetooth/ble/BLEScanIntentSample.kt)
- [Connect to scanned BLE devices](src/main/java/com/example/platform/connectivity/bluetooth/ble/ConnectGATTSample.kt)
- [Create a GATT server, advertise and handle connections](src/main/java/com/example/platform/connectivity/bluetooth/ble/GATTServerSample.kt)

More info at
https://developer.android.com/guide/topics/connectivity/bluetooth/ble-overview
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import android.bluetooth.BluetoothGattCharacteristic
import android.bluetooth.BluetoothGattService
import android.bluetooth.BluetoothProfile
import android.os.Build
import android.util.Log
import androidx.annotation.RequiresApi
import androidx.annotation.RequiresPermission
import androidx.compose.animation.AnimatedContent
Expand Down Expand Up @@ -217,7 +218,11 @@ private data class DeviceConnectionState(
val services: List<BluetoothGattService> = emptyList(),
val messageSent: Boolean = false,
val messageReceived: String = "",
)
) {
companion object {
val None = DeviceConnectionState(null, -1, -1)
}
}

@SuppressLint("InlinedApi")
@RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT)
Expand All @@ -230,13 +235,9 @@ private fun BLEConnectEffect(
val context = LocalContext.current
val currentOnStateChange by rememberUpdatedState(onStateChange)

// Keep the current GATT connection
var gatt by remember(device) {
mutableStateOf<BluetoothGatt?>(null)
}
// Keep the current connection state
var state by remember(gatt) {
mutableStateOf(DeviceConnectionState(gatt, -1, -1))
var state by remember {
mutableStateOf(DeviceConnectionState.None)
}

DisposableEffect(lifecycleOwner, device) {
Expand All @@ -251,6 +252,15 @@ private fun BLEConnectEffect(
super.onConnectionStateChange(gatt, status, newState)
state = state.copy(gatt = gatt, connectionState = newState)
currentOnStateChange(state)

if (status != BluetoothGatt.GATT_SUCCESS) {
// Here you should handle the error returned in status based on the constants
// https://developer.android.com/reference/android/bluetooth/BluetoothGatt#summary
// For example for GATT_INSUFFICIENT_ENCRYPTION or
// GATT_INSUFFICIENT_AUTHENTICATION you should create a bond.
// https://developer.android.com/reference/android/bluetooth/BluetoothDevice#createBond()
Log.e("BLEConnectEffect", "An error happened: $status")
}
}

override fun onMtuChanged(gatt: BluetoothGatt, mtu: Int, status: Int) {
Expand Down Expand Up @@ -305,16 +315,16 @@ private fun BLEConnectEffect(

val observer = LifecycleEventObserver { _, event ->
if (event == Lifecycle.Event.ON_START) {
if (gatt != null) {
if (state.gatt != null) {
// If we previously had a GATT connection let's reestablish it
gatt!!.connect()
state.gatt?.connect()
} else {
// Otherwise create a new GATT connection
gatt = device.connectGatt(context, false, callback)
state = state.copy(gatt = device.connectGatt(context, false, callback))
}
} else if (event == Lifecycle.Event.ON_STOP) {
// Unless you have a reason to keep connected while in the bg you should disconnect
gatt?.disconnect()
state.gatt?.disconnect()
}
}

Expand All @@ -324,7 +334,8 @@ private fun BLEConnectEffect(
// When the effect leaves the Composition, remove the observer and close the connection
onDispose {
lifecycleOwner.lifecycle.removeObserver(observer)
gatt?.close()
state.gatt?.close()
state = DeviceConnectionState.None
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -188,10 +188,10 @@ internal fun GATTServerScreen(adapter: BluetoothAdapter) {
}

// Random UUID for our service known between the client and server to allow communication
val SERVICE_UUID: UUID = UUID.fromString("CDB7950D-73F1-4D4D-8E47-C090502DBD63")
val SERVICE_UUID: UUID = UUID.fromString("00002222-0000-1000-8000-00805f9b34fb")

// Same as the service but for the characteristic
val CHARACTERISTIC_UUID: UUID = UUID.fromString("5aade5a7-14ea-43f7-a136-16cb92cddf35")
val CHARACTERISTIC_UUID: UUID = UUID.fromString("00001111-0000-1000-8000-00805f9b34fb")

@SuppressLint("MissingPermission")
@Composable
Expand Down Expand Up @@ -242,6 +242,7 @@ private fun GATTServerEffect(
.build()

val data = AdvertiseData.Builder()
.setIncludeDeviceName(true)
.addServiceUuid(ParcelUuid(SERVICE_UUID))
.build()

Expand All @@ -263,10 +264,10 @@ private fun GATTServerEffect(
onDispose {
lifecycleOwner.lifecycle.removeObserver(observer)
bluetoothLeAdvertiser.stopAdvertising(advertiseCallback)
gattServer?.close()
manager.getConnectedDevices(BluetoothProfile.GATT_SERVER)?.forEach {
gattServer?.cancelConnection(it)
}
gattServer?.close()
}
}
}
38 changes: 38 additions & 0 deletions samples/connectivity/bluetooth/companion/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# Companion Device Manager Sample

This sample showcases the use of the
[Companion Device Manager](https://developer.android.com/reference/android/companion/CompanionDeviceManager#startSystemDataTransfer(int,%20java.util.concurrent.Executor,%20android.os.OutcomeReceiver%3Cjava.lang.Void,android.companion.CompanionException%3E))
(CDM) to find and associate devices.

## How to use the sample:

1. Use two devices running the sample
2. In one device start the [GATTServerSample](../ble/src/main/java/com/example/platform/connectivity/bluetooth/ble/GATTServerSample.kt)
3. In the other open the [CompanionDeviceManagerSample](/src/main/java/com/example/platform/connectivity/bluetooth/cdm/CompanionDeviceManagerSample.kt))
4. Click start button in the CDM sample

It should directly find the server and a system request will appear. Once accepted both devices will
be associated. You can then connect (see [ConnectGATTSample](../ble/src/main/java/com/example/platform/connectivity/bluetooth/ble/ConnectGATTSample.kt)
and receive (see [CompanionDeviceSampleService](/src/main/java/com/example/platform/connectivity/bluetooth/cdm/CompanionDeviceSampleService.kt))
appear and disappear events (if running A12+).

> Note: You can associate multiple devices. When the server closes and opens again a new mac address
> will be used, thus you need to associate them again.

## License

```
Copyright 2022 The Android Open Source Project

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

https://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
```
28 changes: 28 additions & 0 deletions samples/connectivity/bluetooth/companion/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/*
* Copyright 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

plugins {
id("com.example.platform.sample")
}


android {
namespace = "com.example.platform.connectivity.bluetooth.companion"
}

dependencies {
implementation(project(":samples:connectivity:bluetooth:ble"))
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ Copyright 2023 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ https://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->

<manifest xmlns:android="http://schemas.android.com/apk/res/android">

<!-- Define that the app uses CDM -->
<uses-feature android:name="android.software.companion_device_setup" />

<!-- Only needed to connect to the device once associated -->
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />

<!-- Needed if we want to be notified when an associated device is in range -->
<uses-permission android:name="android.permission.REQUEST_OBSERVE_COMPANION_DEVICE_PRESENCE" />

<!-- Not required by CDM but we use it for notifying when device is in range -->
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />

<application>

<service
android:name="com.example.platform.connectivity.bluetooth.cdm.CompanionDeviceSampleService"
android:exported="true"
android:permission="android.permission.BIND_COMPANION_DEVICE_SERVICE">
<intent-filter>
<action android:name="android.companion.CompanionDeviceService" />
</intent-filter>
</service>
</application>

</manifest>
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
/*
* Copyright 2023 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.example.platform.connectivity.bluetooth.cdm

import android.bluetooth.BluetoothDevice
import android.companion.AssociationInfo
import android.companion.CompanionDeviceManager
import android.os.Build
import androidx.annotation.RequiresApi

/**
* Wrapper for the different type of classes the CDM returns
*/
data class AssociatedDeviceCompat(
val id: Int,
val address: String,
val name: String,
val device: BluetoothDevice?,
)

@RequiresApi(Build.VERSION_CODES.O)
internal fun CompanionDeviceManager.getAssociatedDevices(): List<AssociatedDeviceCompat> {
val associatedDevice = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
myAssociations.map { it.toAssociatedDevice() }
} else {
// Before Android 34 we can only get the MAC. We could use the BT adapter to find the
// device, but to use CDM we only need the MAC.
@Suppress("DEPRECATION")
associations.map {
AssociatedDeviceCompat(
id = -1,
address = it,
name = "",
device = null,
)
}
}
return associatedDevice
}

@RequiresApi(Build.VERSION_CODES.TIRAMISU)
internal fun AssociationInfo.toAssociatedDevice() = AssociatedDeviceCompat(
id = id,
address = deviceMacAddress?.toString() ?: "N/A",
name = displayName?.ifBlank { "N/A" }?.toString() ?: "N/A",
device = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
associatedDevice?.bleDevice?.device
} else {
null
},
)
Loading