diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index aaf59211..0446599f 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -27,7 +27,7 @@ android {
defaultConfig {
applicationId = "com.example.platform"
- minSdk = 21
+ minSdk = 23
targetSdk = 36
versionCode = 1
versionName = "1.0"
@@ -80,6 +80,7 @@ dependencies {
implementation(project(":samples:connectivity:bluetooth:companion"))
implementation(project(":samples:connectivity:callnotification"))
implementation(project(":samples:connectivity:telecom"))
+ implementation(project(":samples:connectivity:UwbRanging"))
implementation(project(":samples:graphics:pdf"))
implementation(project(":samples:graphics:ultrahdr"))
implementation(project(":samples:location"))
diff --git a/app/src/main/java/com/example/platform/app/ApiSurface.kt b/app/src/main/java/com/example/platform/app/ApiSurface.kt
index 9ca31ca0..2189489e 100644
--- a/app/src/main/java/com/example/platform/app/ApiSurface.kt
+++ b/app/src/main/java/com/example/platform/app/ApiSurface.kt
@@ -195,6 +195,12 @@ val UserInterfaceWindowManagerApiSurface = ApiSurface(
null,
)
+val ConnectivityUwbRangingApiSurface = ApiSurface(
+ "connectivity-uwb-ranging",
+ "Connectivity UWB Ranging",
+ null,
+)
+
val API_SURFACES = listOf(
AccessiblityApiSurface,
CameraCamera2ApiSurface,
@@ -204,6 +210,7 @@ val API_SURFACES = listOf(
ConnectivityBluetoothCompanionApiSurface,
ConnectivityCallNotificationApiSurface,
ConnectivityTelecomApiSurface,
+ ConnectivityUwbRangingApiSurface,
GraphicsPdfApiSurface,
GraphicsUltraHdrApiSurface,
LocationApiSurface,
diff --git a/app/src/main/java/com/example/platform/app/SampleDemo.kt b/app/src/main/java/com/example/platform/app/SampleDemo.kt
index 1019b215..c61fb309 100644
--- a/app/src/main/java/com/example/platform/app/SampleDemo.kt
+++ b/app/src/main/java/com/example/platform/app/SampleDemo.kt
@@ -128,6 +128,7 @@ import com.example.platform.ui.text.LineBreak
import com.example.platform.ui.text.Linkify
import com.example.platform.ui.text.TextSpanFragment
import com.example.platform.ui.windowmanager.demos.WindowDemosActivity
+import com.google.uwb.hellouwb.ui.UwbRangingActivity
interface SampleDemo : CatalogItem {
override val id: String
@@ -367,6 +368,14 @@ val SAMPLE_DEMOS by lazy {
}
},
),
+ ActivitySampleDemo(
+ id = "connectivity-uwb-ranging",
+ name = "UWB Ranging",
+ description = "Demonstrates how to use the UWB APIs to perform ranging.",
+ documentation = "https://developer.android.com/guide/topics/connectivity/uwb",
+ apiSurface = ConnectivityUwbRangingApiSurface,
+ content = UwbRangingActivity::class.java
+ ),
ComposableSampleDemo(
id = "pdf-renderer",
name = "PDF Renderer",
diff --git a/build.gradle.kts b/build.gradle.kts
index 63ab9bb0..19c3c23c 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -18,4 +18,5 @@ plugins {
alias(libs.plugins.kotlin.android) apply false
alias(libs.plugins.kotlin.compose) apply false
alias(libs.plugins.android.library) apply false
+ alias(libs.plugins.protobuf) apply false
}
\ No newline at end of file
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index fee87e84..eaf275b6 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -63,11 +63,16 @@ tensorflowLiteGpuDelegatePlugin = "0.4.4"
tensorflowLiteSupport = "0.4.2"
barcodeScanningCommon = "17.0.0"
playServicesMlkitBarcodeScanning = "18.3.1"
+protobuf = "0.9.4"
+firebaseCrashlyticsBuildtools = "3.0.5"
+uwb = "1.0.0-alpha10"
+
[libraries]
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
androidx-fragment-compose = { module = "androidx.fragment:fragment-compose", version.ref = "fragmentCompose" }
androidx-navigation-compose = { module = "androidx.navigation:navigation-compose", version.ref = "navigationCompose" }
+androidx-uwb = { module = "androidx.core.uwb:uwb", version.ref = "uwb" }
androidx-photopicker-compose = { module = "androidx.photopicker:photopicker-compose", version.ref = "photopickerCompose" }
junit = { group = "junit", name = "junit", version.ref = "junit" }
androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
@@ -198,6 +203,7 @@ tensorflow-lite-select-tf-ops = { module = "org.tensorflow:tensorflow-lite-selec
tensorflow-lite-support = { module = "org.tensorflow:tensorflow-lite-support", version.ref = "tensorflowLiteSupport" }
barcode-scanning-common = { group = "com.google.mlkit", name = "barcode-scanning-common", version.ref = "barcodeScanningCommon" }
play-services-mlkit-barcode-scanning = { group = "com.google.android.gms", name = "play-services-mlkit-barcode-scanning", version.ref = "playServicesMlkitBarcodeScanning" }
+firebase-crashlytics-buildtools = { group = "com.google.firebase", name = "firebase-crashlytics-buildtools", version.ref = "firebaseCrashlyticsBuildtools" }
[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }
@@ -214,3 +220,4 @@ hilt = { id = "com.google.dagger.hilt.android", version.ref = "hilt" }
kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }
kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
+protobuf = { id = "com.google.protobuf", version.ref = "protobuf" }
diff --git a/samples/connectivity/UwbRanging/.idea/workspace.xml b/samples/connectivity/UwbRanging/.idea/workspace.xml
new file mode 100644
index 00000000..6a5944c6
--- /dev/null
+++ b/samples/connectivity/UwbRanging/.idea/workspace.xml
@@ -0,0 +1,249 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {
+ "associatedIndex": 2
+}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 1680803164569
+
+
+ 1680803164569
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/samples/connectivity/UwbRanging/README.md b/samples/connectivity/UwbRanging/README.md
new file mode 100644
index 00000000..45134c52
--- /dev/null
+++ b/samples/connectivity/UwbRanging/README.md
@@ -0,0 +1,74 @@
+# Android Ultra-wideband sample
+
+## Overview
+This project showcases the current features of the
+[Android UWB Jetpack library](https://developer.android.com/jetpack/androidx/releases/core-uwb).
+
+Includes code examples for:
+
+* Device compatibility - How to check if an Android device supports UWB.
+* Device Discovery - Ultra-wideband currently does not support a native way to discover devices, so an out of band (OOB) mechanism must be provided. This project uses the
+[NearBy Connections API](https://developers.google.com/nearby/connections/overview), but other radio protocols like
+Bluetooth, BLE, or Wi-Fi could also be used.
+* Simple Ranging - The Ranging screen displays the controllee's distance from the the controller.
+* Device Control - The Control screen simulates a use case where a door lock could be
+opened when a UWB-capable device is near by.
+* Share Media - The Share file screen demonstrates how to transfer a media file using the
+selected OOB mechanism when devices are in close proximity.
+* Settings - In this screen you can select which Android device will play each role (Controller or Controlee).
+
+
+## Pre-requisites
+* Two UWB-capable Android phones with Android 12 or higher
+* Latest version of the [Core Ultra Wideband (UWB) library](https://developer.android.com/jetpack/androidx/releases/core-uwb)
+
+
+## What is it not?
+
+* An end to end example of Ultra-wideband technology.
+ The main goal is to demonstrate basic ranging capabilities between two Android devices and
+how a selected OOB mechanism could be used to facilitate real use cases. For the latest information on the library status check [this article](https://developer.android.com/guide/topics/connectivity/uwb)
+* A reference for a real production app with proper security, network access, app permissions, user authentication, etc. Check out the [Now in Android app](https://github.com/android/nowinandroid) instead.
+* A UI/Material Design sample. The interface of the app is deliberately kept simple to focus on the UWB use cases. Check out the [Compose Samples](https://github.com/android/compose-samples) instead.
+* A complete Jetpack sample covering all libraries. Check out [Android Sunflower](https://github.com/googlesamples/android-sunflower) or the advanced [GitHub Browser Sample](https://github.com/googlesamples/android-architecture-components/tree/master/GithubBrowserSample) instead.
+
+
+## Who is it for?
+
+* Intermediate developers looking for a simple way to understand how the UWB Jetpack library can be used.
+* Advanced developers looking for a quick reference.
+
+## Opening in Android Studio
+
+To open this app in Android Studio, begin by checking out the entire ```connectivity-samples``` project:
+
+1. Clone the repository, this step checks out the master branch.:
+
+```
+git clone git@github.com:android/connectivity-samples.git
+
+```
+
+2. Open the ```UwbRanging``` folder in the IDE.
+
+
+### License
+
+```
+Copyright 2023 Google LLC
+
+
+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
+
+
+ http://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.
+```
diff --git a/samples/connectivity/UwbRanging/build.gradle.kts b/samples/connectivity/UwbRanging/build.gradle.kts
new file mode 100644
index 00000000..6c2eddba
--- /dev/null
+++ b/samples/connectivity/UwbRanging/build.gradle.kts
@@ -0,0 +1,104 @@
+/*
+ * 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.
+ */
+
+//@Suppress("DSL_SCOPE_VIOLATION")
+plugins {
+ alias(libs.plugins.android.library)
+ alias(libs.plugins.kotlin.compose)
+ alias(libs.plugins.kotlin.android)
+ id("kotlin-parcelize")
+ alias(libs.plugins.protobuf)
+}
+
+android {
+ namespace = "com.example.platform.connectivity.uwb"
+ compileSdk = 36
+
+ defaultConfig {
+ minSdk = 23
+ targetSdk = 35
+
+ testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
+ }
+ kotlinOptions {
+ jvmTarget = "1.8"
+ }
+ compileOptions {
+ sourceCompatibility(JavaVersion.VERSION_1_8)
+ targetCompatibility(JavaVersion.VERSION_1_8)
+ }
+ sourceSets {
+ getByName("main") {
+ java.srcDirs("src/main/java", "src/main/proto")
+ }
+ }
+}
+
+protobuf {
+ // Configures the protoc compiler
+ protoc {
+ // Automatically download protoc from Maven Central
+ artifact = "com.google.protobuf:protoc:3.25.3"
+ }
+
+ // Configures the code generation tasks
+ generateProtoTasks {
+ all().forEach { task ->
+ // Generate standard Kotlin data classes from your .proto files
+ task.plugins {
+ create("kotlin")
+ create("java")
+ }
+ }
+ }
+}
+
+dependencies {
+ implementation(libs.androidx.uwb)
+ implementation(project(mapOf("path" to ":samples:connectivity:audio")))
+
+ implementation(libs.androidx.activity.compose)
+ implementation(libs.androidx.core)
+ implementation(platform(libs.androidx.compose.bom))
+ implementation(libs.androidx.ui)
+ implementation(libs.androidx.ui.tooling.preview)
+ implementation(libs.androidx.material3)
+ implementation(project(":shared"))
+ implementation(libs.androidx.datastore.preferences)
+ implementation(libs.androidx.navigation.compose)
+ implementation(libs.compose.material.iconsext)
+ implementation(libs.kotlin.coroutines.play)
+ implementation("com.google.android.gms:play-services-nearby:19.2.0")
+ implementation("com.google.protobuf:protobuf-java:3.25.3")
+ implementation("com.google.protobuf:protobuf-kotlin:3.25.3")
+ implementation("androidx.datastore:datastore:1.0.0")
+ implementation(libs.androidx.appcompat)
+ implementation(libs.kotlinx.serialization.json)
+ implementation(libs.firebase.crashlytics.buildtools)
+
+
+ androidTestImplementation(libs.androidx.test.core)
+ androidTestImplementation(libs.androidx.test.espresso.core)
+ androidTestImplementation(libs.androidx.test.rules)
+ androidTestImplementation(libs.androidx.test.runner)
+ debugImplementation(libs.androidx.ui.test.manifest)
+
+ androidTestImplementation(libs.hilt.testing)
+ androidTestImplementation(libs.junit4)
+
+ androidTestImplementation(platform(libs.androidx.compose.bom))
+ androidTestImplementation(libs.androidx.ui.test.junit4)
+}
diff --git a/samples/connectivity/UwbRanging/src/main/AndroidManifest.xml b/samples/connectivity/UwbRanging/src/main/AndroidManifest.xml
new file mode 100644
index 00000000..d9cd6d4f
--- /dev/null
+++ b/samples/connectivity/UwbRanging/src/main/AndroidManifest.xml
@@ -0,0 +1,39 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/samples/connectivity/UwbRanging/src/main/java/com/google/uwb/hellouwb/AppContainer.kt b/samples/connectivity/UwbRanging/src/main/java/com/google/uwb/hellouwb/AppContainer.kt
new file mode 100644
index 00000000..450b46d8
--- /dev/null
+++ b/samples/connectivity/UwbRanging/src/main/java/com/google/uwb/hellouwb/AppContainer.kt
@@ -0,0 +1,29 @@
+/*
+ *
+ * Copyright (C) 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
+ *
+ * http://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.google.uwb.hellouwb
+
+import android.content.ContentResolver
+import com.google.uwb.hellouwb.data.SettingsStore
+import com.google.uwb.hellouwb.data.UwbRangingControlSource
+
+interface AppContainer {
+ val rangingResultSource: UwbRangingControlSource
+ val settingsStore: SettingsStore
+ val contentResolver: ContentResolver
+}
diff --git a/samples/connectivity/UwbRanging/src/main/java/com/google/uwb/hellouwb/HelloUwbApplication.kt b/samples/connectivity/UwbRanging/src/main/java/com/google/uwb/hellouwb/HelloUwbApplication.kt
new file mode 100644
index 00000000..42ef6c91
--- /dev/null
+++ b/samples/connectivity/UwbRanging/src/main/java/com/google/uwb/hellouwb/HelloUwbApplication.kt
@@ -0,0 +1,31 @@
+/*
+ *
+ * Copyright (C) 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
+ *
+ * http://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.google.uwb.hellouwb
+
+import android.app.Application
+import com.google.uwb.hellouwb.data.AppContainerImpl
+
+class HelloUwbApplication : Application() {
+
+ lateinit var container: AppContainer
+
+ fun initContainer(afterLoading: () -> Unit) {
+ container = AppContainerImpl(applicationContext, afterLoading)
+ }
+}
diff --git a/samples/connectivity/UwbRanging/src/main/java/com/google/uwb/hellouwb/data/AppContainerImpl.kt b/samples/connectivity/UwbRanging/src/main/java/com/google/uwb/hellouwb/data/AppContainerImpl.kt
new file mode 100644
index 00000000..9953dc92
--- /dev/null
+++ b/samples/connectivity/UwbRanging/src/main/java/com/google/uwb/hellouwb/data/AppContainerImpl.kt
@@ -0,0 +1,62 @@
+/*
+ *
+ * Copyright (C) 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
+ *
+ * http://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.google.uwb.hellouwb.data
+
+import android.content.ContentResolver
+import android.content.Context
+import com.google.uwb.hellouwb.AppContainer
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.launch
+
+internal class AppContainerImpl(
+ private val context: Context,
+ afterLoading: () -> Unit,
+) : AppContainer {
+
+ private val coroutineScope = CoroutineScope(Dispatchers.IO + Job())
+
+ override val rangingResultSource: UwbRangingControlSource
+ get() =
+ _rangingResultSource
+ ?: throw IllegalStateException("rangingResultSource only can be accessed after loading.")
+
+ private var _rangingResultSource: UwbRangingControlSource? = null
+
+ override val settingsStore = SettingsStoreImpl(context, coroutineScope)
+
+ override val contentResolver: ContentResolver = context.contentResolver
+
+ init {
+ coroutineScope.launch {
+ settingsStore.appSettings.collect {
+ val endpointId = it.deviceDisplayName + "|" + it.deviceUuid
+ if (_rangingResultSource == null) {
+ _rangingResultSource =
+ UwbRangingControlSourceImpl(context, endpointId, coroutineScope)
+ afterLoading()
+ } else {
+ rangingResultSource.deviceType = it.deviceType
+ rangingResultSource.updateEndpointId(endpointId)
+ }
+ }
+ }
+ }
+}
diff --git a/samples/connectivity/UwbRanging/src/main/java/com/google/uwb/hellouwb/data/SettingsStore.kt b/samples/connectivity/UwbRanging/src/main/java/com/google/uwb/hellouwb/data/SettingsStore.kt
new file mode 100644
index 00000000..78f946e7
--- /dev/null
+++ b/samples/connectivity/UwbRanging/src/main/java/com/google/uwb/hellouwb/data/SettingsStore.kt
@@ -0,0 +1,33 @@
+/*
+ *
+ * Copyright (C) 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
+ *
+ * http://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.google.uwb.hellouwb.data
+
+import kotlinx.coroutines.flow.StateFlow
+
+/** Loads and updates [AppSettings]. */
+interface SettingsStore {
+
+ val appSettings: StateFlow
+
+ fun updateDeviceType(deviceType: DeviceType)
+
+ fun updateConfigType(configType: ConfigType)
+
+ fun updateDeviceDisplayName(displayName: String)
+}
diff --git a/samples/connectivity/UwbRanging/src/main/java/com/google/uwb/hellouwb/data/SettingsStoreImpl.kt b/samples/connectivity/UwbRanging/src/main/java/com/google/uwb/hellouwb/data/SettingsStoreImpl.kt
new file mode 100644
index 00000000..a2902f59
--- /dev/null
+++ b/samples/connectivity/UwbRanging/src/main/java/com/google/uwb/hellouwb/data/SettingsStoreImpl.kt
@@ -0,0 +1,98 @@
+/*
+ *
+ * Copyright (C) 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
+ *
+ * http://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.google.uwb.hellouwb.data
+
+import android.content.Context
+import androidx.datastore.core.CorruptionException
+import androidx.datastore.core.DataStore
+import androidx.datastore.core.Serializer
+import androidx.datastore.dataStore
+import com.google.protobuf.InvalidProtocolBufferException
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.*
+import kotlinx.coroutines.launch
+import java.io.InputStream
+import java.io.OutputStream
+import java.util.*
+
+internal class SettingsStoreImpl(
+ private val context: Context,
+ private val coroutineScope: CoroutineScope,
+) : SettingsStore {
+
+ private val Context.settingsDataStore: DataStore by
+ dataStore(fileName = STORE_FILE_NAME, serializer = SettingsSerializer)
+
+ override val appSettings: StateFlow =
+ MutableStateFlow(SettingsSerializer.defaultValue)
+
+ init {
+
+ context.settingsDataStore.data
+ .onEach { settings -> (appSettings as MutableStateFlow).update { settings } }
+ .shareIn(coroutineScope, SharingStarted.Eagerly)
+ }
+
+ override fun updateDeviceType(deviceType: DeviceType) {
+ coroutineScope.launch {
+ context.settingsDataStore.updateData { settings ->
+ settings.toBuilder().setDeviceType(deviceType).build()
+ }
+ }
+ }
+
+ override fun updateConfigType(configType: ConfigType) {
+ coroutineScope.launch {
+ context.settingsDataStore.updateData { settings ->
+ settings.toBuilder().setConfigType(configType).build()
+ }
+ }
+ }
+
+ override fun updateDeviceDisplayName(displayName: String) {
+ coroutineScope.launch {
+ context.settingsDataStore.updateData { settings ->
+ settings.toBuilder().setDeviceDisplayName(displayName).build()
+ }
+ }
+ }
+
+ companion object {
+ private const val STORE_FILE_NAME = "app_settings.pb"
+
+ private object SettingsSerializer : Serializer {
+ override val defaultValue: AppSettings =
+ AppSettings.newBuilder()
+ .setDeviceType(DeviceType.CONTROLLER)
+ .setDeviceDisplayName("UWB")
+ .setDeviceUuid(UUID.randomUUID().toString())
+ .build()
+
+ override suspend fun readFrom(input: InputStream): AppSettings {
+ try {
+ return AppSettings.parseFrom(input)
+ } catch (exception: InvalidProtocolBufferException) {
+ throw CorruptionException("Cannot read proto.", exception)
+ }
+ }
+
+ override suspend fun writeTo(t: AppSettings, output: OutputStream) = t.writeTo(output)
+ }
+ }
+}
diff --git a/samples/connectivity/UwbRanging/src/main/java/com/google/uwb/hellouwb/data/UwbRangingControlSource.kt b/samples/connectivity/UwbRanging/src/main/java/com/google/uwb/hellouwb/data/UwbRangingControlSource.kt
new file mode 100644
index 00000000..f6e55640
--- /dev/null
+++ b/samples/connectivity/UwbRanging/src/main/java/com/google/uwb/hellouwb/data/UwbRangingControlSource.kt
@@ -0,0 +1,43 @@
+/*
+ *
+ * Copyright (C) 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
+ *
+ * http://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.google.uwb.hellouwb.data
+
+import com.google.uwb.uwbranging.EndpointEvents
+import com.google.uwb.uwbranging.UwbEndpoint
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.StateFlow
+
+interface UwbRangingControlSource {
+
+ fun observeRangingResults(): Flow
+
+ var deviceType: DeviceType
+
+ var configType: ConfigType
+
+ fun updateEndpointId(id: String)
+
+ fun start()
+
+ fun stop()
+
+ fun sendOobMessage(endpoint: UwbEndpoint, message: ByteArray)
+
+ val isRunning: StateFlow
+}
diff --git a/samples/connectivity/UwbRanging/src/main/java/com/google/uwb/hellouwb/data/UwbRangingControlSourceImpl.kt b/samples/connectivity/UwbRanging/src/main/java/com/google/uwb/hellouwb/data/UwbRangingControlSourceImpl.kt
new file mode 100644
index 00000000..bfb46c1c
--- /dev/null
+++ b/samples/connectivity/UwbRanging/src/main/java/com/google/uwb/hellouwb/data/UwbRangingControlSourceImpl.kt
@@ -0,0 +1,124 @@
+/*
+ *
+ * Copyright (C) 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
+ *
+ * http://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.google.uwb.hellouwb.data
+
+import android.content.Context
+import androidx.core.uwb.RangingParameters
+import com.google.uwb.uwbranging.EndpointEvents
+import com.google.uwb.uwbranging.UwbConnectionManager
+import com.google.uwb.uwbranging.UwbEndpoint
+import com.google.uwb.uwbranging.UwbSessionScope
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.channels.BufferOverflow
+import kotlinx.coroutines.flow.*
+import kotlinx.coroutines.launch
+import java.security.SecureRandom
+import kotlin.properties.Delegates
+
+internal class UwbRangingControlSourceImpl(
+ context: Context,
+ endpointId: String,
+ private val coroutineScope: CoroutineScope,
+ private val uwbConnectionManager: UwbConnectionManager =
+ UwbConnectionManager.getInstance(context),
+) : UwbRangingControlSource {
+
+ private var uwbEndpoint = UwbEndpoint(endpointId, SecureRandom.getSeed(8))
+
+ private var uwbSessionScope: UwbSessionScope =
+ getSessionScope(DeviceType.CONTROLLER, ConfigType.CONFIG_UNICAST_DS_TWR)
+
+ private var rangingJob: Job? = null
+
+ private val resultFlow = MutableSharedFlow(
+ replay = 0,
+ onBufferOverflow = BufferOverflow.DROP_OLDEST,
+ extraBufferCapacity = 1
+ )
+
+ private val runningStateFlow = MutableStateFlow(false)
+
+ override val isRunning = runningStateFlow.asStateFlow()
+
+ private fun getSessionScope(deviceType: DeviceType, configType: ConfigType): UwbSessionScope {
+ return when (deviceType) {
+ DeviceType.CONTROLEE -> uwbConnectionManager.controleeUwbScope(uwbEndpoint)
+ DeviceType.CONTROLLER ->
+ uwbConnectionManager.controllerUwbScope(uwbEndpoint, when (configType) {
+ ConfigType.CONFIG_UNICAST_DS_TWR -> RangingParameters.CONFIG_UNICAST_DS_TWR
+ ConfigType.CONFIG_MULTICAST_DS_TWR -> RangingParameters.CONFIG_MULTICAST_DS_TWR
+ ConfigType.CONFIG_PROVISIONED_UNICAST -> RangingParameters.CONFIG_PROVISIONED_UNICAST_DS_TWR
+ else -> throw java.lang.IllegalStateException()
+ })
+ else -> throw IllegalStateException()
+ }
+ }
+
+ override fun observeRangingResults(): Flow {
+ return resultFlow
+ }
+
+ override var deviceType: DeviceType by
+ Delegates.observable(DeviceType.CONTROLLER) { _, oldValue, newValue ->
+ if (oldValue != newValue) {
+ stop()
+ uwbSessionScope = getSessionScope(newValue, configType)
+ }
+ }
+
+ override var configType: ConfigType by
+ Delegates.observable(ConfigType.CONFIG_UNICAST_DS_TWR) { _, oldValue, newValue ->
+ if (oldValue != newValue) {
+ stop()
+ uwbSessionScope = getSessionScope(deviceType, newValue)
+ }
+ }
+
+ override fun updateEndpointId(id: String) {
+ if (id != uwbEndpoint.id) {
+ stop()
+ uwbEndpoint = UwbEndpoint(id, SecureRandom.getSeed(8))
+ uwbSessionScope = getSessionScope(deviceType, configType)
+ }
+ }
+
+ override fun start() {
+ if (rangingJob == null) {
+ rangingJob =
+ coroutineScope.launch {
+ uwbSessionScope.prepareSession().collect {
+ resultFlow.tryEmit(it)
+ }
+ }
+ runningStateFlow.update { true }
+ }
+ }
+
+ override fun stop() {
+ val job = rangingJob ?: return
+ job.cancel()
+ rangingJob = null
+ runningStateFlow.update { false }
+ }
+
+ override fun sendOobMessage(endpoint: UwbEndpoint, message: ByteArray) {
+ uwbSessionScope.sendMessage(endpoint, message)
+ }
+}
diff --git a/samples/connectivity/UwbRanging/src/main/java/com/google/uwb/hellouwb/ui/HelloUwbApp.kt b/samples/connectivity/UwbRanging/src/main/java/com/google/uwb/hellouwb/ui/HelloUwbApp.kt
new file mode 100644
index 00000000..7c50a1ab
--- /dev/null
+++ b/samples/connectivity/UwbRanging/src/main/java/com/google/uwb/hellouwb/ui/HelloUwbApp.kt
@@ -0,0 +1,43 @@
+/*
+ *
+ * Copyright (C) 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
+ *
+ * http://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.google.uwb.hellouwb.ui
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
+import androidx.lifecycle.viewmodel.compose.viewModel
+import com.google.uwb.hellouwb.AppContainer
+import com.google.uwb.hellouwb.ui.nav.AppNavBar
+import com.google.uwb.hellouwb.ui.ranging.RangingViewModel
+import com.google.uwb.hellouwb.ui.theme.HellouwbTheme
+
+@Composable
+fun HelloUwbApp(appContainer: AppContainer) {
+ val rangingViewModel: RangingViewModel =
+ viewModel(factory = RangingViewModel.provideFactory(appContainer.rangingResultSource))
+ val uiState by rangingViewModel.uiState.collectAsState()
+ HellouwbTheme {
+ AppNavBar(
+ appContainer = appContainer,
+ isRanging = uiState,
+ startRanging = { rangingViewModel.startRanging() },
+ stopRanging = { rangingViewModel.stopRanging() }
+ )
+ }
+}
diff --git a/samples/connectivity/UwbRanging/src/main/java/com/google/uwb/hellouwb/ui/Screen.kt b/samples/connectivity/UwbRanging/src/main/java/com/google/uwb/hellouwb/ui/Screen.kt
new file mode 100644
index 00000000..08a2c643
--- /dev/null
+++ b/samples/connectivity/UwbRanging/src/main/java/com/google/uwb/hellouwb/ui/Screen.kt
@@ -0,0 +1,33 @@
+/*
+ *
+ * Copyright (C) 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
+ *
+ * http://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.google.uwb.hellouwb.ui
+
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Home
+import androidx.compose.material.icons.filled.Send
+import androidx.compose.material.icons.filled.Settings
+import androidx.compose.material.icons.filled.SettingsRemote
+import androidx.compose.ui.graphics.vector.ImageVector
+
+sealed class Screen(val route: String, val title: String, val icon: ImageVector) {
+ object Home : Screen("home", "Ranging", Icons.Filled.Home)
+ object Control : Screen("control", "Control", Icons.Filled.SettingsRemote)
+ object Send : Screen("send", "Share file", Icons.Filled.Send)
+ object Settings : Screen("settings", "Settings", Icons.Filled.Settings)
+}
diff --git a/samples/connectivity/UwbRanging/src/main/java/com/google/uwb/hellouwb/ui/UwbRangingActivity.kt b/samples/connectivity/UwbRanging/src/main/java/com/google/uwb/hellouwb/ui/UwbRangingActivity.kt
new file mode 100644
index 00000000..539b1a7a
--- /dev/null
+++ b/samples/connectivity/UwbRanging/src/main/java/com/google/uwb/hellouwb/ui/UwbRangingActivity.kt
@@ -0,0 +1,120 @@
+/*
+ *
+ * Copyright (C) 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
+ *
+ * http://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.google.uwb.hellouwb.ui
+
+import android.Manifest
+import android.content.pm.PackageManager
+import android.os.Build
+import android.os.Bundle
+import android.util.Log
+import android.widget.Toast
+import androidx.activity.ComponentActivity
+import androidx.activity.compose.setContent
+import com.google.uwb.hellouwb.HelloUwbApplication
+
+
+private const val PERMISSION_REQUEST_CODE = 1234
+
+class UwbRangingActivity : ComponentActivity() {
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ requestPermissions()
+ (application as HelloUwbApplication).initContainer {
+ runOnUiThread { setContent { HelloUwbApp((application as HelloUwbApplication).container) } }
+ }
+
+ /**
+ * Check if device supports Ultra-wideband
+ */
+ val packageManager: PackageManager = applicationContext.packageManager
+ val deviceSupportsUwb = packageManager.hasSystemFeature("android.hardware.uwb")
+
+ if (!deviceSupportsUwb ) {
+ Log.e("UWB Sample", "Device does not support Ultra-wideband")
+ Toast.makeText(applicationContext, "Device does not support UWB", Toast.LENGTH_SHORT).show()
+ //TODO: Uncomment this if you want to see it running on a non-supported device
+ finishAndRemoveTask();
+ }
+ else {
+ Toast.makeText(applicationContext, "Device supports UWB", Toast.LENGTH_SHORT).show()
+ }
+ }
+
+ private fun requestPermissions() {
+ if (!arePermissionsGranted()) {
+ requestPermissions(PERMISSIONS_REQUIRED, PERMISSION_REQUEST_CODE)
+ }
+ }
+
+ private fun arePermissionsGranted(): Boolean {
+ for (permission in PERMISSIONS_REQUIRED) {
+ if (checkCallingOrSelfPermission(permission) != PackageManager.PERMISSION_GRANTED) {
+ return false
+ }
+ }
+ return true
+ }
+
+ override fun onRequestPermissionsResult(
+ requestCode: Int,
+ permissions: Array,
+ grantResults: IntArray,
+ ) {
+ super.onRequestPermissionsResult(requestCode, permissions, grantResults)
+ for (result in grantResults) {
+ if (result != PackageManager.PERMISSION_GRANTED) {
+ requestPermissions()
+ }
+ }
+ }
+
+ companion object {
+
+ private val PERMISSIONS_REQUIRED_BEFORE_T =
+ listOf(
+ // Permissions needed by Nearby Connection
+ Manifest.permission.BLUETOOTH,
+ Manifest.permission.BLUETOOTH_ADMIN,
+ Manifest.permission.BLUETOOTH_SCAN,
+ Manifest.permission.BLUETOOTH_ADVERTISE,
+ Manifest.permission.BLUETOOTH_CONNECT,
+ Manifest.permission.ACCESS_FINE_LOCATION,
+ Manifest.permission.ACCESS_WIFI_STATE,
+ Manifest.permission.CHANGE_WIFI_STATE,
+
+ // permission required by UWB API
+ Manifest.permission.UWB_RANGING
+ )
+
+ private val PERMISSIONS_REQUIRED_T =
+ arrayOf(
+ Manifest.permission.NEARBY_WIFI_DEVICES,
+ )
+
+ private val PERMISSIONS_REQUIRED =
+ PERMISSIONS_REQUIRED_BEFORE_T.toMutableList()
+ .apply {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ addAll(PERMISSIONS_REQUIRED_T)
+ }
+ }
+ .toTypedArray()
+ }
+}
diff --git a/samples/connectivity/UwbRanging/src/main/java/com/google/uwb/hellouwb/ui/control/ControlRoute.kt b/samples/connectivity/UwbRanging/src/main/java/com/google/uwb/hellouwb/ui/control/ControlRoute.kt
new file mode 100644
index 00000000..7cbf9f62
--- /dev/null
+++ b/samples/connectivity/UwbRanging/src/main/java/com/google/uwb/hellouwb/ui/control/ControlRoute.kt
@@ -0,0 +1,31 @@
+/*
+ *
+ * Copyright (C) 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
+ *
+ * http://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.google.uwb.hellouwb.ui.control
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
+
+@Composable
+fun ControlRoute(controlViewModel : ControlViewModel) {
+ //ControlScreen()
+ val uiState by controlViewModel.uiState.collectAsState()
+ ControlScreen(uiState = uiState)
+
+}
diff --git a/samples/connectivity/UwbRanging/src/main/java/com/google/uwb/hellouwb/ui/control/ControlScreen.kt b/samples/connectivity/UwbRanging/src/main/java/com/google/uwb/hellouwb/ui/control/ControlScreen.kt
new file mode 100644
index 00000000..9d53c5e0
--- /dev/null
+++ b/samples/connectivity/UwbRanging/src/main/java/com/google/uwb/hellouwb/ui/control/ControlScreen.kt
@@ -0,0 +1,90 @@
+/*
+ *
+ * Copyright (C) 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
+ *
+ * http://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.google.uwb.hellouwb.ui.control
+
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.layout.*
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Key
+import androidx.compose.material.icons.filled.Lock
+import androidx.compose.material.icons.filled.LockOpen
+import androidx.compose.material3.CenterAlignedTopAppBar
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun ControlScreen(uiState: ControlUiState, modifier: Modifier = Modifier) {
+
+ CenterAlignedTopAppBar(
+ title = { androidx.compose.material3.Text("Device Control") }, modifier = modifier
+ )
+
+ Column(
+ modifier = Modifier
+ .padding(100.dp)
+ .fillMaxWidth()
+ .fillMaxHeight(),
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ if (uiState is ControlUiState.KeyState) {
+ KeyScreen()
+ } else if (uiState is ControlUiState.LockState) {
+ LockScreen(isLocked = uiState.isLocked)
+ }
+ }
+}
+
+@Composable
+fun LockScreen(isLocked: Boolean) {
+ val icon = if (isLocked) Icons.Filled.Lock else Icons.Filled.LockOpen
+ Image(
+ imageVector = icon,
+ modifier = Modifier
+ .height(200.dp)
+ .fillMaxWidth(),
+ contentDescription = null
+ )
+}
+
+@Composable
+fun KeyScreen() {
+ val icon = Icons.Filled.Key
+
+ Image(
+ imageVector = icon,
+ modifier = Modifier
+ .height(200.dp)
+ .fillMaxWidth(),
+ contentDescription = null
+ )
+
+}
+
+@Preview
+@Composable
+fun PreviewControlScreen() {
+ ControlScreen(
+ uiState = ControlUiState.KeyState
+ )
+}
diff --git a/samples/connectivity/UwbRanging/src/main/java/com/google/uwb/hellouwb/ui/control/ControlViewModel.kt b/samples/connectivity/UwbRanging/src/main/java/com/google/uwb/hellouwb/ui/control/ControlViewModel.kt
new file mode 100644
index 00000000..5c73f592
--- /dev/null
+++ b/samples/connectivity/UwbRanging/src/main/java/com/google/uwb/hellouwb/ui/control/ControlViewModel.kt
@@ -0,0 +1,116 @@
+/*
+ *
+ * Copyright (C) 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
+ *
+ * http://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.google.uwb.hellouwb.ui.control
+
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.ViewModelProvider
+import androidx.lifecycle.viewModelScope
+import com.google.uwb.hellouwb.data.DeviceType
+import com.google.uwb.hellouwb.data.SettingsStore
+import com.google.uwb.hellouwb.data.UwbRangingControlSource
+import com.google.uwb.uwbranging.EndpointEvents
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.flow.*
+import kotlinx.coroutines.launch
+
+private const val LOCK_DISTANCE = 2.0f
+private const val UNLOCK_DISTANCE = 0.25f
+
+class ControlViewModel(
+ private val uwbRangingControlSource: UwbRangingControlSource,
+ settingsStore: SettingsStore
+) : ViewModel() {
+
+ private val _uiState: MutableStateFlow =
+ MutableStateFlow(ControlUiState.KeyState)
+
+ val uiState = _uiState.asStateFlow()
+
+ private var lockJob: Job? = null
+
+ private fun startLockObserving(): Job {
+ return CoroutineScope(Dispatchers.IO).launch {
+ launch {
+ uwbRangingControlSource
+ .observeRangingResults()
+ .filterIsInstance()
+ .collect {
+ it.position.distance?.let {
+ val state = _uiState.value as ControlUiState.LockState
+ if (!state.isLocked && it.value > LOCK_DISTANCE) {
+ _uiState.update { ControlUiState.LockState(isLocked = true) }
+ }
+ if (state.isLocked && it.value < UNLOCK_DISTANCE) {
+ _uiState.update { ControlUiState.LockState(isLocked = false) }
+ }
+ }
+ }
+ }
+ launch {
+ uwbRangingControlSource.isRunning.collect {
+ val state = _uiState.value as ControlUiState.LockState
+ if (!state.isLocked && !it) {
+ _uiState.update { ControlUiState.LockState(isLocked = true) }
+ }
+ }
+ }
+ }
+ }
+
+ init {
+ settingsStore.appSettings
+ .onEach {
+ lockJob?.cancel()
+ lockJob = null
+ when (it.deviceType) {
+ DeviceType.CONTROLEE -> _uiState.update { ControlUiState.KeyState }
+ DeviceType.CONTROLLER -> {
+ if (_uiState.value !is ControlUiState.LockState) {
+ _uiState.update { ControlUiState.LockState(isLocked = true) }
+ lockJob = startLockObserving()
+ }
+ }
+ else -> {}
+ }
+ }
+ .launchIn(viewModelScope)
+ }
+
+ companion object {
+ fun provideFactory(
+ uwbRangingControlSource: UwbRangingControlSource,
+ settingsStore: SettingsStore
+ ): ViewModelProvider.Factory =
+ object : ViewModelProvider.Factory {
+ @Suppress("UNCHECKED_CAST")
+ override fun create(modelClass: Class): T {
+ return ControlViewModel(uwbRangingControlSource, settingsStore) as T
+ }
+ }
+ }
+}
+
+sealed class ControlUiState {
+
+ data class LockState(val isLocked: Boolean) : ControlUiState()
+
+ object KeyState : ControlUiState()
+}
diff --git a/samples/connectivity/UwbRanging/src/main/java/com/google/uwb/hellouwb/ui/home/HomeRoute.kt b/samples/connectivity/UwbRanging/src/main/java/com/google/uwb/hellouwb/ui/home/HomeRoute.kt
new file mode 100644
index 00000000..af3fdd51
--- /dev/null
+++ b/samples/connectivity/UwbRanging/src/main/java/com/google/uwb/hellouwb/ui/home/HomeRoute.kt
@@ -0,0 +1,29 @@
+/*
+ *
+ * Copyright (C) 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
+ *
+ * http://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.google.uwb.hellouwb.ui.home
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
+
+@Composable
+fun HomeRoute(homeViewModel: HomeViewModel) {
+ val uiState by homeViewModel.uiState.collectAsState()
+ HomeScreen(uiState = uiState)
+}
diff --git a/samples/connectivity/UwbRanging/src/main/java/com/google/uwb/hellouwb/ui/home/HomeScreen.kt b/samples/connectivity/UwbRanging/src/main/java/com/google/uwb/hellouwb/ui/home/HomeScreen.kt
new file mode 100644
index 00000000..8418edb9
--- /dev/null
+++ b/samples/connectivity/UwbRanging/src/main/java/com/google/uwb/hellouwb/ui/home/HomeScreen.kt
@@ -0,0 +1,233 @@
+/*
+ *
+ * Copyright (C) 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
+ *
+ * http://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.google.uwb.hellouwb.ui.home
+
+import androidx.compose.foundation.Canvas
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.*
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.NearMe
+import androidx.compose.material.icons.filled.NearMeDisabled
+import androidx.compose.material3.*
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.ColorFilter
+import androidx.compose.ui.graphics.PathEffect
+import androidx.compose.ui.graphics.drawscope.DrawScope
+import androidx.compose.ui.graphics.drawscope.Stroke
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import androidx.core.uwb.RangingMeasurement
+import androidx.core.uwb.RangingPosition
+import com.google.uwb.uwbranging.UwbEndpoint
+import kotlin.math.PI
+import kotlin.math.cos
+import kotlin.math.sin
+
+private val ENDPOINT_COLORS =
+ arrayListOf(
+ Color.Red,
+ Color.Blue,
+ Color.Green,
+ Color.Cyan,
+ Color.Magenta,
+ Color.DarkGray,
+ Color.Yellow
+ )
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun HomeScreen(uiState: HomeUiState, modifier: Modifier = Modifier) {
+ val topAppBarState = rememberTopAppBarState()
+
+ Scaffold(
+ topBar = { HomeTopAppBar(isRanging = uiState.isRanging, topAppBarState = topAppBarState) },
+ modifier = modifier
+ ) { innerPadding ->
+ Column(modifier = Modifier.padding(innerPadding)) {
+ Row(modifier = Modifier.padding(innerPadding)) {
+ ConnectStatusBar(
+ uiState.connectedEndpoints.map { it.endpoint },
+ uiState.disconnectedEndpoints
+ )
+ }
+ Row { RangingPlot(uiState.connectedEndpoints) }
+ }
+ }
+}
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun HomeTopAppBar(
+ isRanging: Boolean,
+ modifier: Modifier = Modifier,
+ topAppBarState: TopAppBarState = rememberTopAppBarState(),
+ scrollBehavior: TopAppBarScrollBehavior? =
+ TopAppBarDefaults.enterAlwaysScrollBehavior(topAppBarState),
+) {
+ CenterAlignedTopAppBar(
+ title = { Text("UWB Ranging") },
+ actions = {
+ val icon = if (isRanging) Icons.Filled.NearMe else Icons.Filled.NearMeDisabled
+ val iconColor = if (isRanging) Color.Green else Color.DarkGray
+ Image(
+ imageVector = icon,
+ colorFilter = ColorFilter.tint(iconColor),
+ contentDescription = null
+ )
+ },
+ scrollBehavior = scrollBehavior,
+ modifier = modifier
+ )
+}
+
+@Composable
+fun RangingPlot(connectedEndpoints: List) {
+ Canvas(modifier = Modifier.fillMaxSize().background(color = Color.White)) {
+ // Assume the canvas is 20 meters wide.
+ val center = Offset(size.width / 2.0f, size.height / 2.0f)
+ val scale = drawPolar(center)
+ connectedEndpoints.forEachIndexed { index, endpoint ->
+ endpoint.position.distance?.let { distance ->
+ endpoint.position.azimuth?.let { azimuth ->
+ drawPosition(
+ distance.value,
+ azimuth.value,
+ scale = scale,
+ centerOffset = center,
+ color = ENDPOINT_COLORS[index % ENDPOINT_COLORS.size]
+ )
+ }
+ }
+ }
+ }
+}
+
+private fun DrawScope.drawPolar(centerOffset: Offset): Float {
+ val scale = size.minDimension / 20.0f
+ (1..10).forEach {
+ drawCircle(
+ center = centerOffset,
+ color = Color.DarkGray,
+ radius = it * scale,
+ style = Stroke(2f)
+ )
+ }
+
+ val angles = floatArrayOf(0f, 30f, 60f, 90f, 120f, 150f)
+ angles.forEach {
+ val rad = it * PI / 180
+ val start =
+ center + Offset((scale * 10f * cos(rad)).toFloat(), (scale * 10f * sin(rad)).toFloat())
+ val end =
+ center - Offset((scale * 10f * cos(rad)).toFloat(), (scale * 10f * sin(rad)).toFloat())
+ drawLine(
+ color = Color.DarkGray,
+ start = start,
+ end = end,
+ strokeWidth = 2f,
+ pathEffect = PathEffect.dashPathEffect(floatArrayOf(5.0f, 5.0f), 10f)
+ )
+ }
+ return scale
+}
+
+private fun DrawScope.drawPosition(
+ distance: Float,
+ azimuth: Float,
+ scale: Float,
+ centerOffset: Offset,
+ color: Color,
+) {
+ val angle = azimuth * PI / 180
+ val x = distance * sin(angle).toFloat()
+ val y = distance * cos(angle).toFloat()
+ drawCircle(
+ center = centerOffset.plus(Offset(x * scale, -y * scale)),
+ color = color,
+ radius = 15.0f
+ )
+}
+
+@Composable
+fun ConnectStatusBar(
+ connectedEndpoints: List,
+ disconnectedEndpoints: List,
+ modifier: Modifier = Modifier,
+) {
+ Box(modifier.height(50.dp)) {
+ Column {
+ //
+ Row {
+ connectedEndpoints.forEachIndexed { index, endpoint ->
+ Text(
+ modifier = Modifier.width(100.dp),
+ text = endpoint.id.split("|")[0],
+ color = ENDPOINT_COLORS[index % ENDPOINT_COLORS.size]
+ )
+ }
+ }
+ Row {
+ disconnectedEndpoints.forEach { endpoint ->
+ Text(modifier = Modifier.width(100.dp), text = endpoint.id, color = Color.DarkGray)
+ }
+ }
+ }
+ }
+}
+
+@Preview
+@Composable
+fun PreviewHomeScreen(modifier: Modifier = Modifier) {
+ HomeScreen(
+ uiState =
+ object : HomeUiState {
+ override val connectedEndpoints =
+ listOf(
+ ConnectedEndpoint(
+ UwbEndpoint("EP1", byteArrayOf()),
+ RangingPosition(
+ distance = RangingMeasurement(2.0f),
+ azimuth = RangingMeasurement(10.0f),
+ elevation = null,
+ elapsedRealtimeNanos = 200L
+ ),
+ ),
+ ConnectedEndpoint(
+ UwbEndpoint("EP2", byteArrayOf()),
+ RangingPosition(
+ distance = RangingMeasurement(10.0f),
+ azimuth = RangingMeasurement(-10.0f),
+ elevation = null,
+ elapsedRealtimeNanos = 200L
+ ),
+ )
+ )
+
+ override val disconnectedEndpoints: List =
+ listOf(UwbEndpoint("EP3", byteArrayOf()), UwbEndpoint("EP4", byteArrayOf()))
+
+ override val isRanging = true
+ },
+ modifier = modifier
+ )
+}
diff --git a/samples/connectivity/UwbRanging/src/main/java/com/google/uwb/hellouwb/ui/home/HomeViewModel.kt b/samples/connectivity/UwbRanging/src/main/java/com/google/uwb/hellouwb/ui/home/HomeViewModel.kt
new file mode 100644
index 00000000..9353198b
--- /dev/null
+++ b/samples/connectivity/UwbRanging/src/main/java/com/google/uwb/hellouwb/ui/home/HomeViewModel.kt
@@ -0,0 +1,111 @@
+/*
+ *
+ * Copyright (C) 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
+ *
+ * http://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.google.uwb.hellouwb.ui.home
+
+import androidx.core.uwb.RangingPosition
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.ViewModelProvider
+import androidx.lifecycle.viewModelScope
+import com.google.uwb.hellouwb.data.UwbRangingControlSource
+import com.google.uwb.uwbranging.EndpointEvents
+import com.google.uwb.uwbranging.UwbEndpoint
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.flow.*
+
+class HomeViewModel(uwbRangingControlSource: UwbRangingControlSource) : ViewModel() {
+
+ private val _uiState: MutableStateFlow =
+ MutableStateFlow(HomeUiStateImpl(listOf(), listOf(), false))
+
+ private val endpoints = mutableListOf()
+ private val endpointPositions = mutableMapOf()
+ private var isRanging = false
+
+ private fun updateUiState(): HomeUiState {
+ return HomeUiStateImpl(
+ endpoints
+ .mapNotNull { endpoint ->
+ endpointPositions[endpoint]?.let { position -> ConnectedEndpoint(endpoint, position) }
+ }
+ .toList(),
+ endpoints.filter { !endpointPositions.containsKey(it) }.toList(),
+ isRanging
+ )
+ }
+
+ val uiState = _uiState.asStateFlow()
+
+ init {
+ uwbRangingControlSource
+ .observeRangingResults()
+ .onEach { result ->
+ when (result) {
+ is EndpointEvents.EndpointFound -> endpoints.add(result.endpoint)
+ is EndpointEvents.UwbDisconnected -> endpointPositions.remove(result.endpoint)
+ is EndpointEvents.PositionUpdated -> endpointPositions[result.endpoint] = result.position
+ is EndpointEvents.EndpointLost -> {
+ endpoints.remove(result.endpoint)
+
+ endpointPositions.remove(result.endpoint)
+ }
+ else -> return@onEach
+ }
+ _uiState.update { updateUiState() }
+ }
+ .launchIn(viewModelScope)
+
+ uwbRangingControlSource.isRunning
+ .onEach { running ->
+ isRanging = running
+ if (!running) {
+ endpoints.clear()
+ endpointPositions.clear()
+ }
+ _uiState.update { updateUiState() }
+ }
+ .launchIn(CoroutineScope(Dispatchers.IO))
+ }
+
+ private data class HomeUiStateImpl(
+ override val connectedEndpoints: List,
+ override val disconnectedEndpoints: List,
+ override val isRanging: Boolean,
+ ) : HomeUiState
+
+ companion object {
+ fun provideFactory(
+ uwbRangingControlSource: UwbRangingControlSource
+ ): ViewModelProvider.Factory =
+ object : ViewModelProvider.Factory {
+ @Suppress("UNCHECKED_CAST")
+ override fun create(modelClass: Class): T {
+ return HomeViewModel(uwbRangingControlSource) as T
+ }
+ }
+ }
+}
+
+interface HomeUiState {
+ val connectedEndpoints: List
+ val disconnectedEndpoints: List
+ val isRanging: Boolean
+}
+
+data class ConnectedEndpoint(val endpoint: UwbEndpoint, val position: RangingPosition)
diff --git a/samples/connectivity/UwbRanging/src/main/java/com/google/uwb/hellouwb/ui/nav/AppNavBar.kt b/samples/connectivity/UwbRanging/src/main/java/com/google/uwb/hellouwb/ui/nav/AppNavBar.kt
new file mode 100644
index 00000000..1f2cd9d9
--- /dev/null
+++ b/samples/connectivity/UwbRanging/src/main/java/com/google/uwb/hellouwb/ui/nav/AppNavBar.kt
@@ -0,0 +1,84 @@
+/*
+ *
+ * Copyright (C) 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
+ *
+ * http://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.google.uwb.hellouwb.ui.nav
+
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.material3.*
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color.Companion.White
+import androidx.navigation.NavDestination.Companion.hierarchy
+import androidx.navigation.compose.currentBackStackEntryAsState
+import androidx.navigation.compose.rememberNavController
+import com.google.uwb.hellouwb.AppContainer
+import com.google.uwb.hellouwb.ui.Screen
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun AppNavBar(
+ appContainer: AppContainer,
+ isRanging: Boolean,
+ startRanging: () -> Unit,
+ stopRanging: () -> Unit,
+) {
+ val navController = rememberNavController()
+ val rangingState = remember { mutableStateOf(isRanging) }
+ Scaffold(
+ bottomBar = {
+ NavigationBar {
+ val navBackStackEntry by navController.currentBackStackEntryAsState()
+ val currentDestination = navBackStackEntry?.destination
+ items.forEach { screen ->
+ NavigationBarItem(
+ icon = { Image(imageVector = screen.icon, contentDescription = null) },
+ label = { Text(screen.title) },
+ selected = currentDestination?.hierarchy?.any { it.route == screen.route } == true,
+ onClick = { AppNavigation(navController).navTo(screen.route) }
+ )
+ }
+ }
+ },
+ floatingActionButtonPosition = FabPosition.End,
+ floatingActionButton = {
+ FloatingActionButton(shape = CircleShape, onClick = {}, contentColor = White) {
+ RangingControlIcon(selected = rangingState.value) {
+ rangingState.value = it
+ if (it) {
+ startRanging()
+ } else {
+ stopRanging()
+ }
+ }
+ }
+ }
+ ) { innerPadding ->
+ AppNavGraph(
+ appContainer = appContainer,
+ modifier = Modifier.padding(innerPadding),
+ navController = navController
+ )
+ }
+}
+
+private val items = listOf(Screen.Home, Screen.Control, Screen.Send, Screen.Settings)
diff --git a/samples/connectivity/UwbRanging/src/main/java/com/google/uwb/hellouwb/ui/nav/AppNavGraph.kt b/samples/connectivity/UwbRanging/src/main/java/com/google/uwb/hellouwb/ui/nav/AppNavGraph.kt
new file mode 100644
index 00000000..0afdd3ee
--- /dev/null
+++ b/samples/connectivity/UwbRanging/src/main/java/com/google/uwb/hellouwb/ui/nav/AppNavGraph.kt
@@ -0,0 +1,86 @@
+/*
+ *
+ * Copyright (C) 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
+ *
+ * http://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.google.uwb.hellouwb.ui.nav
+
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.lifecycle.viewmodel.compose.viewModel
+import androidx.navigation.NavHostController
+import androidx.navigation.compose.NavHost
+import androidx.navigation.compose.composable
+import androidx.navigation.compose.rememberNavController
+import com.google.uwb.hellouwb.AppContainer
+import com.google.uwb.hellouwb.ui.control.ControlRoute
+import com.google.uwb.hellouwb.ui.control.ControlViewModel
+import com.google.uwb.hellouwb.ui.home.HomeRoute
+import com.google.uwb.hellouwb.ui.home.HomeViewModel
+import com.google.uwb.hellouwb.ui.send.SendRoute
+import com.google.uwb.hellouwb.ui.send.SendViewModel
+import com.google.uwb.hellouwb.ui.settings.SettingsRoute
+import com.google.uwb.hellouwb.ui.settings.SettingsViewModel
+
+@Composable
+fun AppNavGraph(
+ appContainer: AppContainer,
+ modifier: Modifier = Modifier,
+ navController: NavHostController = rememberNavController(),
+ startDestination: String = AppDestination.HOME_ROUTE,
+) {
+ NavHost(navController = navController, startDestination = startDestination, modifier = modifier) {
+ composable(AppDestination.HOME_ROUTE) {
+ val homeViewModel: HomeViewModel =
+ viewModel(factory = HomeViewModel.provideFactory(appContainer.rangingResultSource))
+ HomeRoute(homeViewModel = homeViewModel)
+ }
+ composable(AppDestination.CONTROL_ROUTE) {
+ val controlViewModel: ControlViewModel =
+ viewModel(
+ factory = ControlViewModel.provideFactory(
+ appContainer.rangingResultSource,
+ appContainer.settingsStore
+ )
+ )
+ //ControlRoute()
+ ControlRoute(controlViewModel = controlViewModel)
+
+ }
+ composable(AppDestination.SEND_ROUTE) {
+ val sendViewModel: SendViewModel =
+ viewModel(
+ factory =
+ SendViewModel.provideFactory(
+ appContainer.rangingResultSource,
+ appContainer.contentResolver
+ )
+ )
+ SendRoute(sendViewModel = sendViewModel)
+ }
+ composable(AppDestination.SETTINGS_ROUTE) {
+ val settingsViewModel: SettingsViewModel =
+ viewModel(
+ factory =
+ SettingsViewModel.provideFactory(
+ appContainer.rangingResultSource,
+ appContainer.settingsStore
+ )
+ )
+ SettingsRoute(settingsViewModel)
+ }
+ }
+}
diff --git a/samples/connectivity/UwbRanging/src/main/java/com/google/uwb/hellouwb/ui/nav/AppNavigation.kt b/samples/connectivity/UwbRanging/src/main/java/com/google/uwb/hellouwb/ui/nav/AppNavigation.kt
new file mode 100644
index 00000000..f025c824
--- /dev/null
+++ b/samples/connectivity/UwbRanging/src/main/java/com/google/uwb/hellouwb/ui/nav/AppNavigation.kt
@@ -0,0 +1,48 @@
+/*
+ *
+ * Copyright (C) 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
+ *
+ * http://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.google.uwb.hellouwb.ui.nav
+
+import androidx.navigation.NavGraph.Companion.findStartDestination
+import androidx.navigation.NavHostController
+
+object AppDestination {
+ const val HOME_ROUTE = "home"
+ const val CONTROL_ROUTE = "control"
+ const val SEND_ROUTE = "send"
+ const val SETTINGS_ROUTE = "settings"
+}
+
+class AppNavigation(private val navController: NavHostController) {
+
+ val navToHome: () -> Unit = { navTo(AppDestination.HOME_ROUTE) }
+
+ val navToControl: () -> Unit = { navTo(AppDestination.CONTROL_ROUTE) }
+
+ val navToSend: () -> Unit = { navTo(AppDestination.SEND_ROUTE) }
+
+ val navToSettings: () -> Unit = { navTo(AppDestination.SETTINGS_ROUTE) }
+
+ fun navTo(destination: String) {
+ navController.navigate(destination) {
+ popUpTo(navController.graph.findStartDestination().id) { saveState = true }
+ launchSingleTop = true
+ restoreState = true
+ }
+ }
+}
diff --git a/samples/connectivity/UwbRanging/src/main/java/com/google/uwb/hellouwb/ui/ranging/RangingControlIcon.kt b/samples/connectivity/UwbRanging/src/main/java/com/google/uwb/hellouwb/ui/ranging/RangingControlIcon.kt
new file mode 100644
index 00000000..be11256c
--- /dev/null
+++ b/samples/connectivity/UwbRanging/src/main/java/com/google/uwb/hellouwb/ui/ranging/RangingControlIcon.kt
@@ -0,0 +1,86 @@
+/*
+ *
+ * Copyright (C) 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
+ *
+ * http://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.google.uwb.hellouwb.ui.nav
+
+import androidx.compose.foundation.BorderStroke
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.selection.selectable
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Start
+import androidx.compose.material.icons.filled.Stop
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Surface
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.ColorFilter
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+
+@Composable
+fun RangingControlIcon(
+ modifier: Modifier = Modifier,
+ selected: Boolean,
+ onClick: (Boolean) -> Unit,
+) {
+ val selectState = remember { mutableStateOf(selected) }
+ val icon = if (selectState.value) Icons.Filled.Stop else Icons.Filled.Start
+ val iconColor = if (selectState.value) Color.Red else MaterialTheme.colorScheme.scrim
+ val borderColor = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.1f)
+ val backgroundColor =
+ if (selectState.value) {
+ MaterialTheme.colorScheme.error
+ } else {
+ MaterialTheme.colorScheme.onPrimary
+ }
+ Surface(
+ color = backgroundColor,
+ shape = CircleShape,
+ border = BorderStroke(2.dp, borderColor),
+ modifier = modifier.size(36.dp, 36.dp)
+ ) {
+ Image(
+ imageVector = icon,
+ colorFilter = ColorFilter.tint(iconColor),
+ modifier =
+ Modifier.padding(4.dp).selectable(selected = selectState.value) {
+ selectState.value = !selectState.value
+ onClick(selectState.value)
+ },
+ contentDescription = null // toggleable at higher level
+ )
+ }
+}
+
+@Preview("Off")
+@Composable
+fun RangingControlButtonOff() {
+ RangingControlIcon(selected = false) {}
+}
+
+@Preview("On")
+@Composable
+fun RangingControlButtonOn() {
+ RangingControlIcon(selected = true) {}
+}
diff --git a/samples/connectivity/UwbRanging/src/main/java/com/google/uwb/hellouwb/ui/ranging/RangingViewModel.kt b/samples/connectivity/UwbRanging/src/main/java/com/google/uwb/hellouwb/ui/ranging/RangingViewModel.kt
new file mode 100644
index 00000000..5d056b42
--- /dev/null
+++ b/samples/connectivity/UwbRanging/src/main/java/com/google/uwb/hellouwb/ui/ranging/RangingViewModel.kt
@@ -0,0 +1,56 @@
+/*
+ *
+ * Copyright (C) 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
+ *
+ * http://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.google.uwb.hellouwb.ui.ranging
+
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.ViewModelProvider
+import androidx.lifecycle.viewModelScope
+import com.google.uwb.hellouwb.data.UwbRangingControlSource
+import kotlinx.coroutines.flow.*
+
+class RangingViewModel(private val uwbRangingControlSource: UwbRangingControlSource) : ViewModel() {
+
+ private val _uiState: MutableStateFlow = MutableStateFlow(false)
+
+ val uiState = _uiState.asStateFlow()
+
+ init {
+ uwbRangingControlSource.isRunning.onEach { _uiState.update { it } }.launchIn(viewModelScope)
+ }
+
+ fun startRanging() {
+ uwbRangingControlSource.start()
+ }
+
+ fun stopRanging() {
+ uwbRangingControlSource.stop()
+ }
+
+ companion object {
+ fun provideFactory(
+ uwbRangingControlSource: UwbRangingControlSource,
+ ): ViewModelProvider.Factory =
+ object : ViewModelProvider.Factory {
+ @Suppress("UNCHECKED_CAST")
+ override fun create(modelClass: Class): T {
+ return RangingViewModel(uwbRangingControlSource) as T
+ }
+ }
+ }
+}
diff --git a/samples/connectivity/UwbRanging/src/main/java/com/google/uwb/hellouwb/ui/send/SendRoute.kt b/samples/connectivity/UwbRanging/src/main/java/com/google/uwb/hellouwb/ui/send/SendRoute.kt
new file mode 100644
index 00000000..4255130d
--- /dev/null
+++ b/samples/connectivity/UwbRanging/src/main/java/com/google/uwb/hellouwb/ui/send/SendRoute.kt
@@ -0,0 +1,34 @@
+/*
+ *
+ * Copyright (C) 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
+ *
+ * http://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.google.uwb.hellouwb.ui.send
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
+
+@Composable
+fun SendRoute(sendViewModel: SendViewModel) {
+ val uiState by sendViewModel.uiState.collectAsState()
+ SendScreen(
+ uiState = uiState,
+ onImagePicked = { sendViewModel.setSentUri(it) },
+ onImageCleared = { sendViewModel.clear() },
+ onMessageDisplayed = { sendViewModel.messageShown() }
+ )
+}
diff --git a/samples/connectivity/UwbRanging/src/main/java/com/google/uwb/hellouwb/ui/send/SendScreen.kt b/samples/connectivity/UwbRanging/src/main/java/com/google/uwb/hellouwb/ui/send/SendScreen.kt
new file mode 100644
index 00000000..bcab9191
--- /dev/null
+++ b/samples/connectivity/UwbRanging/src/main/java/com/google/uwb/hellouwb/ui/send/SendScreen.kt
@@ -0,0 +1,145 @@
+/*
+ *
+ * Copyright (C) 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
+ *
+ * http://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.google.uwb.hellouwb.ui.send
+
+import android.graphics.ImageDecoder
+import android.net.Uri
+import android.widget.Toast
+import androidx.activity.compose.rememberLauncherForActivityResult
+import androidx.activity.result.contract.ActivityResultContracts
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.layout.*
+import androidx.compose.material3.Button
+import androidx.compose.material3.Text
+import androidx.compose.material3.CenterAlignedTopAppBar
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.Scaffold
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.asImageBitmap
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun SendScreen(
+ uiState: SendUiState,
+ onImagePicked: (Uri) -> Unit,
+ onImageCleared: () -> Unit,
+ onMessageDisplayed: () -> Unit,
+ modifier: Modifier = Modifier
+) {
+ Scaffold(
+ topBar = { CenterAlignedTopAppBar(
+ title = { androidx.compose.material3.Text("Data Transfer") },
+ modifier = modifier
+ ) },
+ modifier = modifier
+ ) { innerPadding ->
+
+
+ Column(
+ modifier = Modifier
+ .padding(innerPadding)
+ .fillMaxWidth()
+ .fillMaxHeight(),
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ when (uiState) {
+ is SendUiState.InitialState -> InitialScreen(onImagePicked)
+ is SendUiState.SendingState -> {
+ SendingScreen(uiState.sendImageUri, onImageCleared)
+ uiState.message?.let {
+ Toast.makeText(LocalContext.current, it, Toast.LENGTH_LONG).show()
+ onMessageDisplayed()
+ }
+ }
+
+ is SendUiState.ReceivedState -> ReceivedScreen(
+ uiState.receivedImageUri,
+ onImageCleared
+ )
+ }
+ }
+ }
+}
+
+@Composable
+fun InitialScreen(onImagePicked: (Uri) -> Unit) {
+ val launcher =
+ rememberLauncherForActivityResult(contract = ActivityResultContracts.GetContent()) { uri: Uri?
+ ->
+ uri?.let { onImagePicked(it) }
+ }
+
+ Button(onClick = { launcher.launch("image/*") }) { Text(text = "Select a picture") }
+
+ Spacer(modifier = Modifier.height(12.dp))
+ Text(text = "Receiving images...")
+}
+
+@Composable
+fun SendingScreen(imgUri: Uri, onImageCleared: () -> Unit) {
+ Button(onClick = { onImageCleared() }) { Text(text = "Clear") }
+
+ Spacer(modifier = Modifier.height(12.dp))
+
+ val source = ImageDecoder.createSource(LocalContext.current.contentResolver, imgUri)
+ val image = ImageDecoder.decodeBitmap(source)
+ Box(
+ Modifier
+ .height(400.dp)
+ .fillMaxWidth()) {
+ Image(
+ bitmap = image.asImageBitmap(),
+ contentDescription = null,
+ modifier = Modifier.fillMaxSize()
+ )
+ }
+ Text(text = "Sending images...")
+}
+
+@Composable
+fun ReceivedScreen(imgUri: Uri, onImageCleared: () -> Unit) {
+ Button(onClick = { onImageCleared() }) { Text(text = "Clear") }
+
+ Spacer(modifier = Modifier.height(12.dp))
+
+ val source = ImageDecoder.createSource(LocalContext.current.contentResolver, imgUri)
+ val image = ImageDecoder.decodeBitmap(source)
+ Box(
+ Modifier
+ .height(400.dp)
+ .fillMaxWidth()) {
+ Image(
+ bitmap = image.asImageBitmap(),
+ contentDescription = null,
+ modifier = Modifier.fillMaxSize()
+ )
+ }
+ Text(text = "Image is received.")
+}
+
+@Preview
+@Composable
+fun PreviewSendScreen(modifier: Modifier = Modifier) {
+ InitialScreen {}
+}
diff --git a/samples/connectivity/UwbRanging/src/main/java/com/google/uwb/hellouwb/ui/send/SendViewModel.kt b/samples/connectivity/UwbRanging/src/main/java/com/google/uwb/hellouwb/ui/send/SendViewModel.kt
new file mode 100644
index 00000000..31d573d3
--- /dev/null
+++ b/samples/connectivity/UwbRanging/src/main/java/com/google/uwb/hellouwb/ui/send/SendViewModel.kt
@@ -0,0 +1,151 @@
+/*
+ *
+ * Copyright (C) 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
+ *
+ * http://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.google.uwb.hellouwb.ui.send
+
+import android.content.ContentResolver
+import android.net.Uri
+import androidx.core.net.toUri
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.ViewModelProvider
+import com.google.uwb.hellouwb.data.UwbRangingControlSource
+import com.google.uwb.uwbranging.EndpointEvents
+import com.google.uwb.uwbranging.UwbEndpoint
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.flow.*
+import kotlinx.coroutines.launch
+import java.io.File
+
+private const val SEND_IMAGE_OP_CODE: Byte = 1
+
+private const val RECEIVED_FILE_PATH = "received"
+
+class SendViewModel(
+ private val uwbRangingControlSource: UwbRangingControlSource,
+ private val contentResolver: ContentResolver
+) : ViewModel() {
+
+ private val _uiState: MutableStateFlow = MutableStateFlow(SendUiState.InitialState)
+
+ val uiState = _uiState.asStateFlow()
+
+ private var sendJob: Job? = null
+
+ private var receiveJob: Job? = null
+
+ fun clear() {
+ receiveJob?.cancel()
+ receiveJob = startReceivingJob()
+ sendJob?.cancel()
+ sendJob = null
+ _uiState.update { SendUiState.InitialState }
+ }
+
+ fun setSentUri(uri: Uri) {
+ sendJob?.cancel()
+ sendJob = null
+ startSendingJob(uri)?.let {
+ sendJob = it
+ _uiState.update { SendUiState.SendingState(uri, null) }
+ }
+ }
+
+ fun messageShown() {
+ when (val state = _uiState.value) {
+ is SendUiState.SendingState -> _uiState.update { state.copy(message = null) }
+ else -> {}
+ }
+ }
+
+ private fun startReceivingJob(): Job {
+ return CoroutineScope(Dispatchers.IO).launch {
+ uwbRangingControlSource
+ .observeRangingResults()
+ .filterIsInstance()
+ .filter { it.message[0] == SEND_IMAGE_OP_CODE }
+ .collect { event ->
+ onImageReceived(event.endpoint, event.message.sliceArray(1 until event.message.size))
+ }
+ }
+ }
+
+ private fun onImageReceived(endpoint: UwbEndpoint, imageBytes: ByteArray) {
+ receiveJob?.cancel()
+ receiveJob = null
+ val file = File.createTempFile(RECEIVED_FILE_PATH, null)
+ contentResolver.openOutputStream(file.toUri())?.use { it.write(imageBytes) }
+ _uiState.update { SendUiState.ReceivedState(endpoint, file.toUri()) }
+ }
+
+ private fun startSendingJob(uri: Uri): Job? {
+ contentResolver.openInputStream(uri)?.use { inputStream ->
+ val bytesToSend = byteArrayOf(SEND_IMAGE_OP_CODE) + inputStream.readBytes()
+ val endpointsSent = mutableSetOf()
+ return CoroutineScope(Dispatchers.IO).launch {
+ uwbRangingControlSource
+ .observeRangingResults()
+ .filterNot { it.endpoint in endpointsSent }
+ .filterIsInstance()
+ .collect { event ->
+ event.position.azimuth?.let { azimuth ->
+ if (azimuth.value > -5.0f && azimuth.value < 5.0f) {
+ endpointsSent.add(event.endpoint)
+ uwbRangingControlSource.sendOobMessage(event.endpoint, bytesToSend)
+ val endpointDisplayName = event.endpoint.id.split("|")[0]
+ _uiState.update {
+ SendUiState.SendingState(
+ uri,
+ "Image has been sent to $endpointDisplayName"
+ )
+ }
+ }
+ }
+ }
+ }
+ }
+ return null
+ }
+
+ init {
+ clear()
+ }
+
+ companion object {
+ fun provideFactory(
+ uwbRangingControlSource: UwbRangingControlSource,
+ contentResolver: ContentResolver
+ ): ViewModelProvider.Factory =
+ object : ViewModelProvider.Factory {
+ @Suppress("UNCHECKED_CAST")
+ override fun create(modelClass: Class): T {
+ return SendViewModel(uwbRangingControlSource, contentResolver) as T
+ }
+ }
+ }
+}
+
+sealed class SendUiState {
+
+ data class SendingState(val sendImageUri: Uri, val message: String?) : SendUiState()
+
+ data class ReceivedState(val endpoint: UwbEndpoint, val receivedImageUri: Uri) : SendUiState()
+
+ object InitialState : SendUiState()
+}
diff --git a/samples/connectivity/UwbRanging/src/main/java/com/google/uwb/hellouwb/ui/settings/SettingsRoute.kt b/samples/connectivity/UwbRanging/src/main/java/com/google/uwb/hellouwb/ui/settings/SettingsRoute.kt
new file mode 100644
index 00000000..8f7897cb
--- /dev/null
+++ b/samples/connectivity/UwbRanging/src/main/java/com/google/uwb/hellouwb/ui/settings/SettingsRoute.kt
@@ -0,0 +1,34 @@
+/*
+ *
+ * Copyright (C) 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
+ *
+ * http://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.google.uwb.hellouwb.ui.settings
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
+
+@Composable
+fun SettingsRoute(settingsViewModel: SettingsViewModel) {
+ val uiState by settingsViewModel.uiState.collectAsState()
+ SettingsScreen(
+ uiState = uiState,
+ updateDeviceDisplayName = { settingsViewModel.updateDeviceDisplayName(it) },
+ updateDeviceType = { settingsViewModel.updateDeviceType(it) },
+ updateConfigType = { settingsViewModel.updateConfigType(it) }
+ )
+}
diff --git a/samples/connectivity/UwbRanging/src/main/java/com/google/uwb/hellouwb/ui/settings/SettingsScreen.kt b/samples/connectivity/UwbRanging/src/main/java/com/google/uwb/hellouwb/ui/settings/SettingsScreen.kt
new file mode 100644
index 00000000..ebefb73b
--- /dev/null
+++ b/samples/connectivity/UwbRanging/src/main/java/com/google/uwb/hellouwb/ui/settings/SettingsScreen.kt
@@ -0,0 +1,171 @@
+/*
+ *
+ * Copyright (C) 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
+ *
+ * http://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.google.uwb.hellouwb.ui.settings
+
+import androidx.compose.foundation.layout.*
+import androidx.compose.foundation.text.KeyboardActions
+import androidx.compose.foundation.text.KeyboardOptions
+import androidx.compose.material3.*
+import androidx.compose.runtime.*
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.ExperimentalComposeUiApi
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalFocusManager
+import androidx.compose.ui.text.input.ImeAction
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import com.google.uwb.hellouwb.data.AppSettings
+import com.google.uwb.hellouwb.data.DeviceType
+import com.google.uwb.hellouwb.data.ConfigType
+
+@OptIn(ExperimentalMaterial3Api::class, ExperimentalComposeUiApi::class)
+@Composable
+fun SettingsScreen(
+ uiState: AppSettings,
+ updateDeviceDisplayName: (String) -> Unit,
+ updateDeviceType: (DeviceType) -> Unit,
+ updateConfigType: (ConfigType) -> Unit,
+ modifier: Modifier = Modifier,
+) {
+ Scaffold(
+ topBar = { CenterAlignedTopAppBar(
+ title = { Text("Device Settings") },
+ modifier = modifier
+ ) },
+ modifier = modifier
+ ) { innerPadding ->
+ val focusManager = LocalFocusManager.current
+ Column(
+ modifier = Modifier
+ .padding(innerPadding)
+ .fillMaxWidth()
+ .fillMaxHeight(),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ )
+ {
+ Text("Display Name")
+ var fieldValue by remember { mutableStateOf(uiState.deviceDisplayName) }
+ OutlinedTextField(
+ fieldValue,
+ onValueChange = { fieldValue = it },
+ keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done),
+ keyboardActions =
+ KeyboardActions(
+ onDone = {
+ updateDeviceDisplayName(fieldValue)
+ focusManager.clearFocus(true)
+ }
+ ),
+ singleLine = true
+ )
+
+ Row {
+ Column (horizontalAlignment = Alignment.CenterHorizontally) {
+ Text("Device Type:", Modifier.padding(20.dp))
+ Row(Modifier.padding(5.dp)) {
+ val selectedValue = remember { mutableStateOf(uiState.deviceType) }
+ Column(Modifier.width(120.dp)) {
+ RadioButton(
+ selected = selectedValue.value == DeviceType.CONTROLLER,
+ onClick = {
+ updateDeviceType(DeviceType.CONTROLLER)
+ selectedValue.value = DeviceType.CONTROLLER
+ },
+ )
+ Text("Controller")
+ }
+ Column(Modifier.width(120.dp)) {
+ RadioButton(
+ selected = selectedValue.value == DeviceType.CONTROLEE,
+ onClick = {
+ updateDeviceType(DeviceType.CONTROLEE)
+ selectedValue.value = DeviceType.CONTROLEE
+ }
+ )
+ Text("Controlee")
+ }
+ }
+ Text("Config Type:", Modifier.padding(20.dp))
+ Column(Modifier.padding(20.dp), horizontalAlignment = Alignment.CenterHorizontally) {
+ val selectedValue = remember { mutableStateOf(uiState.configType) }
+ Row(
+ modifier = Modifier.fillMaxWidth(), // Increased width for better spacing
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ RadioButton(
+ selected = selectedValue.value == ConfigType.CONFIG_UNICAST_DS_TWR,
+ onClick = {
+ val newType = ConfigType.CONFIG_UNICAST_DS_TWR
+ updateConfigType(newType)
+ selectedValue.value = newType
+ }
+ )
+ Text("Static Unicast")
+ }
+
+ Row(
+ modifier = Modifier.fillMaxWidth(), // Increased width for better spacing
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ RadioButton(
+ selected = selectedValue.value == ConfigType.CONFIG_MULTICAST_DS_TWR,
+ onClick = {
+ val newType = ConfigType.CONFIG_MULTICAST_DS_TWR
+ updateConfigType(newType)
+ selectedValue.value = newType
+ }
+ )
+ Text("Static Multicast")
+ }
+
+ Row(
+ modifier = Modifier.fillMaxWidth(), // Increased width for better spacing
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ RadioButton(
+ selected = selectedValue.value == ConfigType.CONFIG_PROVISIONED_UNICAST,
+ onClick = {
+ val newType = ConfigType.CONFIG_PROVISIONED_UNICAST
+ updateConfigType(newType)
+ selectedValue.value = newType
+ }
+ )
+ Text("Provisioned Unicast")
+ }
+ }
+ }
+ }
+ }
+ }
+}
+
+@Preview
+@Composable
+fun PreviewSettingsScreen() {
+ SettingsScreen(
+ AppSettings.newBuilder()
+ .setDeviceDisplayName("UWB")
+ .setDeviceType(DeviceType.CONTROLEE)
+ .setConfigType(ConfigType.CONFIG_PROVISIONED_UNICAST)
+ .build(),
+ {},
+ {},
+ {}
+ )
+}
diff --git a/samples/connectivity/UwbRanging/src/main/java/com/google/uwb/hellouwb/ui/settings/SettingsViewModel.kt b/samples/connectivity/UwbRanging/src/main/java/com/google/uwb/hellouwb/ui/settings/SettingsViewModel.kt
new file mode 100644
index 00000000..cd7e14f0
--- /dev/null
+++ b/samples/connectivity/UwbRanging/src/main/java/com/google/uwb/hellouwb/ui/settings/SettingsViewModel.kt
@@ -0,0 +1,70 @@
+/*
+ *
+ * Copyright (C) 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
+ *
+ * http://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.google.uwb.hellouwb.ui.settings
+
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.ViewModelProvider
+import com.google.uwb.hellouwb.data.ConfigType
+import com.google.uwb.hellouwb.data.DeviceType
+import com.google.uwb.hellouwb.data.SettingsStore
+import com.google.uwb.hellouwb.data.UwbRangingControlSource
+
+class SettingsViewModel(
+ private val uwbRangingControlSource: UwbRangingControlSource,
+ private val settingsStore: SettingsStore,
+) : ViewModel() {
+
+ val uiState = settingsStore.appSettings
+
+ fun updateDeviceDisplayName(deviceName: String) {
+ if (deviceName == uiState.value.deviceDisplayName) {
+ return
+ }
+ settingsStore.updateDeviceDisplayName(deviceName)
+ }
+
+ fun updateDeviceType(deviceType: DeviceType) {
+ if (deviceType == uiState.value.deviceType) {
+ return
+ }
+ uwbRangingControlSource.deviceType = deviceType
+ settingsStore.updateDeviceType(deviceType)
+ }
+
+ fun updateConfigType(configType: ConfigType) {
+ if (configType == uiState.value.configType) {
+ return
+ }
+ uwbRangingControlSource.configType = configType
+ settingsStore.updateConfigType(configType)
+ }
+
+ companion object {
+ fun provideFactory(
+ uwbRangingControlSource: UwbRangingControlSource,
+ settingsStore: SettingsStore,
+ ): ViewModelProvider.Factory =
+ object : ViewModelProvider.Factory {
+ @Suppress("UNCHECKED_CAST")
+ override fun create(modelClass: Class): T {
+ return SettingsViewModel(uwbRangingControlSource, settingsStore) as T
+ }
+ }
+ }
+}
diff --git a/samples/connectivity/UwbRanging/src/main/java/com/google/uwb/hellouwb/ui/theme/Color.kt b/samples/connectivity/UwbRanging/src/main/java/com/google/uwb/hellouwb/ui/theme/Color.kt
new file mode 100644
index 00000000..b4a74cce
--- /dev/null
+++ b/samples/connectivity/UwbRanging/src/main/java/com/google/uwb/hellouwb/ui/theme/Color.kt
@@ -0,0 +1,29 @@
+/*
+ *
+ * Copyright (C) 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
+ *
+ * http://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.google.uwb.hellouwb.ui.theme
+
+import androidx.compose.ui.graphics.Color
+
+val Purple80 = Color(0xFFD0BCFF)
+val PurpleGrey80 = Color(0xFFCCC2DC)
+val Pink80 = Color(0xFFEFB8C8)
+
+val Purple40 = Color(0xFF6650a4)
+val PurpleGrey40 = Color(0xFF625b71)
+val Pink40 = Color(0xFF7D5260)
diff --git a/samples/connectivity/UwbRanging/src/main/java/com/google/uwb/hellouwb/ui/theme/Theme.kt b/samples/connectivity/UwbRanging/src/main/java/com/google/uwb/hellouwb/ui/theme/Theme.kt
new file mode 100644
index 00000000..65e02047
--- /dev/null
+++ b/samples/connectivity/UwbRanging/src/main/java/com/google/uwb/hellouwb/ui/theme/Theme.kt
@@ -0,0 +1,77 @@
+/*
+ *
+ * Copyright (C) 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
+ *
+ * http://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.google.uwb.hellouwb.ui.theme
+
+import android.app.Activity
+import androidx.compose.foundation.isSystemInDarkTheme
+import androidx.compose.material3.*
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.SideEffect
+import androidx.compose.ui.graphics.toArgb
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.platform.LocalView
+import androidx.core.view.WindowCompat
+
+private val DarkColorScheme =
+ darkColorScheme(primary = Purple80, secondary = PurpleGrey80, tertiary = Pink80)
+
+private val LightColorScheme =
+ lightColorScheme(
+ primary = Purple40,
+ secondary = PurpleGrey40,
+ tertiary = Pink40
+
+ /* Other default colors to override
+ background = Color(0xFFFFFBFE),
+ surface = Color(0xFFFFFBFE),
+ onPrimary = Color.White,
+ onSecondary = Color.White,
+ onTertiary = Color.White,
+ onBackground = Color(0xFF1C1B1F),
+ onSurface = Color(0xFF1C1B1F),
+ */
+ )
+
+@Composable
+fun HellouwbTheme(
+ darkTheme: Boolean = isSystemInDarkTheme(),
+ // Dynamic color is available on Android 12+
+ dynamicColor: Boolean = true,
+ content: @Composable () -> Unit,
+) {
+ val colorScheme =
+ when {
+ dynamicColor -> {
+ val context = LocalContext.current
+ if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
+ }
+ darkTheme -> DarkColorScheme
+ else -> LightColorScheme
+ }
+ val view = LocalView.current
+ if (!view.isInEditMode) {
+ SideEffect {
+ (view.context as Activity).window.statusBarColor = colorScheme.primary.toArgb()
+ WindowCompat.getInsetsController((view.context as Activity).window, view)
+ .isAppearanceLightStatusBars = darkTheme
+ }
+ }
+
+ MaterialTheme(colorScheme = colorScheme, typography = Typography, content = content)
+}
diff --git a/samples/connectivity/UwbRanging/src/main/java/com/google/uwb/hellouwb/ui/theme/Type.kt b/samples/connectivity/UwbRanging/src/main/java/com/google/uwb/hellouwb/ui/theme/Type.kt
new file mode 100644
index 00000000..335323a1
--- /dev/null
+++ b/samples/connectivity/UwbRanging/src/main/java/com/google/uwb/hellouwb/ui/theme/Type.kt
@@ -0,0 +1,54 @@
+/*
+ *
+ * Copyright (C) 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
+ *
+ * http://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.google.uwb.hellouwb.ui.theme
+
+import androidx.compose.material3.Typography
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.font.FontFamily
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.unit.sp
+
+// Set of Material typography styles to start with
+val Typography =
+ Typography(
+ bodyLarge =
+ TextStyle(
+ fontFamily = FontFamily.Default,
+ fontWeight = FontWeight.Normal,
+ fontSize = 16.sp,
+ lineHeight = 24.sp,
+ letterSpacing = 0.5.sp
+ )
+ /* Other default text styles to override
+ titleLarge = TextStyle(
+ fontFamily = FontFamily.Default,
+ fontWeight = FontWeight.Normal,
+ fontSize = 22.sp,
+ lineHeight = 28.sp,
+ letterSpacing = 0.sp
+ ),
+ labelSmall = TextStyle(
+ fontFamily = FontFamily.Default,
+ fontWeight = FontWeight.Medium,
+ fontSize = 11.sp,
+ lineHeight = 16.sp,
+ letterSpacing = 0.5.sp
+ )
+ */
+ )
diff --git a/samples/connectivity/UwbRanging/src/main/java/com/google/uwb/uwbranging/EndpointEvents.kt b/samples/connectivity/UwbRanging/src/main/java/com/google/uwb/uwbranging/EndpointEvents.kt
new file mode 100644
index 00000000..6fcb8fe5
--- /dev/null
+++ b/samples/connectivity/UwbRanging/src/main/java/com/google/uwb/uwbranging/EndpointEvents.kt
@@ -0,0 +1,48 @@
+/*
+ *
+ * Copyright (C) 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
+ *
+ * http://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.google.uwb.uwbranging
+
+import androidx.core.uwb.RangingPosition
+
+/** A data class for ranging result update. */
+abstract class EndpointEvents internal constructor() {
+ /** Represents a UWB endpoint. */
+ abstract val endpoint: UwbEndpoint
+
+ /**
+ * Device position update.
+ *
+ * @property position Position of the UWB device during Ranging
+ */
+ data class PositionUpdated(override val endpoint: UwbEndpoint, val position: RangingPosition) :
+ EndpointEvents()
+
+ /** A ranging result with peer disconnected status update. */
+ data class UwbDisconnected(override val endpoint: UwbEndpoint) : EndpointEvents()
+
+ /** Endpoint is found. */
+ data class EndpointFound(override val endpoint: UwbEndpoint) : EndpointEvents()
+
+ /** Endpoint is lost. */
+ data class EndpointLost(override val endpoint: UwbEndpoint) : EndpointEvents()
+
+ /** Received message through OOB. */
+ data class EndpointMessage(override val endpoint: UwbEndpoint, val message: ByteArray) :
+ EndpointEvents()
+}
diff --git a/samples/connectivity/UwbRanging/src/main/java/com/google/uwb/uwbranging/UwbConnectionManager.kt b/samples/connectivity/UwbRanging/src/main/java/com/google/uwb/uwbranging/UwbConnectionManager.kt
new file mode 100644
index 00000000..46301698
--- /dev/null
+++ b/samples/connectivity/UwbRanging/src/main/java/com/google/uwb/uwbranging/UwbConnectionManager.kt
@@ -0,0 +1,38 @@
+/*
+ *
+ * Copyright (C) 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
+ *
+ * http://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.google.uwb.uwbranging
+
+import android.content.Context
+import com.google.uwb.uwbranging.impl.UwbConnectionManagerImpl
+
+/**
+ * Manages OOB connection and UWB ranging. It starts OOB connection first, if peer is found, it
+ * starts the UWB ranging.
+ */
+interface UwbConnectionManager {
+ fun controllerUwbScope(endpoint: UwbEndpoint, configId: Int): UwbSessionScope
+
+ fun controleeUwbScope(endpoint: UwbEndpoint): UwbSessionScope
+
+ companion object {
+ fun getInstance(context: Context): UwbConnectionManager {
+ return UwbConnectionManagerImpl(context)
+ }
+ }
+}
diff --git a/samples/connectivity/UwbRanging/src/main/java/com/google/uwb/uwbranging/UwbEndpoint.kt b/samples/connectivity/UwbRanging/src/main/java/com/google/uwb/uwbranging/UwbEndpoint.kt
new file mode 100644
index 00000000..58af6139
--- /dev/null
+++ b/samples/connectivity/UwbRanging/src/main/java/com/google/uwb/uwbranging/UwbEndpoint.kt
@@ -0,0 +1,37 @@
+/*
+ *
+ * Copyright (C) 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
+ *
+ * http://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.google.uwb.uwbranging
+
+/**
+ * A token class that describes a UWB device.
+ *
+ * @param id a unique identifier that identifies the UWB device. Unlike UWB address, this identifier
+ * is consistent during different UWB sessions.
+ */
+data class UwbEndpoint(val id: String, val metadata: ByteArray) {
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (other !is UwbEndpoint) return false
+ return id == other.id
+ }
+
+ override fun hashCode(): Int {
+ return id.hashCode()
+ }
+}
diff --git a/samples/connectivity/UwbRanging/src/main/java/com/google/uwb/uwbranging/UwbSessionScope.kt b/samples/connectivity/UwbRanging/src/main/java/com/google/uwb/uwbranging/UwbSessionScope.kt
new file mode 100644
index 00000000..053ac856
--- /dev/null
+++ b/samples/connectivity/UwbRanging/src/main/java/com/google/uwb/uwbranging/UwbSessionScope.kt
@@ -0,0 +1,28 @@
+/*
+ *
+ * Copyright (C) 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
+ *
+ * http://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.google.uwb.uwbranging
+
+import kotlinx.coroutines.flow.Flow
+
+interface UwbSessionScope {
+
+ fun prepareSession(): Flow
+
+ fun sendMessage(endpoint: UwbEndpoint, message: ByteArray)
+}
diff --git a/samples/connectivity/UwbRanging/src/main/java/com/google/uwb/uwbranging/impl/NearbyConnections.kt b/samples/connectivity/UwbRanging/src/main/java/com/google/uwb/uwbranging/impl/NearbyConnections.kt
new file mode 100644
index 00000000..ea007e0c
--- /dev/null
+++ b/samples/connectivity/UwbRanging/src/main/java/com/google/uwb/uwbranging/impl/NearbyConnections.kt
@@ -0,0 +1,168 @@
+/*
+ *
+ * Copyright (C) 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
+ *
+ * http://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.google.uwb.uwbranging.impl
+
+import android.content.Context
+import android.util.Log
+import com.google.android.gms.nearby.Nearby
+import com.google.android.gms.nearby.connection.*
+import kotlinx.coroutines.*
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.callbackFlow
+import kotlinx.coroutines.tasks.await
+
+private const val CONNECTION_SERVICE_ID = "com.google.apps.hellouwb"
+private const val CONNECTION_NAME = "com.google.apps.hellouwb"
+
+/**
+ * Nearby Connections API wrapper.
+ *
+ * @param name Human readable name used in Nearby Connections.
+ * @param dispatcher Dispatcher that runs Nearby Connections API calls.
+ * @param connectionsClient a Nearby Connections client. Exposed for unit testing.
+ */
+internal class NearbyConnections(
+ context: Context,
+ private val dispatcher: CoroutineDispatcher,
+ private val connectionsClient: ConnectionsClient = Nearby.getConnectionsClient(context),
+) {
+
+ private val coroutineScope =
+ CoroutineScope(
+ dispatcher +
+ Job() +
+ CoroutineExceptionHandler { _, e -> Log.e("NearbyConnections", "Connection Error", e) }
+ )
+
+ // Connection-phase Callbacks used by both controller and controlee
+ private val connectionLifecycleCallback =
+ object : ConnectionLifecycleCallback() {
+ override fun onConnectionInitiated(endpointId: String, connectionInfo: ConnectionInfo) {
+ coroutineScope.launch {
+ connectionsClient.acceptConnection(endpointId, payloadCallback).await()
+ }
+ }
+
+ override fun onConnectionResult(endpointId: String, result: ConnectionResolution) {
+ if (result.status.statusCode == ConnectionsStatusCodes.STATUS_OK) {
+ dispatchEvent(NearbyEvent.EndpointConnected(endpointId))
+ }
+ }
+
+ override fun onDisconnected(endpointId: String) {
+ dispatchEvent(NearbyEvent.EndpointLost(endpointId))
+ }
+ }
+
+ private val payloadCallback =
+ object : PayloadCallback() {
+ override fun onPayloadReceived(endpointId: String, payload: Payload) {
+ val bytes = payload.asBytes() ?: return
+ dispatchEvent(NearbyEvent.PayloadReceived(endpointId, bytes))
+ }
+
+ override fun onPayloadTransferUpdate(endpointId: String, update: PayloadTransferUpdate) {}
+ }
+
+ private val endpointDiscoveryCallback =
+ object : EndpointDiscoveryCallback() {
+ override fun onEndpointFound(endpointId: String, info: DiscoveredEndpointInfo) {
+ coroutineScope.launch {
+ connectionsClient
+ .requestConnection(CONNECTION_NAME, endpointId, connectionLifecycleCallback)
+ }
+ }
+
+ override fun onEndpointLost(endpointId: String) {
+ dispatchEvent(NearbyEvent.EndpointLost(endpointId))
+
+ }
+ }
+
+ fun sendPayload(endpointId: String, bytes: ByteArray) {
+ coroutineScope.launch {
+ connectionsClient.sendPayload(endpointId, Payload.fromBytes(bytes)).await()
+ }
+ }
+
+ private var dispatchEvent: (event: NearbyEvent) -> Unit = {}
+
+ /**
+ * Starts discovery.
+ * @return a flow of [NearbyEvent].
+ */
+ fun startDiscovery() = callbackFlow {
+ dispatchEvent = { trySend(it) }
+ coroutineScope.launch {
+ connectionsClient
+ .startDiscovery(
+ CONNECTION_SERVICE_ID,
+ endpointDiscoveryCallback,
+ DiscoveryOptions.Builder().setStrategy(Strategy.P2P_CLUSTER).build()
+ )
+ .await()
+ }
+ awaitClose {
+ disconnectAll()
+ connectionsClient.stopDiscovery()
+ }
+ }
+
+ /**
+ * Starts advertising.
+ * @return a flow of [NearbyEvent].
+ */
+ fun startAdvertising() = callbackFlow {
+ dispatchEvent = { trySend(it) }
+ coroutineScope.launch {
+ connectionsClient
+ .startAdvertising(
+ CONNECTION_NAME,
+ CONNECTION_SERVICE_ID,
+ connectionLifecycleCallback,
+ AdvertisingOptions.Builder().setStrategy(Strategy.P2P_CLUSTER).build()
+ )
+ .await()
+ }
+ awaitClose {
+ disconnectAll()
+ connectionsClient.stopAdvertising()
+ }
+ }
+
+ private fun disconnectAll() {
+ connectionsClient.stopAllEndpoints()
+ }
+}
+
+/** Events that happen in a Nearby Connections session. */
+abstract class NearbyEvent private constructor() {
+
+ abstract val endpointId: String
+
+ /** An event that notifies a NC endpoint is connected. */
+ data class EndpointConnected(override val endpointId: String) : NearbyEvent()
+
+ /** An event that notifies a NC endpoint is lost. */
+ data class EndpointLost(override val endpointId: String) : NearbyEvent()
+
+ /** An event that notifies a UWB device is lost. */
+ data class PayloadReceived(override val endpointId: String, val payload: ByteArray) :
+ NearbyEvent()
+}
diff --git a/samples/connectivity/UwbRanging/src/main/java/com/google/uwb/uwbranging/impl/NearbyConnector.kt b/samples/connectivity/UwbRanging/src/main/java/com/google/uwb/uwbranging/impl/NearbyConnector.kt
new file mode 100644
index 00000000..c060ddb1
--- /dev/null
+++ b/samples/connectivity/UwbRanging/src/main/java/com/google/uwb/uwbranging/impl/NearbyConnector.kt
@@ -0,0 +1,123 @@
+/*
+ *
+ * Copyright (C) 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
+ *
+ * http://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.google.uwb.uwbranging.impl
+
+import com.google.uwb.uwbranging.UwbEndpoint
+import com.google.uwb.uwbranging.impl.proto.Control
+import com.google.uwb.uwbranging.impl.proto.Data
+import com.google.uwb.uwbranging.impl.proto.Oob
+import com.google.protobuf.ByteString
+import com.google.protobuf.InvalidProtocolBufferException
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.channelFlow
+import kotlinx.coroutines.launch
+
+internal abstract class NearbyConnector(protected val connections: NearbyConnections) :
+ OobConnector {
+
+ private val peerMap = mutableMapOf()
+
+ protected fun addEndpoint(endpointId: String, endpoint: UwbEndpoint) {
+ peerMap[endpointId] = endpoint
+ }
+
+ private fun lookupEndpoint(endpointId: String): UwbEndpoint? {
+ return peerMap[endpointId]
+ }
+
+ private fun lookupEndpointId(endpoint: UwbEndpoint): String? {
+ return peerMap.firstNotNullOfOrNull { if (it.value == endpoint) it.key else null }
+ }
+
+ private fun tryParseOobMessage(payload: ByteArray): Data? {
+ return try {
+ val oob = Oob.parseFrom(payload)
+ if (oob.hasData()) oob.data else null
+ } catch (_: InvalidProtocolBufferException) {
+ null
+ }
+ }
+
+ private fun tryParseUwbSessionInfo(payload: ByteArray): Control? {
+ return try {
+ val oob = Oob.parseFrom(payload)
+ if (oob.hasControl()) oob.control else null
+ } catch (_: InvalidProtocolBufferException) {
+ null
+ }
+ }
+
+ protected abstract fun prepareEventFlow(): Flow
+
+ protected abstract suspend fun processEndpointConnected(endpointId: String)
+
+ protected abstract suspend fun processUwbSessionInfo(
+ endpointId: String,
+ sessionInfo: Control,
+ ): UwbOobEvent.UwbEndpointFound?
+
+ private fun processEndpointLost(event: NearbyEvent.EndpointLost): UwbOobEvent? {
+ val endpoint = peerMap.remove(event.endpointId) ?: return null
+ return UwbOobEvent.UwbEndpointLost(endpoint)
+ }
+
+ override fun start() = channelFlow {
+ val events = prepareEventFlow()
+ val job = launch {
+ events.collect { event ->
+ when (event) {
+ is NearbyEvent.EndpointConnected -> {
+ processEndpointConnected(event.endpointId)
+ null
+ }
+ is NearbyEvent.PayloadReceived -> processPayload(event)
+ is NearbyEvent.EndpointLost -> processEndpointLost(event)
+ else -> null
+ }?.let { trySend(it) }
+ }
+ }
+ awaitClose { job.cancel() }
+ }
+
+ private suspend fun processPayload(event: NearbyEvent.PayloadReceived): UwbOobEvent? {
+
+ tryParseUwbSessionInfo(event.payload)?.let {
+ return processUwbSessionInfo(event.endpointId, it)
+ }
+
+ val endpoint = lookupEndpoint(event.endpointId) ?: return null
+
+ tryParseOobMessage(event.payload)?.let {
+ return UwbOobEvent.MessageReceived(endpoint, it.message.toByteArray())
+ }
+ return null
+ }
+
+ override fun sendMessage(endpoint: UwbEndpoint, message: ByteArray) {
+ val endpointId = lookupEndpointId(endpoint) ?: return
+ connections.sendPayload(
+ endpointId,
+ Oob.newBuilder()
+ .setData(Data.newBuilder().setMessage(ByteString.copyFrom(message)).build())
+ .build()
+ .toByteArray()
+ )
+ }
+}
diff --git a/samples/connectivity/UwbRanging/src/main/java/com/google/uwb/uwbranging/impl/NearbyControleeConnector.kt b/samples/connectivity/UwbRanging/src/main/java/com/google/uwb/uwbranging/impl/NearbyControleeConnector.kt
new file mode 100644
index 00000000..c2c9fb7e
--- /dev/null
+++ b/samples/connectivity/UwbRanging/src/main/java/com/google/uwb/uwbranging/impl/NearbyControleeConnector.kt
@@ -0,0 +1,108 @@
+/*
+ *
+ * Copyright (C) 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
+ *
+ * http://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.google.uwb.uwbranging.impl
+
+import androidx.core.uwb.RangingParameters
+import androidx.core.uwb.UwbAddress
+import androidx.core.uwb.UwbClientSessionScope
+import androidx.core.uwb.UwbComplexChannel
+import androidx.core.uwb.UwbControleeSessionScope
+import com.google.uwb.uwbranging.UwbEndpoint
+import com.google.uwb.uwbranging.impl.proto.Control
+import com.google.uwb.uwbranging.impl.proto.Oob
+import com.google.uwb.uwbranging.impl.proto.UwbCapabilities
+import com.google.uwb.uwbranging.impl.proto.UwbConnectionInfo
+import com.google.firebase.crashlytics.buildtools.reloc.com.google.common.primitives.Shorts
+import com.google.protobuf.ByteString
+import kotlinx.coroutines.flow.Flow
+
+/**
+ * UWB OOB connector for a controlee device using Nearby Connections.
+ *
+ * @param localEndpoint Local endpoint.
+ * @param connections: OOB connection.
+ * @param sessionScopeCreator a function that returns a [UwbControleeSessionScope].
+ */
+internal class NearbyControleeConnector(
+ private val localEndpoint: UwbEndpoint,
+ connections: NearbyConnections,
+ private val sessionScopeCreator: suspend () -> UwbControleeSessionScope,
+) : NearbyConnector(connections) {
+
+ private val sessionMap = mutableMapOf()
+
+ override fun prepareEventFlow(): Flow {
+ return connections.startAdvertising()
+ }
+
+ override suspend fun processEndpointConnected(endpointId: String) {
+ val sessionScope = sessionScopeCreator()
+ sessionMap[endpointId] = sessionScope
+ connections.sendPayload(
+ endpointId,
+ Oob.newBuilder().setControl(uwbSessionInfo(sessionScope)).build().toByteArray()
+ )
+ }
+
+ override suspend fun processUwbSessionInfo(
+ endpointId: String,
+ sessionInfo: Control,
+ ): UwbOobEvent.UwbEndpointFound? {
+ val configuration =
+ if (sessionInfo.connectionInfo.hasConfiguration()) sessionInfo.connectionInfo.configuration
+ else return null
+ val sessionScope = sessionMap[endpointId] ?: return null
+ val endpoint = UwbEndpoint(sessionInfo.id, sessionInfo.metadata.toByteArray())
+ addEndpoint(endpointId, endpoint)
+
+ return UwbOobEvent.UwbEndpointFound(
+ endpoint,
+ configuration.configId,
+ UwbAddress(Shorts.toByteArray(sessionInfo.localAddress.toShort())),
+ UwbComplexChannel(configuration.channel, configuration.preambleIndex),
+ configuration.sessionId,
+ configuration.securityInfo.toByteArray(),
+ sessionScope,
+ configuration.subSessionId,
+ configuration.subSessionKey.toByteArray(),
+ )
+ }
+
+ private fun uwbSessionInfo(scope: UwbClientSessionScope) =
+ Control.newBuilder()
+ .setId(localEndpoint.id)
+ .setMetadata(ByteString.copyFrom(localEndpoint.metadata))
+ .setLocalAddress(Shorts.fromByteArray(scope.localAddress.address).toInt())
+ .setConnectionInfo(
+ UwbConnectionInfo.newBuilder()
+ .setCapabilities(
+ UwbCapabilities.newBuilder()
+ .addAllSupportedConfigIds(listOf(RangingParameters.CONFIG_UNICAST_DS_TWR,
+ RangingParameters.CONFIG_MULTICAST_DS_TWR,
+ RangingParameters.CONFIG_PROVISIONED_UNICAST_DS_TWR,
+ RangingParameters.CONFIG_PROVISIONED_INDIVIDUAL_MULTICAST_DS_TWR,
+ RangingParameters.CONFIG_PROVISIONED_MULTICAST_DS_TWR))
+ .setSupportsAzimuth(scope.rangingCapabilities.isAzimuthalAngleSupported)
+ .setSupportsElevation(scope.rangingCapabilities.isElevationAngleSupported)
+ .build()
+ )
+ .build()
+ )
+ .build()
+}
diff --git a/samples/connectivity/UwbRanging/src/main/java/com/google/uwb/uwbranging/impl/NearbyControllerConnector.kt b/samples/connectivity/UwbRanging/src/main/java/com/google/uwb/uwbranging/impl/NearbyControllerConnector.kt
new file mode 100644
index 00000000..61a8c844
--- /dev/null
+++ b/samples/connectivity/UwbRanging/src/main/java/com/google/uwb/uwbranging/impl/NearbyControllerConnector.kt
@@ -0,0 +1,120 @@
+/*
+ *
+ * Copyright (C) 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
+ *
+ * http://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.google.uwb.uwbranging.impl
+
+import androidx.core.uwb.RangingParameters
+import androidx.core.uwb.UwbAddress
+import androidx.core.uwb.UwbControllerSessionScope
+import com.google.uwb.uwbranging.UwbEndpoint
+import com.google.uwb.uwbranging.impl.proto.Control
+import com.google.uwb.uwbranging.impl.proto.Oob
+import com.google.uwb.uwbranging.impl.proto.UwbConfiguration
+import com.google.uwb.uwbranging.impl.proto.UwbConnectionInfo
+import com.google.firebase.crashlytics.buildtools.reloc.com.google.common.primitives.Shorts
+import com.google.protobuf.ByteString
+import kotlinx.coroutines.flow.Flow
+import kotlin.random.Random
+
+/**
+ * UWB OOB connector for a controller device using Nearby Connections.
+ *
+ * @param localEndpoint Local [UwbEndpoint].
+ * @param configId UWB Config ID
+ * @param connections OOB connections.
+ * @param sessionScopeCreator a function that returns a [UwbControllerSessionScope].
+ */
+internal class NearbyControllerConnector(
+ private val localEndpoint: UwbEndpoint,
+ private val configId: Int,
+ connections: NearbyConnections,
+ private val sessionScopeCreator: suspend () -> UwbControllerSessionScope,
+) : NearbyConnector(connections) {
+
+ override fun prepareEventFlow(): Flow {
+ return connections.startDiscovery()
+ }
+
+ override suspend fun processEndpointConnected(endpointId: String) {}
+
+ override suspend fun processUwbSessionInfo(
+ endpointId: String,
+ sessionInfo: Control,
+ ): UwbOobEvent.UwbEndpointFound? {
+
+ val capabilities =
+ if (sessionInfo.connectionInfo.hasCapabilities()) sessionInfo.connectionInfo.capabilities
+ else return null
+ if (!capabilities.supportedConfigIdsList.contains(configId)) {
+ return null
+ }
+ val endpoint = UwbEndpoint(sessionInfo.id, sessionInfo.metadata.toByteArray())
+ addEndpoint(endpointId, endpoint)
+ val sessionScope = sessionScopeCreator()
+ val sessionId = Random.nextInt()
+ val sessionKeyInfo = if (configId == RangingParameters.CONFIG_PROVISIONED_UNICAST_DS_TWR)
+ Random.nextBytes(16) else Random.nextBytes(8)
+ val subSessionId = Random.nextInt(1,100)
+ val subSessionKeyInfo = Random.nextBytes(16)
+ val endpointAddress = UwbAddress(Shorts.toByteArray(sessionInfo.localAddress.toShort()))
+ val localAddress = sessionScope.localAddress
+ val complexChannel = sessionScope.uwbComplexChannel
+ val endpointFoundEvent =
+ UwbOobEvent.UwbEndpointFound(
+ endpoint,
+ configId,
+ endpointAddress,
+ complexChannel,
+ sessionId,
+ sessionKeyInfo,
+ sessionScope,
+ subSessionId,
+ subSessionKeyInfo
+ )
+
+ connections.sendPayload(
+ endpointId,
+ Oob.newBuilder()
+ .setControl(
+ Control.newBuilder()
+ .setId(localEndpoint.id)
+ .setMetadata(ByteString.copyFrom(localEndpoint.metadata))
+ .setLocalAddress(Shorts.fromByteArray(localAddress.address).toInt())
+ .setConnectionInfo(
+ UwbConnectionInfo.newBuilder()
+ .setConfiguration(
+ UwbConfiguration.newBuilder()
+ .setConfigId(configId)
+ .setSessionId(sessionId)
+ .setChannel(complexChannel.channel)
+ .setPreambleIndex(complexChannel.preambleIndex)
+ .setSecurityInfo(ByteString.copyFrom(sessionKeyInfo))
+ .setSubSessionId(subSessionId)
+ .setSubSessionKey(ByteString.copyFrom(subSessionKeyInfo))
+ .build()
+ )
+ .build()
+ )
+ .build()
+ )
+ .build()
+ .toByteArray()
+ )
+ return endpointFoundEvent
+ }
+}
diff --git a/samples/connectivity/UwbRanging/src/main/java/com/google/uwb/uwbranging/impl/OobConnector.kt b/samples/connectivity/UwbRanging/src/main/java/com/google/uwb/uwbranging/impl/OobConnector.kt
new file mode 100644
index 00000000..fe38bde6
--- /dev/null
+++ b/samples/connectivity/UwbRanging/src/main/java/com/google/uwb/uwbranging/impl/OobConnector.kt
@@ -0,0 +1,27 @@
+/*
+ *
+ * Copyright (C) 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
+ *
+ * http://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.google.uwb.uwbranging.impl
+
+import com.google.uwb.uwbranging.UwbEndpoint
+import kotlinx.coroutines.flow.Flow
+
+internal interface OobConnector {
+ fun start(): Flow
+ fun sendMessage(endpoint: UwbEndpoint, message: ByteArray)
+}
\ No newline at end of file
diff --git a/samples/connectivity/UwbRanging/src/main/java/com/google/uwb/uwbranging/impl/UwbConnectionManagerImpl.kt b/samples/connectivity/UwbRanging/src/main/java/com/google/uwb/uwbranging/impl/UwbConnectionManagerImpl.kt
new file mode 100644
index 00000000..6913a86f
--- /dev/null
+++ b/samples/connectivity/UwbRanging/src/main/java/com/google/uwb/uwbranging/impl/UwbConnectionManagerImpl.kt
@@ -0,0 +1,51 @@
+/*
+ *
+ * Copyright (C) 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
+ *
+ * http://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.google.uwb.uwbranging.impl
+
+import android.content.Context
+import androidx.core.uwb.UwbManager
+import com.google.uwb.uwbranging.UwbConnectionManager
+import com.google.uwb.uwbranging.UwbEndpoint
+import com.google.uwb.uwbranging.UwbSessionScope
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.Dispatchers
+
+internal class UwbConnectionManagerImpl(
+ private val context: Context,
+ private val dispatcher: CoroutineDispatcher = Dispatchers.IO,
+) : UwbConnectionManager {
+
+ private val uwbManager = UwbManager.createInstance(context)
+
+ override fun controllerUwbScope(endpoint: UwbEndpoint, configId: Int): UwbSessionScope {
+ val connector =
+ NearbyControllerConnector(endpoint, configId, NearbyConnections(context, dispatcher)) {
+ uwbManager.controllerSessionScope()
+ }
+ return UwbSessionScopeImpl(endpoint, connector)
+ }
+
+ override fun controleeUwbScope(endpoint: UwbEndpoint): UwbSessionScope {
+ val connector =
+ NearbyControleeConnector(endpoint, NearbyConnections(context, dispatcher)) {
+ uwbManager.controleeSessionScope()
+ }
+ return UwbSessionScopeImpl(endpoint, connector)
+ }
+}
diff --git a/samples/connectivity/UwbRanging/src/main/java/com/google/uwb/uwbranging/impl/UwbOobEvent.kt b/samples/connectivity/UwbRanging/src/main/java/com/google/uwb/uwbranging/impl/UwbOobEvent.kt
new file mode 100644
index 00000000..9f0656c6
--- /dev/null
+++ b/samples/connectivity/UwbRanging/src/main/java/com/google/uwb/uwbranging/impl/UwbOobEvent.kt
@@ -0,0 +1,96 @@
+/*
+ *
+ * Copyright (C) 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
+ *
+ * http://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.google.uwb.uwbranging.impl
+
+import androidx.core.uwb.UwbAddress
+import androidx.core.uwb.UwbClientSessionScope
+import androidx.core.uwb.UwbComplexChannel
+import com.google.uwb.uwbranging.UwbEndpoint
+
+/** Events that happen during UWB OOB Connections. */
+internal abstract class UwbOobEvent private constructor() {
+
+ abstract val endpoint: UwbEndpoint
+
+ /** An event that notifies an endpoint is found through OOB. */
+ data class UwbEndpointFound(
+ override val endpoint: UwbEndpoint,
+ val configId: Int,
+ val endpointAddress: UwbAddress,
+ val complexChannel: UwbComplexChannel,
+ val sessionId: Int,
+ val sessionKeyInfo: ByteArray,
+ val sessionScope: UwbClientSessionScope,
+ val subSessionid: Int,
+ val subSessionKeyInfo: ByteArray?,
+ ) : UwbOobEvent() {
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (other !is UwbEndpointFound) return false
+
+ if (configId != other.configId) return false
+ if (sessionId != other.sessionId) return false
+ if (subSessionid != other.subSessionid) return false
+ if (endpoint != other.endpoint) return false
+ if (endpointAddress != other.endpointAddress) return false
+ if (complexChannel != other.complexChannel) return false
+ if (!sessionKeyInfo.contentEquals(other.sessionKeyInfo)) return false
+ if (sessionScope != other.sessionScope) return false
+ if (!subSessionKeyInfo.contentEquals(other.subSessionKeyInfo)) return false
+
+ return true
+ }
+
+ override fun hashCode(): Int {
+ var result = configId
+ result = 31 * result + sessionId
+ result = 31 * result + subSessionid
+ result = 31 * result + endpoint.hashCode()
+ result = 31 * result + endpointAddress.hashCode()
+ result = 31 * result + complexChannel.hashCode()
+ result = 31 * result + sessionKeyInfo.contentHashCode()
+ result = 31 * result + sessionScope.hashCode()
+ result = 31 * result + (subSessionKeyInfo?.contentHashCode() ?: 0)
+ return result
+ }
+ }
+
+ /** An event that notifies a UWB endpoint is lost. */
+ data class UwbEndpointLost(override val endpoint: UwbEndpoint) : UwbOobEvent()
+
+ /** Notifies that a message is received. */
+ data class MessageReceived(override val endpoint: UwbEndpoint, val message: ByteArray) :
+ UwbOobEvent() {
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (other !is MessageReceived) return false
+
+ if (endpoint != other.endpoint) return false
+ if (!message.contentEquals(other.message)) return false
+
+ return true
+ }
+
+ override fun hashCode(): Int {
+ var result = endpoint.hashCode()
+ result = 31 * result + message.contentHashCode()
+ return result
+ }
+ }
+}
diff --git a/samples/connectivity/UwbRanging/src/main/java/com/google/uwb/uwbranging/impl/UwbSessionScopeImpl.kt b/samples/connectivity/UwbRanging/src/main/java/com/google/uwb/uwbranging/impl/UwbSessionScopeImpl.kt
new file mode 100644
index 00000000..2e6c42f9
--- /dev/null
+++ b/samples/connectivity/UwbRanging/src/main/java/com/google/uwb/uwbranging/impl/UwbSessionScopeImpl.kt
@@ -0,0 +1,106 @@
+/*
+ *
+ * Copyright (C) 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
+ *
+ * http://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.google.uwb.uwbranging.impl
+
+import androidx.core.uwb.RangingParameters
+import androidx.core.uwb.RangingResult
+import androidx.core.uwb.UwbAddress
+import androidx.core.uwb.UwbDevice
+import com.google.uwb.uwbranging.EndpointEvents
+import com.google.uwb.uwbranging.UwbEndpoint
+import com.google.uwb.uwbranging.UwbSessionScope
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.channels.ProducerScope
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.channelFlow
+import kotlinx.coroutines.launch
+
+internal class UwbSessionScopeImpl(
+ private val localEndpoint: UwbEndpoint,
+ private val connector: OobConnector,
+) : UwbSessionScope {
+
+ private val localAddresses = mutableSetOf()
+
+ private val remoteDeviceMap = mutableMapOf()
+
+ private val activeJobs = mutableMapOf()
+
+ override fun prepareSession() = channelFlow {
+ val job = launch {
+ connector.start().collect { event ->
+ when (event) {
+ is UwbOobEvent.UwbEndpointFound -> {
+ val rangingEvents = processEndpointFound(event)
+ activeJobs[event.endpoint] = launch { rangingEvents.collect { sendResult(it) } }
+ }
+ is UwbOobEvent.UwbEndpointLost -> processEndpointLost(event.endpoint)
+ is UwbOobEvent.MessageReceived ->
+ trySend(EndpointEvents.EndpointMessage(event.endpoint, event.message))
+ }
+ }
+ }
+ awaitClose {
+ job.cancel()
+ remoteDeviceMap.clear()
+ }
+ }
+
+ override fun sendMessage(endpoint: UwbEndpoint, message: ByteArray) {
+ connector.sendMessage(endpoint, message)
+ }
+
+ private fun ProducerScope.processEndpointLost(endpoint: UwbEndpoint) {
+ trySend(EndpointEvents.EndpointLost(endpoint))
+ activeJobs[endpoint]?.cancel()
+ }
+
+ private fun ProducerScope.processEndpointFound(
+ event: UwbOobEvent.UwbEndpointFound,
+ ): Flow {
+ remoteDeviceMap[event.endpointAddress] = event.endpoint
+ localAddresses.add(event.sessionScope.localAddress)
+ val rangingParameters =
+ RangingParameters(
+ event.configId,
+ event.sessionId,
+ event.subSessionid,
+ event.sessionKeyInfo,
+ event.subSessionKeyInfo,
+ event.complexChannel,
+ listOf(UwbDevice(event.endpointAddress)),
+ RangingParameters.RANGING_UPDATE_RATE_FREQUENT
+ )
+ trySend(EndpointEvents.EndpointFound(event.endpoint))
+ return event.sessionScope.prepareSession(rangingParameters)
+ }
+
+ private fun ProducerScope.sendResult(result: RangingResult) {
+ val endpoint =
+ if (localAddresses.contains(result.device.address)) localEndpoint
+ else remoteDeviceMap[result.device.address] ?: return
+ when (result) {
+ is RangingResult.RangingResultPosition ->
+ trySend(EndpointEvents.PositionUpdated(endpoint, result.position))
+ is RangingResult.RangingResultPeerDisconnected ->
+ trySend(EndpointEvents.UwbDisconnected(endpoint))
+ }
+ }
+}
diff --git a/samples/connectivity/UwbRanging/src/main/proto/appsetttings.proto b/samples/connectivity/UwbRanging/src/main/proto/appsetttings.proto
new file mode 100644
index 00000000..678d803f
--- /dev/null
+++ b/samples/connectivity/UwbRanging/src/main/proto/appsetttings.proto
@@ -0,0 +1,39 @@
+/*
+ *
+ * Copyright (C) 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
+ *
+ * http://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.
+ *
+ */
+syntax = "proto3";
+
+option java_package = "com.google.uwb.hellouwb.data";
+option java_multiple_files = true;
+
+enum DeviceType {
+ CONTROLLER = 0;
+ CONTROLEE = 1;
+}
+
+enum ConfigType {
+ CONFIG_UNICAST_DS_TWR = 0;
+ CONFIG_MULTICAST_DS_TWR = 1;
+ CONFIG_PROVISIONED_UNICAST = 2;
+}
+
+message AppSettings {
+ DeviceType device_type = 1;
+ string device_display_name = 2;
+ string device_uuid = 3;
+ ConfigType config_type = 4;
+}
diff --git a/samples/connectivity/UwbRanging/src/main/proto/uwbinfo.proto b/samples/connectivity/UwbRanging/src/main/proto/uwbinfo.proto
new file mode 100644
index 00000000..f4e02035
--- /dev/null
+++ b/samples/connectivity/UwbRanging/src/main/proto/uwbinfo.proto
@@ -0,0 +1,69 @@
+/*
+ *
+ * Copyright (C) 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
+ *
+ * http://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.
+ *
+ */
+syntax = "proto3";
+
+package hello_uwb;
+
+option java_multiple_files = true;
+option java_package = "com.google.uwb.uwbranging.impl.proto";
+
+// Represents a Controlee's capability parameters.
+message UwbCapabilities {
+ repeated int32 supported_config_ids = 1;
+ bool supports_azimuth = 2;
+ bool supports_elevation = 3;
+}
+
+// Represents the UWB configuration created by the controller.
+message UwbConfiguration {
+ int32 config_id = 1;
+ int32 channel = 2;
+ int32 preamble_index = 3;
+ int32 session_id = 4;
+ bytes security_info = 5;
+ int32 sub_session_id = 6;
+ bytes sub_session_key = 7;
+}
+
+// Connection info can be capabilities or configuration.
+message UwbConnectionInfo {
+ oneof info {
+ UwbCapabilities capabilities = 1;
+ UwbConfiguration configuration = 2;
+ }
+}
+
+// A control message
+message Control {
+ string id = 1;
+ bytes metadata = 2;
+ int32 local_address = 3;
+ UwbConnectionInfo connection_info = 4;
+}
+
+// A data message
+message Data {
+ bytes message = 1;
+}
+
+message Oob {
+ oneof content {
+ Control control = 1;
+ Data data = 2;
+ }
+}
diff --git a/samples/connectivity/UwbRanging/src/main/res/drawable-v24/ic_launcher_foreground.xml b/samples/connectivity/UwbRanging/src/main/res/drawable-v24/ic_launcher_foreground.xml
new file mode 100644
index 00000000..ef975c6b
--- /dev/null
+++ b/samples/connectivity/UwbRanging/src/main/res/drawable-v24/ic_launcher_foreground.xml
@@ -0,0 +1,42 @@
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/samples/connectivity/UwbRanging/src/main/res/drawable/ic_launcher_background.xml b/samples/connectivity/UwbRanging/src/main/res/drawable/ic_launcher_background.xml
new file mode 100644
index 00000000..34f6ade7
--- /dev/null
+++ b/samples/connectivity/UwbRanging/src/main/res/drawable/ic_launcher_background.xml
@@ -0,0 +1,182 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/samples/connectivity/UwbRanging/src/main/res/drawable/lock_close24px.xml b/samples/connectivity/UwbRanging/src/main/res/drawable/lock_close24px.xml
new file mode 100644
index 00000000..241d95d9
--- /dev/null
+++ b/samples/connectivity/UwbRanging/src/main/res/drawable/lock_close24px.xml
@@ -0,0 +1,23 @@
+
+
+
+
+
diff --git a/samples/connectivity/UwbRanging/src/main/res/drawable/lock_open24px.xml b/samples/connectivity/UwbRanging/src/main/res/drawable/lock_open24px.xml
new file mode 100644
index 00000000..dad9d3cb
--- /dev/null
+++ b/samples/connectivity/UwbRanging/src/main/res/drawable/lock_open24px.xml
@@ -0,0 +1,23 @@
+
+
+
+
+
diff --git a/samples/connectivity/UwbRanging/src/main/res/drawable/uwb.png b/samples/connectivity/UwbRanging/src/main/res/drawable/uwb.png
new file mode 100644
index 00000000..b1b30a8a
Binary files /dev/null and b/samples/connectivity/UwbRanging/src/main/res/drawable/uwb.png differ
diff --git a/samples/connectivity/UwbRanging/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/samples/connectivity/UwbRanging/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
new file mode 100644
index 00000000..14bc66ad
--- /dev/null
+++ b/samples/connectivity/UwbRanging/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/samples/connectivity/UwbRanging/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/samples/connectivity/UwbRanging/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
new file mode 100644
index 00000000..14bc66ad
--- /dev/null
+++ b/samples/connectivity/UwbRanging/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/samples/connectivity/UwbRanging/src/main/res/mipmap-hdpi/ic_launcher.webp b/samples/connectivity/UwbRanging/src/main/res/mipmap-hdpi/ic_launcher.webp
new file mode 100644
index 00000000..c209e78e
Binary files /dev/null and b/samples/connectivity/UwbRanging/src/main/res/mipmap-hdpi/ic_launcher.webp differ
diff --git a/samples/connectivity/UwbRanging/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/samples/connectivity/UwbRanging/src/main/res/mipmap-hdpi/ic_launcher_round.webp
new file mode 100644
index 00000000..b2dfe3d1
Binary files /dev/null and b/samples/connectivity/UwbRanging/src/main/res/mipmap-hdpi/ic_launcher_round.webp differ
diff --git a/samples/connectivity/UwbRanging/src/main/res/mipmap-mdpi/ic_launcher.webp b/samples/connectivity/UwbRanging/src/main/res/mipmap-mdpi/ic_launcher.webp
new file mode 100644
index 00000000..4f0f1d64
Binary files /dev/null and b/samples/connectivity/UwbRanging/src/main/res/mipmap-mdpi/ic_launcher.webp differ
diff --git a/samples/connectivity/UwbRanging/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/samples/connectivity/UwbRanging/src/main/res/mipmap-mdpi/ic_launcher_round.webp
new file mode 100644
index 00000000..62b611da
Binary files /dev/null and b/samples/connectivity/UwbRanging/src/main/res/mipmap-mdpi/ic_launcher_round.webp differ
diff --git a/samples/connectivity/UwbRanging/src/main/res/mipmap-xhdpi/ic_launcher.webp b/samples/connectivity/UwbRanging/src/main/res/mipmap-xhdpi/ic_launcher.webp
new file mode 100644
index 00000000..948a3070
Binary files /dev/null and b/samples/connectivity/UwbRanging/src/main/res/mipmap-xhdpi/ic_launcher.webp differ
diff --git a/samples/connectivity/UwbRanging/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/samples/connectivity/UwbRanging/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
new file mode 100644
index 00000000..1b9a6956
Binary files /dev/null and b/samples/connectivity/UwbRanging/src/main/res/mipmap-xhdpi/ic_launcher_round.webp differ
diff --git a/samples/connectivity/UwbRanging/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/samples/connectivity/UwbRanging/src/main/res/mipmap-xxhdpi/ic_launcher.webp
new file mode 100644
index 00000000..28d4b77f
Binary files /dev/null and b/samples/connectivity/UwbRanging/src/main/res/mipmap-xxhdpi/ic_launcher.webp differ
diff --git a/samples/connectivity/UwbRanging/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/samples/connectivity/UwbRanging/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
new file mode 100644
index 00000000..9287f508
Binary files /dev/null and b/samples/connectivity/UwbRanging/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp differ
diff --git a/samples/connectivity/UwbRanging/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/samples/connectivity/UwbRanging/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
new file mode 100644
index 00000000..aa7d6427
Binary files /dev/null and b/samples/connectivity/UwbRanging/src/main/res/mipmap-xxxhdpi/ic_launcher.webp differ
diff --git a/samples/connectivity/UwbRanging/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/samples/connectivity/UwbRanging/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
new file mode 100644
index 00000000..9126ae37
Binary files /dev/null and b/samples/connectivity/UwbRanging/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp differ
diff --git a/samples/connectivity/UwbRanging/src/main/res/values/colors.xml b/samples/connectivity/UwbRanging/src/main/res/values/colors.xml
new file mode 100644
index 00000000..fdc2e4d1
--- /dev/null
+++ b/samples/connectivity/UwbRanging/src/main/res/values/colors.xml
@@ -0,0 +1,22 @@
+
+
+
+ #FFBB86FC
+ #FF6200EE
+ #FF3700B3
+ #FF03DAC5
+ #FF018786
+ #FF000000
+ #FFFFFFFF
+
\ No newline at end of file
diff --git a/samples/connectivity/UwbRanging/src/main/res/values/strings.xml b/samples/connectivity/UwbRanging/src/main/res/values/strings.xml
new file mode 100644
index 00000000..6100d585
--- /dev/null
+++ b/samples/connectivity/UwbRanging/src/main/res/values/strings.xml
@@ -0,0 +1,15 @@
+
+
+ UWB Samples
+
\ No newline at end of file
diff --git a/samples/connectivity/UwbRanging/src/main/res/values/themes.xml b/samples/connectivity/UwbRanging/src/main/res/values/themes.xml
new file mode 100644
index 00000000..4763d157
--- /dev/null
+++ b/samples/connectivity/UwbRanging/src/main/res/values/themes.xml
@@ -0,0 +1,16 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/samples/connectivity/UwbRanging/src/main/res/xml/backup_rules.xml b/samples/connectivity/UwbRanging/src/main/res/xml/backup_rules.xml
new file mode 100644
index 00000000..27a07304
--- /dev/null
+++ b/samples/connectivity/UwbRanging/src/main/res/xml/backup_rules.xml
@@ -0,0 +1,26 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/samples/connectivity/UwbRanging/src/main/res/xml/data_extraction_rules.xml b/samples/connectivity/UwbRanging/src/main/res/xml/data_extraction_rules.xml
new file mode 100644
index 00000000..2b4fc856
--- /dev/null
+++ b/samples/connectivity/UwbRanging/src/main/res/xml/data_extraction_rules.xml
@@ -0,0 +1,32 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/settings.gradle.kts b/settings.gradle.kts
index d4f4933a..178c8daa 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -47,6 +47,7 @@ include(":samples:connectivity:bluetooth:ble")
include(":samples:connectivity:bluetooth:companion")
include(":samples:connectivity:callnotification")
include(":samples:connectivity:telecom")
+include(":samples:connectivity:UwbRanging")
include(":samples:graphics:pdf")
include(":samples:graphics:ultrahdr")
include(":samples:location")