Skip to content

Commit

Permalink
Update sample with more documentation and remove Toasts
Browse files Browse the repository at this point in the history
  • Loading branch information
kul3r4 committed Jun 6, 2024
1 parent e7ed38f commit 6b1f5c7
Show file tree
Hide file tree
Showing 14 changed files with 499 additions and 184 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,18 @@ import androidx.activity.compose.setContent
import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels
import androidx.compose.material.MaterialTheme
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import com.google.android.gms.common.GoogleApiAvailability
import com.google.android.gms.common.api.AvailabilityException
import com.google.android.gms.common.api.GoogleApi
import com.google.android.gms.wearable.Asset
import com.google.android.gms.wearable.CapabilityClient
import com.google.android.gms.wearable.DataClient
Expand Down Expand Up @@ -85,32 +94,45 @@ class MainActivity : ComponentActivity() {
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)

// Increment the count to send next time
count++
if (isAvailable(capabilityClient)) {
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
// This count is local to the specific instance of this activity and may reset
// when a new instance is recreated. For a more complex example where the counter
// is stored in a DataStore and modeled as a proto, see thre Horologist DataLayer sample in
// https://google.github.io/horologist/datalayer/
count++
}
}
}
}

setContent {
MaterialTheme {
val coroutineScope = rememberCoroutineScope()
var apiAvailable by remember { mutableStateOf(false) }
LaunchedEffect(Unit) {
apiAvailable = isAvailable(capabilityClient)
}
MainApp(
events = clientDataViewModel.events,
image = clientDataViewModel.image,
isCameraSupported = isCameraSupported,
apiAvailable = apiAvailable,
onTakePhotoClick = ::takePhoto,
onSendPhotoClick = ::sendPhoto,
onStartWearableActivityClick = ::startWearableActivity
Expand Down Expand Up @@ -163,6 +185,11 @@ class MainActivity : ComponentActivity() {
}
}

// This method starts the Wearable app on the connected Wear device.
// Alternative to this implementation, Horologist offers a DataHelper API which allows to
// start the main activity or a different activity of your choice from the Wearable app
// see https://google.github.io/horologist/datalayer-helpers-guide/#launching-a-specific-activity-on-the-other-device
// for details
private fun startWearableActivity() {
lifecycleScope.launch {
try {
Expand All @@ -172,6 +199,9 @@ class MainActivity : ComponentActivity() {
.nodes

// Send a message to all nodes in parallel
// If you need an acknowledge for the start activity use case, you can alternatively use
// [MessageClient.sendRequest](https://developers.google.com/android/reference/com/google/android/gms/wearable/MessageClient#sendRequest(java.lang.String,%20java.lang.String,%20byte[]))
// See an implementation in Horologist DataHelper https://github.com/google/horologist/blob/release-0.5.x/datalayer/core/src/main/java/com/google/android/horologist/data/apphelper/DataLayerAppHelper.kt#L210
nodes.map { node ->
async {
messageClient.sendMessage(node.id, START_ACTIVITY_PATH, byteArrayOf())
Expand Down Expand Up @@ -245,6 +275,26 @@ class MainActivity : ComponentActivity() {
}
}

// This function checks that the Wearable API is available on the mobile device.
// If you are using the Horologist DataHelpers, this method is available in
// https://google.github.io/horologist/datalayer-helpers-guide/#check-api-availability

private suspend fun isAvailable(api: GoogleApi<*>): Boolean {
return try {
GoogleApiAvailability.getInstance()
.checkApiAvailability(api)
.await()

true
} catch (e: AvailabilityException) {
Log.d(
TAG,
"${api.javaClass.simpleName} API is not available in this device."
)
false
}
}

companion object {
private const val TAG = "MainActivity"

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,13 @@ import androidx.compose.material.Divider
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp

Expand All @@ -49,11 +49,22 @@ fun MainApp(
events: List<Event>,
image: Bitmap?,
isCameraSupported: Boolean,
apiAvailable: Boolean,
onTakePhotoClick: () -> Unit,
onSendPhotoClick: () -> Unit,
onStartWearableActivityClick: () -> Unit
) {
LazyColumn(contentPadding = PaddingValues(16.dp)) {
if (!apiAvailable) {
item {
Text(
text = stringResource(R.string.wearable_api_unavailable),
color = Color.Red,
textAlign = TextAlign.Center
)
}
}

item {
Row(
verticalAlignment = Alignment.CenterVertically
Expand Down Expand Up @@ -151,6 +162,7 @@ fun MainAppPreview() {
isCameraSupported = true,
onTakePhotoClick = {},
onSendPhotoClick = {},
onStartWearableActivityClick = {}
onStartWearableActivityClick = {},
apiAvailable = true
)
}
1 change: 1 addition & 0 deletions DataLayer/Application/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,5 @@
<string name="data_item_unknown">Unknown DataItem type</string>
<string name="message_from_watch">Message from watch</string>
<string name="capability_changed">Capability changed</string>
<string name="wearable_api_unavailable">The Wearable API is not available on this device</string>
</resources>
91 changes: 78 additions & 13 deletions DataLayer/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,26 +2,91 @@
Android DataLayer Sample
========================

This sample demonstrates how to work with a WearableListenerService,
to produce and consume DataEvents and effectively work with the DataLayer.
This sample demonstrates how to work with a WearableListenerService using:

Introduction
------------
- [DataClient][2] to exchange data events
- [MessageClient][3] to send messages
- [CapabilityClient][4] to find nodes with specific capabilities

This sample demonstrates how to make a handheld and an Wear device communicate
using the [DataClient][2].
It does this by sending a picture between connected devices.

An Activity is being used for both the connected devices which implement their parts of
the required interfaces using Jetpack Compose.
Introduction
------------

It showcases how to use an [WearableListenerService][1] to consume DataEvents
as well as implementations for various required listeners when using the [DataClient][2],
[MessageClient][3].
This sample showcases how a phone and a Wear OS app can exchange data. It implements 3 use cases:

1. Send a data asset from the phone to the watch
In the sample you can take a photo on the phone and send it to the paired watch. The photo is sent
as a [DataAsset][6] by using [DataClient][2].
```
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()
```
This use case is successful if the watch is connected and has the Wear app
installed which is implemented by using [CapabilityClient][4].
2. Send data from the phone to the watch and ackownledge via a message
The phone app increments a counter and send it over a period of 5 seconds as a [DataItem] by using [DataClient][2].
The Wear app receives the [DataItem][5] by implementing a [WearableListenerService][1] and acknowledge
by sending a [Message] via [MessageClient][3]
```
messageClient.sendMessage(
nodeId,
DATA_ITEM_RECEIVED_PATH,
payload
)
.await()
```
3. Launch the Wear app from the phone app
The phone app checks if there is a connected node with a specific capability that identifies the
correspondent Wear app. The capability is declared in the wear.xml file:
```
<string-array name="android_wear_capabilities" tools:ignore="UnusedResources" translatable="false">
<!-- declaring the provided capabilities -->
<item>wear</item>
```
Then the phone app sends a message to the Wear app by specifying the node id of the device and
the path of the activity.
```
nodes.map { node ->
async {
messageClient.sendMessage(node.id, START_ACTIVITY_PATH, byteArrayOf())
.await()
}
}.awaitAll()
```
The Wearable app is listening to events by implementing a [WearableListenerService][1] an upon receiving
the message starts the Activity.

This samples is useful to learn about how to use the Wearable API clients and WearableListenerService.
Alternatively Horologist provides some API which facilitates some use cases such as like:

- [Installing][7] the Wear app on another connected device by opening the Playstore app on the phone
- [Starting][8] the Wear app on another connected device
- [Finishing sign-in][9] the Wear app on another connected device
- [Installing a Wear Tile][10] from the phone by redirecting to the Tile settings editor screen (supported only on
some devices)
- [Persisting data on the DataStore and model as a proto][11]
- Check if the [Wearable API is supported][12] on mobile
- Find [connected nodes and understand if the app is already installed][13]

[1]: https://developers.google.com/android/reference/com/google/android/gms/wearable/WearableListenerService
[2]: https://developers.google.com/android/reference/com/google/android/gms/wearable/DataClient
[3]: https://developers.google.com/android/reference/com/google/android/gms/wearable/MessageClient
[4]: https://developers.google.com/android/reference/com/google/android/gms/wearable/CapabilityClient
[5]: https://developers.google.com/android/reference/com/google/android/gms/wearable/DataItem
[6]: https://developers.google.com/android/reference/com/google/android/gms/wearable/DataAsset
[7]: https://google.github.io/horologist/datalayer-phone-ui/#install-app
[8]: https://google.github.io/horologist/datalayer-phone-ui/#reengage-prompt
[9]: https://google.github.io/horologist/datalayer-phone-ui/#signin-prompt
[10]: https://google.github.io/horologist/datalayer-phone-ui/#install-tile-prompt
[11]: https://google.github.io/horologist/datalayer/
[12]: https://google.github.io/horologist/datalayer-helpers-guide/#check-api-availability
[13]: https://google.github.io/horologist/datalayer-helpers-guide/#connection-and-installation-status

Pre-requisites
--------------
Expand All @@ -31,7 +96,7 @@ Pre-requisites
Screenshots
-------------

<img src="screenshots/phone_image.png" height="400" alt="Screenshot"/> <img src="screenshots/wearable_background_image.png" height="400" alt="Screenshot"/>
<img src="screenshots/phone_image.png" height="400" alt="Screenshot"/> <img src="screenshots/wearable_image.png" height="400" alt="Screenshot"/>

Getting Started
---------------
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/*
* Copyright 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
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.example.android.wearable.datalayer

import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.lifecycle.compose.collectAsStateWithLifecycle

@Composable
fun CameraNodesScreen(
viewModel: NodesViewModel
) {
val state by viewModel.state.collectAsStateWithLifecycle()

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

import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.lifecycle.compose.collectAsStateWithLifecycle

@Composable
fun ConnectedNodesScreen(
viewModel: NodesViewModel
) {
val state by viewModel.state.collectAsStateWithLifecycle()

NodesScreen(nodes = state.nodes, modifier = Modifier)
}
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,11 @@ class DataLayerListenerService : WearableListenerService() {
}
}

// When the message to start the Wearable app is received, this method starts the Wearable app.
// Alternative to this implementation, Horologist offers a DataHelper API which allows to
// start the main activity or a different activity of your choice from the Wearable app
// see https://google.github.io/horologist/datalayer-helpers-guide/#launching-a-specific-activity-on-the-other-device
// for details
override fun onMessageReceived(messageEvent: MessageEvent) {
super.onMessageReceived(messageEvent)

Expand Down
Loading

0 comments on commit 6b1f5c7

Please sign in to comment.