From c36e8ac924ae2017a45ab126de5b9055af5a84a5 Mon Sep 17 00:00:00 2001 From: Chiara Chiappini Date: Wed, 29 May 2024 16:29:56 +0100 Subject: [PATCH] Uses DataHelpers in DataLayer samp[le --- DataLayer/Application/build.gradle | 4 +- .../wearable/datalayer/MainActivity.kt | 133 ++++++++++-------- .../Application/src/main/res/values/wear.xml | 34 ++--- DataLayer/Wearable/build.gradle | 4 + .../Wearable/src/main/AndroidManifest.xml | 7 - .../wearable/datalayer/MainActivity.kt | 55 +++++--- .../Wearable/src/main/res/values/strings.xml | 1 + .../Wearable/src/main/res/values/wear.xml | 2 +- DataLayer/gradle/libs.versions.toml | 4 + 9 files changed, 146 insertions(+), 98 deletions(-) diff --git a/DataLayer/Application/build.gradle b/DataLayer/Application/build.gradle index 678e36967..1345d9ff4 100644 --- a/DataLayer/Application/build.gradle +++ b/DataLayer/Application/build.gradle @@ -29,7 +29,7 @@ android { applicationId "com.example.android.wearable.datalayer" versionCode 1 versionName "1.0" - minSdk 21 + minSdk 23 targetSdk 33 testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" @@ -76,4 +76,6 @@ dependencies { implementation libs.androidx.lifecycle.viewmodel.compose implementation libs.androidx.lifecycle.runtime.ktx implementation libs.playservices.wearable + implementation libs.horologist.datalayer + implementation libs.horologist.datalayer.phone } diff --git a/DataLayer/Application/src/main/java/com/example/android/wearable/datalayer/MainActivity.kt b/DataLayer/Application/src/main/java/com/example/android/wearable/datalayer/MainActivity.kt index 5b52dda10..4a7a16d90 100644 --- a/DataLayer/Application/src/main/java/com/example/android/wearable/datalayer/MainActivity.kt +++ b/DataLayer/Application/src/main/java/com/example/android/wearable/datalayer/MainActivity.kt @@ -34,15 +34,16 @@ import com.google.android.gms.wearable.CapabilityClient import com.google.android.gms.wearable.DataClient import com.google.android.gms.wearable.MessageClient import com.google.android.gms.wearable.PutDataMapRequest -import com.google.android.gms.wearable.Wearable +import com.google.android.horologist.annotations.ExperimentalHorologistApi +import com.google.android.horologist.data.WearDataLayerRegistry +import com.google.android.horologist.data.activityConfig +import com.google.android.horologist.datalayer.phone.PhoneDataLayerAppHelper import java.io.ByteArrayOutputStream import java.time.Duration import java.time.Instant import kotlinx.coroutines.CancellationException import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.NonCancellable -import kotlinx.coroutines.async -import kotlinx.coroutines.awaitAll import kotlinx.coroutines.delay import kotlinx.coroutines.isActive import kotlinx.coroutines.launch @@ -55,17 +56,22 @@ import kotlinx.coroutines.withContext * While resumed, this activity periodically sends a count through the [DataClient], and offers * the ability for the user to take and send a photo over the [DataClient]. * - * This activity also allows the user to launch the companion wear activity via the [MessageClient]. + * This activity also allows the user to launch the companion wear activity using Horologist + * PhoneDataLayerAppHelper, see https://google.github.io/horologist/api/datalayer/phone/com.google.android.horologist.datalayer.phone/-phone-data-layer-app-helper/index.html. * * While resumed, this activity also logs all interactions across the clients, which includes events * sent from this activity and from the watch(es). */ @SuppressLint("VisibleForTests") +@OptIn(ExperimentalHorologistApi::class) class MainActivity : ComponentActivity() { + private val SAMPLE_CLASS_FULL_NAME = + "com.example.android.wearable.datalayer.MainActivity" - private val dataClient by lazy { Wearable.getDataClient(this) } - private val messageClient by lazy { Wearable.getMessageClient(this) } - private val capabilityClient by lazy { Wearable.getCapabilityClient(this) } + private lateinit var phoneDataLayerAppHelper: PhoneDataLayerAppHelper + private lateinit var dataClient: DataClient + private lateinit var messageClient: MessageClient + private lateinit var capabilityClient: CapabilityClient private val isCameraSupported by lazy { packageManager.hasSystemFeature(PackageManager.FEATURE_CAMERA_ANY) @@ -82,25 +88,42 @@ class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + val registry = WearDataLayerRegistry.fromContext( + application = this@MainActivity.applicationContext, + coroutineScope = lifecycleScope + ) + + dataClient = registry.dataClient + messageClient = registry.messageClient + capabilityClient = registry.capabilityClient + + phoneDataLayerAppHelper = PhoneDataLayerAppHelper( + context = this, + registry = registry + ) + var count = 0 lifecycleScope.launch { - lifecycle.repeatOnLifecycle(Lifecycle.State.RESUMED) { - // Set the initial trigger such that the first count will happen in one second. - var lastTriggerTime = Instant.now() - (countInterval - Duration.ofSeconds(1)) - while (isActive) { - // Figure out how much time we still have to wait until our next desired trigger - // point. This could be less than the count interval if sending the count took - // some time. - delay( - Duration.between(Instant.now(), lastTriggerTime + countInterval).toMillis() - ) - // Update when we are triggering sending the count - lastTriggerTime = Instant.now() - sendCount(count) + if (phoneDataLayerAppHelper.isAvailable()) { + lifecycle.repeatOnLifecycle(Lifecycle.State.RESUMED) { + // Set the initial trigger such that the first count will happen in one second. + var lastTriggerTime = Instant.now() - (countInterval - Duration.ofSeconds(1)) + while (isActive) { + // Figure out how much time we still have to wait until our next desired trigger + // point. This could be less than the count interval if sending the count took + // some time. + delay( + Duration.between(Instant.now(), lastTriggerTime + countInterval) + .toMillis() + ) + // Update when we are triggering sending the count + lastTriggerTime = Instant.now() + sendCount(count) - // Increment the count to send next time - count++ + // Increment the count to send next time + count++ + } } } } @@ -165,25 +188,22 @@ class MainActivity : ComponentActivity() { private fun startWearableActivity() { lifecycleScope.launch { - try { - val nodes = capabilityClient - .getCapability(WEAR_CAPABILITY, CapabilityClient.FILTER_REACHABLE) - .await() - .nodes - - // Send a message to all nodes in parallel - nodes.map { node -> - async { - messageClient.sendMessage(node.id, START_ACTIVITY_PATH, byteArrayOf()) - .await() + if (phoneDataLayerAppHelper.isAvailable()) { + try { + val config = activityConfig { + classFullName = SAMPLE_CLASS_FULL_NAME } - }.awaitAll() - - Log.d(TAG, "Starting activity requests sent successfully") - } catch (cancellationException: CancellationException) { - throw cancellationException - } catch (exception: Exception) { - Log.d(TAG, "Starting activity failed: $exception") + phoneDataLayerAppHelper.connectedAndInstalledNodes.collect { nodes -> + nodes.mapTo(mutableSetOf()) { + phoneDataLayerAppHelper.startRemoteActivity(it.id, config) + } + } + Log.d(TAG, "Starting activity requests sent successfully") + } catch (cancellationException: CancellationException) { + throw cancellationException + } catch (exception: Exception) { + Log.d(TAG, "Starting activity failed: $exception") + } } } } @@ -195,23 +215,25 @@ class MainActivity : ComponentActivity() { private fun sendPhoto() { lifecycleScope.launch { - try { - val image = clientDataViewModel.image ?: return@launch - val imageAsset = image.toAsset() - val request = PutDataMapRequest.create(IMAGE_PATH).apply { - dataMap.putAsset(IMAGE_KEY, imageAsset) - dataMap.putLong(TIME_KEY, Instant.now().epochSecond) - } - .asPutDataRequest() - .setUrgent() + if (phoneDataLayerAppHelper.isAvailable()) { + try { + val image = clientDataViewModel.image ?: return@launch + val imageAsset = image.toAsset() + val request = PutDataMapRequest.create(IMAGE_PATH).apply { + dataMap.putAsset(IMAGE_KEY, imageAsset) + dataMap.putLong(TIME_KEY, Instant.now().epochSecond) + } + .asPutDataRequest() + .setUrgent() - val result = dataClient.putDataItem(request).await() + val result = dataClient.putDataItem(request).await() - Log.d(TAG, "DataItem saved: $result") - } catch (cancellationException: CancellationException) { - throw cancellationException - } catch (exception: Exception) { - Log.d(TAG, "Saving DataItem failed: $exception") + Log.d(TAG, "DataItem saved: $result") + } catch (cancellationException: CancellationException) { + throw cancellationException + } catch (exception: Exception) { + Log.d(TAG, "Saving DataItem failed: $exception") + } } } } @@ -248,7 +270,6 @@ class MainActivity : ComponentActivity() { companion object { private const val TAG = "MainActivity" - private const val START_ACTIVITY_PATH = "/start-activity" private const val COUNT_PATH = "/count" private const val IMAGE_PATH = "/image" private const val IMAGE_KEY = "photo" diff --git a/DataLayer/Application/src/main/res/values/wear.xml b/DataLayer/Application/src/main/res/values/wear.xml index 2050ac5f6..6bddd82d6 100644 --- a/DataLayer/Application/src/main/res/values/wear.xml +++ b/DataLayer/Application/src/main/res/values/wear.xml @@ -1,25 +1,25 @@ - - + - - mobile + + data_layer_app_helper_device_phone diff --git a/DataLayer/Wearable/build.gradle b/DataLayer/Wearable/build.gradle index 420aa155e..993f515c4 100644 --- a/DataLayer/Wearable/build.gradle +++ b/DataLayer/Wearable/build.gradle @@ -98,6 +98,10 @@ dependencies { implementation libs.horologist.composables implementation libs.horologist.compose.layout implementation libs.horologist.compose.material + implementation libs.horologist.datalayer.watch + implementation libs.horologist.datalayer + + implementation libs.kotlin.stdlib coreLibraryDesugaring libs.desugar.jdk.libs diff --git a/DataLayer/Wearable/src/main/AndroidManifest.xml b/DataLayer/Wearable/src/main/AndroidManifest.xml index af60081b0..02c0c509f 100644 --- a/DataLayer/Wearable/src/main/AndroidManifest.xml +++ b/DataLayer/Wearable/src/main/AndroidManifest.xml @@ -45,13 +45,6 @@ android:pathPrefix="/count" android:scheme="wear" /> - - - - () override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + val registry = WearDataLayerRegistry.fromContext( + application = this@MainActivity.applicationContext, + coroutineScope = lifecycleScope + ) + wearDataLayerAppHelper = WearDataLayerAppHelper( + context = applicationContext, + registry = registry, + scope = lifecycleScope + ) + dataClient = registry.dataClient + messageClient = registry.messageClient + capabilityClient = registry.capabilityClient + setContent { MainApp( events = clientDataViewModel.events, @@ -54,9 +73,11 @@ class MainActivity : ComponentActivity() { private fun onQueryOtherDevicesClicked() { lifecycleScope.launch { try { - val nodes = getCapabilitiesForReachableNodes() - .filterValues { MOBILE_CAPABILITY in it || WEAR_CAPABILITY in it }.keys - displayNodes(nodes) + wearDataLayerAppHelper.connectedAndInstalledNodes.collect { nodes -> + var nodeList = nodes.mapTo(mutableSetOf()) { it } + displayResult(nodeList) + } + } catch (cancellationException: CancellationException) { throw cancellationException } catch (exception: Exception) { @@ -68,9 +89,9 @@ class MainActivity : ComponentActivity() { private fun onQueryMobileCameraClicked() { lifecycleScope.launch { try { - val nodes = getCapabilitiesForReachableNodes() - .filterValues { MOBILE_CAPABILITY in it && CAMERA_CAPABILITY in it }.keys - displayNodes(nodes) + var nodes = getCapabilitiesForReachableNodes() + .filterValues { MOBILE_CAPABILITY in it && CAMERA_CAPABILITY in it }.keys + displayResult(nodes) } catch (cancellationException: CancellationException) { throw cancellationException } catch (exception: Exception) { @@ -102,12 +123,14 @@ class MainActivity : ComponentActivity() { // Transform the capability list for each node into a set .mapValues { it.value.toSet() } - private fun displayNodes(nodes: Set) { - val message = if (nodes.isEmpty()) { - getString(R.string.no_device) - } else { - getString(R.string.connected_nodes, nodes.joinToString(", ") { it.displayName }) - } + // TODO remove Toast as it's not a best practice on Wear and improve handling of result + private fun displayResult(nodes: Set) { + val message = + if (nodes.isEmpty()) { + getString(R.string.no_device) + } else { + getString(R.string.connected_nodes, nodes.joinToString(", ") { it.displayName }) + } Toast.makeText(this, message, Toast.LENGTH_SHORT).show() } diff --git a/DataLayer/Wearable/src/main/res/values/strings.xml b/DataLayer/Wearable/src/main/res/values/strings.xml index b8744d37a..5f35b3b18 100644 --- a/DataLayer/Wearable/src/main/res/values/strings.xml +++ b/DataLayer/Wearable/src/main/res/values/strings.xml @@ -20,6 +20,7 @@ Query for connected devices Query for mobile camera No device provides the requested capabilities + Your device does not support the Wearable API Nodes: %1$s Message Capability changed diff --git a/DataLayer/Wearable/src/main/res/values/wear.xml b/DataLayer/Wearable/src/main/res/values/wear.xml index e48d0b660..cf30754d6 100644 --- a/DataLayer/Wearable/src/main/res/values/wear.xml +++ b/DataLayer/Wearable/src/main/res/values/wear.xml @@ -17,6 +17,6 @@ - wear + data_layer_app_helper_device_watch diff --git a/DataLayer/gradle/libs.versions.toml b/DataLayer/gradle/libs.versions.toml index 0d87d1f60..df1dcd663 100644 --- a/DataLayer/gradle/libs.versions.toml +++ b/DataLayer/gradle/libs.versions.toml @@ -36,8 +36,12 @@ wear-compose-ui-tooling = { module = "androidx.wear.compose:compose-ui-tooling", horologist-composables = { module = "com.google.android.horologist:horologist-composables", version.ref = "horologist" } horologist-compose-layout = { module = "com.google.android.horologist:horologist-compose-layout", version.ref = "horologist" } horologist-compose-material = { module = "com.google.android.horologist:horologist-compose-material", version.ref = "horologist" } +horologist-datalayer-watch = { module = "com.google.android.horologist:horologist-datalayer-watch", version.ref = "horologist" } +horologist-datalayer-phone = { module = "com.google.android.horologist:horologist-datalayer-phone", version.ref = "horologist" } +horologist-datalayer= { module = "com.google.android.horologist:horologist-datalayer", version.ref = "horologist" } compose-ui-tooling-preview = { module = "androidx.compose.ui:ui-tooling-preview" } desugar-jdk-libs = "com.android.tools:desugar_jdk_libs:2.0.4" +kotlin-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib", version.ref = "org-jetbrains-kotlin" } [plugins] com-android-application = { id = "com.android.application", version.ref = "android-gradle-plugin" }