Skip to content

Commit

Permalink
Add sample screen for connectedAndInstalledNodes function (#1999)
Browse files Browse the repository at this point in the history
  • Loading branch information
luizgrp authored Jan 23, 2024
1 parent 38b0994 commit 4409a2c
Show file tree
Hide file tree
Showing 11 changed files with 283 additions and 8 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -230,7 +230,7 @@ fun NodesScreenPreview() {

@Preview(showBackground = true)
@Composable
fun NodesActionsScreenPreviewEmptyNodes() {
fun NodesScreenPreviewEmptyNodes() {
NodesScreen(
state = NodesScreenState.Loaded(emptyList()),
onRefreshClick = { },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ fun NodesListenerScreen(
item {
Text(
text = stringResource(id = R.string.nodes_listener_screen_header),
modifier = Modifier.padding(vertical = 10.dp),
modifier = Modifier.padding(10.dp),
style = MaterialTheme.typography.titleLarge,
)
}
Expand All @@ -80,7 +80,7 @@ fun NodesListenerScreen(
item {
Text(
text = stringResource(id = R.string.nodes_listener_screen_message),
modifier = Modifier.padding(vertical = 10.dp),
modifier = Modifier.padding(10.dp),
style = MaterialTheme.typography.bodyLarge,
)
}
Expand Down Expand Up @@ -165,6 +165,22 @@ fun NodesListenerScreenPreview() {
)
}

@Preview(showBackground = true)
@Composable
fun NodesListenerScreenPreviewEmptyNodes() {
NodesListenerScreen(
state = NodesListenerScreenState.Loaded(
nodeList = emptySet(),
),
)
}

@Preview(showBackground = true)
@Composable
fun NodesListenerScreenPreviewApiNotAvailable() {
NodesListenerScreen(state = NodesListenerScreenState.ApiNotAvailable)
}

private class NodePreviewImpl(
private val displayName: String,
private val id: String,
Expand Down
4 changes: 2 additions & 2 deletions datalayer/sample/phone/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -58,10 +58,10 @@
<string name="node_status_start_remote_activity_button_label">Start remote activity</string>

<!-- App Helper Nodes Listener screen -->
<string name="nodes_listener_screen_header">Nodes</string>
<string name="nodes_listener_screen_header">Nodes Listener</string>
<string name="nodes_listener_screen_message">This screen shows all the connected nodes that have the app installed. The list updates automatically.</string>
<string name="nodes_listener_screen_no_nodes">No nodes were found.</string>
<string name="nodes_listener_screen_node_name_label">Node: %1$s</string>
<string name="nodes_listener_screen_node_name_label">Name: %1$s</string>
<string name="nodes_listener_screen_node_id_label">ID: %1$s</string>

<!-- In-app prompt - Install App Demo 1 screen -->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ sealed class Screen(

data object AppHelperTrackingScreen : Screen("appHelperTrackingScreen")
data object AppHelperNodesActionsScreen : Screen("appHelperNodesActionsScreen")
data object AppHelperNodesListenerScreen : Screen("appHelperNodesListenerScreen")

data object AppHelperNodeDetailsScreen : Screen(nodeDetailsScreenRoute)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import com.google.android.horologist.datalayer.sample.screens.nodes.DataLayerNod
import com.google.android.horologist.datalayer.sample.screens.nodesactions.NodesActionsScreen
import com.google.android.horologist.datalayer.sample.screens.nodesactions.navigateToNodeDetailsScreen
import com.google.android.horologist.datalayer.sample.screens.nodesactions.nodeDetailsScreen
import com.google.android.horologist.datalayer.sample.screens.nodeslistener.NodesListenerScreen
import com.google.android.horologist.datalayer.sample.screens.tracking.TrackingScreen

@Composable
Expand Down Expand Up @@ -96,6 +97,13 @@ fun WearApp(
)
}
}
composable(route = Screen.AppHelperNodesListenerScreen.route) {
val columnState = rememberColumnState()

ScreenScaffold(scrollState = columnState) {
NodesListenerScreen(columnState = columnState)
}
}
nodeDetailsScreen()
infoScreen(
onDismissClick = navController::popBackStack,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ private fun SectionedListScope.appHelpersSection(navigateToRoute: (String) -> Un
listOf(
Pair(R.string.main_menu_apphelpers_tracking_item, Screen.AppHelperTrackingScreen.route),
Pair(R.string.main_menu_apphelpers_nodes_actions_item, Screen.AppHelperNodesActionsScreen.route),
Pair(R.string.main_menu_apphelpers_nodes_listener_item, Screen.AppHelperNodesListenerScreen.route),
),
) {
header {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,9 @@ fun DataLayerNodesScreen(
}
items(state.nodes) {
Chip(
label = "${it.displayName}(${it.id}) ${if (it.isNearby) "NEAR" else ""}",
label = it.displayName,
onClick = { },
secondaryLabel = "${it.id} ${if (it.isNearby) "(NEAR)" else ""}",
)
}
item {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ import com.google.android.horologist.compose.layout.belowTimeTextPreview
import com.google.android.horologist.compose.material.Chip
import com.google.android.horologist.compose.material.Confirmation
import com.google.android.horologist.compose.material.Icon
import com.google.android.horologist.compose.material.Title
import com.google.android.horologist.compose.material.util.DECORATIVE_ELEMENT_CONTENT_DESCRIPTION
import com.google.android.horologist.datalayer.sample.R
import com.google.android.horologist.images.base.paintable.ImageVectorPaintable
Expand Down Expand Up @@ -93,8 +94,8 @@ fun NodeDetailsScreen(
modifier = modifier.fillMaxSize(),
) {
item {
Text(
text = stringResource(id = R.string.node_details_header),
Title(
textId = R.string.node_details_header,
modifier = Modifier.padding(bottom = 10.dp),
)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
/*
* 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.google.android.horologist.datalayer.sample.screens.nodeslistener

import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.runtime.SideEffect
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.wear.compose.foundation.lazy.items
import androidx.wear.compose.material.CircularProgressIndicator
import androidx.wear.compose.material.Text
import androidx.wear.compose.ui.tooling.preview.WearPreviewDevices
import com.google.android.gms.wearable.Node
import com.google.android.horologist.compose.layout.ScalingLazyColumn
import com.google.android.horologist.compose.layout.ScalingLazyColumnState
import com.google.android.horologist.compose.layout.belowTimeTextPreview
import com.google.android.horologist.compose.material.Chip
import com.google.android.horologist.compose.material.Title
import com.google.android.horologist.datalayer.sample.R

@Composable
fun NodesListenerScreen(
columnState: ScalingLazyColumnState,
modifier: Modifier = Modifier,
viewModel: NodesListenerViewModel = hiltViewModel(),
) {
val state by viewModel.uiState.collectAsStateWithLifecycle()

if (state == NodesListenerScreenState.Idle) {
SideEffect {
viewModel.initialize()
}
}

NodesListenerScreen(
state = state,
columnState = columnState,
modifier = modifier,
)
}

@Composable
fun NodesListenerScreen(
state: NodesListenerScreenState,
columnState: ScalingLazyColumnState,
modifier: Modifier = Modifier,
) {
ScalingLazyColumn(
columnState = columnState,
modifier = modifier.fillMaxSize(),
) {
item {
Title(
textId = R.string.nodes_listener_screen_header,
modifier = Modifier.padding(bottom = 10.dp),
)
}
when (state) {
NodesListenerScreenState.Idle,
NodesListenerScreenState.Loading,
-> {
item {
CircularProgressIndicator()
}
}

is NodesListenerScreenState.Loaded -> {
if (state.nodeList.isNotEmpty()) {
item {
Text(stringResource(id = R.string.nodes_listener_screen_message))
}
items(state.nodeList.toList()) { node ->
Chip(
label = node.displayName,
onClick = { /* do nothing */ },
secondaryLabel = node.id,
)
}
} else {
item {
Text(stringResource(id = R.string.nodes_listener_screen_no_nodes))
}
}
}

NodesListenerScreenState.ApiNotAvailable -> {
item {
Text(stringResource(id = R.string.wearable_message_api_unavailable))
}
}
}
}
}

@WearPreviewDevices
@Composable
fun NodesListenerScreenPreviewLoaded() {
NodesListenerScreen(
state = NodesListenerScreenState.Loaded(
nodeList = setOf(
NodePreviewImpl(
displayName = "Google Pixel Watch",
id = "903b8371",
isNearby = true,
),
NodePreviewImpl(
displayName = "Galaxy Watch4 Classic",
id = "813d1812",
isNearby = false,
),
),
),
columnState = belowTimeTextPreview(),
)
}

@WearPreviewDevices
@Composable
fun NodesListenerScreenPreviewEmptyNodes() {
NodesListenerScreen(
state = NodesListenerScreenState.Loaded(emptySet()),
columnState = belowTimeTextPreview(),
)
}

@WearPreviewDevices
@Composable
fun NodesListenerScreenPreviewApiNotAvailable() {
NodesListenerScreen(
state = NodesListenerScreenState.ApiNotAvailable,
columnState = belowTimeTextPreview(),
)
}

private class NodePreviewImpl(
private val displayName: String,
private val id: String,
private val isNearby: Boolean,
) : Node {
override fun getDisplayName(): String = displayName

override fun getId(): String = id

override fun isNearby(): Boolean = isNearby
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
/*
* 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.google.android.horologist.datalayer.sample.screens.nodeslistener

import androidx.annotation.MainThread
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.google.android.gms.wearable.Node
import com.google.android.horologist.datalayer.watch.WearDataLayerAppHelper
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
import javax.inject.Inject

@HiltViewModel
class NodesListenerViewModel
@Inject
constructor(
private val wearDataLayerAppHelper: WearDataLayerAppHelper,
) : ViewModel() {

private var initializeCalled = false

private val _uiState =
MutableStateFlow<NodesListenerScreenState>(NodesListenerScreenState.Idle)
public val uiState: StateFlow<NodesListenerScreenState> = _uiState

@MainThread
fun initialize() {
if (initializeCalled) return
initializeCalled = true

_uiState.value = NodesListenerScreenState.Loading

viewModelScope.launch {
if (!wearDataLayerAppHelper.isAvailable()) {
_uiState.value = NodesListenerScreenState.ApiNotAvailable
} else {
loadNodes()
}
}
}

private suspend fun loadNodes() {
_uiState.value = NodesListenerScreenState.Loading

wearDataLayerAppHelper.connectedAndInstalledNodes.collect {
_uiState.value = NodesListenerScreenState.Loaded(nodeList = it)
}
}
}

sealed class NodesListenerScreenState {
data object Idle : NodesListenerScreenState()

data object Loading : NodesListenerScreenState()

data class Loaded(val nodeList: Set<Node>) : NodesListenerScreenState()

data object ApiNotAvailable : NodesListenerScreenState()
}
6 changes: 6 additions & 0 deletions datalayer/sample/wear/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
<string name="main_menu_apphelpers_header">App Helpers</string>
<string name="main_menu_apphelpers_tracking_item">Tracking</string>
<string name="main_menu_apphelpers_nodes_actions_item">Nodes actions</string>
<string name="main_menu_apphelpers_nodes_listener_item">Nodes listener</string>

<!-- Counter screen -->
<string name="server_counter_message">Open the datalayer sample on your phone to observe how the counter number is incremented.</string>
Expand Down Expand Up @@ -70,6 +71,11 @@
<string name="node_details_success_dialog_message">Success!</string>
<string name="node_details_failure_dialog_message">Failed: \n%1$s</string>

<!-- App Helper Nodes Listener screen -->
<string name="nodes_listener_screen_header">Nodes Listener</string>
<string name="nodes_listener_screen_message">This screen shows all the connected nodes that have the app installed.\nOn the watch, there should be only a single connected node: the paired phone.\nThe list updates automatically.</string>
<string name="nodes_listener_screen_no_nodes">No nodes were found.</string>

<!-- Start Remote Sample Activity screen -->
<string name="app_helper_start_remote_activity_message">This is a sample activity to demonstrate it being launched remotely from the phone.</string>
</resources>

0 comments on commit 4409a2c

Please sign in to comment.