diff options
| author | Albin <albin@mullvad.net> | 2022-05-31 14:37:45 +0200 |
|---|---|---|
| committer | Albin <albin@mullvad.net> | 2022-05-31 14:37:45 +0200 |
| commit | 3bc1c244c32b324202f972fa7ed840b57129304e (patch) | |
| tree | 528478e55283aec682f046b1783d683b46198d9a | |
| parent | 31ec1f1449a7633f9fa977a0bae1611a4658fb59 (diff) | |
| parent | e18b1b4b645d5e2de947f65261ff6fbbec5645b1 (diff) | |
| download | mullvadvpn-3bc1c244c32b324202f972fa7ed840b57129304e.tar.xz mullvadvpn-3bc1c244c32b324202f972fa7ed840b57129304e.zip | |
Merge branch 'improve-android-service-connection-management'
18 files changed, 265 insertions, 187 deletions
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 17cfd3c922..7e503e4a33 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 @@ -6,7 +6,10 @@ import kotlinx.coroutines.Dispatchers import net.mullvad.mullvadvpn.applist.ApplicationsIconManager import net.mullvad.mullvadvpn.applist.ApplicationsProvider import net.mullvad.mullvadvpn.ipc.EventDispatcher +import net.mullvad.mullvadvpn.ui.serviceconnection.DeviceRepository +import net.mullvad.mullvadvpn.ui.serviceconnection.ServiceConnectionManager import net.mullvad.mullvadvpn.ui.serviceconnection.SplitTunneling +import net.mullvad.mullvadvpn.viewmodel.LoginViewModel import net.mullvad.mullvadvpn.viewmodel.SplitTunnelingViewModel import org.koin.android.ext.koin.androidContext import org.koin.androidx.viewmodel.dsl.viewModel @@ -30,6 +33,10 @@ val uiModule = module { SplitTunneling(messenger, dispatcher) } } + + single { ServiceConnectionManager(androidContext()) } + single { DeviceRepository(get()) } + viewModel { LoginViewModel() } } const val APPS_SCOPE = "APPS_SCOPE" const val SERVICE_CONNECTION_SCOPE = "SERVICE_CONNECTION_SCOPE" diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/AccountFragment.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/AccountFragment.kt index 526b5356f4..4b5fda7bbe 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/AccountFragment.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/AccountFragment.kt @@ -5,12 +5,16 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.fragment.app.FragmentManager +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.flowWithLifecycle +import androidx.lifecycle.lifecycleScope import java.text.DateFormat import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.launch import net.mullvad.mullvadvpn.R import net.mullvad.mullvadvpn.model.TunnelState +import net.mullvad.mullvadvpn.ui.serviceconnection.DeviceRepository import net.mullvad.mullvadvpn.ui.widget.Button import net.mullvad.mullvadvpn.ui.widget.CopyableInformationView import net.mullvad.mullvadvpn.ui.widget.InformationView @@ -19,8 +23,11 @@ import net.mullvad.mullvadvpn.ui.widget.SitePaymentButton import net.mullvad.mullvadvpn.util.capitalizeFirstCharOfEachWord import net.mullvad.talpid.tunnel.ErrorStateCause import org.joda.time.DateTime +import org.koin.android.ext.android.inject class AccountFragment : ServiceDependentFragment(OnNoService.GoBack) { + private val deviceRepository: DeviceRepository by inject() + override val isSecureScreen = true private val dateStyle = DateFormat.MEDIUM @@ -97,27 +104,20 @@ class AccountFragment : ServiceDependentFragment(OnNoService.GoBack) { return view } - override fun onSafelyStart() { - jobTracker.newUiJob("updateAccountNumber") { - deviceRepository.deviceState - .onEach { state -> - if (state.isInitialState()) deviceRepository.refreshDeviceState() - } - .collect { state -> - accountNumberView.information = state.token() - } - } + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) - jobTracker.newUiJob("updateDeviceName") { + lifecycleScope.launch { deviceRepository.deviceState - .onEach { state -> - if (state.isInitialState()) deviceRepository.refreshDeviceState() - } + .flowWithLifecycle(lifecycle, Lifecycle.State.RESUMED) .collect { state -> + accountNumberView.information = state.token() deviceNameView.information = state.deviceName()?.capitalizeFirstCharOfEachWord() } } + } + override fun onSafelyStart() { jobTracker.newUiJob("updateAccountExpiry") { accountCache.accountExpiryState .map { state -> state.date() } diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/AdvancedFragment.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/AdvancedFragment.kt index bfb24c4f33..275981bd83 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/AdvancedFragment.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/AdvancedFragment.kt @@ -11,7 +11,7 @@ import net.mullvad.mullvadvpn.R import net.mullvad.mullvadvpn.model.Settings import net.mullvad.mullvadvpn.ui.customdns.CustomDnsAdapter import net.mullvad.mullvadvpn.ui.fragments.SplitTunnelingFragment -import net.mullvad.mullvadvpn.ui.serviceconnection.ServiceConnection +import net.mullvad.mullvadvpn.ui.serviceconnection.ServiceConnectionContainer import net.mullvad.mullvadvpn.ui.widget.CellSwitch import net.mullvad.mullvadvpn.ui.widget.CustomRecyclerView import net.mullvad.mullvadvpn.ui.widget.MtuCell @@ -84,8 +84,8 @@ class AdvancedFragment : ServiceDependentFragment(OnNoService.GoBack) { return view } - override fun onNewServiceConnection(serviceConnection: ServiceConnection) { - super.onNewServiceConnection(serviceConnection) + override fun onNewServiceConnection(serviceConnectionContainer: ServiceConnectionContainer) { + super.onNewServiceConnection(serviceConnectionContainer) subscribeToCustomDnsChanges() } diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/ConnectFragment.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/ConnectFragment.kt index efc32a303f..43d23445ce 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/ConnectFragment.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/ConnectFragment.kt @@ -5,7 +5,6 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.core.content.ContextCompat -import androidx.lifecycle.lifecycleScope import kotlinx.coroutines.delay import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.map @@ -96,7 +95,7 @@ class ConnectFragment : } connectionProxy.onUiStateChange.subscribe(this) { uiState -> - viewLifecycleOwner.lifecycleScope.launchWhenStarted { + jobTracker.newUiJob("updateTunnelState") { updateTunnelState(uiState, connectionProxy.state) } } @@ -114,7 +113,7 @@ class ConnectFragment : } override fun onSafelyStop() { - jobTracker.cancelJob("updateAccountExpiry") + jobTracker.cancelAllJobs() locationInfoCache.onNewLocation = null relayListListener.onRelayListChange = null diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/LaunchFragment.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/LaunchFragment.kt index 9499d1c9f1..420581785b 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/LaunchFragment.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/LaunchFragment.kt @@ -4,14 +4,19 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.flow.onEach +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.flowWithLifecycle +import androidx.lifecycle.lifecycleScope +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.launch import net.mullvad.mullvadvpn.R import net.mullvad.mullvadvpn.model.DeviceState import net.mullvad.mullvadvpn.ui.serviceconnection.DeviceRepository -import net.mullvad.mullvadvpn.ui.serviceconnection.ServiceConnection +import net.mullvad.mullvadvpn.ui.serviceconnection.ServiceConnectionContainer +import org.koin.android.ext.android.inject class LaunchFragment : ServiceAwareFragment() { + private val deviceRepository: DeviceRepository by inject() override fun onCreateView( inflater: LayoutInflater, @@ -27,23 +32,16 @@ class LaunchFragment : ServiceAwareFragment() { return view } - override fun onStop() { - jobTracker.cancelJob("advanceToNextScreen") - super.onStop() + override fun onNewServiceConnection(serviceConnectionContainer: ServiceConnectionContainer) { } - override fun onNewServiceConnection(serviceConnection: ServiceConnection) { - advanceToNextScreen(serviceConnection.deviceRepository) - } + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) - private fun advanceToNextScreen(deviceRepository: DeviceRepository) { - jobTracker.newUiJob("advanceToNextScreen") { + lifecycleScope.launch { deviceRepository.deviceState - .onEach { state -> - if (state.isInitialState()) deviceRepository.refreshDeviceState() - } - .first { state -> state.isInitialState().not() } - .let { deviceState -> + .flowWithLifecycle(lifecycle, Lifecycle.State.RESUMED) + .collect { deviceState -> when (deviceState) { is DeviceState.LoggedIn -> advanceToConnectScreen() is DeviceState.LoggedOut -> advanceToLoginScreen() diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/LoginFragment.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/LoginFragment.kt index 056bff9227..d16cf2f0a9 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/LoginFragment.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/LoginFragment.kt @@ -10,23 +10,23 @@ import android.widget.TextView import androidx.core.content.ContextCompat import androidx.fragment.app.Fragment import androidx.lifecycle.Lifecycle -import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.lifecycleScope import androidx.lifecycle.repeatOnLifecycle import kotlinx.coroutines.flow.collect import kotlinx.coroutines.launch import net.mullvad.mullvadvpn.R import net.mullvad.mullvadvpn.model.AccountHistory -import net.mullvad.mullvadvpn.ui.serviceconnection.ServiceConnection +import net.mullvad.mullvadvpn.ui.serviceconnection.ServiceConnectionContainer import net.mullvad.mullvadvpn.ui.widget.AccountLogin import net.mullvad.mullvadvpn.ui.widget.HeaderBar import net.mullvad.mullvadvpn.viewmodel.LoginViewModel +import org.koin.androidx.viewmodel.ext.android.viewModel class LoginFragment : ServiceDependentFragment(OnNoService.GoToLaunchScreen), NavigationBarPainter { - private lateinit var loginViewModel: LoginViewModel + private val loginViewModel: LoginViewModel by viewModel() private lateinit var title: TextView private lateinit var subtitle: TextView @@ -52,10 +52,7 @@ class LoginFragment : loggedInStatus = view.findViewById(R.id.logged_in_status) loginFailStatus = view.findViewById(R.id.login_fail_status) - val factory = LoginViewModel.Factory(requireActivity().application) - loginViewModel = ViewModelProvider(this, factory)[LoginViewModel::class.java].apply { - updateAccountCacheInstance(accountCache) - } + loginViewModel.updateAccountCacheInstance(accountCache) accountLogin = view.findViewById<AccountLogin>(R.id.account_login).apply { onLogin = loginViewModel::login @@ -73,26 +70,19 @@ class LoginFragment : scrollToShow(accountLogin) - return view - } - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) setupLifecycleSubscriptionsToViewModel() + + return view } - override fun onNewServiceConnection(serviceConnection: ServiceConnection) { - super.onNewServiceConnection(serviceConnection) - if (this::loginViewModel.isInitialized) { - loginViewModel.updateAccountCacheInstance(accountCache) - } + override fun onNewServiceConnection(serviceConnectionContainer: ServiceConnectionContainer) { + super.onNewServiceConnection(serviceConnectionContainer) + loginViewModel.updateAccountCacheInstance(accountCache) } override fun onNoServiceConnection() { super.onNoServiceConnection() - if (this::loginViewModel.isInitialized) { - loginViewModel.updateAccountCacheInstance(null) - } + loginViewModel.updateAccountCacheInstance(null) } override fun onSafelyStart() { 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 16673ea65d..cb39336651 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 @@ -2,34 +2,26 @@ package net.mullvad.mullvadvpn.ui import android.app.Activity import android.app.UiModeManager -import android.content.ComponentName import android.content.Intent import android.content.pm.ActivityInfo import android.content.res.Configuration import android.net.VpnService import android.os.Bundle -import android.os.IBinder -import android.os.Messenger import android.util.Log import android.view.WindowManager import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentActivity import androidx.fragment.app.FragmentManager -import kotlin.properties.Delegates.observable import net.mullvad.mullvadvpn.BuildConfig import net.mullvad.mullvadvpn.R import net.mullvad.mullvadvpn.dataproxy.MullvadProblemReport import net.mullvad.mullvadvpn.di.uiModule -import net.mullvad.mullvadvpn.service.MullvadVpnService -import net.mullvad.mullvadvpn.ui.serviceconnection.ServiceConnection -import net.mullvad.talpid.util.EventNotifier +import net.mullvad.mullvadvpn.ui.serviceconnection.ServiceConnectionManager +import org.koin.android.ext.android.getKoin import org.koin.core.context.loadKoinModules -import org.koin.core.context.unloadKoinModules open class MainActivity : FragmentActivity() { - val problemReport = MullvadProblemReport() - val serviceNotifier = EventNotifier<ServiceConnection?>(null) private var visibleSecureScreens = HashSet<Fragment>() @@ -39,37 +31,13 @@ open class MainActivity : FragmentActivity() { uiModeManager.currentModeType == Configuration.UI_MODE_TYPE_TELEVISION } - private var serviceConnection by observable<ServiceConnection?>( - null - ) { _, oldConnection, newConnection -> - oldConnection?.onDestroy() - - if (newConnection == null) { - serviceNotifier.notify(null) - } else { - newConnection.vpnPermission.onRequest = { -> - Unit - this.requestVpnPermission() - } - } - } - - private val serviceConnectionManager = object : android.content.ServiceConnection { - override fun onServiceConnected(className: ComponentName, binder: IBinder) { - android.util.Log.d("mullvad", "UI successfully connected to the service") - serviceConnection = ServiceConnection(Messenger(binder), ::handleNewServiceConnection) - } - - override fun onServiceDisconnected(className: ComponentName) { - android.util.Log.d("mullvad", "UI lost the connection to the service") - serviceConnection = null - } - } - var backButtonHandler: (() -> Boolean)? = null + private lateinit var serviceConnectionManager: ServiceConnectionManager + override fun onCreate(savedInstanceState: Bundle?) { loadKoinModules(uiModule) + serviceConnectionManager = getKoin().get() requestedOrientation = if (deviceIsTv) { ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE @@ -94,15 +62,11 @@ open class MainActivity : FragmentActivity() { override fun onStart() { Log.d("mullvad", "Starting main activity") super.onStart() - - val intent = Intent(this, MullvadVpnService::class.java) - - startService(intent) - bindService(intent, serviceConnectionManager, 0) + serviceConnectionManager.bind(vpnPermissionRequestHandler = ::requestVpnPermission) } override fun onActivityResult(requestCode: Int, resultCode: Int, resultData: Intent?) { - serviceConnection?.vpnPermission?.grant(resultCode == Activity.RESULT_OK) + serviceConnectionManager.onVpnPermissionResult(resultCode == Activity.RESULT_OK) } override fun onBackPressed() { @@ -114,19 +78,16 @@ open class MainActivity : FragmentActivity() { } override fun onStop() { - Log.d("mullvad", "Stoping main activity") - unbindService(serviceConnectionManager) - unloadKoinModules(uiModule) - + Log.d("mullvad", "Stopping main activity") super.onStop() - serviceConnection = null + // NOTE: `super.onStop()` must be called before unbinding due to the fragment state handling + // otherwise the fragments will believe there was an unexpected disconnect. + serviceConnectionManager.unbind() } override fun onDestroy() { - serviceNotifier.unsubscribeAll() - serviceConnection = null - + serviceConnectionManager.onDestroy() super.onDestroy() } @@ -175,10 +136,6 @@ open class MainActivity : FragmentActivity() { } } - private fun handleNewServiceConnection(connection: ServiceConnection) { - serviceNotifier.notify(connection) - } - @Suppress("DEPRECATION") private fun requestVpnPermission() { val intent = VpnService.prepare(this) diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/RedeemVoucherDialogFragment.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/RedeemVoucherDialogFragment.kt index eb64642a45..dce06a445c 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/RedeemVoucherDialogFragment.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/RedeemVoucherDialogFragment.kt @@ -18,15 +18,19 @@ import net.mullvad.mullvadvpn.R import net.mullvad.mullvadvpn.model.VoucherSubmissionError import net.mullvad.mullvadvpn.model.VoucherSubmissionResult import net.mullvad.mullvadvpn.ui.serviceconnection.AccountCache +import net.mullvad.mullvadvpn.ui.serviceconnection.ServiceConnectionManager import net.mullvad.mullvadvpn.ui.serviceconnection.VoucherRedeemer import net.mullvad.mullvadvpn.ui.widget.Button import net.mullvad.mullvadvpn.util.JobTracker import net.mullvad.mullvadvpn.util.SegmentedInputFormatter import org.joda.time.DateTime +import org.koin.android.ext.android.inject const val FULL_VOUCHER_CODE_LENGTH = "XXXX-XXXX-XXXX-XXXX".length class RedeemVoucherDialogFragment : DialogFragment() { + private val serviceConnectionManager: ServiceConnectionManager by inject() + private val jobTracker = JobTracker() private lateinit var parentActivity: MainActivity @@ -49,7 +53,7 @@ class RedeemVoucherDialogFragment : DialogFragment() { parentActivity = context as MainActivity - parentActivity.serviceNotifier.subscribe(this) { connection -> + serviceConnectionManager.serviceNotifier.subscribe(this) { connection -> accountCache = connection?.accountCache voucherRedeemer = connection?.voucherRedeemer } @@ -123,7 +127,7 @@ class RedeemVoucherDialogFragment : DialogFragment() { override fun onDetach() { jobTracker.cancelJob("updateExpiry") - parentActivity.serviceNotifier.unsubscribe(this) + serviceConnectionManager.serviceNotifier.unsubscribe(this) super.onDetach() } diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/ServiceAwareFragment.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/ServiceAwareFragment.kt index 5788c60ad8..32ad70daaa 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/ServiceAwareFragment.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/ServiceAwareFragment.kt @@ -2,10 +2,14 @@ package net.mullvad.mullvadvpn.ui import android.content.Context import net.mullvad.mullvadvpn.ui.fragments.BaseFragment -import net.mullvad.mullvadvpn.ui.serviceconnection.ServiceConnection +import net.mullvad.mullvadvpn.ui.serviceconnection.ServiceConnectionContainer +import net.mullvad.mullvadvpn.ui.serviceconnection.ServiceConnectionManager import net.mullvad.mullvadvpn.util.JobTracker +import org.koin.android.ext.android.inject abstract class ServiceAwareFragment : BaseFragment() { + private val serviceConnectionManager: ServiceConnectionManager by inject() + val jobTracker = JobTracker() open val isSecureScreen = false @@ -13,7 +17,7 @@ abstract class ServiceAwareFragment : BaseFragment() { lateinit var parentActivity: MainActivity private set - var serviceConnection: ServiceConnection? = null + var serviceConnectionContainer: ServiceConnectionContainer? = null private set override fun onAttach(context: Context) { @@ -25,7 +29,7 @@ abstract class ServiceAwareFragment : BaseFragment() { parentActivity.enterSecureScreen(this) } - parentActivity.serviceNotifier.subscribe(this) { connection -> + serviceConnectionManager.serviceNotifier.subscribe(this) { connection -> configureServiceConnection(connection) } } @@ -37,7 +41,7 @@ abstract class ServiceAwareFragment : BaseFragment() { } override fun onDetach() { - parentActivity.serviceNotifier.unsubscribe(this) + serviceConnectionManager.serviceNotifier.unsubscribe(this) if (isSecureScreen) { parentActivity.leaveSecureScreen(this) @@ -46,16 +50,18 @@ abstract class ServiceAwareFragment : BaseFragment() { super.onDetach() } - abstract fun onNewServiceConnection(serviceConnection: ServiceConnection) + abstract fun onNewServiceConnection(serviceConnectionContainer: ServiceConnectionContainer) open fun onNoServiceConnection() { } - private fun configureServiceConnection(connection: ServiceConnection?) { - serviceConnection = connection + private fun configureServiceConnection( + serviceConnectionContainer: ServiceConnectionContainer? + ) { + this.serviceConnectionContainer = serviceConnectionContainer - if (connection != null) { - onNewServiceConnection(connection) + if (serviceConnectionContainer != null) { + onNewServiceConnection(serviceConnectionContainer) } else { onNoServiceConnection() } diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/ServiceDependentFragment.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/ServiceDependentFragment.kt index f40f497857..2911dcfb9f 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/ServiceDependentFragment.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/ServiceDependentFragment.kt @@ -10,10 +10,10 @@ import net.mullvad.mullvadvpn.ui.serviceconnection.AppVersionInfoCache import net.mullvad.mullvadvpn.ui.serviceconnection.AuthTokenCache import net.mullvad.mullvadvpn.ui.serviceconnection.ConnectionProxy import net.mullvad.mullvadvpn.ui.serviceconnection.CustomDns -import net.mullvad.mullvadvpn.ui.serviceconnection.DeviceRepository import net.mullvad.mullvadvpn.ui.serviceconnection.LocationInfoCache import net.mullvad.mullvadvpn.ui.serviceconnection.RelayListListener -import net.mullvad.mullvadvpn.ui.serviceconnection.ServiceConnection +import net.mullvad.mullvadvpn.ui.serviceconnection.ServiceConnectionContainer +import net.mullvad.mullvadvpn.ui.serviceconnection.ServiceConnectionDeviceDataSource import net.mullvad.mullvadvpn.ui.serviceconnection.SettingsListener import net.mullvad.mullvadvpn.ui.serviceconnection.SplitTunneling @@ -48,7 +48,7 @@ abstract class ServiceDependentFragment(private val onNoService: OnNoService) : lateinit var customDns: CustomDns private set - lateinit var deviceRepository: DeviceRepository + lateinit var deviceDataSource: ServiceConnectionDeviceDataSource private set lateinit var locationInfoCache: LocationInfoCache @@ -63,19 +63,20 @@ abstract class ServiceDependentFragment(private val onNoService: OnNoService) : lateinit var splitTunneling: SplitTunneling private set - override fun onNewServiceConnection(serviceConnection: ServiceConnection) { + override fun onNewServiceConnection(serviceConnectionContainer: ServiceConnectionContainer) { // This method is always either called first or after an `onNoServiceConnection`, so the // initialization of the fields doesn't have to be synchronized - accountCache = serviceConnection.accountCache - appVersionInfoCache = serviceConnection.appVersionInfoCache - authTokenCache = serviceConnection.authTokenCache - connectionProxy = serviceConnection.connectionProxy - deviceRepository = serviceConnection.deviceRepository - customDns = serviceConnection.customDns - locationInfoCache = serviceConnection.locationInfoCache - relayListListener = serviceConnection.relayListListener - settingsListener = serviceConnection.settingsListener - splitTunneling = serviceConnection.splitTunneling + accountCache = serviceConnectionContainer.accountCache + appVersionInfoCache = serviceConnectionContainer.appVersionInfoCache + authTokenCache = serviceConnectionContainer.authTokenCache + connectionProxy = serviceConnectionContainer.connectionProxy + deviceDataSource = serviceConnectionContainer.deviceDataSource + customDns = serviceConnectionContainer.customDns + locationInfoCache = serviceConnectionContainer.locationInfoCache + relayListListener = serviceConnectionContainer.relayListListener + settingsListener = serviceConnectionContainer.settingsListener + + splitTunneling = serviceConnectionContainer.splitTunneling synchronized(this) { when (state) { diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/SettingsFragment.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/SettingsFragment.kt index 08afd5c59e..2cf83fc4c7 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/SettingsFragment.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/SettingsFragment.kt @@ -6,21 +6,26 @@ import android.view.View import android.view.ViewGroup import android.widget.ImageButton import androidx.core.content.ContextCompat +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.flowWithLifecycle import androidx.lifecycle.lifecycleScope import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.launch import net.mullvad.mullvadvpn.R import net.mullvad.mullvadvpn.model.DeviceState import net.mullvad.mullvadvpn.ui.serviceconnection.AccountCache import net.mullvad.mullvadvpn.ui.serviceconnection.AppVersionInfoCache import net.mullvad.mullvadvpn.ui.serviceconnection.DeviceRepository -import net.mullvad.mullvadvpn.ui.serviceconnection.ServiceConnection +import net.mullvad.mullvadvpn.ui.serviceconnection.ServiceConnectionContainer import net.mullvad.mullvadvpn.ui.widget.AccountCell import net.mullvad.mullvadvpn.ui.widget.AppVersionCell import net.mullvad.mullvadvpn.ui.widget.NavigateCell +import org.koin.android.ext.android.inject class SettingsFragment : ServiceAwareFragment(), StatusBarPainter, NavigationBarPainter { + private val deviceRepository: DeviceRepository by inject() + private lateinit var accountMenu: AccountCell private lateinit var appVersionMenu: AppVersionCell private lateinit var preferencesMenu: View @@ -30,13 +35,11 @@ class SettingsFragment : ServiceAwareFragment(), StatusBarPainter, NavigationBar private var active = false private var accountCache: AccountCache? = null - private var deviceRepository: DeviceRepository? = null private var versionInfoCache: AppVersionInfoCache? = null - override fun onNewServiceConnection(serviceConnection: ServiceConnection) { - accountCache = serviceConnection.accountCache - deviceRepository = serviceConnection.deviceRepository - versionInfoCache = serviceConnection.appVersionInfoCache + override fun onNewServiceConnection(serviceConnectionContainer: ServiceConnectionContainer) { + accountCache = serviceConnectionContainer.accountCache + versionInfoCache = serviceConnectionContainer.appVersionInfoCache if (active) { configureListeners() @@ -45,7 +48,6 @@ class SettingsFragment : ServiceAwareFragment(), StatusBarPainter, NavigationBar override fun onNoServiceConnection() { accountCache = null - deviceRepository = null versionInfoCache = null } @@ -90,6 +92,14 @@ class SettingsFragment : ServiceAwareFragment(), StatusBarPainter, NavigationBar paintStatusBar(ContextCompat.getColor(requireContext(), R.color.darkBlue)) } } + + lifecycleScope.launch { + deviceRepository.deviceState + .flowWithLifecycle(lifecycle, Lifecycle.State.RESUMED) + .collect { device -> + updateLoggedInStatus(device is DeviceState.LoggedIn) + } + } } override fun onResume() { @@ -131,16 +141,6 @@ class SettingsFragment : ServiceAwareFragment(), StatusBarPainter, NavigationBar fetchAccountExpiry() } - jobTracker.newUiJob("updateLoggedInStatus") { - deviceRepository?.let { repository -> - repository.deviceState - .onEach { state -> if (state.isInitialState()) repository.refreshDeviceState() } - .collect { device -> - updateLoggedInStatus(device is DeviceState.LoggedIn) - } - } - } - versionInfoCache?.onUpdate = { jobTracker.newUiJob("updateVersionInfo") { updateVersionInfo() diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/WelcomeFragment.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/WelcomeFragment.kt index 92364fdf0f..f83660bd06 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/WelcomeFragment.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/WelcomeFragment.kt @@ -13,14 +13,18 @@ import kotlinx.coroutines.delay import kotlinx.coroutines.flow.collect import net.mullvad.mullvadvpn.R import net.mullvad.mullvadvpn.model.TunnelState +import net.mullvad.mullvadvpn.ui.serviceconnection.DeviceRepository import net.mullvad.mullvadvpn.ui.widget.HeaderBar import net.mullvad.mullvadvpn.ui.widget.RedeemVoucherButton import net.mullvad.mullvadvpn.ui.widget.SitePaymentButton import org.joda.time.DateTime +import org.koin.android.ext.android.inject val POLL_INTERVAL: Long = 15 /* s */ * 1000 /* ms */ class WelcomeFragment : ServiceDependentFragment(OnNoService.GoToLaunchScreen) { + private val deviceRepository: DeviceRepository by inject() + private lateinit var accountLabel: TextView private lateinit var sitePaymentButton: SitePaymentButton diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/serviceconnection/DeviceRepository.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/serviceconnection/DeviceRepository.kt index 7d1eed2961..f2a6fe7ff1 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/serviceconnection/DeviceRepository.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/serviceconnection/DeviceRepository.kt @@ -1,20 +1,35 @@ package net.mullvad.mullvadvpn.ui.serviceconnection import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.flow.SharingStarted.Companion.Eagerly +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.emptyFlow +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.stateIn import net.mullvad.mullvadvpn.model.DeviceState class DeviceRepository( - private val dataSource: ServiceConnectionDeviceDataSource, - externalScope: CoroutineScope + private val serviceConnectionManager: ServiceConnectionManager ) { - val deviceState = dataSource.deviceStateUpdates - .stateIn( - externalScope, - Eagerly, - DeviceState.InitialState - ) + val deviceState = serviceConnectionManager.connectionState + .flatMapLatest { state -> + if (state is ServiceConnectionState.ConnectedReady) { + state.container.deviceDataSource.deviceStateUpdates + .onStart { + state.container.deviceDataSource.refreshDevice() + } + } else { + emptyFlow() + } + } + .stateIn(CoroutineScope(Dispatchers.IO), SharingStarted.Lazily, DeviceState.InitialState) - fun refreshDeviceState() = dataSource.refreshDevice() + fun refreshDeviceState() { + container()?.deviceDataSource?.refreshDevice() + } + + private fun container(): ServiceConnectionContainer? { + return serviceConnectionManager.connectionState.value.readyContainer() + } } diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/serviceconnection/ServiceConnection.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/serviceconnection/ServiceConnectionContainer.kt index 3e3496fa14..e430c2d81f 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/serviceconnection/ServiceConnection.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/serviceconnection/ServiceConnectionContainer.kt @@ -4,7 +4,6 @@ import android.os.Looper import android.os.Messenger import android.os.RemoteException import android.util.Log -import kotlinx.coroutines.MainScope import net.mullvad.mullvadvpn.di.SERVICE_CONNECTION_SCOPE import net.mullvad.mullvadvpn.ipc.DispatchingHandler import net.mullvad.mullvadvpn.ipc.Event @@ -20,15 +19,16 @@ import org.koin.core.scope.get // The properties of this class can be used to send events to the service, to listen for events from // the service and to get values received from events. @OptIn(KoinApiExtension::class) -class ServiceConnection( +class ServiceConnectionContainer( connection: Messenger, - onServiceReady: ((ServiceConnection) -> Unit)? = null + onServiceReady: (ServiceConnectionContainer) -> Unit, + onVpnPermissionRequest: () -> Unit ) : KoinScopeComponent { private val dispatcher = DispatchingHandler(Looper.getMainLooper()) { message -> Event.fromMessage(message) } - override val scope = getKoin().createScope( + override val scope = getKoin().getOrCreateScope( SERVICE_CONNECTION_SCOPE, named(SERVICE_CONNECTION_SCOPE), this ) @@ -36,10 +36,10 @@ class ServiceConnection( val accountCache = AccountCache(connection, dispatcher) val authTokenCache = AuthTokenCache(connection, dispatcher) val connectionProxy = ConnectionProxy(connection, dispatcher) - val deviceRepository = - DeviceRepository(ServiceConnectionDeviceDataSource(connection, dispatcher), MainScope()) + val deviceDataSource = ServiceConnectionDeviceDataSource(connection, dispatcher) val locationInfoCache = LocationInfoCache(dispatcher) val settingsListener = SettingsListener(connection, dispatcher) + // NOTE: `org.koin.core.scope.get` must be used here rather than `org.koin.core.component.get`. val splitTunneling = get<SplitTunneling>(parameters = { parametersOf(connection, dispatcher) }) val voucherRedeemer = VoucherRedeemer(connection, dispatcher) val vpnPermission = VpnPermission(connection, dispatcher) @@ -49,8 +49,10 @@ class ServiceConnection( var relayListListener = RelayListListener(connection, dispatcher, settingsListener) init { + vpnPermission.onRequest = onVpnPermissionRequest + dispatcher.registerHandler(Event.ListenerReady::class) { _ -> - onServiceReady?.invoke(this@ServiceConnection) + onServiceReady.invoke(this@ServiceConnectionContainer) } registerListener(connection) diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/serviceconnection/ServiceConnectionManager.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/serviceconnection/ServiceConnectionManager.kt new file mode 100644 index 0000000000..55270863e8 --- /dev/null +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/serviceconnection/ServiceConnectionManager.kt @@ -0,0 +1,94 @@ +package net.mullvad.mullvadvpn.ui.serviceconnection + +import android.content.ComponentName +import android.content.Context +import android.content.Intent +import android.os.IBinder +import android.os.Messenger +import android.util.Log +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import net.mullvad.mullvadvpn.service.MullvadVpnService +import net.mullvad.talpid.util.EventNotifier + +class ServiceConnectionManager( + private val context: Context +) { + private val _connectionState = + MutableStateFlow<ServiceConnectionState>(ServiceConnectionState.Disconnected) + + val connectionState = _connectionState.asStateFlow() + + // TODO: Remove after refactoring fragments to support flow. + @Deprecated(message = "Use connectionState") + val serviceNotifier = EventNotifier<ServiceConnectionContainer?>(null) + + private var vpnPermissionRequestHandler: (() -> Unit)? = null + + private val serviceConnection = object : android.content.ServiceConnection { + override fun onServiceConnected(className: ComponentName, binder: IBinder) { + Log.d("mullvad", "UI successfully connected to the service") + + notify( + ServiceConnectionState.ConnectedNotReady( + ServiceConnectionContainer( + Messenger(binder), + ::handleNewServiceConnection, + ::handleVpnPermissionRequest + ) + ) + ) + } + + override fun onServiceDisconnected(className: ComponentName) { + Log.d("mullvad", "UI lost the connection to the service") + notify(ServiceConnectionState.Disconnected) + } + } + + fun bind(vpnPermissionRequestHandler: () -> Unit) { + this.vpnPermissionRequestHandler = vpnPermissionRequestHandler + val intent = Intent(context, MullvadVpnService::class.java) + context.startService(intent) + context.bindService(intent, serviceConnection, 0) + } + + fun unbind() { + context.unbindService(serviceConnection) + notify(ServiceConnectionState.Disconnected) + vpnPermissionRequestHandler = null + } + + fun onDestroy() { + serviceNotifier.unsubscribeAll() + notify(ServiceConnectionState.Disconnected) + vpnPermissionRequestHandler = null + } + + fun onVpnPermissionResult(isGranted: Boolean) { + _connectionState.value.let { state -> + if (state is ServiceConnectionState.ConnectedReady) { + state.container.vpnPermission.grant(isGranted) + } + } + } + + private fun notify(state: ServiceConnectionState) { + _connectionState.value = state + + // TODO: Remove once `serviceNotifier` is no longer used. + if (state is ServiceConnectionState.ConnectedReady) { + serviceNotifier.notify(state.container) + } else if (state is ServiceConnectionState.Disconnected) { + serviceNotifier.notify(null) + } + } + + private fun handleVpnPermissionRequest() { + vpnPermissionRequestHandler?.invoke() + } + + private fun handleNewServiceConnection(serviceConnectionContainer: ServiceConnectionContainer) { + notify(ServiceConnectionState.ConnectedReady(serviceConnectionContainer)) + } +} diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/serviceconnection/ServiceConnectionState.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/serviceconnection/ServiceConnectionState.kt new file mode 100644 index 0000000000..ca868e5cfa --- /dev/null +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/serviceconnection/ServiceConnectionState.kt @@ -0,0 +1,14 @@ +package net.mullvad.mullvadvpn.ui.serviceconnection + +sealed class ServiceConnectionState { + data class ConnectedReady(val container: ServiceConnectionContainer) : ServiceConnectionState() + + data class ConnectedNotReady(val container: ServiceConnectionContainer) : + ServiceConnectionState() + + object Disconnected : ServiceConnectionState() + + fun readyContainer(): ServiceConnectionContainer? { + return (this as? ConnectedReady)?.container + } +} diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/LoginViewModel.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/LoginViewModel.kt index cb4f151fd1..e9cb27fda6 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/LoginViewModel.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/LoginViewModel.kt @@ -1,9 +1,6 @@ package net.mullvad.mullvadvpn.viewmodel -import android.app.Application -import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.ViewModel -import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.viewModelScope import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -15,9 +12,7 @@ import net.mullvad.mullvadvpn.model.AccountHistory import net.mullvad.mullvadvpn.model.LoginResult import net.mullvad.mullvadvpn.ui.serviceconnection.AccountCache -class LoginViewModel( - application: Application -) : AndroidViewModel(application) { +class LoginViewModel : ViewModel() { private val _uiState = MutableStateFlow<LoginUiState>(LoginUiState.Default) val uiState: StateFlow<LoginUiState> = _uiState @@ -99,11 +94,4 @@ class LoginViewModel( else -> LoginUiState.OtherError(errorMessage = this.toString()) } } - - class Factory(val application: Application) : - ViewModelProvider.Factory { - override fun <T : ViewModel> create(modelClass: Class<T>): T { - return LoginViewModel(application) as T - } - } } diff --git a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/LoginViewModelTest.kt b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/LoginViewModelTest.kt index 2a615fcc72..a0dc80957b 100644 --- a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/LoginViewModelTest.kt +++ b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/LoginViewModelTest.kt @@ -5,7 +5,6 @@ import app.cash.turbine.test import io.mockk.MockKAnnotations import io.mockk.every import io.mockk.impl.annotations.MockK -import io.mockk.mockk import io.mockk.verify import junit.framework.Assert.assertEquals import kotlinx.coroutines.Dispatchers @@ -41,7 +40,7 @@ class LoginViewModelTest { every { mockedAccountCache.accountHistoryEvents } returns accountHistoryTestEvents every { mockedAccountCache.loginEvents } returns loginTestEvents - loginViewModel = LoginViewModel(mockk()) + loginViewModel = LoginViewModel() } @Test |
