From 2f110b0b07fa59b06e38040016fea8d45ccaefea Mon Sep 17 00:00:00 2001 From: Gustavo Pagani Date: Wed, 28 Feb 2024 17:58:05 +0000 Subject: [PATCH] Add performAction function to SignInPrompt --- .../signin/SignInBottomSheetActivity.kt | 5 +- .../phone/ui/prompt/signin/SignInPrompt.kt | 10 + .../ui/prompt/signin/SignInPromptAction.kt | 24 +++ .../datalayer/sample/screens/Screen.kt | 1 + .../InstallAppCustomPromptDemoScreen.kt | 1 - .../signin/SignInCustomPromptDemoScreen.kt | 177 ++++++++++++++++++ .../signin/SignInCustomPromptDemoViewModel.kt | 96 ++++++++++ .../sample/screens/main/MainScreen.kt | 4 + .../sample/screens/menu/MenuScreen.kt | 4 + .../phone/src/main/res/values/strings.xml | 15 +- 10 files changed, 334 insertions(+), 3 deletions(-) create mode 100644 datalayer/phone-ui/src/main/java/com/google/android/horologist/datalayer/phone/ui/prompt/signin/SignInPromptAction.kt create mode 100644 datalayer/sample/phone/src/main/java/com/google/android/horologist/datalayer/sample/screens/inappprompts/custom/signin/SignInCustomPromptDemoScreen.kt create mode 100644 datalayer/sample/phone/src/main/java/com/google/android/horologist/datalayer/sample/screens/inappprompts/custom/signin/SignInCustomPromptDemoViewModel.kt diff --git a/datalayer/phone-ui/src/main/java/com/google/android/horologist/datalayer/phone/ui/prompt/signin/SignInBottomSheetActivity.kt b/datalayer/phone-ui/src/main/java/com/google/android/horologist/datalayer/phone/ui/prompt/signin/SignInBottomSheetActivity.kt index 2a8001e7ed..d3a5c8163b 100644 --- a/datalayer/phone-ui/src/main/java/com/google/android/horologist/datalayer/phone/ui/prompt/signin/SignInBottomSheetActivity.kt +++ b/datalayer/phone-ui/src/main/java/com/google/android/horologist/datalayer/phone/ui/prompt/signin/SignInBottomSheetActivity.kt @@ -116,7 +116,10 @@ internal class SignInBottomSheetActivity : ComponentActivity() { // Can't use the Activity's lifecycleScope as it is going to finish the activity immediately // after this call coroutineAppScope.launch { - phoneDataLayerAppHelper.startRemoteOwnApp(nodeId = nodeId) + SignInPromptAction.run( + phoneDataLayerAppHelper = phoneDataLayerAppHelper, + nodeId = nodeId, + ) } // It returns OK to indicate that the user tapped on the positive button. diff --git a/datalayer/phone-ui/src/main/java/com/google/android/horologist/datalayer/phone/ui/prompt/signin/SignInPrompt.kt b/datalayer/phone-ui/src/main/java/com/google/android/horologist/datalayer/phone/ui/prompt/signin/SignInPrompt.kt index 36d6ba390e..bf7f416596 100644 --- a/datalayer/phone-ui/src/main/java/com/google/android/horologist/datalayer/phone/ui/prompt/signin/SignInPrompt.kt +++ b/datalayer/phone-ui/src/main/java/com/google/android/horologist/datalayer/phone/ui/prompt/signin/SignInPrompt.kt @@ -109,4 +109,14 @@ public class SignInPrompt( positiveButtonLabel = positiveButtonLabel, negativeButtonLabel = negativeButtonLabel, ) + + /** + * Performs the same action taken by the prompt when the user taps on the positive button. + */ + public suspend fun performAction(nodeId: String) { + SignInPromptAction.run( + phoneDataLayerAppHelper = phoneDataLayerAppHelper, + nodeId = nodeId, + ) + } } diff --git a/datalayer/phone-ui/src/main/java/com/google/android/horologist/datalayer/phone/ui/prompt/signin/SignInPromptAction.kt b/datalayer/phone-ui/src/main/java/com/google/android/horologist/datalayer/phone/ui/prompt/signin/SignInPromptAction.kt new file mode 100644 index 0000000000..1461b2cac9 --- /dev/null +++ b/datalayer/phone-ui/src/main/java/com/google/android/horologist/datalayer/phone/ui/prompt/signin/SignInPromptAction.kt @@ -0,0 +1,24 @@ +/* + * 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.phone.ui.prompt.signin + +import com.google.android.horologist.datalayer.phone.PhoneDataLayerAppHelper + +internal object SignInPromptAction { + suspend fun run(phoneDataLayerAppHelper: PhoneDataLayerAppHelper, nodeId: String) = + phoneDataLayerAppHelper.startRemoteOwnApp(nodeId = nodeId) +} 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 33a9983742..e2baa81d7e 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 @@ -26,5 +26,6 @@ sealed class Screen( data object ReEngagePromptDemoScreen : Screen("reEngagePromptDemoScreen") data object SignInPromptDemoScreen : Screen("signInPromptDemoScreen") data object InstallAppCustomPromptDemoScreen : Screen("installAppCustomPromptDemoScreen") + data object SignInCustomPromptDemoScreen : Screen("signInCustomPromptDemoScreen") data object CounterScreen : Screen("counterScreen") } diff --git a/datalayer/sample/phone/src/main/java/com/google/android/horologist/datalayer/sample/screens/inappprompts/custom/installapp/InstallAppCustomPromptDemoScreen.kt b/datalayer/sample/phone/src/main/java/com/google/android/horologist/datalayer/sample/screens/inappprompts/custom/installapp/InstallAppCustomPromptDemoScreen.kt index e4d62d16c1..0b5e592993 100644 --- a/datalayer/sample/phone/src/main/java/com/google/android/horologist/datalayer/sample/screens/inappprompts/custom/installapp/InstallAppCustomPromptDemoScreen.kt +++ b/datalayer/sample/phone/src/main/java/com/google/android/horologist/datalayer/sample/screens/inappprompts/custom/installapp/InstallAppCustomPromptDemoScreen.kt @@ -29,7 +29,6 @@ import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable import androidx.compose.runtime.SideEffect import androidx.compose.runtime.getValue -import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext diff --git a/datalayer/sample/phone/src/main/java/com/google/android/horologist/datalayer/sample/screens/inappprompts/custom/signin/SignInCustomPromptDemoScreen.kt b/datalayer/sample/phone/src/main/java/com/google/android/horologist/datalayer/sample/screens/inappprompts/custom/signin/SignInCustomPromptDemoScreen.kt new file mode 100644 index 0000000000..c445ce47ec --- /dev/null +++ b/datalayer/sample/phone/src/main/java/com/google/android/horologist/datalayer/sample/screens/inappprompts/custom/signin/SignInCustomPromptDemoScreen.kt @@ -0,0 +1,177 @@ +/* + * 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.inappprompts.custom.signin + +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.padding +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Watch +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.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.SideEffect +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.horologist.datalayer.sample.R + +@Composable +fun SignInCustomPromptDemoScreen( + modifier: Modifier = Modifier, + viewModel: SignInCustomPromptDemoViewModel = hiltViewModel(), +) { + val state by viewModel.uiState.collectAsStateWithLifecycle() + + if (state == SignInCustomPromptDemoScreenState.Idle) { + SideEffect { + viewModel.initialize() + } + } + + SignInCustomPromptDemoScreen( + state = state, + onRunDemoClick = viewModel::onRunDemoClick, + onPromptSignInClick = viewModel::onPromptSignInClick, + onPromptDismiss = viewModel::onPromptDismiss, + modifier = modifier, + ) +} + +@Composable +fun SignInCustomPromptDemoScreen( + state: SignInCustomPromptDemoScreenState, + onRunDemoClick: () -> Unit, + onPromptSignInClick: (nodeId: String) -> Unit, + onPromptDismiss: () -> Unit, + modifier: Modifier = Modifier, +) { + Column( + modifier = modifier.padding(all = 10.dp), + ) { + Text(text = stringResource(id = R.string.signin_custom_prompt_api_call_demo_message)) + + Button( + onClick = onRunDemoClick, + modifier = Modifier + .padding(top = 10.dp) + .align(Alignment.CenterHorizontally), + enabled = state != SignInCustomPromptDemoScreenState.ApiNotAvailable, + ) { + Text(text = stringResource(id = R.string.signin_custom_prompt_run_demo_button_label)) + } + + when (state) { + SignInCustomPromptDemoScreenState.Idle, + SignInCustomPromptDemoScreenState.Loaded, + -> { + /* do nothing */ + } + + SignInCustomPromptDemoScreenState.Loading -> { + CircularProgressIndicator() + } + + is SignInCustomPromptDemoScreenState.WatchFound -> { + Row( + modifier = Modifier + .padding(top = 30.dp) + .border(width = 1.dp, color = MaterialTheme.colorScheme.onSurface) + .padding(vertical = 10.dp), + ) { + Icon( + imageVector = Icons.Default.Watch, + contentDescription = null, + modifier = Modifier.padding(20.dp), + ) + Column { + Text(text = stringResource(id = R.string.signin_custom_prompt_demo_prompt_top_message)) + Text(text = stringResource(id = R.string.signin_custom_prompt_demo_prompt_bottom_message)) + Row { + TextButton( + onClick = onPromptDismiss, + ) { + Text(text = stringResource(id = R.string.signin_custom_prompt_demo_prompt_dismiss_button_label)) + } + + Spacer(modifier = Modifier.weight(1f)) + + TextButton( + onClick = { onPromptSignInClick(state.nodeId) }, + ) { + Text(text = stringResource(id = R.string.signin_custom_prompt_demo_prompt_confirm_button_label)) + } + } + } + } + } + + SignInCustomPromptDemoScreenState.WatchNotFound -> { + Text( + stringResource( + id = R.string.signin_custom_prompt_demo_result_label, + stringResource(id = R.string.signin_custom_prompt_demo_no_watches_found_label), + ), + ) + } + + SignInCustomPromptDemoScreenState.PromptSignInClicked -> { + Text( + stringResource( + id = R.string.signin_custom_prompt_demo_result_label, + stringResource(id = R.string.signin_custom_prompt_demo_prompt_positive_result_label), + ), + ) + } + + SignInCustomPromptDemoScreenState.PromptDismissed -> { + Text( + stringResource( + id = R.string.signin_custom_prompt_demo_result_label, + stringResource(id = R.string.signin_custom_prompt_demo_prompt_dismiss_result_label), + ), + ) + } + + SignInCustomPromptDemoScreenState.ApiNotAvailable -> { + Text(stringResource(id = R.string.wearable_message_api_unavailable)) + } + } + } +} + +@Preview(showBackground = true) +@Composable +fun SignInCustomPromptDemoScreenPreview() { + SignInCustomPromptDemoScreen( + state = SignInCustomPromptDemoScreenState.WatchFound("nodeId"), + onRunDemoClick = { }, + onPromptSignInClick = { }, + onPromptDismiss = { }, + ) +} diff --git a/datalayer/sample/phone/src/main/java/com/google/android/horologist/datalayer/sample/screens/inappprompts/custom/signin/SignInCustomPromptDemoViewModel.kt b/datalayer/sample/phone/src/main/java/com/google/android/horologist/datalayer/sample/screens/inappprompts/custom/signin/SignInCustomPromptDemoViewModel.kt new file mode 100644 index 0000000000..72cc27121f --- /dev/null +++ b/datalayer/sample/phone/src/main/java/com/google/android/horologist/datalayer/sample/screens/inappprompts/custom/signin/SignInCustomPromptDemoViewModel.kt @@ -0,0 +1,96 @@ +/* + * 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.inappprompts.custom.signin + +import androidx.annotation.MainThread +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.google.android.horologist.datalayer.phone.PhoneDataLayerAppHelper +import com.google.android.horologist.datalayer.phone.ui.prompt.signin.SignInPrompt +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 SignInCustomPromptDemoViewModel + @Inject + constructor( + private val phoneDataLayerAppHelper: PhoneDataLayerAppHelper, + val signInCustomPrompt: SignInPrompt, + ) : ViewModel() { + + private var initializeCalled = false + + private val _uiState = + MutableStateFlow(SignInCustomPromptDemoScreenState.Idle) + public val uiState: StateFlow = _uiState + + @MainThread + fun initialize() { + if (initializeCalled) return + initializeCalled = true + + _uiState.value = SignInCustomPromptDemoScreenState.Loading + + viewModelScope.launch { + if (!phoneDataLayerAppHelper.isAvailable()) { + _uiState.value = SignInCustomPromptDemoScreenState.ApiNotAvailable + } else { + _uiState.value = SignInCustomPromptDemoScreenState.Loaded + } + } + } + + fun onRunDemoClick() { + _uiState.value = SignInCustomPromptDemoScreenState.Loading + + viewModelScope.launch { + val node = signInCustomPrompt.shouldDisplayPrompt() + + _uiState.value = if (node != null) { + SignInCustomPromptDemoScreenState.WatchFound(node.id) + } else { + SignInCustomPromptDemoScreenState.WatchNotFound + } + } + } + + fun onPromptSignInClick(nodeId: String) { + viewModelScope.launch { + signInCustomPrompt.performAction(nodeId = nodeId) + } + + _uiState.value = SignInCustomPromptDemoScreenState.PromptSignInClicked + } + + fun onPromptDismiss() { + _uiState.value = SignInCustomPromptDemoScreenState.PromptDismissed + } + } + +sealed class SignInCustomPromptDemoScreenState { + data object Idle : SignInCustomPromptDemoScreenState() + data object Loading : SignInCustomPromptDemoScreenState() + data object Loaded : SignInCustomPromptDemoScreenState() + data class WatchFound(val nodeId: String) : SignInCustomPromptDemoScreenState() + data object WatchNotFound : SignInCustomPromptDemoScreenState() + data object PromptSignInClicked : SignInCustomPromptDemoScreenState() + data object PromptDismissed : SignInCustomPromptDemoScreenState() + data object ApiNotAvailable : SignInCustomPromptDemoScreenState() +} 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 index 0b13910339..6ac0ffa4da 100644 --- 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 @@ -31,6 +31,7 @@ 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.inappprompts.custom.installapp.InstallAppCustomPromptDemoScreen +import com.google.android.horologist.datalayer.sample.screens.inappprompts.custom.signin.SignInCustomPromptDemoScreen import com.google.android.horologist.datalayer.sample.screens.inappprompts.installapp.InstallAppPromptDemoScreen import com.google.android.horologist.datalayer.sample.screens.inappprompts.reengage.ReEngagePromptDemoScreen import com.google.android.horologist.datalayer.sample.screens.inappprompts.signin.SignInPromptDemoScreen @@ -79,6 +80,9 @@ fun MainScreen( composable(route = Screen.InstallAppCustomPromptDemoScreen.route) { InstallAppCustomPromptDemoScreen() } + composable(route = Screen.SignInCustomPromptDemoScreen.route) { + SignInCustomPromptDemoScreen() + } composable(route = Screen.CounterScreen.route) { CounterScreen() } 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 fee00fcea7..3bda3f135c 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 @@ -69,6 +69,10 @@ fun MenuScreen( Text(text = stringResource(id = R.string.menu_screen_install_app_custom_demo_item)) } + Button(onClick = { navController.navigate(Screen.SignInCustomPromptDemoScreen.route) }) { + Text(text = stringResource(id = R.string.menu_screen_signin_custom_demo_item)) + } + Text( text = stringResource(id = R.string.menu_screen_datalayer_header), modifier = Modifier.padding(top = 10.dp), diff --git a/datalayer/sample/phone/src/main/res/values/strings.xml b/datalayer/sample/phone/src/main/res/values/strings.xml index b817230599..e06d162a6c 100644 --- a/datalayer/sample/phone/src/main/res/values/strings.xml +++ b/datalayer/sample/phone/src/main/res/values/strings.xml @@ -28,6 +28,7 @@ Re-Engage Sign-in Install app (custom) + Sign-in (custom) Data Layer Counter sample @@ -73,7 +74,7 @@ User tapped install on the prompt. User dismissed the prompt. - + This demo calls the Horologist API to check if there is a watch connected and the watch does not have the app installed.\nIt then displays a custom prompt asking the user to install the app.\nOnce the user taps install, it uses Horologist API to launch Google Play.\n\nGoogle Play won\'t find this app as it is not published. Run demo Test the interactions between the phone and the watch with the demo app. @@ -100,11 +101,23 @@ Run demo Test the interactions between the phone and the watch with the demo app. Sign in to the demo app on your Wear OS watch. + Sign in on watch + Not now Result: %1$s No watches meeting the required conditions were found. User tapped sign-in on the prompt. User dismissed the prompt. + + This demo calls the Horologist API to show the sign-in prompt for the watch demo app. The API will only display the prompt if there is a watch connected, the watch has the app already installed, and the app is not signed in. + Run demo + Test the interactions between the phone and the watch with the demo app. + Sign in to the demo app on your Wear OS watch. + Result: %1$s + No watches meeting the required conditions were found. + User tapped sign-in on the prompt. + User dismissed the prompt. + Counter: %1$s Increase counter