From 1f874696349f346d3fd27768c8d3d9e1c3915c57 Mon Sep 17 00:00:00 2001 From: Jamil Date: Mon, 26 May 2025 21:20:53 -0700 Subject: [PATCH] fix(android): ensure connectOnStart applies only to start (#9279) A bug was introduced in #9227 where the `connectOnStart` was being read for both resumed launches and initial launches. This PR updates that logic so that we only check `connectOnStart` when the app is actually launched. On mobile platforms, this flag is not as clearcut as on desktop platforms, but it's maintained here because ChromeOS is also used on desktop systems. --- .../features/splash/ui/SplashFragment.kt | 7 ++++- .../features/splash/ui/SplashViewModel.kt | 28 +++++++++++++++++-- 2 files changed, 32 insertions(+), 3 deletions(-) diff --git a/kotlin/android/app/src/main/java/dev/firezone/android/features/splash/ui/SplashFragment.kt b/kotlin/android/app/src/main/java/dev/firezone/android/features/splash/ui/SplashFragment.kt index 2fdab7e42..385e8441c 100644 --- a/kotlin/android/app/src/main/java/dev/firezone/android/features/splash/ui/SplashFragment.kt +++ b/kotlin/android/app/src/main/java/dev/firezone/android/features/splash/ui/SplashFragment.kt @@ -25,11 +25,16 @@ internal class SplashFragment : Fragment(R.layout.fragment_splash) { binding = FragmentSplashBinding.bind(view) setupActionObservers() + + if (savedInstanceState == null) { + // Trigger the initial check for tunnel state + viewModel.checkTunnelState(requireContext(), isInitialLaunch = true) + } } override fun onResume() { super.onResume() - viewModel.checkTunnelState(requireContext()) + viewModel.checkTunnelState(requireContext(), isInitialLaunch = false) } private fun setupActionObservers() { diff --git a/kotlin/android/app/src/main/java/dev/firezone/android/features/splash/ui/SplashViewModel.kt b/kotlin/android/app/src/main/java/dev/firezone/android/features/splash/ui/SplashViewModel.kt index 2ce9a255c..76ddf788d 100644 --- a/kotlin/android/app/src/main/java/dev/firezone/android/features/splash/ui/SplashViewModel.kt +++ b/kotlin/android/app/src/main/java/dev/firezone/android/features/splash/ui/SplashViewModel.kt @@ -28,17 +28,36 @@ internal class SplashViewModel private val actionMutableLiveData = MutableLiveData() val actionLiveData: LiveData = actionMutableLiveData - internal fun checkTunnelState(context: Context) { + // This flag is used to ensure that the initial launch check is only performed once, so + // that we can differentiate between a fresh launch and subsequent resumes for the connect + // on start logic. + private var hasPerformedInitialLaunchCheck = false + + internal fun checkTunnelState( + context: Context, + isInitialLaunch: Boolean = false, + ) { viewModelScope.launch { // Stay a while and enjoy the logo delay(REQUEST_DELAY) + + // If this is an 'initial launch' call, but we've already handled the + // initial launch logic for this ViewModel instance, then do nothing. + if (isInitialLaunch && hasPerformedInitialLaunchCheck) { + return@launch + } + if (!hasVpnPermissions(context) && applicationMode != ApplicationMode.TESTING) { actionMutableLiveData.postValue(ViewAction.NavigateToVpnPermission) } else { val token = applicationRestrictions.getString("token") ?: repo.getTokenSync() val connectOnStart = repo.getConfigSync().connectOnStart - if (!token.isNullOrBlank() && connectOnStart) { + // Determine if the tunnel should connect: + // 1. If it's an initial launch AND connectOnStart is true. + // OR + // 2. If it's NOT an initial launch (meaning it's a resume), always try to connect if a token exists. + if (!token.isNullOrBlank() && (isInitialLaunch && connectOnStart || !isInitialLaunch)) { // token will be re-read by the TunnelService if (!TunnelService.isRunning(context)) TunnelService.start(context) @@ -48,6 +67,11 @@ internal class SplashViewModel } } } + + // Set the flag to true after the initial launch check is performed + if (isInitialLaunch) { + hasPerformedInitialLaunchCheck = true + } } private fun hasVpnPermissions(context: Context): Boolean = android.net.VpnService.prepare(context) == null