From da31111f6947fb59173528f3a1dc37697d0e9ec0 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 | 118 +++++++++++------- .../Application/src/main/res/values/wear.xml | 34 ++--- DataLayer/Wearable/build.gradle | 4 + .../Wearable/src/main/AndroidManifest.xml | 7 -- .../wearable/datalayer/MainActivity.kt | 60 ++++++--- .../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, 147 insertions(+), 87 deletions(-) diff --git a/DataLayer/Application/build.gradle b/DataLayer/Application/build.gradle index 678e36967..78b11dcad 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..87923c343 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 @@ -35,6 +35,10 @@ 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 @@ -55,17 +59,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 +91,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,26 +191,23 @@ class MainActivity : ComponentActivity() { private fun startWearableActivity() { lifecycleScope.launch { + if(phoneDataLayerAppHelper.isAvailable()){ 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() + val config = activityConfig { + classFullName = SAMPLE_CLASS_FULL_NAME + } + phoneDataLayerAppHelper.connectedAndInstalledNodes.collect { nodes -> + nodes.mapTo(mutableSetOf()) { + phoneDataLayerAppHelper.startRemoteActivity(it.id, config) } - }.awaitAll() - + } 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 +218,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 +273,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 +74,13 @@ class MainActivity : ComponentActivity() { private fun onQueryOtherDevicesClicked() { lifecycleScope.launch { try { - val nodes = getCapabilitiesForReachableNodes() - .filterValues { MOBILE_CAPABILITY in it || WEAR_CAPABILITY in it }.keys - displayNodes(nodes) + var nodeList: Set? = null + if (wearDataLayerAppHelper.isAvailable()){ + wearDataLayerAppHelper.connectedAndInstalledNodes.collect { nodes -> + nodeList = nodes.mapTo(mutableSetOf()) { it } + } + } + displayResult(nodeList) } catch (cancellationException: CancellationException) { throw cancellationException } catch (exception: Exception) { @@ -68,9 +92,12 @@ 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: Set? = null + if (wearDataLayerAppHelper.isAvailable()) { + nodes = getCapabilitiesForReachableNodes() + .filterValues { MOBILE_CAPABILITY in it && CAMERA_CAPABILITY in it }.keys + } + displayResult(nodes) } catch (cancellationException: CancellationException) { throw cancellationException } catch (exception: Exception) { @@ -102,11 +129,16 @@ 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 != null){ + if (nodes.isEmpty()) { + getString(R.string.no_device) + } else { + getString(R.string.connected_nodes, nodes.joinToString(", ") { it.displayName }) + } + } else{ + getString(R.string.unsupported_feature) } 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" }