Skip to content

Commit

Permalink
Add sample screen for connectedAndInstalledNodes function
Browse files Browse the repository at this point in the history
  • Loading branch information
luizgrp committed Jan 22, 2024
1 parent e520f0c commit d19b81f
Show file tree
Hide file tree
Showing 8 changed files with 270 additions and 4 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ sealed class Screen(
) {
data object MenuScreen : Screen("menuScreen")
data object AppHelperNodesScreen : Screen("appHelperNodesScreen")
data object AppHelperNodesListenerScreen : Screen("appHelperNodesListenerScreen")
data object InstallAppPromptDemoScreen : Screen("installAppPromptDemoScreen")
data object InstallAppPromptDemo2Screen : Screen("installAppPromptDemo2Screen")
data object CounterScreen : Screen("counterScreen")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import com.google.android.horologist.datalayer.sample.screens.inappprompts.Insta
import com.google.android.horologist.datalayer.sample.screens.inappprompts.InstallAppPromptDemoScreen
import com.google.android.horologist.datalayer.sample.screens.menu.MenuScreen
import com.google.android.horologist.datalayer.sample.screens.nodes.NodesScreen
import com.google.android.horologist.datalayer.sample.screens.nodeslistener.NodesListenerScreen

@Composable
fun MainScreen(
Expand Down Expand Up @@ -62,6 +63,9 @@ fun MainScreen(
composable(route = Screen.AppHelperNodesScreen.route) {
NodesScreen()
}
composable(route = Screen.AppHelperNodesListenerScreen.route) {
NodesListenerScreen()
}
composable(route = Screen.InstallAppPromptDemoScreen.route) {
InstallAppPromptDemoScreen(onShowInstallAppPrompt = onShowInstallAppPrompt)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,10 @@ fun MenuScreen(
Text(text = stringResource(id = R.string.menu_screen_nodes_item))
}

Button(onClick = { navController.navigate(Screen.AppHelperNodesListenerScreen.route) }) {
Text(text = stringResource(id = R.string.menu_screen_nodes_listener_item))
}

Text(
text = stringResource(id = R.string.menu_screen_inapp_prompts_header),
modifier = Modifier.padding(top = 10.dp),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ import com.google.android.horologist.datalayer.sample.util.toProtoTimestamp
@Composable
fun NodesScreen(
modifier: Modifier = Modifier,
viewModel: NodesActionViewModel = hiltViewModel(),
viewModel: NodesViewModel = hiltViewModel(),
) {
val state by viewModel.uiState.collectAsStateWithLifecycle()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ private const val REMOTE_ACTIVITY_SAMPLE_CLASS_FULL_NAME =
"com.google.android.horologist.datalayer.sample.screens.startremote.StartRemoteSampleActivity"

@HiltViewModel
class NodesActionViewModel
class NodesViewModel
@Inject
constructor(
private val phoneDataLayerAppHelper: PhoneDataLayerAppHelper,
Expand Down Expand Up @@ -61,8 +61,6 @@ class NodesActionViewModel
}

fun onRefreshClick() {
_uiState.value = NodesScreenState.Loading

viewModelScope.launch {
loadNodes()
}
Expand Down Expand Up @@ -108,6 +106,8 @@ class NodesActionViewModel
}

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

cachedNodeList = phoneDataLayerAppHelper.connectedNodes()
_uiState.value = NodesScreenState.Loaded(nodeList = cachedNodeList)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
/*
* 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.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.Card
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.google.android.gms.wearable.Node
import com.google.android.horologist.datalayer.sample.R

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

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

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

@Composable
fun NodesListenerScreen(
state: NodesListenerScreenState,
modifier: Modifier = Modifier,
) {
LazyColumn(
modifier = modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
) {
item {
Text(
text = stringResource(id = R.string.nodes_listener_screen_header),
modifier = Modifier.padding(vertical = 10.dp),
style = MaterialTheme.typography.titleLarge,
)
}

if (state != NodesListenerScreenState.ApiNotAvailable) {
item {
Text(
text = stringResource(id = R.string.nodes_listener_screen_message),
modifier = Modifier.padding(vertical = 10.dp),
style = MaterialTheme.typography.bodyLarge,
)
}
}

when (state) {
NodesListenerScreenState.Idle,
NodesListenerScreenState.Loading -> {
item {
CircularProgressIndicator()
}
}

is NodesListenerScreenState.Loaded -> {
if (state.nodeList.isNotEmpty()) {
items(items = state.nodeList.toList()) { node ->
Box(
modifier = modifier
.padding(16.dp)
.fillMaxWidth(),
contentAlignment = Alignment.Center,
) {
Card {
Column(
modifier = Modifier
.padding(16.dp)
.fillMaxWidth(),

) {
Text(
stringResource(
R.string.nodes_listener_screen_node_name_label,
node.displayName
)
)
Text(
style = MaterialTheme.typography.labelMedium,
text = stringResource(
R.string.nodes_listener_screen_node_id_label,
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))
}
}
}
}
}

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

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,75 @@
/*
* 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.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.google.android.gms.wearable.Node
import com.google.android.horologist.datalayer.phone.PhoneDataLayerAppHelper
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 phoneDataLayerAppHelper: PhoneDataLayerAppHelper,
) : ViewModel() {

private var initializeCalled = false

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

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

_uiState.value = NodesListenerScreenState.Loading

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

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

phoneDataLayerAppHelper.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()
}
8 changes: 8 additions & 0 deletions datalayer/sample/phone/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
<!-- Main Menu screen -->
<string name="menu_screen_apphelper_header">App Helper</string>
<string name="menu_screen_nodes_item">Nodes</string>
<string name="menu_screen_nodes_listener_item">Nodes Listener</string>
<string name="menu_screen_inapp_prompts_header">In-App prompts</string>
<string name="menu_screen_install_app_demo1_item">Install app - demo 1</string>
<string name="menu_screen_install_app_demo2_item">Install app - demo 2</string>
Expand Down Expand Up @@ -56,6 +57,13 @@
<string name="node_status_start_own_app_button_label">Start remote own app</string>
<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_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_id_label">ID: %1$s</string>

<!-- In-app prompt - Install App Demo 1 screen -->
<string name="install_app_prompt_api_call_demo_message">This demo calls the Horologist API to show the install app prompt and the invocation of the deeplink to the Google Play by using the Gmail app as an example. The developer has to gather all the information to display on the prompt, such as app name and watch name to use the prompt in their app.</string>
<string name="install_app_prompt_run_demo_button_label">Run demo</string>
Expand Down

0 comments on commit d19b81f

Please sign in to comment.