From 28e1ad15233e70efcce08f4166cd8d046c77f92c Mon Sep 17 00:00:00 2001 From: Pratik Velani Date: Fri, 27 Oct 2023 19:45:57 +0530 Subject: [PATCH] refactor(android): Refactor navigation and session functionality (#2469) This PR refactors navigation to fix nav issues and session implementation to fix: Fixes #2300 Fixes #2186 --------- Signed-off-by: Pratik Velani --- .../android/app/src/main/AndroidManifest.xml | 4 + .../applink/ui/AppLinkHandlerActivity.kt | 16 ++-- .../features/applink/ui/AppLinkViewModel.kt | 1 - .../android/features/auth/ui/AuthActivity.kt | 2 +- .../android/features/auth/ui/AuthViewModel.kt | 4 +- ...{SessionFragment.kt => SessionActivity.kt} | 49 +++++------- .../features/session/ui/SessionViewModel.kt | 50 ++++++++++-- .../features/settings/ui/SettingsFragment.kt | 2 +- .../features/settings/ui/SettingsViewModel.kt | 4 +- .../features/signin/ui/SignInFragment.kt | 4 +- .../features/splash/ui/SplashFragment.kt | 14 ++-- .../features/splash/ui/SplashViewModel.kt | 12 +-- .../firezone/android/tunnel/TunnelManager.kt | 23 ++++-- .../firezone/android/tunnel/TunnelService.kt | 65 +++++++++++----- .../tunnel/data/TunnelRepositoryImpl.kt | 2 +- .../firezone/android/tunnel/model/Tunnel.kt | 3 +- ...gment_session.xml => activity_session.xml} | 2 +- .../src/main/res/layout/fragment_settings.xml | 4 +- .../src/main/res/layout/fragment_sign_in.xml | 6 +- .../src/main/res/navigation/app_nav_graph.xml | 77 ++----------------- .../app/src/main/res/values/strings.xml | 21 +++-- 21 files changed, 187 insertions(+), 178 deletions(-) rename kotlin/android/app/src/main/java/dev/firezone/android/features/session/ui/{SessionFragment.kt => SessionActivity.kt} (63%) rename kotlin/android/app/src/main/res/layout/{fragment_session.xml => activity_session.xml} (97%) diff --git a/kotlin/android/app/src/main/AndroidManifest.xml b/kotlin/android/app/src/main/AndroidManifest.xml index eb8901661..e687b2497 100644 --- a/kotlin/android/app/src/main/AndroidManifest.xml +++ b/kotlin/android/app/src/main/AndroidManifest.xml @@ -72,6 +72,10 @@ android:name="dev.firezone.android.features.permission.vpn.ui.VpnPermissionActivity" android:exported="false" /> + + when (action) { AppLinkViewModel.ViewAction.AuthFlowComplete -> { - // TODO: Continue starting the session showing sessionFragment - Log.d("AppLinkHandlerActivity", "AuthFlowComplete") - - val intent = Intent(this@AppLinkHandlerActivity, MainActivity::class.java) - this@AppLinkHandlerActivity.startActivity(intent) - this@AppLinkHandlerActivity.finish() + startActivity( + Intent(this@AppLinkHandlerActivity, MainActivity::class.java).apply { + flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP + }, + ) + finish() } AppLinkViewModel.ViewAction.ShowError -> showError() - else -> { - Log.d("AppLinkHandlerActivity", "Unhandled action: $action") - } } } } diff --git a/kotlin/android/app/src/main/java/dev/firezone/android/features/applink/ui/AppLinkViewModel.kt b/kotlin/android/app/src/main/java/dev/firezone/android/features/applink/ui/AppLinkViewModel.kt index b857f475c..1ca4629ea 100644 --- a/kotlin/android/app/src/main/java/dev/firezone/android/features/applink/ui/AppLinkViewModel.kt +++ b/kotlin/android/app/src/main/java/dev/firezone/android/features/applink/ui/AppLinkViewModel.kt @@ -41,7 +41,6 @@ internal class AppLinkViewModel } intent.data?.getQueryParameter(QUERY_CLIENT_AUTH_TOKEN)?.let { token -> if (token.isNotBlank()) { - // TODO: Don't log auth token Log.d("AppLinkViewModel", "Found valid auth token in response") saveTokenUseCase(token).collect() } else { diff --git a/kotlin/android/app/src/main/java/dev/firezone/android/features/auth/ui/AuthActivity.kt b/kotlin/android/app/src/main/java/dev/firezone/android/features/auth/ui/AuthActivity.kt index e5f6549ac..dae1fbeef 100644 --- a/kotlin/android/app/src/main/java/dev/firezone/android/features/auth/ui/AuthActivity.kt +++ b/kotlin/android/app/src/main/java/dev/firezone/android/features/auth/ui/AuthActivity.kt @@ -38,7 +38,7 @@ class AuthActivity : AppCompatActivity(R.layout.activity_auth) { viewModel.actionLiveData.observe(this) { action -> when (action) { is AuthViewModel.ViewAction.LaunchAuthFlow -> setupWebView(action.url) - is AuthViewModel.ViewAction.NavigateToSignInFragment -> { + is AuthViewModel.ViewAction.NavigateToSignIn -> { navigateToSignIn() } is AuthViewModel.ViewAction.ShowError -> showError() diff --git a/kotlin/android/app/src/main/java/dev/firezone/android/features/auth/ui/AuthViewModel.kt b/kotlin/android/app/src/main/java/dev/firezone/android/features/auth/ui/AuthViewModel.kt index 736e41f00..0c03a1f69 100644 --- a/kotlin/android/app/src/main/java/dev/firezone/android/features/auth/ui/AuthViewModel.kt +++ b/kotlin/android/app/src/main/java/dev/firezone/android/features/auth/ui/AuthViewModel.kt @@ -39,7 +39,7 @@ internal class AuthViewModel actionMutableLiveData.postValue( if (authFlowLaunched || config.token != null) { - ViewAction.NavigateToSignInFragment + ViewAction.NavigateToSignIn } else { authFlowLaunched = true ViewAction.LaunchAuthFlow( @@ -59,7 +59,7 @@ internal class AuthViewModel internal sealed class ViewAction { data class LaunchAuthFlow(val url: String) : ViewAction() - object NavigateToSignInFragment : ViewAction() + object NavigateToSignIn : ViewAction() object ShowError : ViewAction() } diff --git a/kotlin/android/app/src/main/java/dev/firezone/android/features/session/ui/SessionFragment.kt b/kotlin/android/app/src/main/java/dev/firezone/android/features/session/ui/SessionActivity.kt similarity index 63% rename from kotlin/android/app/src/main/java/dev/firezone/android/features/session/ui/SessionFragment.kt rename to kotlin/android/app/src/main/java/dev/firezone/android/features/session/ui/SessionActivity.kt index 8da8fa00f..88b65a313 100644 --- a/kotlin/android/app/src/main/java/dev/firezone/android/features/session/ui/SessionFragment.kt +++ b/kotlin/android/app/src/main/java/dev/firezone/android/features/session/ui/SessionActivity.kt @@ -3,11 +3,9 @@ package dev.firezone.android.features.session.ui import android.content.Intent import android.os.Bundle -import android.util.Log -import android.view.View +import androidx.activity.viewModels import androidx.appcompat.app.AlertDialog -import androidx.fragment.app.Fragment -import androidx.fragment.app.viewModels +import androidx.appcompat.app.AppCompatActivity import androidx.lifecycle.Lifecycle import androidx.lifecycle.lifecycleScope import androidx.lifecycle.repeatOnLifecycle @@ -17,41 +15,38 @@ import dagger.hilt.android.AndroidEntryPoint import dev.firezone.android.R import dev.firezone.android.core.presentation.MainActivity import dev.firezone.android.core.utils.ClipboardUtils -import dev.firezone.android.databinding.FragmentSessionBinding +import dev.firezone.android.databinding.ActivitySessionBinding import kotlinx.coroutines.launch @AndroidEntryPoint -internal class SessionFragment : Fragment(R.layout.fragment_session) { - private lateinit var binding: FragmentSessionBinding +internal class SessionActivity : AppCompatActivity() { + private lateinit var binding: ActivitySessionBinding private val viewModel: SessionViewModel by viewModels() private val resourcesAdapter: ResourcesAdapter = ResourcesAdapter { resource -> - ClipboardUtils.copyToClipboard(requireContext(), resource.name, resource.address) + ClipboardUtils.copyToClipboard(this@SessionActivity, resource.name, resource.address) } - override fun onViewCreated( - view: View, - savedInstanceState: Bundle?, - ) { - super.onViewCreated(view, savedInstanceState) - binding = FragmentSessionBinding.bind(view) + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + binding = ActivitySessionBinding.inflate(layoutInflater) + setContentView(binding.root) setupViews() setupObservers() - Log.d("SessionFragment", "Starting session...") - viewModel.startSession() + viewModel.connect(this@SessionActivity) } private fun setupViews() { binding.btSignOut.setOnClickListener { - viewModel.onDisconnect() + viewModel.disconnect() } - val layoutManager = LinearLayoutManager(requireContext()) + val layoutManager = LinearLayoutManager(this@SessionActivity) val dividerItemDecoration = DividerItemDecoration( - requireContext(), + this@SessionActivity, layoutManager.orientation, ) binding.resourcesList.addItemDecoration(dividerItemDecoration) @@ -60,15 +55,13 @@ internal class SessionFragment : Fragment(R.layout.fragment_session) { } private fun setupObservers() { - viewModel.actionLiveData.observe(viewLifecycleOwner) { action -> + viewModel.actionLiveData.observe(this@SessionActivity) { action -> when (action) { - SessionViewModel.ViewAction.NavigateToSignInFragment -> { - requireActivity().run { - startActivity( - Intent(this, MainActivity::class.java), - ) - finish() - } + SessionViewModel.ViewAction.NavigateToSignIn -> { + startActivity( + Intent(this, MainActivity::class.java), + ) + finish() } SessionViewModel.ViewAction.ShowError -> showError() } @@ -86,7 +79,7 @@ internal class SessionFragment : Fragment(R.layout.fragment_session) { } private fun showError() { - AlertDialog.Builder(requireContext()) + AlertDialog.Builder(this@SessionActivity) .setTitle(R.string.error_dialog_title) .setMessage(R.string.error_dialog_message) .setPositiveButton( diff --git a/kotlin/android/app/src/main/java/dev/firezone/android/features/session/ui/SessionViewModel.kt b/kotlin/android/app/src/main/java/dev/firezone/android/features/session/ui/SessionViewModel.kt index f89dc8da2..c2813e334 100644 --- a/kotlin/android/app/src/main/java/dev/firezone/android/features/session/ui/SessionViewModel.kt +++ b/kotlin/android/app/src/main/java/dev/firezone/android/features/session/ui/SessionViewModel.kt @@ -1,6 +1,7 @@ /* Licensed under Apache 2.0 (C) 2023 Firezone, Inc. */ package dev.firezone.android.features.session.ui +import android.content.Context import android.util.Log import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData @@ -8,7 +9,9 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel import dev.firezone.android.tunnel.TunnelManager +import dev.firezone.android.tunnel.TunnelService import dev.firezone.android.tunnel.callback.TunnelListener +import dev.firezone.android.tunnel.data.TunnelRepository import dev.firezone.android.tunnel.model.Resource import dev.firezone.android.tunnel.model.Tunnel import kotlinx.coroutines.flow.MutableStateFlow @@ -21,6 +24,7 @@ internal class SessionViewModel @Inject constructor( private val tunnelManager: TunnelManager, + private val tunnelRepository: TunnelRepository, ) : ViewModel() { private val _uiState = MutableStateFlow(UiState()) val uiState: StateFlow = _uiState @@ -31,7 +35,20 @@ internal class SessionViewModel private val tunnelListener = object : TunnelListener { override fun onTunnelStateUpdate(state: Tunnel.State) { - TODO("Not yet implemented") + when (state) { + Tunnel.State.Down -> { + onDisconnect() + } + Tunnel.State.Closed -> { + onClosed() + } + else -> { + _uiState.value = + _uiState.value.copy( + state = state, + ) + } + } } override fun onResourcesUpdate(resources: List) { @@ -48,10 +65,23 @@ internal class SessionViewModel } } - fun startSession() { + fun connect(context: Context) { viewModelScope.launch { tunnelManager.addListener(tunnelListener) - tunnelManager.connect() + + val isServiceRunning = TunnelService.isRunning(context) + if (!isServiceRunning || + tunnelRepository.getState() == Tunnel.State.Down || + tunnelRepository.getState() == Tunnel.State.Closed + ) { + tunnelManager.connect() + } else { + _uiState.value = + _uiState.value.copy( + state = tunnelRepository.getState(), + resources = tunnelRepository.getResources(), + ) + } } } @@ -61,18 +91,26 @@ internal class SessionViewModel tunnelManager.removeListener(tunnelListener) } - fun onDisconnect() { + fun disconnect() { tunnelManager.disconnect() + } + + private fun onDisconnect() { + // no-op + } + + private fun onClosed() { tunnelManager.removeListener(tunnelListener) - actionMutableLiveData.postValue(ViewAction.NavigateToSignInFragment) + actionMutableLiveData.postValue(ViewAction.NavigateToSignIn) } internal data class UiState( + val state: Tunnel.State = Tunnel.State.Down, val resources: List? = null, ) internal sealed class ViewAction { - object NavigateToSignInFragment : ViewAction() + object NavigateToSignIn : ViewAction() object ShowError : ViewAction() } diff --git a/kotlin/android/app/src/main/java/dev/firezone/android/features/settings/ui/SettingsFragment.kt b/kotlin/android/app/src/main/java/dev/firezone/android/features/settings/ui/SettingsFragment.kt index 5ac305242..f5cd4ce33 100644 --- a/kotlin/android/app/src/main/java/dev/firezone/android/features/settings/ui/SettingsFragment.kt +++ b/kotlin/android/app/src/main/java/dev/firezone/android/features/settings/ui/SettingsFragment.kt @@ -43,7 +43,7 @@ internal class SettingsFragment : Fragment(R.layout.fragment_settings) { private fun setupActionObservers() { viewModel.actionLiveData.observe(viewLifecycleOwner) { action -> when (action) { - is SettingsViewModel.ViewAction.NavigateToSignInFragment -> + is SettingsViewModel.ViewAction.NavigateToSignIn -> findNavController().navigate( R.id.signInFragment, ) diff --git a/kotlin/android/app/src/main/java/dev/firezone/android/features/settings/ui/SettingsViewModel.kt b/kotlin/android/app/src/main/java/dev/firezone/android/features/settings/ui/SettingsViewModel.kt index e3a3596eb..ef002ffbf 100644 --- a/kotlin/android/app/src/main/java/dev/firezone/android/features/settings/ui/SettingsViewModel.kt +++ b/kotlin/android/app/src/main/java/dev/firezone/android/features/settings/ui/SettingsViewModel.kt @@ -40,7 +40,7 @@ internal class SettingsViewModel fun onSaveSettingsCompleted() { viewModelScope.launch { saveAccountIdUseCase(input).collect { - actionMutableLiveData.postValue(ViewAction.NavigateToSignInFragment) + actionMutableLiveData.postValue(ViewAction.NavigateToSignIn) } } } @@ -59,7 +59,7 @@ internal class SettingsViewModel } internal sealed class ViewAction { - object NavigateToSignInFragment : ViewAction() + object NavigateToSignIn : ViewAction() data class FillAccountId(val value: String) : ViewAction() } diff --git a/kotlin/android/app/src/main/java/dev/firezone/android/features/signin/ui/SignInFragment.kt b/kotlin/android/app/src/main/java/dev/firezone/android/features/signin/ui/SignInFragment.kt index 8a9f6ef18..7efb066e1 100644 --- a/kotlin/android/app/src/main/java/dev/firezone/android/features/signin/ui/SignInFragment.kt +++ b/kotlin/android/app/src/main/java/dev/firezone/android/features/signin/ui/SignInFragment.kt @@ -11,7 +11,6 @@ import dagger.hilt.android.AndroidEntryPoint import dev.firezone.android.R import dev.firezone.android.databinding.FragmentSignInBinding import dev.firezone.android.features.auth.ui.AuthActivity -import dev.firezone.android.features.splash.ui.SplashFragmentDirections @AndroidEntryPoint internal class SignInFragment : Fragment(R.layout.fragment_sign_in) { @@ -37,10 +36,11 @@ internal class SignInFragment : Fragment(R.layout.fragment_sign_in) { AuthActivity::class.java, ), ) + requireActivity().finish() } btSettings.setOnClickListener { findNavController().navigate( - SplashFragmentDirections.navigateToSettingsFragment(), + R.id.settingsFragment, ) } } 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 8a644b69d..420068eb6 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 @@ -1,6 +1,7 @@ /* Licensed under Apache 2.0 (C) 2023 Firezone, Inc. */ package dev.firezone.android.features.splash.ui +import android.content.Intent import android.os.Bundle import android.view.View import androidx.fragment.app.Fragment @@ -9,6 +10,7 @@ import androidx.navigation.fragment.findNavController import dagger.hilt.android.AndroidEntryPoint import dev.firezone.android.R import dev.firezone.android.databinding.FragmentSplashBinding +import dev.firezone.android.features.session.ui.SessionActivity @AndroidEntryPoint internal class SplashFragment : Fragment(R.layout.fragment_splash) { @@ -37,17 +39,19 @@ internal class SplashFragment : Fragment(R.layout.fragment_splash) { findNavController().navigate( R.id.vpnPermissionActivity, ) - SplashViewModel.ViewAction.NavigateToSignInFragment -> + SplashViewModel.ViewAction.NavigateToSignIn -> findNavController().navigate( R.id.signInFragment, ) - SplashViewModel.ViewAction.NavigateToSettingsFragment -> + SplashViewModel.ViewAction.NavigateToSettings -> findNavController().navigate( R.id.settingsFragment, ) - SplashViewModel.ViewAction.NavigateToSessionFragment -> - findNavController().navigate( - R.id.sessionFragment, + SplashViewModel.ViewAction.NavigateToSession -> + startActivity( + Intent(requireContext(), SessionActivity::class.java).apply { + flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP + }, ) } } 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 b1f5cbad9..0990c1bac 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 @@ -37,11 +37,11 @@ internal class SplashViewModel } .collect { user -> if (user.accountId.isNullOrEmpty()) { - actionMutableLiveData.postValue(ViewAction.NavigateToSettingsFragment) + actionMutableLiveData.postValue(ViewAction.NavigateToSettings) } else if (user.token.isNullOrBlank()) { - actionMutableLiveData.postValue(ViewAction.NavigateToSignInFragment) + actionMutableLiveData.postValue(ViewAction.NavigateToSignIn) } else { - actionMutableLiveData.postValue(ViewAction.NavigateToSessionFragment) + actionMutableLiveData.postValue(ViewAction.NavigateToSession) } } } @@ -54,8 +54,8 @@ internal class SplashViewModel internal sealed class ViewAction { object NavigateToVpnPermission : ViewAction() - object NavigateToSettingsFragment : ViewAction() - object NavigateToSignInFragment : ViewAction() - object NavigateToSessionFragment : ViewAction() + object NavigateToSettings : ViewAction() + object NavigateToSignIn : ViewAction() + object NavigateToSession : ViewAction() } } diff --git a/kotlin/android/app/src/main/java/dev/firezone/android/tunnel/TunnelManager.kt b/kotlin/android/app/src/main/java/dev/firezone/android/tunnel/TunnelManager.kt index 8b7fd3c90..a70a4f80d 100644 --- a/kotlin/android/app/src/main/java/dev/firezone/android/tunnel/TunnelManager.kt +++ b/kotlin/android/app/src/main/java/dev/firezone/android/tunnel/TunnelManager.kt @@ -8,7 +8,6 @@ import android.util.Log import dev.firezone.android.core.data.PreferenceRepository import dev.firezone.android.tunnel.callback.TunnelListener import dev.firezone.android.tunnel.data.TunnelRepository -import dev.firezone.android.tunnel.model.Tunnel import java.lang.ref.WeakReference import javax.inject.Inject import javax.inject.Singleton @@ -24,10 +23,17 @@ internal class TunnelManager private val listeners: MutableSet> = mutableSetOf() private val tunnelRepositoryListener = - SharedPreferences.OnSharedPreferenceChangeListener { _, s -> - if (s == TunnelRepository.RESOURCES_KEY) { - listeners.forEach { - it.get()?.onResourcesUpdate(tunnelRepository.getResources()) + SharedPreferences.OnSharedPreferenceChangeListener { _, key -> + when (key) { + TunnelRepository.STATE_KEY -> { + listeners.forEach { + it.get()?.onTunnelStateUpdate(tunnelRepository.getState()) + } + } + TunnelRepository.RESOURCES_KEY -> { + listeners.forEach { + it.get()?.onResourcesUpdate(tunnelRepository.getResources()) + } } } } @@ -43,7 +49,6 @@ internal class TunnelManager } tunnelRepository.addListener(tunnelRepositoryListener) - tunnelRepository.setState(Tunnel.State.Connecting) } fun removeListener(listener: TunnelListener) { @@ -65,6 +70,7 @@ internal class TunnelManager fun disconnect() { stopVPNService() + clearSessionData() } private fun startVPNService() { @@ -77,8 +83,11 @@ internal class TunnelManager val intent = Intent(appContext, TunnelService::class.java) intent.action = TunnelService.ACTION_DISCONNECT appContext.startService(intent) - tunnelRepository.clearAll() + } + + private fun clearSessionData() { preferenceRepository.clearToken() + tunnelRepository.clearAll() } internal companion object { diff --git a/kotlin/android/app/src/main/java/dev/firezone/android/tunnel/TunnelService.kt b/kotlin/android/app/src/main/java/dev/firezone/android/tunnel/TunnelService.kt index 8c95c549a..3cd6df592 100644 --- a/kotlin/android/app/src/main/java/dev/firezone/android/tunnel/TunnelService.kt +++ b/kotlin/android/app/src/main/java/dev/firezone/android/tunnel/TunnelService.kt @@ -1,10 +1,12 @@ /* Licensed under Apache 2.0 (C) 2023 Firezone, Inc. */ package dev.firezone.android.tunnel +import android.app.ActivityManager import android.app.Notification import android.app.NotificationChannel import android.app.NotificationManager import android.app.PendingIntent +import android.content.Context import android.content.Intent import android.net.VpnService import android.system.OsConstants @@ -16,6 +18,7 @@ import com.squareup.moshi.adapter import dagger.hilt.android.AndroidEntryPoint import dev.firezone.android.BuildConfig import dev.firezone.android.R +import dev.firezone.android.core.data.PreferenceRepository import dev.firezone.android.core.domain.preference.GetConfigUseCase import dev.firezone.android.core.presentation.MainActivity import dev.firezone.android.tunnel.callback.ConnlibCallback @@ -38,11 +41,16 @@ class TunnelService : VpnService() { @Inject internal lateinit var tunnelRepository: TunnelRepository + @Inject + internal lateinit var preferenceRepository: PreferenceRepository + @Inject internal lateinit var moshi: Moshi private var sessionPtr: Long? = null + private var shouldReconnect: Boolean = false + private val activeTunnel: Tunnel? get() = tunnelRepository.get() @@ -129,9 +137,9 @@ class TunnelService : VpnService() { } override fun onDisconnect(error: String?): Boolean { - Log.d(TAG, "onDisconnect $error") - - onTunnelStateUpdate(Tunnel.State.Down) + onSessionDisconnected( + error = error?.takeUnless { it == "null" }, + ) return true } } @@ -169,11 +177,14 @@ class TunnelService : VpnService() { try { val config = getConfigUseCase.sync() - Log.d("Connlib", "accountId: ${config.accountId}") - Log.d("Connlib", "token: ${config.token}") + Log.d("Connlib", "connect(): accountId: ${config.accountId}") + if (tunnelRepository.getState() == Tunnel.State.Up) { + shouldReconnect = true + disconnect() + } else if (config.accountId != null && config.token != null) { + onTunnelStateUpdate(Tunnel.State.Connecting) + updateStatusNotification("Status: Connecting...") - if (config.accountId != null && config.token != null) { - Log.d("Connlib", "Attempting to establish TunnelSession...") sessionPtr = TunnelSession.connect( controlPlaneUrl = BuildConfig.CONTROL_PLANE_URL, @@ -183,33 +194,40 @@ class TunnelService : VpnService() { logFilter = BuildConfig.CONNLIB_LOG_FILTER_STRING, callback = callback, ) - Log.d(TAG, "connlib session started! sessionPtr: $sessionPtr") - - onTunnelStateUpdate(Tunnel.State.Connecting) - - updateStatusNotification("Status: Connecting...") } } catch (exception: Exception) { - Log.e(TAG, exception.message.toString()) + Log.e(TAG, "connect(): " + exception.message.toString()) } } private fun disconnect() { - Log.d(TAG, "Attempting to disconnect session") + Log.d(TAG, "disconnect(): Attempting to disconnect session") try { sessionPtr?.let { - Log.d(TAG, "calling TunnelSession.disconnect") TunnelSession.disconnect(it) - } + } ?: onSessionDisconnected(null) } catch (exception: Exception) { Log.e(TAG, exception.message.toString()) } - stopForeground(STOP_FOREGROUND_REMOVE) + } + + private fun onSessionDisconnected(error: String?) { + sessionPtr = null + onTunnelStateUpdate(Tunnel.State.Down) + + if (shouldReconnect && error == null) { + shouldReconnect = false + connect() + } else { + tunnelRepository.clearAll() + preferenceRepository.clearToken() + onTunnelStateUpdate(Tunnel.State.Closed) + stopForeground(STOP_FOREGROUND_REMOVE) + } } private fun deviceId(): String { val deviceId = FirebaseInstallations.getInstance().id - Log.d(TAG, "Device ID: $deviceId") return deviceId.toString() @@ -294,5 +312,16 @@ class TunnelService : VpnService() { private const val TAG: String = "TunnelService" private const val SESSION_NAME: String = "Firezone Connection" private const val DEFAULT_MTU: Int = 1280 + + @SuppressWarnings("deprecation") + fun isRunning(context: Context): Boolean { + val manager = context.getSystemService(ACTIVITY_SERVICE) as ActivityManager + for (service in manager.getRunningServices(Int.MAX_VALUE)) { + if (TunnelService::class.java.name == service.service.className) { + return true + } + } + return false + } } } diff --git a/kotlin/android/app/src/main/java/dev/firezone/android/tunnel/data/TunnelRepositoryImpl.kt b/kotlin/android/app/src/main/java/dev/firezone/android/tunnel/data/TunnelRepositoryImpl.kt index f94b8384c..ef2540e34 100644 --- a/kotlin/android/app/src/main/java/dev/firezone/android/tunnel/data/TunnelRepositoryImpl.kt +++ b/kotlin/android/app/src/main/java/dev/firezone/android/tunnel/data/TunnelRepositoryImpl.kt @@ -67,7 +67,7 @@ class TunnelRepositoryImpl override fun getState(): Tunnel.State { val json = sharedPreferences.getString(STATE_KEY, null) - return json?.let { Tunnel.State.valueOf(it) } ?: Tunnel.State.Down + return json?.let { Tunnel.State.valueOf(it) } ?: Tunnel.State.Closed } override fun setResources(resources: List) { diff --git a/kotlin/android/app/src/main/java/dev/firezone/android/tunnel/model/Tunnel.kt b/kotlin/android/app/src/main/java/dev/firezone/android/tunnel/model/Tunnel.kt index 216cc8951..a87272805 100644 --- a/kotlin/android/app/src/main/java/dev/firezone/android/tunnel/model/Tunnel.kt +++ b/kotlin/android/app/src/main/java/dev/firezone/android/tunnel/model/Tunnel.kt @@ -14,8 +14,9 @@ data class Tunnel( val resources: List = emptyList(), ) : Parcelable { enum class State { - Up, Connecting, + Up, Down, + Closed, } } diff --git a/kotlin/android/app/src/main/res/layout/fragment_session.xml b/kotlin/android/app/src/main/res/layout/activity_session.xml similarity index 97% rename from kotlin/android/app/src/main/res/layout/fragment_session.xml rename to kotlin/android/app/src/main/res/layout/activity_session.xml index ec1b4cf06..38b5b3770 100644 --- a/kotlin/android/app/src/main/res/layout/fragment_session.xml +++ b/kotlin/android/app/src/main/res/layout/activity_session.xml @@ -53,7 +53,7 @@ android:id="@+id/btSignOut" android:layout_width="0dp" android:layout_height="wrap_content" - android:text="@string/sign_out_activity_button_text" + android:text="@string/sign_out" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" /> diff --git a/kotlin/android/app/src/main/res/layout/fragment_settings.xml b/kotlin/android/app/src/main/res/layout/fragment_settings.xml index cd47fc28f..4e13b09ed 100644 --- a/kotlin/android/app/src/main/res/layout/fragment_settings.xml +++ b/kotlin/android/app/src/main/res/layout/fragment_settings.xml @@ -47,7 +47,7 @@ android:id="@+id/etInput" android:layout_width="match_parent" android:layout_height="wrap_content" - android:hint="@string/settings_fragment_input_hint" + android:hint="@string/account_id" android:importantForAutofill="no" android:inputType="text" /> @@ -58,7 +58,7 @@ android:layout_width="0dp" android:layout_height="wrap_content" android:enabled="false" - android:text="@string/settings_fragment_button_text" + android:text="@string/save" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" /> diff --git a/kotlin/android/app/src/main/res/layout/fragment_sign_in.xml b/kotlin/android/app/src/main/res/layout/fragment_sign_in.xml index ebf096b12..dae93dd55 100644 --- a/kotlin/android/app/src/main/res/layout/fragment_sign_in.xml +++ b/kotlin/android/app/src/main/res/layout/fragment_sign_in.xml @@ -39,7 +39,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginStart="@dimen/spacing_small" - android:text="@string/sign_in_fragment_sign_status_text" + android:text="@string/signed_out" app:layout_constraintBottom_toTopOf="@+id/btSignIn" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" @@ -49,7 +49,7 @@ android:id="@+id/btSignIn" android:layout_width="0dp" android:layout_height="wrap_content" - android:text="@string/sign_in_fragment_button_text" + android:text="@string/sign_in" app:layout_constraintBottom_toTopOf="@+id/btSettings" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" /> @@ -58,7 +58,7 @@ android:id="@+id/btSettings" android:layout_width="0dp" android:layout_height="wrap_content" - android:text="@string/sign_in_settings_button_text" + android:text="@string/settings" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" /> diff --git a/kotlin/android/app/src/main/res/navigation/app_nav_graph.xml b/kotlin/android/app/src/main/res/navigation/app_nav_graph.xml index 45284e88a..2846af408 100644 --- a/kotlin/android/app/src/main/res/navigation/app_nav_graph.xml +++ b/kotlin/android/app/src/main/res/navigation/app_nav_graph.xml @@ -8,85 +8,22 @@ - - - - - - - - + tools:layout="@layout/fragment_splash" /> - - - + tools:layout="@layout/fragment_settings" /> + tools:layout="@layout/fragment_sign_in" /> - - - - - - - - - - - + firezone - - Login URL - account-id - Save + + account-id + Save - - Sign In - Settings - Signed Out + + Sign In + Settings + Signed Out - + Resources - Sign Out + Sign Out - + Launching Auth Flow…