Check if app installed on phone

pull/3080/head
Alex Baker 1 year ago
parent f36e900627
commit c13cf982b7

@ -31,4 +31,6 @@
<string name="add_task">Add task</string> <string name="add_task">Add task</string>
<string name="show_unstarted">Show unstarted</string> <string name="show_unstarted">Show unstarted</string>
<string name="show_completed">Show completed</string> <string name="show_completed">Show completed</string>
<string name="wear_install_app">Install on phone</string>
<string name="wear_unknown_error">Unknown error</string>
</resources> </resources>

@ -8,14 +8,17 @@ package org.tasks.presentation
import android.os.Bundle import android.os.Bundle
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent import androidx.activity.compose.setContent
import androidx.activity.viewModels
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
@ -23,6 +26,8 @@ import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavType import androidx.navigation.NavType
import androidx.navigation.navArgument import androidx.navigation.navArgument
import androidx.paging.compose.collectAsLazyPagingItems 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.MaterialTheme
import androidx.wear.compose.material.Text import androidx.wear.compose.material.Text
import androidx.wear.compose.material.TimeText 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.compose.navigation.rememberSwipeDismissableNavController
import androidx.wear.tooling.preview.devices.WearDevices import androidx.wear.tooling.preview.devices.WearDevices
import com.google.android.horologist.compose.layout.AppScaffold 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.MenuScreen
import org.tasks.presentation.screens.MenuViewModel import org.tasks.presentation.screens.MenuViewModel
import org.tasks.presentation.screens.SettingsScreen 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.TaskListScreen
import org.tasks.presentation.screens.TaskListViewModel import org.tasks.presentation.screens.TaskListViewModel
import org.tasks.presentation.theme.TasksTheme 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() { class MainActivity : ComponentActivity() {
private val viewModel: MainActivityViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
installSplashScreen() installSplashScreen()
@ -57,10 +69,90 @@ class MainActivity : ComponentActivity() {
val taskListViewModel: TaskListViewModel = viewModel() val taskListViewModel: TaskListViewModel = viewModel()
val taskListItems = taskListViewModel.uiItems.collectAsLazyPagingItems() val taskListItems = taskListViewModel.uiItems.collectAsLazyPagingItems()
val settingsViewModel: SettingsViewModel = viewModel() 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( SwipeDismissableNavHost(
startDestination = "task_list", startDestination = "loading",
navController = navController, 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") { composable("task_list") {
TaskListScreen( TaskListScreen(
uiItems = taskListItems, uiItems = taskListItems,

@ -1,4 +1,108 @@
package org.tasks.presentation package org.tasks.presentation
class MainActivityViewModel { 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>(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<NodeUiModel>) : NodesActionScreenState()
data object ApiNotAvailable : NodesActionScreenState()
} }
Loading…
Cancel
Save