Skip to content

Commit

Permalink
Uses DataHelpers in DataLayer samp[le
Browse files Browse the repository at this point in the history
  • Loading branch information
kul3r4 committed May 31, 2024
1 parent af89f1a commit c36e8ac
Show file tree
Hide file tree
Showing 9 changed files with 146 additions and 98 deletions.
4 changes: 3 additions & 1 deletion DataLayer/Application/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
Expand All @@ -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++
}
}
}
}
Expand Down Expand Up @@ -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")
}
}
}
}
Expand All @@ -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")
}
}
}
}
Expand Down Expand Up @@ -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"
Expand Down
34 changes: 17 additions & 17 deletions DataLayer/Application/src/main/res/values/wear.xml
Original file line number Diff line number Diff line change
@@ -1,25 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2021 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.
-->
<?xml version="1.0" encoding="utf-8"?><!--
~ Copyright 2023 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ https://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->

<resources xmlns:tools="http://schemas.android.com/tools">
<string-array
name="android_wear_capabilities"
translatable="false"
tools:ignore="UnusedResources">
<!-- declaring the provided capabilities -->
<item>mobile</item>
<!-- Used to indicate that the app is installed on this device -->
<item>data_layer_app_helper_device_phone</item>
</string-array>
</resources>
4 changes: 4 additions & 0 deletions DataLayer/Wearable/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
7 changes: 0 additions & 7 deletions DataLayer/Wearable/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -45,13 +45,6 @@
android:pathPrefix="/count"
android:scheme="wear" />
</intent-filter>
<intent-filter>
<action android:name="com.google.android.gms.wearable.MESSAGE_RECEIVED" />
<data
android:host="*"
android:pathPrefix="/start-activity"
android:scheme="wear" />
</intent-filter>
</service>
<activity
android:name=".MainActivity"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,23 +24,42 @@ import androidx.activity.compose.setContent
import androidx.activity.viewModels
import androidx.lifecycle.lifecycleScope
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.Node
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.datalayer.watch.WearDataLayerAppHelper
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.launch
import kotlinx.coroutines.tasks.await

@OptIn(ExperimentalHorologistApi::class)
class MainActivity : ComponentActivity() {

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 wearDataLayerAppHelper: WearDataLayerAppHelper
private lateinit var dataClient: DataClient
private lateinit var messageClient: MessageClient
private lateinit var capabilityClient: CapabilityClient

private val clientDataViewModel by viewModels<ClientDataViewModel>()

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,
Expand All @@ -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) {
Expand All @@ -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) {
Expand Down Expand Up @@ -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<Node>) {
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<Node>) {
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()
}
Expand Down
1 change: 1 addition & 0 deletions DataLayer/Wearable/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
<string name="query_other_devices">Query for connected devices</string>
<string name="query_mobile_camera">Query for mobile camera</string>
<string name="no_device">No device provides the requested capabilities</string>
<string name="unsupported_feature">Your device does not support the Wearable API</string>
<string name="connected_nodes">Nodes: %1$s</string>
<string name="message">Message</string>
<string name="capability_changed">Capability changed</string>
Expand Down
2 changes: 1 addition & 1 deletion DataLayer/Wearable/src/main/res/values/wear.xml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,6 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<string-array name="android_wear_capabilities" tools:ignore="UnusedResources" translatable="false">
<!-- declaring the provided capabilities -->
<item>wear</item>
<item>data_layer_app_helper_device_watch</item>
</string-array>
</resources>
Loading

0 comments on commit c36e8ac

Please sign in to comment.