diff --git a/kmp/src/commonMain/composeResources/values/strings.xml b/kmp/src/commonMain/composeResources/values/strings.xml
index ea22763c5..22ffc9f86 100644
--- a/kmp/src/commonMain/composeResources/values/strings.xml
+++ b/kmp/src/commonMain/composeResources/values/strings.xml
@@ -31,4 +31,6 @@
Add task
Show unstarted
Show completed
+ Install on phone
+ Unknown error
\ No newline at end of file
diff --git a/wear/src/main/java/org/tasks/presentation/MainActivity.kt b/wear/src/main/java/org/tasks/presentation/MainActivity.kt
index 092d97615..8edb0073a 100644
--- a/wear/src/main/java/org/tasks/presentation/MainActivity.kt
+++ b/wear/src/main/java/org/tasks/presentation/MainActivity.kt
@@ -8,14 +8,17 @@ package org.tasks.presentation
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
+import androidx.activity.viewModels
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.lifecycle.compose.collectAsStateWithLifecycle
@@ -23,6 +26,8 @@ import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavType
import androidx.navigation.navArgument
import androidx.paging.compose.collectAsLazyPagingItems
+import androidx.wear.compose.material.Chip
+import androidx.wear.compose.material.CircularProgressIndicator
import androidx.wear.compose.material.MaterialTheme
import androidx.wear.compose.material.Text
import androidx.wear.compose.material.TimeText
@@ -31,6 +36,8 @@ import androidx.wear.compose.navigation.composable
import androidx.wear.compose.navigation.rememberSwipeDismissableNavController
import androidx.wear.tooling.preview.devices.WearDevices
import com.google.android.horologist.compose.layout.AppScaffold
+import kotlinx.coroutines.delay
+import org.jetbrains.compose.resources.stringResource
import org.tasks.presentation.screens.MenuScreen
import org.tasks.presentation.screens.MenuViewModel
import org.tasks.presentation.screens.SettingsScreen
@@ -38,8 +45,13 @@ import org.tasks.presentation.screens.SettingsViewModel
import org.tasks.presentation.screens.TaskListScreen
import org.tasks.presentation.screens.TaskListViewModel
import org.tasks.presentation.theme.TasksTheme
+import tasks.kmp.generated.resources.Res
+import tasks.kmp.generated.resources.wear_install_app
+import tasks.kmp.generated.resources.wear_unknown_error
class MainActivity : ComponentActivity() {
+ private val viewModel: MainActivityViewModel by viewModels()
+
override fun onCreate(savedInstanceState: Bundle?) {
installSplashScreen()
@@ -57,10 +69,90 @@ class MainActivity : ComponentActivity() {
val taskListViewModel: TaskListViewModel = viewModel()
val taskListItems = taskListViewModel.uiItems.collectAsLazyPagingItems()
val settingsViewModel: SettingsViewModel = viewModel()
+ val connected = viewModel.uiState.collectAsStateWithLifecycle().value
+ LaunchedEffect(connected) {
+ when (connected) {
+ NodesActionScreenState.ApiNotAvailable -> {
+ navController.popBackStack()
+ navController.navigate("error")
+ }
+
+ is NodesActionScreenState.Loaded -> {
+ navController.popBackStack()
+ if (connected.nodeList.any { it.type == NodeTypeUiModel.PHONE && it.appInstalled }) {
+ navController.navigate("task_list")
+ } else {
+ connected
+ .nodeList
+ .firstOrNull { it.type == NodeTypeUiModel.UNKNOWN }
+ ?.let {
+ navController.navigate("error?type=${Errors.APP_NOT_INSTALLED},nodeId=${it.id}")
+ }
+ ?: navController.navigate("error")
+ }
+ }
+
+ else -> {}
+ }
+ }
SwipeDismissableNavHost(
- startDestination = "task_list",
+ startDestination = "loading",
navController = navController,
) {
+ composable("loading") {
+ Box(
+ modifier = Modifier.fillMaxSize(),
+ contentAlignment = Alignment.Center
+ ) {
+ CircularProgressIndicator()
+ }
+ }
+ composable(
+ route = "error?type={type},nodeId={nodeId}",
+ arguments = listOf(
+ navArgument("type") {
+ type = NavType.EnumType(Errors::class.java)
+ defaultValue = Errors.UNKNOWN
+ },
+ navArgument("nodeId") {
+ type = NavType.StringType
+ nullable = true
+ }
+ )
+ ) {
+ val type = it.arguments?.getSerializable("type") as? Errors
+ val nodeId = it.arguments?.getString("nodeId")
+ Box(
+ modifier = Modifier.fillMaxSize(),
+ contentAlignment = Alignment.Center
+ ) {
+ when (type) {
+ Errors.APP_NOT_INSTALLED -> {
+ LaunchedEffect(Unit) {
+ while (true) {
+ delay(5000)
+ viewModel.loadNodes()
+ }
+ }
+ Chip(
+ onClick = { viewModel.installOnNode(nodeId) },
+ label = {
+ Text(
+ text = stringResource(Res.string.wear_install_app),
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis
+ )
+ },
+ )
+ }
+
+ else ->
+ Text(
+ text = stringResource(Res.string.wear_unknown_error),
+ )
+ }
+ }
+ }
composable("task_list") {
TaskListScreen(
uiItems = taskListItems,
diff --git a/wear/src/main/java/org/tasks/presentation/MainActivityViewModel.kt b/wear/src/main/java/org/tasks/presentation/MainActivityViewModel.kt
index e47ce2448..a99e146d9 100644
--- a/wear/src/main/java/org/tasks/presentation/MainActivityViewModel.kt
+++ b/wear/src/main/java/org/tasks/presentation/MainActivityViewModel.kt
@@ -1,4 +1,108 @@
package org.tasks.presentation
-class MainActivityViewModel {
-}
\ No newline at end of file
+import android.app.Application
+import android.util.Log
+import androidx.lifecycle.AndroidViewModel
+import androidx.lifecycle.viewModelScope
+import com.google.android.horologist.annotations.ExperimentalHorologistApi
+import com.google.android.horologist.data.WearDataLayerRegistry
+import com.google.android.horologist.data.apphelper.AppInstallationStatus
+import com.google.android.horologist.data.apphelper.AppInstallationStatusNodeType
+import com.google.android.horologist.datalayer.watch.WearDataLayerAppHelper
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.launch
+import org.tasks.extensions.wearDataLayerRegistry
+
+@OptIn(ExperimentalHorologistApi::class)
+class MainActivityViewModel(
+ application: Application,
+) : AndroidViewModel(application) {
+ fun installOnNode(nodeId: String?) = viewModelScope.launch {
+ if (nodeId == null) {
+ Log.d("MainActivityViewModel", "Missing nodeId")
+ } else {
+ helper.installOnNode(nodeId)
+ }
+ }
+
+ private val _uiState =
+ MutableStateFlow(NodesActionScreenState.Idle)
+ val uiState = _uiState.asStateFlow()
+ private val registry: WearDataLayerRegistry by lazy {
+ application.wearDataLayerRegistry(viewModelScope)
+ }
+ private val helper: WearDataLayerAppHelper by lazy {
+ WearDataLayerAppHelper(
+ context = application,
+ registry = registry,
+ scope = viewModelScope,
+ )
+ }
+
+ init {
+ _uiState.value = NodesActionScreenState.Loading
+
+ loadNodes()
+ }
+
+ fun loadNodes() = viewModelScope.launch {
+ if (!helper.isAvailable()) {
+ _uiState.value = NodesActionScreenState.ApiNotAvailable
+ } else {
+ _uiState.value = NodesActionScreenState.Loaded(
+ nodeList = helper.connectedNodes().map { node ->
+ val type = when (node.appInstallationStatus) {
+ is AppInstallationStatus.Installed -> {
+ val status =
+ node.appInstallationStatus as AppInstallationStatus.Installed
+ when (status.nodeType) {
+ AppInstallationStatusNodeType.WATCH -> NodeTypeUiModel.WATCH
+ AppInstallationStatusNodeType.PHONE -> NodeTypeUiModel.PHONE
+ }
+ }
+
+ AppInstallationStatus.NotInstalled -> NodeTypeUiModel.UNKNOWN
+ }
+
+ NodeUiModel(
+ id = node.id,
+ name = node.displayName,
+ appInstalled = node.appInstallationStatus is AppInstallationStatus.Installed,
+ type = type,
+ )
+ },
+ ).also {
+ Log.d("MainActivityViewModel", "Loaded: $it")
+ }
+ }
+ }
+}
+
+data class NodeUiModel(
+ val id: String,
+ val name: String,
+ val appInstalled: Boolean,
+ val type: NodeTypeUiModel,
+)
+
+enum class NodeTypeUiModel {
+ WATCH,
+ PHONE,
+ UNKNOWN,
+}
+
+enum class Errors {
+ APP_NOT_INSTALLED,
+ UNKNOWN
+}
+
+sealed class NodesActionScreenState {
+ data object Idle : NodesActionScreenState()
+
+ data object Loading : NodesActionScreenState()
+
+ data class Loaded(val nodeList: List) : NodesActionScreenState()
+
+ data object ApiNotAvailable : NodesActionScreenState()
+}