diff --git a/core/testing/src/main/java/com/android/developers/testing/repository/FakeWatchFaceInstallationRepository.kt b/core/testing/src/main/java/com/android/developers/testing/repository/FakeWatchFaceInstallationRepository.kt index 6be2a15c..527f47a9 100644 --- a/core/testing/src/main/java/com/android/developers/testing/repository/FakeWatchFaceInstallationRepository.kt +++ b/core/testing/src/main/java/com/android/developers/testing/repository/FakeWatchFaceInstallationRepository.kt @@ -80,6 +80,8 @@ class FakeWatchFaceInstallationRepository : WatchFaceInstallationRepository { _watchFaceInstallationStatus.value = WatchFaceInstallationStatus.Preparing } + override suspend fun installAndroidify(nodeId: String) { } + private fun generateTransferId() = UUID.randomUUID().toString().take(8) public fun setWatchAsConnected() { diff --git a/feature/results/src/main/java/com/android/developers/androidify/customize/CustomizeExportScreen.kt b/feature/results/src/main/java/com/android/developers/androidify/customize/CustomizeExportScreen.kt index 51a58c2a..46213581 100644 --- a/feature/results/src/main/java/com/android/developers/androidify/customize/CustomizeExportScreen.kt +++ b/feature/results/src/main/java/com/android/developers/androidify/customize/CustomizeExportScreen.kt @@ -130,6 +130,9 @@ fun CustomizeAndExportScreen( onInstallWatchFaceClicked = { viewModel.installWatchFace() }, + onInstallAndroidifyClicked = { + viewModel.launchPlayInstallOnWatch() + }, onResetWatchFaceSend = { viewModel.resetWatchFaceSend() }, @@ -151,6 +154,7 @@ private fun CustomizeExportContents( onToolSelected: (CustomizeTool) -> Unit, onSelectedToolStateChanged: (ToolState) -> Unit, onInstallWatchFaceClicked: () -> Unit, + onInstallAndroidifyClicked: suspend () -> Boolean, onResetWatchFaceSend: () -> Unit, layoutType: CustomizeExportLayoutType, snackbarHostState: SnackbarHostState, @@ -263,6 +267,9 @@ private fun CustomizeExportContents( onWatchFaceInstallClick = { onInstallWatchFaceClicked() }, + onAndroidifyInstallClick = { + onInstallAndroidifyClicked() + }, onLoad = loadWatchFaces, watchFaceSelectionState = state.watchFaceSelectionState, onWatchFaceSelect = onWatchFaceSelect, @@ -599,6 +606,7 @@ fun CustomizeExportPreview() { layoutType = CustomizeExportLayoutType.Compact, onSelectedToolStateChanged = {}, onInstallWatchFaceClicked = {}, + onInstallAndroidifyClicked = { true }, onResetWatchFaceSend = {}, loadWatchFaces = {}, onWatchFaceSelect = {}, @@ -640,6 +648,7 @@ fun CustomizeExportPreviewLarge() { layoutType = CustomizeExportLayoutType.Medium, onSelectedToolStateChanged = {}, onInstallWatchFaceClicked = {}, + onInstallAndroidifyClicked = { true }, onResetWatchFaceSend = {}, loadWatchFaces = {}, onWatchFaceSelect = {}, diff --git a/feature/results/src/main/java/com/android/developers/androidify/customize/CustomizeExportViewModel.kt b/feature/results/src/main/java/com/android/developers/androidify/customize/CustomizeExportViewModel.kt index 3112cca8..23eb4cbf 100644 --- a/feature/results/src/main/java/com/android/developers/androidify/customize/CustomizeExportViewModel.kt +++ b/feature/results/src/main/java/com/android/developers/androidify/customize/CustomizeExportViewModel.kt @@ -22,6 +22,7 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.material3.SnackbarHostState import androidx.compose.ui.Modifier import androidx.lifecycle.AndroidViewModel +import androidx.lifecycle.application import androidx.lifecycle.viewModelScope import com.android.developers.androidify.RemoteConfigDataSource import com.android.developers.androidify.customize.watchface.WatchFaceSelectionState @@ -367,6 +368,19 @@ class CustomizeExportViewModel @AssistedInject constructor( } } + suspend fun launchPlayInstallOnWatch(): Boolean { + try { + val watch = state.value.connectedWatch + watch?.let { + watchfaceInstallationRepository.installAndroidify(it.nodeId) + } + return true + } catch (e: Exception) { + Timber.e(e, "Failed to open Play Store on watch") + } + return false + } + fun installWatchFace() { val watchFaceToInstall = _state.value.watchFaceSelectionState.selectedWatchFace ?: return val bitmap = state.value.exportImageCanvas.imageBitmap diff --git a/feature/results/src/main/java/com/android/developers/androidify/customize/watchface/InstallAndroidifyPanel.kt b/feature/results/src/main/java/com/android/developers/androidify/customize/watchface/InstallAndroidifyPanel.kt index 1a9af1cf..6308d28e 100644 --- a/feature/results/src/main/java/com/android/developers/androidify/customize/watchface/InstallAndroidifyPanel.kt +++ b/feature/results/src/main/java/com/android/developers/androidify/customize/watchface/InstallAndroidifyPanel.kt @@ -15,7 +15,6 @@ */ package com.android.developers.androidify.customize.watchface -import android.content.Intent import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row @@ -24,24 +23,33 @@ 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.material3.ButtonDefaults import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +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.core.net.toUri import com.android.developers.androidify.results.R import com.android.developers.androidify.theme.AndroidifyTheme import com.android.developers.androidify.watchface.WatchFaceAsset +import kotlinx.coroutines.launch @Composable fun InstallAndroidifyPanel( + onInstallClick: suspend () -> Boolean, modifier: Modifier = Modifier, ) { val context = LocalContext.current + var isPlayLaunched by remember { mutableStateOf(false) } val placeholderWatchFace = WatchFaceAsset( id = "watch_face_1", previewPath = R.drawable.watch_app_placeholder, @@ -60,21 +68,47 @@ fun InstallAndroidifyPanel( ) { WatchFacePreviewItem( watchFace = placeholderWatchFace, - isSelected = false, + isSelected = true, onClick = { }, ) } Spacer(modifier = Modifier.height(24.dp)) + + val buttonText = if (isPlayLaunched) { + stringResource(R.string.continue_on_watch) + } else { + stringResource(R.string.install_androidify) + } + val launchedColors = ButtonDefaults.buttonColors( + contentColor = MaterialTheme.colorScheme.onSurface, + containerColor = MaterialTheme.colorScheme.secondaryContainer, + ) + val installColors = ButtonDefaults.buttonColors( + contentColor = MaterialTheme.colorScheme.surface, + containerColor = MaterialTheme.colorScheme.onSurface, + ) + val scope = rememberCoroutineScope() WatchFacePanelButton( modifier = modifier.padding(horizontal = 16.dp), - buttonText = stringResource(R.string.install_androidify), + buttonText = buttonText, iconResId = R.drawable.watch_arrow_24, onClick = { - val uri = "market://details?id=${context.packageName}".toUri() - val intent = Intent(Intent.ACTION_VIEW, uri) - context.startActivity(intent) + if (!isPlayLaunched) { + scope.launch { + val launchResult = onInstallClick() + if (launchResult) { + isPlayLaunched = true + } + } + } + }, + colors = if (isPlayLaunched) { + launchedColors + } else { + installColors }, + isInProgress = isPlayLaunched, ) } } @@ -84,6 +118,6 @@ fun InstallAndroidifyPanel( @Composable private fun InstallAndroidifyPanelPreview() { AndroidifyTheme { - InstallAndroidifyPanel() + InstallAndroidifyPanel({ true }) } } diff --git a/feature/results/src/main/java/com/android/developers/androidify/customize/watchface/TransferringWatchFacePanel.kt b/feature/results/src/main/java/com/android/developers/androidify/customize/watchface/TransferringWatchFacePanel.kt index 777df80f..804861cc 100644 --- a/feature/results/src/main/java/com/android/developers/androidify/customize/watchface/TransferringWatchFacePanel.kt +++ b/feature/results/src/main/java/com/android/developers/androidify/customize/watchface/TransferringWatchFacePanel.kt @@ -64,7 +64,7 @@ fun TransferringWatchFacePanel( WatchFacePanelButton( modifier = Modifier.padding(horizontal = 16.dp), buttonText = transferLabel, - isSending = true, + isInProgress = true, colors = ButtonDefaults.buttonColors( contentColor = MaterialTheme.colorScheme.onSurface, containerColor = MaterialTheme.colorScheme.secondaryContainer, diff --git a/feature/results/src/main/java/com/android/developers/androidify/customize/watchface/WatchFaceModalSheet.kt b/feature/results/src/main/java/com/android/developers/androidify/customize/watchface/WatchFaceModalSheet.kt index 5a2af3fb..027907d1 100644 --- a/feature/results/src/main/java/com/android/developers/androidify/customize/watchface/WatchFaceModalSheet.kt +++ b/feature/results/src/main/java/com/android/developers/androidify/customize/watchface/WatchFaceModalSheet.kt @@ -64,6 +64,7 @@ import com.android.developers.androidify.wear.common.WatchFaceInstallationStatus fun WatchFaceModalSheet( connectedWatch: ConnectedWatch, onWatchFaceInstallClick: (String) -> Unit, + onAndroidifyInstallClick: suspend () -> Boolean, installationStatus: WatchFaceInstallationStatus, sheetState: SheetState, watchFaceSelectionState: WatchFaceSelectionState, @@ -202,7 +203,9 @@ fun WatchFaceModalSheet( } else -> { - InstallAndroidifyPanel() + InstallAndroidifyPanel( + onInstallClick = onAndroidifyInstallClick, + ) } } } @@ -243,6 +246,7 @@ private fun WatchFaceModalSheetPreview() { onLoad = {}, onDismiss = {}, onWatchFaceInstallClick = {}, + onAndroidifyInstallClick = { true }, sheetState = sheetState, ) } diff --git a/feature/results/src/main/java/com/android/developers/androidify/customize/watchface/WatchFacePanelButton.kt b/feature/results/src/main/java/com/android/developers/androidify/customize/watchface/WatchFacePanelButton.kt index 899a7b84..ade05f60 100644 --- a/feature/results/src/main/java/com/android/developers/androidify/customize/watchface/WatchFacePanelButton.kt +++ b/feature/results/src/main/java/com/android/developers/androidify/customize/watchface/WatchFacePanelButton.kt @@ -48,7 +48,7 @@ fun WatchFacePanelButton( modifier: Modifier = Modifier, onClick: () -> Unit = {}, buttonText: String, - isSending: Boolean = false, + isInProgress: Boolean = false, iconResId: Int? = null, colors: ButtonColors = ButtonDefaults.buttonColors( contentColor = MaterialTheme.colorScheme.surface, @@ -67,7 +67,7 @@ fun WatchFacePanelButton( Row( verticalAlignment = Alignment.CenterVertically, ) { - if (isSending) { + if (isInProgress) { ContainedLoadingIndicator( modifier = Modifier.size(24.dp), containerColor = colors.containerColor, @@ -92,7 +92,7 @@ private fun WatchFaceInstallButtonPreview() { WatchFacePanelButton( onClick = { }, buttonText = stringResource(R.string.send_to_watch), - isSending = false, + isInProgress = false, iconResId = R.drawable.watch_arrow_24, ) } @@ -105,7 +105,7 @@ private fun WatchFaceInstalledButtonPreview() { WatchFacePanelButton( onClick = { }, buttonText = stringResource(R.string.watch_face_sent), - isSending = false, + isInProgress = false, iconResId = R.drawable.check_24, colors = ButtonDefaults.buttonColors( contentColor = MaterialTheme.colorScheme.onSurface, @@ -122,7 +122,7 @@ private fun WatchFaceInstallingButtonPreview() { WatchFacePanelButton( onClick = { }, buttonText = stringResource(R.string.sending_to_watch), - isSending = true, + isInProgress = true, colors = ButtonDefaults.buttonColors( contentColor = MaterialTheme.colorScheme.onSurface, containerColor = MaterialTheme.colorScheme.secondaryContainer, diff --git a/feature/results/src/main/res/values/strings.xml b/feature/results/src/main/res/values/strings.xml index 6f3d9532..2b270080 100644 --- a/feature/results/src/main/res/values/strings.xml +++ b/feature/results/src/main/res/values/strings.xml @@ -59,4 +59,5 @@ Check your watch is connected and try again "No available watch faces" OK + Continue on watch diff --git a/watchface/build.gradle.kts b/watchface/build.gradle.kts index ad85d46e..60305601 100644 --- a/watchface/build.gradle.kts +++ b/watchface/build.gradle.kts @@ -61,6 +61,7 @@ dependencies { implementation(libs.validator.push.android) { exclude(group = "com.google.guava", "listenablefuture") } + implementation(libs.androidx.wear.remote.interactions) implementation(libs.bcpkix.jdk18on) implementation(libs.play.services.wearable) implementation(libs.kotlinx.coroutines.play.services) diff --git a/watchface/src/main/java/com/android/developers/androidify/watchface/transfer/EmptyWatchFaceInstallationRepository.kt b/watchface/src/main/java/com/android/developers/androidify/watchface/transfer/EmptyWatchFaceInstallationRepository.kt index d77cdd31..7838c0cf 100644 --- a/watchface/src/main/java/com/android/developers/androidify/watchface/transfer/EmptyWatchFaceInstallationRepository.kt +++ b/watchface/src/main/java/com/android/developers/androidify/watchface/transfer/EmptyWatchFaceInstallationRepository.kt @@ -48,4 +48,6 @@ class EmptyWatchFaceInstallationRepositoryImpl @Inject constructor() : WatchFace override suspend fun resetInstallationStatus() { } override suspend fun prepareForTransfer() { } + + override suspend fun installAndroidify(nodeId: String) { } } diff --git a/watchface/src/main/java/com/android/developers/androidify/watchface/transfer/WatchFaceInstallationRepository.kt b/watchface/src/main/java/com/android/developers/androidify/watchface/transfer/WatchFaceInstallationRepository.kt index 98da3c70..be5843f2 100644 --- a/watchface/src/main/java/com/android/developers/androidify/watchface/transfer/WatchFaceInstallationRepository.kt +++ b/watchface/src/main/java/com/android/developers/androidify/watchface/transfer/WatchFaceInstallationRepository.kt @@ -16,7 +16,11 @@ package com.android.developers.androidify.watchface.transfer import android.content.Context +import android.content.Intent import android.graphics.Bitmap +import android.net.Uri +import androidx.concurrent.futures.await +import androidx.wear.remote.interactions.RemoteActivityHelper import com.android.developers.androidify.watchface.WatchFaceAsset import com.android.developers.androidify.watchface.creator.WatchFaceCreator import com.android.developers.androidify.wear.common.ConnectedWatch @@ -24,6 +28,7 @@ import com.android.developers.androidify.wear.common.WatchFaceInstallError import com.android.developers.androidify.wear.common.WatchFaceInstallationStatus import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.asExecutor import kotlinx.coroutines.channels.BufferOverflow import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow @@ -70,6 +75,8 @@ interface WatchFaceInstallationRepository { suspend fun resetInstallationStatus() suspend fun prepareForTransfer() + + suspend fun installAndroidify(nodeId: String) } class WatchFaceInstallationRepositoryImpl @Inject constructor( @@ -133,4 +140,18 @@ class WatchFaceInstallationRepositoryImpl @Inject constructor( override suspend fun prepareForTransfer() { manualStatusUpdates.tryEmit(WatchFaceInstallationStatus.Preparing) } + + override suspend fun installAndroidify(nodeId: String) { + val backgroundExecutor = Dispatchers.IO.asExecutor() + val remoteActivityHelper = RemoteActivityHelper(context, backgroundExecutor) + + remoteActivityHelper.startRemoteActivity( + Intent(Intent.ACTION_VIEW) + .setData( + Uri.parse("market://details?id=${context.packageName}"), + ) + .addCategory(Intent.CATEGORY_BROWSABLE), + nodeId, + ).await() + } }