Skip to content

Commit

Permalink
Add performAction function to InstallPrompt (google#2072)
Browse files Browse the repository at this point in the history
  • Loading branch information
luizgrp authored and kul3r4 committed Feb 29, 2024
1 parent 5c913b5 commit a0eda9d
Show file tree
Hide file tree
Showing 10 changed files with 327 additions and 2 deletions.
1 change: 1 addition & 0 deletions datalayer/phone-ui/api/current.api
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ package com.google.android.horologist.datalayer.phone.ui.prompt.installapp {
@com.google.android.horologist.annotations.ExperimentalHorologistApi public final class InstallAppPrompt {
ctor public InstallAppPrompt(com.google.android.horologist.datalayer.phone.PhoneDataLayerAppHelper phoneDataLayerAppHelper);
method public android.content.Intent getIntent(android.content.Context context, String appPackageName, @DrawableRes int image, String topMessage, String bottomMessage);
method public void performAction(android.content.Context context, String appPackageName);
method public suspend Object? shouldDisplayPrompt(optional kotlin.jvm.functions.Function1<? super com.google.android.horologist.data.apphelper.AppHelperNodeStatus,java.lang.Boolean>? predicate, optional kotlin.coroutines.Continuation<? super com.google.android.horologist.data.apphelper.AppHelperNodeStatus>);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@ import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.res.painterResource
import com.google.android.horologist.datalayer.phone.ui.play.launchPlay
import kotlinx.coroutines.launch

internal const val INSTALL_APP_KEY_APP_PACKAGE_NAME = "HOROLOGIST_INSTALL_APP_KEY_APP_PACKAGE_NAME"
Expand Down Expand Up @@ -80,7 +79,7 @@ internal class InstallAppBottomSheetActivity : ComponentActivity() {
}
},
onConfirmation = {
this.launchPlay(appPackageName)
InstallAppPromptAction.run(context = this, appPackageName = appPackageName)

setResult(RESULT_OK)
finishWithoutAnimation()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -94,4 +94,10 @@ public class InstallAppPrompt(private val phoneDataLayerAppHelper: PhoneDataLaye
topMessage = topMessage,
bottomMessage = bottomMessage,
)

/**
* Performs the same action taken by the prompt when the user taps on "install".
*/
public fun performAction(context: Context, appPackageName: String): Unit =
InstallAppPromptAction.run(context = context, appPackageName = appPackageName)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/*
* 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.installapp

import android.content.Context
import com.google.android.horologist.datalayer.phone.ui.play.launchPlay

internal object InstallAppPromptAction {
fun run(context: Context, appPackageName: String) {
context.launchPlay(appPackageName)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,5 +25,6 @@ sealed class Screen(
data object InstallAppPromptDemoScreen : Screen("installAppPromptDemoScreen")
data object ReEngagePromptDemoScreen : Screen("reEngagePromptDemoScreen")
data object SignInPromptDemoScreen : Screen("signInPromptDemoScreen")
data object InstallAppCustomPromptDemoScreen : Screen("installAppCustomPromptDemoScreen")
data object CounterScreen : Screen("counterScreen")
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
/*
* 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.installapp

import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Watch
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.Icon
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.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
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 InstallAppCustomPromptDemoScreen(
modifier: Modifier = Modifier,
viewModel: InstallAppPromptDemoViewModel = hiltViewModel(),
) {
val state by viewModel.uiState.collectAsStateWithLifecycle()

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

val context = LocalContext.current

InstallAppCustomPromptDemoScreen(
state = state,
onRunDemoClick = viewModel::onRunDemoClick,
onInstallPromptInstallClick = {
viewModel.installAppPrompt.performAction(
context = context,
appPackageName = context.packageName,
)
viewModel.onInstallPromptInstallClick()
},
onInstallPromptCancel = viewModel::onInstallPromptCancel,
modifier = modifier,
)
}

@Composable
fun InstallAppCustomPromptDemoScreen(
state: InstallAppCustomPromptDemoScreenState,
onRunDemoClick: () -> Unit,
onInstallPromptInstallClick: () -> Unit,
onInstallPromptCancel: () -> Unit,
modifier: Modifier = Modifier,
) {
Column(
modifier = modifier.padding(all = 10.dp),
) {
Text(text = stringResource(id = R.string.install_app_custom_prompt_api_call_demo_message))

Button(
onClick = onRunDemoClick,
modifier = Modifier
.padding(top = 10.dp)
.align(Alignment.CenterHorizontally),
enabled = state != InstallAppCustomPromptDemoScreenState.ApiNotAvailable,
) {
Text(text = stringResource(id = R.string.install_app_custom_prompt_run_demo_button_label))
}

when (state) {
InstallAppCustomPromptDemoScreenState.Idle,
InstallAppCustomPromptDemoScreenState.Loaded,
-> {
/* do nothing */
}

InstallAppCustomPromptDemoScreenState.Loading -> {
CircularProgressIndicator()
}

is InstallAppCustomPromptDemoScreenState.WatchFound -> {
AlertDialog(
icon = {
Icon(imageVector = Icons.Default.Watch, contentDescription = null)
},
title = {
Text(text = stringResource(id = R.string.install_app_custom_prompt_demo_prompt_top_message))
},
text = {
Text(text = stringResource(id = R.string.install_app_custom_prompt_demo_prompt_bottom_message))
},
onDismissRequest = onInstallPromptCancel,
confirmButton = {
TextButton(
onClick = onInstallPromptInstallClick,
) {
Text(text = stringResource(id = R.string.install_app_custom_prompt_demo_prompt_confirm_button_label))
}
},
dismissButton = {
TextButton(
onClick = onInstallPromptCancel,
) {
Text(text = stringResource(id = R.string.install_app_custom_prompt_demo_prompt_dismiss_button_label))
}
},
)
}

InstallAppCustomPromptDemoScreenState.WatchNotFound -> {
Text(
stringResource(
id = R.string.install_app_custom_prompt_demo_result_label,
stringResource(id = R.string.install_app_custom_prompt_demo_no_watches_found_label),
),
)
}

InstallAppCustomPromptDemoScreenState.InstallPromptInstallClicked -> {
Text(
stringResource(
id = R.string.install_app_custom_prompt_demo_result_label,
stringResource(id = R.string.install_app_custom_prompt_demo_prompt_install_result_label),
),
)
}

InstallAppCustomPromptDemoScreenState.InstallPromptInstallCancelled -> {
Text(
stringResource(
id = R.string.install_app_custom_prompt_demo_result_label,
stringResource(id = R.string.install_app_custom_prompt_demo_prompt_cancel_result_label),
),
)
}

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

@Preview(showBackground = true)
@Composable
fun InstallAppCustomPromptDemoScreenPreview() {
InstallAppCustomPromptDemoScreen(
state = InstallAppCustomPromptDemoScreenState.Idle,
onRunDemoClick = { },
onInstallPromptInstallClick = { },
onInstallPromptCancel = { },
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
/*
* 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.installapp

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.installapp.InstallAppPrompt
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 InstallAppPromptDemoViewModel
@Inject
constructor(
private val phoneDataLayerAppHelper: PhoneDataLayerAppHelper,
val installAppPrompt: InstallAppPrompt,
) : ViewModel() {

private var initializeCalled = false

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

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

_uiState.value = InstallAppCustomPromptDemoScreenState.Loading

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

fun onRunDemoClick() {
_uiState.value = InstallAppCustomPromptDemoScreenState.Loading

viewModelScope.launch {
val node = installAppPrompt.shouldDisplayPrompt()

_uiState.value = if (node != null) {
InstallAppCustomPromptDemoScreenState.WatchFound
} else {
InstallAppCustomPromptDemoScreenState.WatchNotFound
}
}
}

fun onInstallPromptInstallClick() {
_uiState.value = InstallAppCustomPromptDemoScreenState.InstallPromptInstallClicked
}

fun onInstallPromptCancel() {
_uiState.value = InstallAppCustomPromptDemoScreenState.InstallPromptInstallCancelled
}
}

sealed class InstallAppCustomPromptDemoScreenState {
data object Idle : InstallAppCustomPromptDemoScreenState()
data object Loading : InstallAppCustomPromptDemoScreenState()
data object Loaded : InstallAppCustomPromptDemoScreenState()
data object WatchFound : InstallAppCustomPromptDemoScreenState()
data object WatchNotFound : InstallAppCustomPromptDemoScreenState()
data object InstallPromptInstallClicked : InstallAppCustomPromptDemoScreenState()
data object InstallPromptInstallCancelled : InstallAppCustomPromptDemoScreenState()
data object ApiNotAvailable : InstallAppCustomPromptDemoScreenState()
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ 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.inappprompts.custom.installapp.InstallAppCustomPromptDemoScreen
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
Expand Down Expand Up @@ -75,6 +76,9 @@ fun MainScreen(
composable(route = Screen.SignInPromptDemoScreen.route) {
SignInPromptDemoScreen()
}
composable(route = Screen.InstallAppCustomPromptDemoScreen.route) {
InstallAppCustomPromptDemoScreen()
}
composable(route = Screen.CounterScreen.route) {
CounterScreen()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,10 @@ fun MenuScreen(
Text(text = stringResource(id = R.string.menu_screen_signin_demo_item))
}

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

Text(
text = stringResource(id = R.string.menu_screen_datalayer_header),
modifier = Modifier.padding(top = 10.dp),
Expand Down
13 changes: 13 additions & 0 deletions datalayer/sample/phone/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
<string name="menu_screen_install_app_demo_item">Install app</string>
<string name="menu_screen_reengage_demo_item">Re-Engage</string>
<string name="menu_screen_signin_demo_item">Sign-in</string>
<string name="menu_screen_install_app_custom_demo_item">Install app (custom)</string>
<string name="menu_screen_datalayer_header">Data Layer</string>
<string name="menu_screen_counter_item">Counter sample</string>

Expand Down Expand Up @@ -72,6 +73,18 @@
<string name="install_app_prompt_demo_prompt_install_result_label">User tapped install on the prompt.</string>
<string name="install_app_prompt_demo_prompt_cancel_result_label">User dismissed the prompt.</string>

<!-- In-app prompt - Install App Demo screen -->
<string name="install_app_custom_prompt_api_call_demo_message">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.</string>
<string name="install_app_custom_prompt_run_demo_button_label">Run demo</string>
<string name="install_app_custom_prompt_demo_prompt_top_message">Test the interactions between the phone and the watch with the demo app.</string>
<string name="install_app_custom_prompt_demo_prompt_bottom_message">Install the demo app on your Wear OS watch.</string>
<string name="install_app_custom_prompt_demo_prompt_confirm_button_label">Install</string>
<string name="install_app_custom_prompt_demo_prompt_dismiss_button_label">Not now</string>
<string name="install_app_custom_prompt_demo_result_label">Result: %1$s</string>
<string name="install_app_custom_prompt_demo_no_watches_found_label">No watches meeting the required conditions were found.</string>
<string name="install_app_custom_prompt_demo_prompt_install_result_label">User tapped install on the prompt.</string>
<string name="install_app_custom_prompt_demo_prompt_cancel_result_label">User dismissed the prompt.</string>

<!-- In-app prompt - Re-Engage Demo screen -->
<string name="reengage_prompt_api_call_demo_message">This demo calls the Horologist API to show the re-engage prompt for the watch demo app. The API will only display the prompt if there is a watch connected and the watch has the app already installed.</string>
<string name="reengage_prompt_run_demo_button_label">Run demo</string>
Expand Down

0 comments on commit a0eda9d

Please sign in to comment.