Skip to content

Commit

Permalink
Overlay splash screen on no service connection
Browse files Browse the repository at this point in the history
  • Loading branch information
Rawa committed Nov 27, 2023
1 parent 6b76c67 commit ca83029
Show file tree
Hide file tree
Showing 4 changed files with 116 additions and 77 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -3,50 +3,65 @@ 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
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
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<ServiceConnectionViewModel>()
val serviceState by serviceVm.connectionState.collectAsState()
val currentDestination by navController.currentDestinationAsState()

// Globally show the changelog
val changeLogsViewModel = koinViewModel<ChangelogViewModel>()
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<ChangelogViewModel>()

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)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -43,95 +43,85 @@ 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
@RootNavGraph(start = true)
@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,
color =
MaterialTheme.colorScheme.onPrimary
.copy(alpha = AlphaDescription)
.compositeOver(backgroundColor),
modifier = Modifier.padding(top = 12.dp)
)
}
modifier = Modifier.padding(top = 12.dp))
}
}
}
)
})
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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"
Expand Down
Original file line number Diff line number Diff line change
@@ -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
}

0 comments on commit ca83029

Please sign in to comment.