diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/MullvadApp.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/MullvadApp.kt index 6702d393dda5..047fd190c98c 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/MullvadApp.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/MullvadApp.kt @@ -3,6 +3,8 @@ package net.mullvad.mullvadvpn.compose.screen import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier import androidx.compose.ui.semantics.semantics @@ -10,6 +12,7 @@ import androidx.compose.ui.semantics.testTagsAsResourceId import androidx.navigation.NavHostController import com.ramcosta.composedestinations.DestinationsNavHost import com.ramcosta.composedestinations.rememberNavHostEngine +import com.ramcosta.composedestinations.utils.currentDestinationAsState import com.ramcosta.composedestinations.utils.destination import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.map @@ -17,36 +20,48 @@ import net.mullvad.mullvadvpn.compose.NavGraphs import net.mullvad.mullvadvpn.compose.destinations.ChangelogDestination import net.mullvad.mullvadvpn.compose.destinations.ConnectDestination import net.mullvad.mullvadvpn.compose.destinations.OutOfTimeDestination +import net.mullvad.mullvadvpn.compose.destinations.PrivacyDisclaimerDestination +import net.mullvad.mullvadvpn.compose.destinations.SplashDestination import net.mullvad.mullvadvpn.viewmodel.ChangelogViewModel +import net.mullvad.mullvadvpn.viewmodel.ServiceConnectionViewModel +import net.mullvad.mullvadvpn.viewmodel.ServiceState import org.koin.androidx.compose.koinViewModel private val changeLogDestinations = listOf(ConnectDestination, OutOfTimeDestination) +private val noServiceDestinations = listOf(SplashDestination, PrivacyDisclaimerDestination) @OptIn(ExperimentalComposeUiApi::class) @Composable fun MullvadApp() { - val engine = rememberNavHostEngine() - val navController: NavHostController = engine.rememberNavController() + val engine = rememberNavHostEngine() + val navController: NavHostController = engine.rememberNavController() - DestinationsNavHost( - modifier = Modifier.semantics { testTagsAsResourceId = true }.fillMaxSize(), - engine = engine, - navController = navController, - navGraph = NavGraphs.root - ) + val serviceVm = koinViewModel() + val serviceState by serviceVm.connectionState.collectAsState() + val currentDestination by navController.currentDestinationAsState() - // Globally show the changelog - val changeLogsViewModel = koinViewModel() + DestinationsNavHost( + modifier = Modifier.semantics { testTagsAsResourceId = true }.fillMaxSize(), + engine = engine, + navController = navController, + navGraph = NavGraphs.root) - LaunchedEffect(Unit) { - changeLogsViewModel.uiSideEffect.collect { + if (serviceState == ServiceState.Disconnected && currentDestination !in noServiceDestinations) { + SplashScreen() + } - // Wait until we are in an acceptable destination - navController.currentBackStackEntryFlow - .map { it.destination() } - .first { it in changeLogDestinations } + // Globally show the changelog + val changeLogsViewModel = koinViewModel() - navController.navigate(ChangelogDestination(it).route) - } + LaunchedEffect(Unit) { + changeLogsViewModel.uiSideEffect.collect { + + // Wait until we are in an acceptable destination + navController.currentBackStackEntryFlow + .map { it.destination() } + .first { it in changeLogDestinations } + + navController.navigate(ChangelogDestination(it).route) } + } } diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/SplashScreen.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/SplashScreen.kt index d95cb00cc90d..14383af41787 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/SplashScreen.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/SplashScreen.kt @@ -43,7 +43,7 @@ import org.koin.androidx.compose.koinViewModel @Preview @Composable private fun PreviewLoadingScreen() { - AppTheme { SplashScreen() } + AppTheme { SplashScreen() } } // Set this as the start destination of the default nav graph @@ -51,76 +51,68 @@ private fun PreviewLoadingScreen() { @Destination @Composable fun Splash(navigator: DestinationsNavigator) { - val viewModel: SplashViewModel = koinViewModel() + val viewModel: SplashViewModel = koinViewModel() - LaunchedEffect(Unit) { - viewModel.uiSideEffect.collect { - when (it) { - SplashUiSideEffect.NavigateToConnect -> { - navigator.navigate(ConnectDestination) { - popUpTo(NavGraphs.root) { inclusive = true } - } - } - SplashUiSideEffect.NavigateToLogin -> { - navigator.navigate(LoginDestination()) { - popUpTo(NavGraphs.root) { inclusive = true } - } - } - SplashUiSideEffect.NavigateToPrivacyDisclaimer -> { - navigator.navigate(PrivacyDisclaimerDestination) { popUpTo(NavGraphs.root) {} } - } - SplashUiSideEffect.NavigateToRevoked -> { - navigator.navigate(DeviceRevokedDestination) { - popUpTo(NavGraphs.root) { inclusive = true } - } - } - SplashUiSideEffect.NavigateToOutOfTime -> - navigator.navigate(OutOfTimeDestination) { - popUpTo(NavGraphs.root) { inclusive = true } - } - } + LaunchedEffect(Unit) { + viewModel.uiSideEffect.collect { + when (it) { + SplashUiSideEffect.NavigateToConnect -> { + navigator.navigate(ConnectDestination) { popUpTo(NavGraphs.root) { inclusive = true } } + } + SplashUiSideEffect.NavigateToLogin -> { + navigator.navigate(LoginDestination()) { popUpTo(NavGraphs.root) { inclusive = true } } + } + SplashUiSideEffect.NavigateToPrivacyDisclaimer -> { + navigator.navigate(PrivacyDisclaimerDestination) { popUpTo(NavGraphs.root) {} } } + SplashUiSideEffect.NavigateToRevoked -> { + navigator.navigate(DeviceRevokedDestination) { + popUpTo(NavGraphs.root) { inclusive = true } + } + } + SplashUiSideEffect.NavigateToOutOfTime -> + navigator.navigate(OutOfTimeDestination) { + popUpTo(NavGraphs.root) { inclusive = true } + } + } } + } - LaunchedEffect(Unit) { viewModel.start() } + LaunchedEffect(Unit) { viewModel.start() } - SplashScreen() + SplashScreen() } @Composable -private fun SplashScreen() { +fun SplashScreen() { - val backgroundColor = MaterialTheme.colorScheme.primary + val backgroundColor = MaterialTheme.colorScheme.primary - ScaffoldWithTopBar( - topBarColor = backgroundColor, - onSettingsClicked = {}, - onAccountClicked = null, - isIconAndLogoVisible = false, - content = { - Box( - contentAlignment = Alignment.Center, - modifier = - Modifier.background(backgroundColor) - .padding(it) - .padding(bottom = it.calculateTopPadding()) - .fillMaxSize() - ) { - Column( - verticalArrangement = Arrangement.Center, - horizontalAlignment = Alignment.CenterHorizontally - ) { + ScaffoldWithTopBar( + topBarColor = backgroundColor, + onSettingsClicked = null, + onAccountClicked = null, + isIconAndLogoVisible = false, + content = { + Box( + contentAlignment = Alignment.Center, + modifier = + Modifier.background(backgroundColor) + .padding(it) + .padding(bottom = it.calculateTopPadding()) + .fillMaxSize()) { + Column( + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally) { Image( painter = painterResource(id = R.drawable.launch_logo), contentDescription = "", - modifier = Modifier.size(120.dp) - ) + modifier = Modifier.size(120.dp)) Image( painter = painterResource(id = R.drawable.logo_text), contentDescription = "", alpha = 0.6f, - modifier = Modifier.padding(top = 12.dp).height(18.dp) - ) + modifier = Modifier.padding(top = 12.dp).height(18.dp)) Text( text = stringResource(id = R.string.connecting_to_daemon), fontSize = 13.sp, @@ -128,10 +120,8 @@ private fun SplashScreen() { MaterialTheme.colorScheme.onPrimary .copy(alpha = AlphaDescription) .compositeOver(backgroundColor), - modifier = Modifier.padding(top = 12.dp) - ) - } + modifier = Modifier.padding(top = 12.dp)) + } } - } - ) + }) } diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/di/UiModule.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/di/UiModule.kt index 22dba11861da..7584fb361682 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/di/UiModule.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/di/UiModule.kt @@ -48,6 +48,7 @@ import net.mullvad.mullvadvpn.viewmodel.PaymentViewModel import net.mullvad.mullvadvpn.viewmodel.PrivacyDisclaimerViewModel import net.mullvad.mullvadvpn.viewmodel.ReportProblemViewModel import net.mullvad.mullvadvpn.viewmodel.SelectLocationViewModel +import net.mullvad.mullvadvpn.viewmodel.ServiceConnectionViewModel import net.mullvad.mullvadvpn.viewmodel.SettingsViewModel import net.mullvad.mullvadvpn.viewmodel.SplashViewModel import net.mullvad.mullvadvpn.viewmodel.SplitTunnelingViewModel @@ -149,6 +150,7 @@ val uiModule = module { viewModel { ViewLogsViewModel(get()) } viewModel { OutOfTimeViewModel(get(), get(), get(), get()) } viewModel { PaymentViewModel(get()) } + viewModel { ServiceConnectionViewModel(get()) } } const val SELF_PACKAGE_NAME = "SELF_PACKAGE_NAME" diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/ServiceConnectionViewModel.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/ServiceConnectionViewModel.kt new file mode 100644 index 000000000000..6fb150c834ce --- /dev/null +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/ServiceConnectionViewModel.kt @@ -0,0 +1,32 @@ +package net.mullvad.mullvadvpn.viewmodel + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn +import net.mullvad.mullvadvpn.ui.serviceconnection.ServiceConnectionManager +import net.mullvad.mullvadvpn.ui.serviceconnection.ServiceConnectionState + +class ServiceConnectionViewModel(serviceConnectionManager: ServiceConnectionManager) : ViewModel() { + val connectionState = + serviceConnectionManager.connectionState + .map { + when (it) { + is ServiceConnectionState.ConnectedNotReady -> ServiceState.Disconnected + is ServiceConnectionState.ConnectedReady -> ServiceState.Connected + ServiceConnectionState.Disconnected -> ServiceState.Disconnected + } + } + .stateIn( + scope = viewModelScope, + started = SharingStarted.Eagerly, + initialValue = ServiceState.Disconnected + ) +} + +sealed interface ServiceState { + data object Disconnected : ServiceState + + data object Connected : ServiceState +}