summaryrefslogtreecommitdiffhomepage
path: root/android
diff options
context:
space:
mode:
Diffstat (limited to 'android')
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/PrivacyDisclaimerScreen.kt54
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/constant/StartupConstant.kt4
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/MainActivity.kt15
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/PrivacyDisclaimerViewModel.kt22
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/SplashViewModel.kt3
5 files changed, 87 insertions, 11 deletions
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/PrivacyDisclaimerScreen.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/PrivacyDisclaimerScreen.kt
index b57e66c151..3f52af5fc3 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/PrivacyDisclaimerScreen.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/PrivacyDisclaimerScreen.kt
@@ -16,6 +16,8 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
@@ -34,54 +36,81 @@ import androidx.constraintlayout.compose.Dimension
import com.ramcosta.composedestinations.annotation.Destination
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
import com.ramcosta.composedestinations.navigation.popUpTo
+import kotlinx.coroutines.CancellationException
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withTimeout
import net.mullvad.mullvadvpn.R
import net.mullvad.mullvadvpn.compose.NavGraphs
import net.mullvad.mullvadvpn.compose.button.PrimaryButton
+import net.mullvad.mullvadvpn.compose.component.MullvadCircularProgressIndicatorMedium
import net.mullvad.mullvadvpn.compose.component.ScaffoldWithTopBar
import net.mullvad.mullvadvpn.compose.component.drawVerticalScrollbar
import net.mullvad.mullvadvpn.compose.destinations.LoginDestination
-import net.mullvad.mullvadvpn.compose.transitions.DefaultTransition
+import net.mullvad.mullvadvpn.compose.destinations.SplashDestination
import net.mullvad.mullvadvpn.compose.util.toDp
+import net.mullvad.mullvadvpn.constant.DAEMON_READY_TIMEOUT_MS
import net.mullvad.mullvadvpn.lib.theme.AppTheme
import net.mullvad.mullvadvpn.lib.theme.Dimens
import net.mullvad.mullvadvpn.lib.theme.color.AlphaScrollbar
import net.mullvad.mullvadvpn.ui.MainActivity
import net.mullvad.mullvadvpn.viewmodel.PrivacyDisclaimerUiSideEffect
import net.mullvad.mullvadvpn.viewmodel.PrivacyDisclaimerViewModel
+import net.mullvad.mullvadvpn.viewmodel.PrivacyDisclaimerViewState
import org.koin.androidx.compose.koinViewModel
@Preview
@Composable
private fun PreviewPrivacyDisclaimerScreen() {
- AppTheme { PrivacyDisclaimerScreen({}, {}) }
+ AppTheme { PrivacyDisclaimerScreen(PrivacyDisclaimerViewState(false), {}, {}) }
}
-@Destination(style = DefaultTransition::class)
+@Destination
@Composable
fun PrivacyDisclaimer(
navigator: DestinationsNavigator,
) {
val viewModel: PrivacyDisclaimerViewModel = koinViewModel()
+ val uiState = viewModel.uiState.collectAsState()
val context = LocalContext.current
+ val scope = rememberCoroutineScope()
LaunchedEffect(Unit) {
viewModel.uiSideEffect.collect {
when (it) {
PrivacyDisclaimerUiSideEffect.NavigateToLogin -> {
- (context as MainActivity).initializeStateHandlerAndServiceConnection()
navigator.navigate(LoginDestination(null)) {
launchSingleTop = true
popUpTo(NavGraphs.root) { inclusive = true }
}
}
+ PrivacyDisclaimerUiSideEffect.StartService -> {
+ scope.launch {
+ try {
+ withTimeout(DAEMON_READY_TIMEOUT_MS) {
+ (context as MainActivity).startServiceSuspend()
+ }
+ viewModel.onServiceStartedSuccessful()
+ } catch (e: CancellationException) {
+ // Timeout
+ viewModel.onServiceStartedTimeout()
+ }
+ }
+ }
+ PrivacyDisclaimerUiSideEffect.NavigateToSplash -> {
+ navigator.navigate(SplashDestination) {
+ launchSingleTop = true
+ popUpTo(NavGraphs.root) { inclusive = true }
+ }
+ }
}
}
}
- PrivacyDisclaimerScreen({}, viewModel::setPrivacyDisclosureAccepted)
+ PrivacyDisclaimerScreen(uiState.value, {}, viewModel::setPrivacyDisclosureAccepted)
}
@Composable
fun PrivacyDisclaimerScreen(
+ uiState: PrivacyDisclaimerViewState,
onPrivacyPolicyLinkClicked: () -> Unit,
onAcceptClicked: () -> Unit,
) {
@@ -170,12 +199,17 @@ fun PrivacyDisclaimerScreen(
bottom.linkTo(parent.bottom, margin = sideMargin)
width = Dimension.fillToConstraints
height = Dimension.preferredWrapContent
- }
+ },
+ horizontalAlignment = Alignment.CenterHorizontally
) {
- PrimaryButton(
- text = stringResource(id = R.string.agree_and_continue),
- onClick = onAcceptClicked::invoke
- )
+ if (uiState.isStartingService) {
+ MullvadCircularProgressIndicatorMedium()
+ } else {
+ PrimaryButton(
+ text = stringResource(id = R.string.agree_and_continue),
+ onClick = onAcceptClicked::invoke
+ )
+ }
}
}
}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/constant/StartupConstant.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/constant/StartupConstant.kt
new file mode 100644
index 0000000000..4dec5cffcc
--- /dev/null
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/constant/StartupConstant.kt
@@ -0,0 +1,4 @@
+package net.mullvad.mullvadvpn.constant
+
+const val ACCOUNT_EXPIRY_TIMEOUT_MS = 1000L // 1 second
+const val DAEMON_READY_TIMEOUT_MS = 3000L // 3 seconds
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/MainActivity.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/MainActivity.kt
index c5d7c84a19..4298fa17fa 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/MainActivity.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/MainActivity.kt
@@ -9,6 +9,8 @@ import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.view.WindowCompat
+import kotlinx.coroutines.flow.filterIsInstance
+import kotlinx.coroutines.flow.first
import net.mullvad.mullvadvpn.compose.screen.MullvadApp
import net.mullvad.mullvadvpn.di.paymentModule
import net.mullvad.mullvadvpn.di.uiModule
@@ -19,6 +21,7 @@ import net.mullvad.mullvadvpn.repository.AccountRepository
import net.mullvad.mullvadvpn.repository.DeviceRepository
import net.mullvad.mullvadvpn.repository.PrivacyDisclaimerRepository
import net.mullvad.mullvadvpn.ui.serviceconnection.ServiceConnectionManager
+import net.mullvad.mullvadvpn.ui.serviceconnection.ServiceConnectionState
import net.mullvad.mullvadvpn.viewmodel.ChangelogViewModel
import net.mullvad.mullvadvpn.viewmodel.NoDaemonViewModel
import org.koin.android.ext.android.getKoin
@@ -67,6 +70,18 @@ class MainActivity : ComponentActivity() {
)
}
+ suspend fun startServiceSuspend() {
+ requestNotificationPermissionIfMissing(requestNotificationPermissionLauncher)
+ serviceConnectionManager.bind(
+ vpnPermissionRequestHandler = ::requestVpnPermission,
+ apiEndpointConfiguration = intent?.getApiEndpointConfigurationExtras()
+ )
+ // Ensure we wait until the service is ready
+ serviceConnectionManager.connectionState
+ .filterIsInstance<ServiceConnectionState.ConnectedReady>()
+ .first()
+ }
+
override fun onActivityResult(requestCode: Int, resultCode: Int, resultData: Intent?) {
serviceConnectionManager.onVpnPermissionResult(resultCode == Activity.RESULT_OK)
}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/PrivacyDisclaimerViewModel.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/PrivacyDisclaimerViewModel.kt
index f8e6b13f3d..d765698b90 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/PrivacyDisclaimerViewModel.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/PrivacyDisclaimerViewModel.kt
@@ -4,24 +4,46 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.channels.Channel
+import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.receiveAsFlow
+import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import net.mullvad.mullvadvpn.repository.PrivacyDisclaimerRepository
+data class PrivacyDisclaimerViewState(val isStartingService: Boolean)
+
class PrivacyDisclaimerViewModel(
private val privacyDisclaimerRepository: PrivacyDisclaimerRepository
) : ViewModel() {
+ private val _uiState = MutableStateFlow(PrivacyDisclaimerViewState(false))
+ val uiState = _uiState
+
private val _uiSideEffect =
Channel<PrivacyDisclaimerUiSideEffect>(1, BufferOverflow.DROP_OLDEST)
val uiSideEffect = _uiSideEffect.receiveAsFlow()
fun setPrivacyDisclosureAccepted() {
privacyDisclaimerRepository.setPrivacyDisclosureAccepted()
+ viewModelScope.launch {
+ _uiState.update { it.copy(isStartingService = true) }
+ _uiSideEffect.send(PrivacyDisclaimerUiSideEffect.StartService)
+ }
+ }
+
+ fun onServiceStartedSuccessful() {
viewModelScope.launch { _uiSideEffect.send(PrivacyDisclaimerUiSideEffect.NavigateToLogin) }
}
+
+ fun onServiceStartedTimeout() {
+ viewModelScope.launch { _uiSideEffect.send(PrivacyDisclaimerUiSideEffect.NavigateToSplash) }
+ }
}
sealed interface PrivacyDisclaimerUiSideEffect {
data object NavigateToLogin : PrivacyDisclaimerUiSideEffect
+
+ data object StartService : PrivacyDisclaimerUiSideEffect
+
+ data object NavigateToSplash : PrivacyDisclaimerUiSideEffect
}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/SplashViewModel.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/SplashViewModel.kt
index 8163fb9770..1a7937e9bf 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/SplashViewModel.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/SplashViewModel.kt
@@ -12,6 +12,7 @@ import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.selects.onTimeout
import kotlinx.coroutines.selects.select
+import net.mullvad.mullvadvpn.constant.ACCOUNT_EXPIRY_TIMEOUT_MS
import net.mullvad.mullvadvpn.lib.ipc.Event
import net.mullvad.mullvadvpn.lib.ipc.MessageHandler
import net.mullvad.mullvadvpn.lib.ipc.events
@@ -73,7 +74,7 @@ class SplashViewModel(
val accountExpiry = select {
expiry.onAwait { it }
// If we don't get a response within 1 second, assume the account expiry is Missing
- onTimeout(1000) { AccountExpiry.Missing }
+ onTimeout(ACCOUNT_EXPIRY_TIMEOUT_MS) { AccountExpiry.Missing }
}
return when (accountExpiry) {