From be18b9c85da5845a93493c8250efbebe2aeb3fe0 Mon Sep 17 00:00:00 2001 From: Gustavo Pagani Date: Tue, 2 Jan 2024 14:54:03 +0000 Subject: [PATCH] Add App Helper Nodes screen on the datalayer phone sample app (#1916) --- .../data/apphelper/DataLayerAppHelper.kt | 3 +- .../sample/phone/src/main/AndroidManifest.xml | 3 +- .../datalayer/sample/MainActivity.kt | 52 +--- .../datalayer/sample/screens/Screen.kt | 2 +- .../screens/listnodes/ListNodesScreen.kt | 164 ----------- .../screens/listnodes/ListNodesViewModel.kt | 87 ------ .../sample/screens/main/MainScreen.kt | 69 +++++ .../sample/screens/menu/MenuScreen.kt | 12 +- .../AppHelperNodeStatusCard.kt | 74 +++-- .../sample/screens/nodes/NodeActionDialogs.kt | 139 ++++++++++ .../sample/screens/nodes/NodesScreen.kt | 254 ++++++++++++++++++ .../sample/screens/nodes/NodesViewModel.kt | 146 ++++++++++ .../startremote/StartRemoteSampleActivity.kt | 1 - .../phone/src/main/res/values/strings.xml | 40 ++- .../sample/wear/src/main/AndroidManifest.xml | 5 + .../startremote/StartRemoteSampleActivity.kt | 53 ++++ .../wear/src/main/res/values/strings.xml | 3 + 17 files changed, 763 insertions(+), 344 deletions(-) delete mode 100644 datalayer/sample/phone/src/main/java/com/google/android/horologist/datalayer/sample/screens/listnodes/ListNodesScreen.kt delete mode 100644 datalayer/sample/phone/src/main/java/com/google/android/horologist/datalayer/sample/screens/listnodes/ListNodesViewModel.kt create mode 100644 datalayer/sample/phone/src/main/java/com/google/android/horologist/datalayer/sample/screens/main/MainScreen.kt rename datalayer/sample/phone/src/main/java/com/google/android/horologist/datalayer/sample/screens/{listnodes => nodes}/AppHelperNodeStatusCard.kt (67%) create mode 100644 datalayer/sample/phone/src/main/java/com/google/android/horologist/datalayer/sample/screens/nodes/NodeActionDialogs.kt create mode 100644 datalayer/sample/phone/src/main/java/com/google/android/horologist/datalayer/sample/screens/nodes/NodesScreen.kt create mode 100644 datalayer/sample/phone/src/main/java/com/google/android/horologist/datalayer/sample/screens/nodes/NodesViewModel.kt create mode 100644 datalayer/sample/wear/src/main/java/com/google/android/horologist/datalayer/sample/screens/startremote/StartRemoteSampleActivity.kt diff --git a/datalayer/core/src/main/java/com/google/android/horologist/data/apphelper/DataLayerAppHelper.kt b/datalayer/core/src/main/java/com/google/android/horologist/data/apphelper/DataLayerAppHelper.kt index b58177c55a..d4c70977ca 100644 --- a/datalayer/core/src/main/java/com/google/android/horologist/data/apphelper/DataLayerAppHelper.kt +++ b/datalayer/core/src/main/java/com/google/android/horologist/data/apphelper/DataLayerAppHelper.kt @@ -213,7 +213,8 @@ abstract class DataLayerAppHelper( * apps are not being launched as a result of a background process on the calling device. */ protected fun checkIsForegroundOrThrow() { - val isForeground = activityManager.runningAppProcesses.find { + val runningAppProcesses = activityManager.runningAppProcesses ?: emptyList() + val isForeground = runningAppProcesses.find { it.pid == Process.myPid() }?.importance == IMPORTANCE_FOREGROUND if (!isForeground) { diff --git a/datalayer/sample/phone/src/main/AndroidManifest.xml b/datalayer/sample/phone/src/main/AndroidManifest.xml index d6b82cf1a6..800aaaebf6 100644 --- a/datalayer/sample/phone/src/main/AndroidManifest.xml +++ b/datalayer/sample/phone/src/main/AndroidManifest.xml @@ -46,8 +46,7 @@ android:name=".screens.startremote.StartRemoteSampleActivity" android:exported="false" android:taskAffinity=".main" - android:theme="@style/Theme.Horologist"> - + android:theme="@style/Theme.Horologist" /> diff --git a/datalayer/sample/phone/src/main/java/com/google/android/horologist/datalayer/sample/MainActivity.kt b/datalayer/sample/phone/src/main/java/com/google/android/horologist/datalayer/sample/MainActivity.kt index a99d55b1a9..2045dca237 100644 --- a/datalayer/sample/phone/src/main/java/com/google/android/horologist/datalayer/sample/MainActivity.kt +++ b/datalayer/sample/phone/src/main/java/com/google/android/horologist/datalayer/sample/MainActivity.kt @@ -19,24 +19,11 @@ package com.google.android.horologist.datalayer.sample import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.padding import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Scaffold import androidx.compose.material3.Surface -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.navigation.NavHostController -import androidx.navigation.compose.NavHost -import androidx.navigation.compose.composable -import androidx.navigation.compose.rememberNavController -import com.google.android.horologist.datalayer.sample.screens.Screen -import com.google.android.horologist.datalayer.sample.screens.counter.CounterScreen -import com.google.android.horologist.datalayer.sample.screens.listnodes.ListNodesScreen -import com.google.android.horologist.datalayer.sample.screens.menu.MenuScreen +import com.google.android.horologist.datalayer.sample.screens.main.MainScreen import com.google.android.horologist.datalayer.sample.ui.theme.HorologistTheme import dagger.hilt.android.AndroidEntryPoint @@ -47,7 +34,6 @@ class MainActivity : ComponentActivity() { setContent { HorologistTheme { - // A surface container using the 'background' color from the theme Surface( modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background, @@ -58,39 +44,3 @@ class MainActivity : ComponentActivity() { } } } - -@Composable -fun MainScreen( - modifier: Modifier = Modifier, - navController: NavHostController = rememberNavController(), -) { - Scaffold( - modifier = modifier, - ) { padding -> - Column( - modifier = modifier - .fillMaxSize() - .padding(padding), - verticalArrangement = Arrangement.Center, - horizontalAlignment = Alignment.CenterHorizontally, - ) { - NavHost( - navController = navController, - startDestination = Screen.MenuScreen.route, - modifier = modifier, - ) { - composable(route = Screen.MenuScreen.route) { - MenuScreen(navController = navController) - } - - composable(route = Screen.ListNodesScreen.route) { - ListNodesScreen() - } - - composable(route = Screen.CounterScreen.route) { - CounterScreen() - } - } - } - } -} diff --git a/datalayer/sample/phone/src/main/java/com/google/android/horologist/datalayer/sample/screens/Screen.kt b/datalayer/sample/phone/src/main/java/com/google/android/horologist/datalayer/sample/screens/Screen.kt index 24a215f933..23a2a233b1 100644 --- a/datalayer/sample/phone/src/main/java/com/google/android/horologist/datalayer/sample/screens/Screen.kt +++ b/datalayer/sample/phone/src/main/java/com/google/android/horologist/datalayer/sample/screens/Screen.kt @@ -20,6 +20,6 @@ sealed class Screen( val route: String, ) { data object MenuScreen : Screen("menu") - data object ListNodesScreen : Screen("listNodes") data object CounterScreen : Screen("counter") + data object AppHelperNodesScreen : Screen("appHelperNodesScreen") } diff --git a/datalayer/sample/phone/src/main/java/com/google/android/horologist/datalayer/sample/screens/listnodes/ListNodesScreen.kt b/datalayer/sample/phone/src/main/java/com/google/android/horologist/datalayer/sample/screens/listnodes/ListNodesScreen.kt deleted file mode 100644 index 2955d95f47..0000000000 --- a/datalayer/sample/phone/src/main/java/com/google/android/horologist/datalayer/sample/screens/listnodes/ListNodesScreen.kt +++ /dev/null @@ -1,164 +0,0 @@ -/* - * 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. - */ - -package com.google.android.horologist.datalayer.sample.screens.listnodes - -import androidx.compose.foundation.layout.Arrangement -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.layout.width -import androidx.compose.foundation.layout.wrapContentHeight -import androidx.compose.material3.Button -import androidx.compose.material3.CircularProgressIndicator -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.graphics.Color -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 -import androidx.hilt.navigation.compose.hiltViewModel -import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.google.android.horologist.data.apphelper.AppHelperNodeStatus -import com.google.android.horologist.data.apphelper.AppInstallationStatus -import com.google.android.horologist.data.apphelper.AppInstallationStatusNodeType -import com.google.android.horologist.data.complicationInfo -import com.google.android.horologist.data.surfacesInfo -import com.google.android.horologist.data.tileInfo -import com.google.android.horologist.datalayer.sample.R -import com.google.android.horologist.datalayer.sample.util.toProtoTimestamp - -@Composable -fun ListNodesScreen( - modifier: Modifier = Modifier, - viewModel: ListNodesViewModel = hiltViewModel(), -) { - val state by viewModel.uiState.collectAsStateWithLifecycle() - - if (state == ListNodesScreenUiState.Idle) { - viewModel.initialize() - } - - ListNodesScreen( - state = state, - onListNodesClick = viewModel::onListNodesClick, - onInstallClick = { viewModel.onInstallClick(it) }, - onLaunchClick = { viewModel.onLaunchClick(it) }, - onCompanionClick = { viewModel.onCompanionClick(it) }, - modifier = modifier, - ) -} - -@Composable -fun ListNodesScreen( - state: ListNodesScreenUiState, - onListNodesClick: () -> Unit, - onInstallClick: (String) -> Unit, - onLaunchClick: (String) -> Unit, - onCompanionClick: (String) -> Unit, - modifier: Modifier = Modifier, -) { - Column( - modifier = modifier.fillMaxSize(), - verticalArrangement = Arrangement.Center, - horizontalAlignment = Alignment.CenterHorizontally, - ) { - Button( - onClick = { - onListNodesClick() - }, - modifier = Modifier.wrapContentHeight(), - enabled = state != ListNodesScreenUiState.ApiNotAvailable, - ) { Text(stringResource(R.string.app_helper_button_list_nodes)) } - - when (state) { - ListNodesScreenUiState.Idle -> { - /* show nothing */ - } - - ListNodesScreenUiState.Loading -> { - CircularProgressIndicator( - modifier = Modifier - .padding(top = 10.dp) - .width(64.dp), - ) - } - - is ListNodesScreenUiState.Loaded -> { - state.nodeList.forEach { nodeStatus -> - AppHelperNodeStatusCard( - nodeStatus = nodeStatus, - onInstallClick = onInstallClick, - onLaunchClick = onLaunchClick, - onCompanionClick = onCompanionClick, - ) - } - } - - ListNodesScreenUiState.ApiNotAvailable -> { - Text( - text = stringResource(R.string.wearable_message_api_unavailable), - modifier.fillMaxWidth(), - color = Color.Red, - textAlign = TextAlign.Center, - ) - } - } - } -} - -@Preview(showBackground = true) -@Composable -fun ListNodesScreenPreview() { - val nodeList = listOf( - AppHelperNodeStatus( - id = "a1b2c3d4", - displayName = "Pixel Watch", - appInstallationStatus = AppInstallationStatus.Installed( - nodeType = AppInstallationStatusNodeType.WATCH, - ), - surfacesInfo = surfacesInfo { - tiles.add( - tileInfo { - name = "MyTile" - timestamp = System.currentTimeMillis().toProtoTimestamp() - }, - ) - complications.add( - complicationInfo { - name = "MyComplication" - instanceId = 101 - type = "SHORT_TEXT" - timestamp = System.currentTimeMillis().toProtoTimestamp() - }, - ) - }, - ), - ) - - ListNodesScreen( - state = ListNodesScreenUiState.Loaded(nodeList = nodeList), - onListNodesClick = { }, - onInstallClick = { }, - onLaunchClick = { }, - onCompanionClick = { }, - ) -} diff --git a/datalayer/sample/phone/src/main/java/com/google/android/horologist/datalayer/sample/screens/listnodes/ListNodesViewModel.kt b/datalayer/sample/phone/src/main/java/com/google/android/horologist/datalayer/sample/screens/listnodes/ListNodesViewModel.kt deleted file mode 100644 index 1feb0e19ad..0000000000 --- a/datalayer/sample/phone/src/main/java/com/google/android/horologist/datalayer/sample/screens/listnodes/ListNodesViewModel.kt +++ /dev/null @@ -1,87 +0,0 @@ -/* - * 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. - */ - -package com.google.android.horologist.datalayer.sample.screens.listnodes - -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import com.google.android.horologist.data.apphelper.AppHelperNodeStatus -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 ListNodesViewModel - @Inject - constructor( - private val phoneDataLayerAppHelper: PhoneDataLayerAppHelper, - ) : ViewModel() { - - private var initializeCalled = false - - private val _uiState = MutableStateFlow(ListNodesScreenUiState.Idle) - public val uiState: StateFlow = _uiState - - fun initialize() { - if (initializeCalled) return - initializeCalled = true - - viewModelScope.launch { - _uiState.value = ListNodesScreenUiState.Loading - if (!phoneDataLayerAppHelper.isAvailable()) { - _uiState.value = ListNodesScreenUiState.ApiNotAvailable - } else { - _uiState.value = ListNodesScreenUiState.Loaded(nodeList = emptyList()) - } - } - } - - fun onListNodesClick() { - viewModelScope.launch { - _uiState.value = ListNodesScreenUiState.Loading - val nodeList = phoneDataLayerAppHelper.connectedNodes() - _uiState.value = ListNodesScreenUiState.Loaded(nodeList = nodeList) - } - } - - fun onInstallClick(nodeId: String) { - viewModelScope.launch { - phoneDataLayerAppHelper.installOnNode(nodeId) - } - } - - fun onLaunchClick(nodeId: String) { - viewModelScope.launch { - phoneDataLayerAppHelper.startRemoteOwnApp(nodeId) - } - } - - fun onCompanionClick(nodeId: String) { - viewModelScope.launch { - phoneDataLayerAppHelper.startCompanion(nodeId) - } - } - } - -public sealed class ListNodesScreenUiState { - public data object Idle : ListNodesScreenUiState() - public data object Loading : ListNodesScreenUiState() - public data class Loaded(val nodeList: List) : ListNodesScreenUiState() - public data object ApiNotAvailable : ListNodesScreenUiState() -} diff --git a/datalayer/sample/phone/src/main/java/com/google/android/horologist/datalayer/sample/screens/main/MainScreen.kt b/datalayer/sample/phone/src/main/java/com/google/android/horologist/datalayer/sample/screens/main/MainScreen.kt new file mode 100644 index 0000000000..9de154f9c4 --- /dev/null +++ b/datalayer/sample/phone/src/main/java/com/google/android/horologist/datalayer/sample/screens/main/MainScreen.kt @@ -0,0 +1,69 @@ +/* + * 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. + */ + +package com.google.android.horologist.datalayer.sample.screens.main + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Scaffold +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.navigation.NavHostController +import androidx.navigation.compose.NavHost +import androidx.navigation.compose.composable +import androidx.navigation.compose.rememberNavController +import com.google.android.horologist.datalayer.sample.screens.Screen +import com.google.android.horologist.datalayer.sample.screens.counter.CounterScreen +import com.google.android.horologist.datalayer.sample.screens.menu.MenuScreen +import com.google.android.horologist.datalayer.sample.screens.nodes.NodesScreen + +@Composable +fun MainScreen( + modifier: Modifier = Modifier, + navController: NavHostController = rememberNavController(), +) { + Scaffold( + modifier = modifier, + ) { padding -> + Column( + modifier = modifier + .fillMaxSize() + .padding(padding), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally, + ) { + NavHost( + navController = navController, + startDestination = Screen.MenuScreen.route, + modifier = modifier, + ) { + composable(route = Screen.MenuScreen.route) { + MenuScreen(navController = navController) + } + + composable(route = Screen.CounterScreen.route) { + CounterScreen() + } + composable(route = Screen.AppHelperNodesScreen.route) { + NodesScreen() + } + } + } + } +} diff --git a/datalayer/sample/phone/src/main/java/com/google/android/horologist/datalayer/sample/screens/menu/MenuScreen.kt b/datalayer/sample/phone/src/main/java/com/google/android/horologist/datalayer/sample/screens/menu/MenuScreen.kt index 9e72e1eb39..bb367c00c5 100644 --- a/datalayer/sample/phone/src/main/java/com/google/android/horologist/datalayer/sample/screens/menu/MenuScreen.kt +++ b/datalayer/sample/phone/src/main/java/com/google/android/horologist/datalayer/sample/screens/menu/MenuScreen.kt @@ -17,12 +17,14 @@ package com.google.android.horologist.datalayer.sample.screens.menu import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.padding import androidx.compose.material3.Button import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp import androidx.navigation.NavHostController import com.google.android.horologist.datalayer.sample.R import com.google.android.horologist.datalayer.sample.screens.Screen @@ -36,10 +38,16 @@ fun MenuScreen( modifier = modifier, horizontalAlignment = Alignment.CenterHorizontally, ) { - Button(onClick = { navController.navigate(Screen.ListNodesScreen.route) }) { - Text(text = stringResource(id = R.string.menu_screen_list_nodes_item)) + Text(text = stringResource(id = R.string.menu_screen_apphelper_header)) + Button(onClick = { navController.navigate(Screen.AppHelperNodesScreen.route) }) { + Text(text = stringResource(id = R.string.menu_screen_nodes_item)) } + Text( + text = stringResource(id = R.string.menu_screen_datalayer_header), + modifier = Modifier.padding(top = 10.dp), + ) + Button(onClick = { navController.navigate(Screen.CounterScreen.route) }) { Text(text = stringResource(id = R.string.menu_screen_counter_item)) } diff --git a/datalayer/sample/phone/src/main/java/com/google/android/horologist/datalayer/sample/screens/listnodes/AppHelperNodeStatusCard.kt b/datalayer/sample/phone/src/main/java/com/google/android/horologist/datalayer/sample/screens/nodes/AppHelperNodeStatusCard.kt similarity index 67% rename from datalayer/sample/phone/src/main/java/com/google/android/horologist/datalayer/sample/screens/listnodes/AppHelperNodeStatusCard.kt rename to datalayer/sample/phone/src/main/java/com/google/android/horologist/datalayer/sample/screens/nodes/AppHelperNodeStatusCard.kt index a64a0fa180..ee6baf54be 100644 --- a/datalayer/sample/phone/src/main/java/com/google/android/horologist/datalayer/sample/screens/listnodes/AppHelperNodeStatusCard.kt +++ b/datalayer/sample/phone/src/main/java/com/google/android/horologist/datalayer/sample/screens/nodes/AppHelperNodeStatusCard.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.google.android.horologist.datalayer.sample.screens.listnodes +package com.google.android.horologist.datalayer.sample.screens.nodes import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -31,6 +31,7 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier 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 import com.google.android.horologist.data.UsageStatus @@ -49,9 +50,10 @@ import com.google.android.horologist.datalayer.sample.util.toProtoTimestamp @Composable fun AppHelperNodeStatusCard( nodeStatus: AppHelperNodeStatus, - onInstallClick: (String) -> Unit, - onLaunchClick: (String) -> Unit, - onCompanionClick: (String) -> Unit, + onInstallOnNodeClick: (String) -> Unit, + onStartCompanionClick: (String) -> Unit, + onStartRemoteOwnAppClick: (String) -> Unit, + onStartRemoteActivityClick: (nodeId: String) -> Unit, ) { Box( modifier = Modifier @@ -66,32 +68,32 @@ fun AppHelperNodeStatusCard( .fillMaxWidth(), ) { - Text(stringResource(R.string.app_helper_node_name_label, nodeStatus.displayName)) + Text(stringResource(R.string.node_status_node_name_label, nodeStatus.displayName)) Text( style = MaterialTheme.typography.labelMedium, - text = stringResource(R.string.app_helper_node_id_label, nodeStatus.id), + text = stringResource(R.string.node_status_node_id_label, nodeStatus.id), ) Text( style = MaterialTheme.typography.labelMedium, text = stringResource( - R.string.app_helper_is_app_installed_label, + R.string.node_status_is_app_installed_label, nodeStatus.appInstalled, ), ) val nodeType = if (nodeStatus.appInstalled) { (nodeStatus.appInstallationStatus as AppInstallationStatus.Installed).nodeType } else { - stringResource(id = R.string.app_helper_node_type_unknown_label) + stringResource(id = R.string.node_status_node_type_unknown_label) } Text( style = MaterialTheme.typography.labelMedium, - text = stringResource(R.string.app_helper_node_type_label, nodeType), + text = stringResource(R.string.node_status_node_type_label, nodeType), ) if (nodeStatus.surfacesInfo.complicationsList.isNotEmpty()) { Text( style = MaterialTheme.typography.labelMedium, text = stringResource( - R.string.app_helper_complications_label, + R.string.node_status_complications_label, nodeStatus.surfacesInfo.complicationsList.joinToString { it.name }, ), ) @@ -100,7 +102,7 @@ fun AppHelperNodeStatusCard( Text( style = MaterialTheme.typography.labelMedium, text = stringResource( - R.string.app_helper_tiles_label, + R.string.node_status_tiles_label, nodeStatus.surfacesInfo.tilesList.joinToString { it.name }, ), ) @@ -108,7 +110,7 @@ fun AppHelperNodeStatusCard( Text( style = MaterialTheme.typography.labelMedium, text = stringResource( - R.string.app_helper_usage_status, + R.string.node_status_usage_status, nodeStatus.surfacesInfo.usageInfo.usageStatus.name, ), ) @@ -120,21 +122,48 @@ fun AppHelperNodeStatusCard( ) { Button( modifier = Modifier.wrapContentHeight(), - onClick = { onInstallClick(nodeStatus.id) }, + onClick = { onStartCompanionClick(nodeStatus.id) }, ) { - Text(stringResource(id = R.string.app_helper_install_button_label)) + Text( + stringResource(id = R.string.node_status_start_companion_button_label), + textAlign = TextAlign.Center, + ) } Button( - modifier = Modifier.wrapContentHeight(), - onClick = { onLaunchClick(nodeStatus.id) }, + modifier = Modifier.wrapContentHeight().padding(start = 10.dp), + onClick = { onInstallOnNodeClick(nodeStatus.id) }, ) { - Text(stringResource(id = R.string.app_helper_launch_button_label)) + Text( + stringResource(id = R.string.node_status_install_on_node_button_label), + textAlign = TextAlign.Center, + ) } + } + Row( + modifier = Modifier + .padding(8.dp) + .fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + ) { Button( modifier = Modifier.wrapContentHeight(), - onClick = { onCompanionClick(nodeStatus.id) }, + onClick = { onStartRemoteOwnAppClick(nodeStatus.id) }, + ) { + Text( + stringResource(id = R.string.node_status_start_own_app_button_label), + textAlign = TextAlign.Center, + ) + } + Button( + modifier = Modifier + .wrapContentHeight() + .padding(start = 10.dp), + onClick = { onStartRemoteActivityClick(nodeStatus.id) }, ) { - Text(stringResource(id = R.string.app_helper_companion_button_label)) + Text( + stringResource(id = R.string.node_status_start_remote_activity_button_label), + textAlign = TextAlign.Center, + ) } } } @@ -175,9 +204,10 @@ fun NodeCardPreview() { HorologistTheme { AppHelperNodeStatusCard( nodeStatus = nodeStatus, - onCompanionClick = {}, - onInstallClick = {}, - onLaunchClick = {}, + onStartCompanionClick = { }, + onInstallOnNodeClick = { }, + onStartRemoteOwnAppClick = { }, + onStartRemoteActivityClick = { }, ) } } diff --git a/datalayer/sample/phone/src/main/java/com/google/android/horologist/datalayer/sample/screens/nodes/NodeActionDialogs.kt b/datalayer/sample/phone/src/main/java/com/google/android/horologist/datalayer/sample/screens/nodes/NodeActionDialogs.kt new file mode 100644 index 0000000000..90028d50e3 --- /dev/null +++ b/datalayer/sample/phone/src/main/java/com/google/android/horologist/datalayer/sample/screens/nodes/NodeActionDialogs.kt @@ -0,0 +1,139 @@ +/* + * 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. + */ + +package com.google.android.horologist.datalayer.sample.screens.nodes + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Close +import androidx.compose.material.icons.filled.Done +import androidx.compose.material3.Card +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.Dialog +import com.google.android.horologist.datalayer.sample.R + +@Composable +fun NodesActionSucceededDialog( + message: String, + onDismissRequest: () -> Unit, +) { + NodesActionDialog( + message = message, + onDismissRequest = onDismissRequest, + imageVector = Icons.Default.Done, + ) +} + +@Composable +fun NodesActionFailureDialog( + message: String, + onDismissRequest: () -> Unit, +) { + NodesActionDialog( + message = message, + onDismissRequest = onDismissRequest, + imageVector = Icons.Default.Close, + ) +} + +@Composable +fun NodesActionDialog( + message: String, + onDismissRequest: () -> Unit, + imageVector: ImageVector, +) { + Dialog(onDismissRequest = { onDismissRequest() }) { + Card( + modifier = Modifier + .fillMaxWidth() + .height(225.dp) + .padding(16.dp), + shape = RoundedCornerShape(16.dp), + ) { + Column( + modifier = Modifier.fillMaxSize(), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Icon( + imageVector = imageVector, + contentDescription = null, + modifier = Modifier.padding(vertical = 8.dp), + ) + Text( + text = message, + modifier = Modifier.padding(10.dp), + ) + Row( + modifier = Modifier + .fillMaxWidth(), + horizontalArrangement = Arrangement.End, + ) { + TextButton( + onClick = { onDismissRequest() }, + modifier = Modifier.padding(horizontal = 8.dp), + ) { + Text(stringResource(id = R.string.node_screen_action_dialog_dismiss_button_label)) + } + } + } + } + } +} + +@Preview(showBackground = true) +@Composable +fun NodesActionSucceededDialogPreview() { + NodesActionSucceededDialog( + message = "Success!", + onDismissRequest = { }, + ) +} + +@Preview(showBackground = true) +@Composable +fun NodesActionFailureDialogPreview() { + NodesActionFailureDialog( + message = "Failed: RESULT", + onDismissRequest = { }, + ) +} + +@Preview(showBackground = true) +@Composable +fun NodesActionDialogPreview() { + NodesActionDialog( + message = "This is a dialog with a button and an icon.", + onDismissRequest = { }, + imageVector = Icons.Default.Done, + ) +} diff --git a/datalayer/sample/phone/src/main/java/com/google/android/horologist/datalayer/sample/screens/nodes/NodesScreen.kt b/datalayer/sample/phone/src/main/java/com/google/android/horologist/datalayer/sample/screens/nodes/NodesScreen.kt new file mode 100644 index 0000000000..bf3773d7af --- /dev/null +++ b/datalayer/sample/phone/src/main/java/com/google/android/horologist/datalayer/sample/screens/nodes/NodesScreen.kt @@ -0,0 +1,254 @@ +/* + * 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. + */ + +package com.google.android.horologist.datalayer.sample.screens.nodes + +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Refresh +import androidx.compose.material3.Button +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +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.horologist.data.apphelper.AppHelperNodeStatus +import com.google.android.horologist.data.apphelper.AppInstallationStatus +import com.google.android.horologist.data.apphelper.AppInstallationStatusNodeType +import com.google.android.horologist.data.complicationInfo +import com.google.android.horologist.data.surfacesInfo +import com.google.android.horologist.data.tileInfo +import com.google.android.horologist.datalayer.sample.R +import com.google.android.horologist.datalayer.sample.util.toProtoTimestamp + +@Composable +fun NodesScreen( + modifier: Modifier = Modifier, + viewModel: NodesActionViewModel = hiltViewModel(), +) { + val state by viewModel.uiState.collectAsStateWithLifecycle() + + if (state == NodesScreenState.Idle) { + viewModel.initialize() + } + + NodesScreen( + state = state, + onRefreshClick = viewModel::onRefreshClick, + onInstallOnNodeClick = viewModel::onInstallOnNodeClick, + onStartCompanionClick = viewModel::onStartCompanionClick, + onStartRemoteOwnAppClick = viewModel::onStartRemoteOwnAppClick, + onStartRemoteActivityClick = viewModel::onStartRemoteActivityClick, + onDialogDismiss = viewModel::onDialogDismiss, + modifier = modifier, + ) +} + +@Composable +fun NodesScreen( + state: NodesScreenState, + onRefreshClick: () -> Unit, + onInstallOnNodeClick: (nodeId: String) -> Unit, + onStartCompanionClick: (nodeId: String) -> Unit, + onStartRemoteOwnAppClick: (nodeId: String) -> Unit, + onStartRemoteActivityClick: (nodeId: String) -> Unit, + onDialogDismiss: () -> Unit, + modifier: Modifier = Modifier, +) { + var showSuccessDialog by rememberSaveable { mutableStateOf(false) } + var showFailureDialog by rememberSaveable { mutableStateOf(false) } + var errorCode by rememberSaveable { mutableStateOf("") } + + LazyColumn( + modifier = modifier.fillMaxSize(), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + item { + Text( + text = stringResource(id = R.string.nodes_screen_header), + modifier = Modifier.padding(vertical = 10.dp), + style = MaterialTheme.typography.titleLarge, + ) + } + when (state) { + NodesScreenState.Idle, + NodesScreenState.Loading, + NodesScreenState.ActionRunning, + -> { + item { + CircularProgressIndicator() + } + } + + is NodesScreenState.Loaded -> { + if (state.nodeList.isNotEmpty()) { + items(items = state.nodeList) { nodeStatus -> + AppHelperNodeStatusCard( + nodeStatus = nodeStatus, + onInstallOnNodeClick = onInstallOnNodeClick, + onStartCompanionClick = onStartCompanionClick, + onStartRemoteOwnAppClick = onStartRemoteOwnAppClick, + onStartRemoteActivityClick = onStartRemoteActivityClick, + ) + } + } else { + item { + Text(stringResource(id = R.string.nodes_screen_no_nodes)) + } + } + + item { + Button( + onClick = onRefreshClick, + ) { + Row { + Icon( + imageVector = Icons.Default.Refresh, + contentDescription = null, + modifier = Modifier.padding(end = 10.dp), + ) + Text( + stringResource(id = R.string.nodes_screen_refresh_button_label), + modifier = Modifier + .fillMaxHeight() + .align(Alignment.CenterVertically), + ) + } + } + } + } + + NodesScreenState.ApiNotAvailable -> { + item { + Text(stringResource(id = R.string.wearable_message_api_unavailable)) + } + } + + is NodesScreenState.ActionFailed -> { + showFailureDialog = true + errorCode = state.errorCode + } + + NodesScreenState.ActionSucceeded -> { + showSuccessDialog = true + } + } + } + + if (showSuccessDialog) { + NodesActionSucceededDialog( + message = stringResource(id = R.string.node_screen_success_dialog_message), + onDismissRequest = { + showSuccessDialog = false + onDialogDismiss() + }, + ) + } + + if (showFailureDialog) { + NodesActionFailureDialog( + message = stringResource(id = R.string.node_screen_failure_dialog_message, errorCode), + onDismissRequest = { + showFailureDialog = false + onDialogDismiss() + }, + ) + } +} + +@Preview(showBackground = true) +@Composable +fun NodesScreenPreview() { + NodesScreen( + state = NodesScreenState.Loaded( + listOf( + AppHelperNodeStatus( + id = "a1b2c3d4", + displayName = "Pixel Watch", + appInstallationStatus = AppInstallationStatus.Installed( + nodeType = AppInstallationStatusNodeType.WATCH, + ), + surfacesInfo = surfacesInfo { + tiles.add( + tileInfo { + name = "MyTile" + timestamp = System.currentTimeMillis().toProtoTimestamp() + }, + ) + complications.add( + complicationInfo { + name = "MyComplication" + instanceId = 101 + type = "SHORT_TEXT" + timestamp = System.currentTimeMillis().toProtoTimestamp() + }, + ) + }, + ), + ), + ), + onRefreshClick = { }, + onInstallOnNodeClick = { }, + onStartCompanionClick = { }, + onStartRemoteOwnAppClick = { }, + onStartRemoteActivityClick = { }, + onDialogDismiss = { }, + ) +} + +@Preview(showBackground = true) +@Composable +fun NodesActionsScreenPreviewEmptyNodes() { + NodesScreen( + state = NodesScreenState.Loaded(emptyList()), + onRefreshClick = { }, + onInstallOnNodeClick = { }, + onStartCompanionClick = { }, + onStartRemoteOwnAppClick = { }, + onStartRemoteActivityClick = { }, + onDialogDismiss = { }, + ) +} + +@Preview(showBackground = true) +@Composable +fun NodesScreenPreviewApiNotAvailable() { + NodesScreen( + state = NodesScreenState.ApiNotAvailable, + onRefreshClick = { }, + onInstallOnNodeClick = { }, + onStartCompanionClick = { }, + onStartRemoteOwnAppClick = { }, + onStartRemoteActivityClick = { }, + onDialogDismiss = { }, + ) +} diff --git a/datalayer/sample/phone/src/main/java/com/google/android/horologist/datalayer/sample/screens/nodes/NodesViewModel.kt b/datalayer/sample/phone/src/main/java/com/google/android/horologist/datalayer/sample/screens/nodes/NodesViewModel.kt new file mode 100644 index 0000000000..6c7eb79531 --- /dev/null +++ b/datalayer/sample/phone/src/main/java/com/google/android/horologist/datalayer/sample/screens/nodes/NodesViewModel.kt @@ -0,0 +1,146 @@ +/* + * 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. + */ + +package com.google.android.horologist.datalayer.sample.screens.nodes + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.google.android.horologist.data.AppHelperResultCode +import com.google.android.horologist.data.activityConfig +import com.google.android.horologist.data.apphelper.AppHelperNodeStatus +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 + +private const val REMOTE_ACTIVITY_SAMPLE_CLASS_FULL_NAME = + "com.google.android.horologist.datalayer.sample.screens.startremote.StartRemoteSampleActivity" + +@HiltViewModel +class NodesActionViewModel + @Inject + constructor( + private val phoneDataLayerAppHelper: PhoneDataLayerAppHelper, + ) : ViewModel() { + + private var initializeCalled = false + private var cachedNodeList: List = emptyList() + + private val _uiState = + MutableStateFlow(NodesScreenState.Idle) + public val uiState: StateFlow = _uiState + + fun initialize() { + if (initializeCalled) return + initializeCalled = true + + _uiState.value = NodesScreenState.Loading + + viewModelScope.launch { + if (!phoneDataLayerAppHelper.isAvailable()) { + _uiState.value = NodesScreenState.ApiNotAvailable + } else { + loadNodes() + } + } + } + + fun onRefreshClick() { + _uiState.value = NodesScreenState.Loading + + viewModelScope.launch { + loadNodes() + } + } + + fun onStartCompanionClick(nodeId: String) { + runActionAndHandleAppHelperResult { + phoneDataLayerAppHelper.startCompanion(node = nodeId) + } + } + + fun onInstallOnNodeClick(nodeId: String) { + _uiState.value = NodesScreenState.ActionRunning + viewModelScope.launch { + try { + phoneDataLayerAppHelper.installOnNode(node = nodeId) + + _uiState.value = NodesScreenState.ActionSucceeded + } catch (e: Exception) { + // This should be handled with AppHelperResultCode if API gets improved: + // https://github.com/google/horologist/issues/1902 + _uiState.value = NodesScreenState.ActionFailed(errorCode = e::class.java.simpleName) + e.printStackTrace() + } + } + } + + fun onStartRemoteOwnAppClick(nodeId: String) { + runActionAndHandleAppHelperResult { + phoneDataLayerAppHelper.startRemoteOwnApp(node = nodeId) + } + } + + fun onStartRemoteActivityClick(nodeId: String) { + runActionAndHandleAppHelperResult { + val config = activityConfig { + classFullName = REMOTE_ACTIVITY_SAMPLE_CLASS_FULL_NAME + } + phoneDataLayerAppHelper.startRemoteActivity(nodeId, config) + } + } + + fun onDialogDismiss() { + _uiState.value = NodesScreenState.Loaded(nodeList = cachedNodeList) + } + + private suspend fun loadNodes() { + cachedNodeList = phoneDataLayerAppHelper.connectedNodes() + _uiState.value = NodesScreenState.Loaded(nodeList = cachedNodeList) + } + + private fun runActionAndHandleAppHelperResult(action: suspend () -> AppHelperResultCode) { + _uiState.value = NodesScreenState.ActionRunning + viewModelScope.launch { + when (val result = action()) { + AppHelperResultCode.APP_HELPER_RESULT_SUCCESS -> { + _uiState.value = NodesScreenState.ActionSucceeded + } + + else -> { + _uiState.value = + NodesScreenState.ActionFailed(errorCode = result.name) + } + } + } + } + } + +sealed class NodesScreenState { + data object Idle : NodesScreenState() + + data object Loading : NodesScreenState() + + data class Loaded(val nodeList: List) : NodesScreenState() + + data object ActionRunning : NodesScreenState() + data object ActionSucceeded : NodesScreenState() + data class ActionFailed(val errorCode: String) : NodesScreenState() + + data object ApiNotAvailable : NodesScreenState() +} diff --git a/datalayer/sample/phone/src/main/java/com/google/android/horologist/datalayer/sample/screens/startremote/StartRemoteSampleActivity.kt b/datalayer/sample/phone/src/main/java/com/google/android/horologist/datalayer/sample/screens/startremote/StartRemoteSampleActivity.kt index bff25424b0..bbfd78453c 100644 --- a/datalayer/sample/phone/src/main/java/com/google/android/horologist/datalayer/sample/screens/startremote/StartRemoteSampleActivity.kt +++ b/datalayer/sample/phone/src/main/java/com/google/android/horologist/datalayer/sample/screens/startremote/StartRemoteSampleActivity.kt @@ -38,7 +38,6 @@ class StartRemoteSampleActivity : ComponentActivity() { setContent { HorologistTheme { - // A surface container using the 'background' color from the theme Surface( modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background, diff --git a/datalayer/sample/phone/src/main/res/values/strings.xml b/datalayer/sample/phone/src/main/res/values/strings.xml index 233df06d3e..700517211b 100644 --- a/datalayer/sample/phone/src/main/res/values/strings.xml +++ b/datalayer/sample/phone/src/main/res/values/strings.xml @@ -15,25 +15,39 @@ --> + Horologist Datalayer Sample + This device does not have capability to communicate to Wearable Data Layer API + + App Helper + Nodes + Data Layer List of nodes sample Counter sample - This device does not have capability to communicate to Wearable Data Layer API - Node: %1$s - ID: %1$s - App installed: %1$s - Type: %1$s - UNKNOWN (app not installed) - Complications: %1$s - Tiles: %1$s - Usage: %1$s - Install - Companion - Launch + + Nodes + No nodes were found. + Refresh + Success! + Failed: \n%1$s + Dismiss - List connected nodes + + Node: %1$s + ID: %1$s + App installed: %1$s + Type: %1$s + UNKNOWN (app not installed) + Complications: %1$s + Tiles: %1$s + Usage: %1$s + Install on node + Start companion + Start remote own app + Start remote activity + This is a sample activity to demonstrate it being launched remotely from the watch. \ No newline at end of file diff --git a/datalayer/sample/wear/src/main/AndroidManifest.xml b/datalayer/sample/wear/src/main/AndroidManifest.xml index ade56c106b..8e16442572 100644 --- a/datalayer/sample/wear/src/main/AndroidManifest.xml +++ b/datalayer/sample/wear/src/main/AndroidManifest.xml @@ -51,6 +51,11 @@ + + diff --git a/datalayer/sample/wear/src/main/java/com/google/android/horologist/datalayer/sample/screens/startremote/StartRemoteSampleActivity.kt b/datalayer/sample/wear/src/main/java/com/google/android/horologist/datalayer/sample/screens/startremote/StartRemoteSampleActivity.kt new file mode 100644 index 0000000000..f1421c6080 --- /dev/null +++ b/datalayer/sample/wear/src/main/java/com/google/android/horologist/datalayer/sample/screens/startremote/StartRemoteSampleActivity.kt @@ -0,0 +1,53 @@ +/* + * 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. + */ + +package com.google.android.horologist.datalayer.sample.screens.startremote + +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.wear.compose.material.Text +import com.google.android.horologist.datalayer.sample.R + +class StartRemoteSampleActivity : ComponentActivity() { + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + setContent { + val scrollState = rememberScrollState() + Box( + modifier = Modifier + .fillMaxSize() + .verticalScroll(state = scrollState), + ) { + Text( + text = stringResource(id = R.string.app_helper_start_remote_activity_message), + modifier = Modifier.align(Alignment.Center), + textAlign = TextAlign.Center, + ) + } + } + } +} diff --git a/datalayer/sample/wear/src/main/res/values/strings.xml b/datalayer/sample/wear/src/main/res/values/strings.xml index 85f29996ae..7e592fe1ba 100644 --- a/datalayer/sample/wear/src/main/res/values/strings.xml +++ b/datalayer/sample/wear/src/main/res/values/strings.xml @@ -69,4 +69,7 @@ Start remote activity Success! Failed: \n%1$s + + + This is a sample activity to demonstrate it being launched remotely from the phone. \ No newline at end of file