diff options
Diffstat (limited to 'android')
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) { |
