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="show_unstarted">Show unstarted</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>

@ -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,

@ -1,4 +1,108 @@
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