From 8a9aab9115e7f6664e95198839e2104c3d9d8d2e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20G=C3=B6ransson?= Date: Fri, 28 Jun 2024 14:48:06 +0200 Subject: [PATCH] Add splash screen support --- .../mullvadvpn/compose/screen/SplashScreen.kt | 3 +- .../net/mullvad/mullvadvpn/di/UiModule.kt | 4 +- .../repository/SplashCompleteRepository.kt | 11 +- .../net/mullvad/mullvadvpn/ui/MainActivity.kt | 10 + .../mullvadvpn/viewmodel/SplashViewModel.kt | 14 +- .../src/main/res/drawable/avd_splash_anim.xml | 175 ++++++++++++++++++ .../src/main/res/values-v33/styles.xml | 17 ++ 7 files changed, 229 insertions(+), 5 deletions(-) create mode 100644 android/lib/resource/src/main/res/drawable/avd_splash_anim.xml create mode 100644 android/lib/resource/src/main/res/values-v33/styles.xml 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 082763ce1b61..9d643fcd2e0f 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 @@ -29,7 +29,6 @@ import com.ramcosta.composedestinations.generated.destinations.PrivacyDisclaimer import com.ramcosta.composedestinations.navigation.DestinationsNavigator import net.mullvad.mullvadvpn.R import net.mullvad.mullvadvpn.compose.component.ScaffoldWithTopBar -import net.mullvad.mullvadvpn.compose.transitions.DefaultTransition import net.mullvad.mullvadvpn.compose.util.CollectSideEffectWithLifecycle import net.mullvad.mullvadvpn.lib.theme.AppTheme import net.mullvad.mullvadvpn.lib.theme.Dimens @@ -45,7 +44,7 @@ private fun PreviewLoadingScreen() { } // Set this as the start destination of the default nav graph -@Destination(start = true, style = DefaultTransition::class) +@Destination(start = true) @Composable fun Splash(navigator: DestinationsNavigator) { val viewModel: SplashViewModel = koinViewModel() 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 d752460ddbf2..7be0a58059ec 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 @@ -26,6 +26,7 @@ import net.mullvad.mullvadvpn.repository.RelayListFilterRepository import net.mullvad.mullvadvpn.repository.RelayListRepository import net.mullvad.mullvadvpn.repository.RelayOverridesRepository import net.mullvad.mullvadvpn.repository.SettingsRepository +import net.mullvad.mullvadvpn.repository.SplashCompleteRepository import net.mullvad.mullvadvpn.repository.SplitTunnelingRepository import net.mullvad.mullvadvpn.ui.serviceconnection.AppVersionInfoRepository import net.mullvad.mullvadvpn.ui.serviceconnection.ServiceConnectionManager @@ -139,6 +140,7 @@ val uiModule = module { single { CustomListRelayItemsUseCase(get(), get()) } single { FilteredRelayListUseCase(get(), get()) } single { LastKnownLocationUseCase(get()) } + single { SplashCompleteRepository() } single { InAppNotificationController(get(), get(), get(), get(), MainScope()) } @@ -188,7 +190,7 @@ val uiModule = module { viewModel { PrivacyDisclaimerViewModel(get(), IS_PLAY_BUILD) } viewModel { SelectLocationViewModel(get(), get(), get(), get(), get(), get()) } viewModel { SettingsViewModel(get(), get(), IS_PLAY_BUILD) } - viewModel { SplashViewModel(get(), get(), get()) } + viewModel { SplashViewModel(get(), get(), get(), get()) } viewModel { VoucherDialogViewModel(get()) } viewModel { VpnSettingsViewModel(get(), get(), get()) } viewModel { WelcomeViewModel(get(), get(), get(), get(), isPlayBuild = IS_PLAY_BUILD) } diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/repository/SplashCompleteRepository.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/repository/SplashCompleteRepository.kt index 41a0f8f4b5b3..fe77760668db 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/repository/SplashCompleteRepository.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/repository/SplashCompleteRepository.kt @@ -1,4 +1,13 @@ package net.mullvad.mullvadvpn.repository +import kotlinx.coroutines.flow.MutableStateFlow + class SplashCompleteRepository { -} \ No newline at end of file + private val _splashComplete = MutableStateFlow(false) + + fun isSplashComplete() = _splashComplete.value + + fun onSplashCompleted() { + _splashComplete.value = true + } +} 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 56494420503e..f1cf4b09bee7 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 @@ -5,6 +5,7 @@ import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.activity.result.contract.ActivityResultContracts +import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen import androidx.core.view.WindowCompat import androidx.lifecycle.Lifecycle import androidx.lifecycle.lifecycleScope @@ -17,6 +18,7 @@ import net.mullvad.mullvadvpn.lib.common.util.SdkUtils.requestNotificationPermis import net.mullvad.mullvadvpn.lib.intent.IntentProvider import net.mullvad.mullvadvpn.lib.theme.AppTheme import net.mullvad.mullvadvpn.repository.PrivacyDisclaimerRepository +import net.mullvad.mullvadvpn.repository.SplashCompleteRepository import net.mullvad.mullvadvpn.ui.serviceconnection.ServiceConnectionManager import net.mullvad.mullvadvpn.viewmodel.NoDaemonViewModel import org.koin.android.ext.android.getKoin @@ -31,6 +33,8 @@ class MainActivity : ComponentActivity() { private lateinit var privacyDisclaimerRepository: PrivacyDisclaimerRepository private lateinit var serviceConnectionManager: ServiceConnectionManager + private lateinit var splashCompleteRepository: SplashCompleteRepository + private var isReadyNextDraw: Boolean = false private lateinit var noDaemonViewModel: NoDaemonViewModel private lateinit var intentProvider: IntentProvider @@ -45,6 +49,7 @@ class MainActivity : ComponentActivity() { serviceConnectionManager = get() noDaemonViewModel = get() intentProvider = get() + splashCompleteRepository = get() } lifecycle.addObserver(noDaemonViewModel) @@ -55,6 +60,11 @@ class MainActivity : ComponentActivity() { intentProvider.setStartIntent(intent) } + installSplashScreen().setKeepOnScreenCondition { + val isReady = isReadyNextDraw + isReadyNextDraw = splashCompleteRepository.isSplashComplete() + !isReady + } setContent { AppTheme { MullvadApp() } } // This is to protect against tapjacking attacks 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 bd34161e2c92..abd9a100a235 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 @@ -3,6 +3,8 @@ package net.mullvad.mullvadvpn.viewmodel import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import kotlinx.coroutines.async +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.flow @@ -14,14 +16,24 @@ import net.mullvad.mullvadvpn.lib.model.DeviceState import net.mullvad.mullvadvpn.lib.shared.AccountRepository import net.mullvad.mullvadvpn.lib.shared.DeviceRepository import net.mullvad.mullvadvpn.repository.PrivacyDisclaimerRepository +import net.mullvad.mullvadvpn.repository.SplashCompleteRepository + +data class SplashScreenState(val splashComplete: Boolean = false) class SplashViewModel( private val privacyDisclaimerRepository: PrivacyDisclaimerRepository, private val accountRepository: AccountRepository, private val deviceRepository: DeviceRepository, + private val splashCompleteRepository: SplashCompleteRepository ) : ViewModel() { - val uiSideEffect = flow { emit(getStartDestination()) } + val uiSideEffect = flow { + emit(getStartDestination()) + splashCompleteRepository.onSplashCompleted() + } + + private val _mutableUiState = MutableStateFlow(SplashScreenState(false)) + val uiState: StateFlow = _mutableUiState private suspend fun getStartDestination(): SplashUiSideEffect { if (!privacyDisclaimerRepository.hasAcceptedPrivacyDisclosure()) { diff --git a/android/lib/resource/src/main/res/drawable/avd_splash_anim.xml b/android/lib/resource/src/main/res/drawable/avd_splash_anim.xml new file mode 100644 index 000000000000..f6a50d341cdb --- /dev/null +++ b/android/lib/resource/src/main/res/drawable/avd_splash_anim.xml @@ -0,0 +1,175 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/android/lib/resource/src/main/res/values-v33/styles.xml b/android/lib/resource/src/main/res/values-v33/styles.xml new file mode 100644 index 000000000000..ea12723e8c57 --- /dev/null +++ b/android/lib/resource/src/main/res/values-v33/styles.xml @@ -0,0 +1,17 @@ + + + + + \ No newline at end of file