summaryrefslogtreecommitdiffhomepage
path: root/android/app/src
diff options
context:
space:
mode:
authorAlbin <albin@mullvad.net>2022-07-20 11:25:46 +0200
committerAlbin <albin@mullvad.net>2022-07-20 11:25:46 +0200
commit7880feb099f6ed4855989ad293bbd3f7cd3e23c0 (patch)
tree64759a99e751f27e0ed5d3deb5819b5235654c7b /android/app/src
parent98af54098d42ef385398a99b13b593da2376528e (diff)
parent83aac3304d4b644e0677bec85527988cbf449ddd (diff)
downloadmullvadvpn-7880feb099f6ed4855989ad293bbd3f7cd3e23c0.tar.xz
mullvadvpn-7880feb099f6ed4855989ad293bbd3f7cd3e23c0.zip
Merge branch 'migrate-from-service-dependent-fragments'
Diffstat (limited to 'android/app/src')
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/di/UiModule.kt7
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/AccountFragment.kt98
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/AdvancedFragment.kt184
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/ConnectFragment.kt187
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/LoginFragment.kt34
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/MainActivity.kt17
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/OutOfTimeFragment.kt97
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/PreferencesFragment.kt84
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/SelectLocationFragment.kt165
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/ServiceAwareFragment.kt69
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/ServiceDependentFragment.kt192
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/SettingsFragment.kt106
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/VersionInfo.kt8
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/WelcomeFragment.kt98
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/extension/ContextExtensions.kt27
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/notification/AccountExpiryNotification.kt21
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/notification/InAppNotification.kt1
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/notification/TunnelStateNotification.kt16
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/notification/VersionInfoNotification.kt31
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/serviceconnection/AccountRepository.kt15
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/serviceconnection/DeviceRepository.kt10
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/serviceconnection/ServiceConnectionManagerExtensions.kt25
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/widget/NavigateCell.kt2
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/widget/SitePaymentButton.kt5
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/widget/UrlButton.kt105
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/util/CacheExtensions.kt22
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/util/DeviceStateExtensions.kt25
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/util/FlowUtils.kt7
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/DeviceRevokedViewModel.kt12
29 files changed, 855 insertions, 815 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 9c0461022f..38fa9117f7 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,6 +6,9 @@ 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.notification.AccountExpiryNotification
+import net.mullvad.mullvadvpn.ui.notification.TunnelStateNotification
+import net.mullvad.mullvadvpn.ui.notification.VersionInfoNotification
import net.mullvad.mullvadvpn.ui.serviceconnection.AccountRepository
import net.mullvad.mullvadvpn.ui.serviceconnection.DeviceRepository
import net.mullvad.mullvadvpn.ui.serviceconnection.ServiceConnectionManager
@@ -39,6 +42,10 @@ val uiModule = module {
single { ServiceConnectionManager(androidContext()) }
+ single { AccountExpiryNotification(get()) }
+ single { TunnelStateNotification(get()) }
+ single { VersionInfoNotification(get()) }
+
single { AccountRepository(get()) }
single { DeviceRepository(get()) }
viewModel { LoginViewModel(get(), get()) }
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 c648c3f7e6..415aa54046 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
@@ -1,5 +1,6 @@
package net.mullvad.mullvadvpn.ui
+import android.app.Activity
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
@@ -10,30 +11,42 @@ import androidx.lifecycle.repeatOnLifecycle
import java.text.DateFormat
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.collect
+import kotlinx.coroutines.flow.debounce
+import kotlinx.coroutines.flow.emptyFlow
+import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.launch
import net.mullvad.mullvadvpn.R
import net.mullvad.mullvadvpn.model.TunnelState
+import net.mullvad.mullvadvpn.ui.extension.openAccountPageInBrowser
+import net.mullvad.mullvadvpn.ui.extension.requireMainActivity
+import net.mullvad.mullvadvpn.ui.fragments.BaseFragment
import net.mullvad.mullvadvpn.ui.serviceconnection.AccountRepository
import net.mullvad.mullvadvpn.ui.serviceconnection.DeviceRepository
+import net.mullvad.mullvadvpn.ui.serviceconnection.ServiceConnectionManager
+import net.mullvad.mullvadvpn.ui.serviceconnection.ServiceConnectionState
+import net.mullvad.mullvadvpn.ui.serviceconnection.authTokenCache
import net.mullvad.mullvadvpn.ui.widget.Button
import net.mullvad.mullvadvpn.ui.widget.CopyableInformationView
import net.mullvad.mullvadvpn.ui.widget.InformationView
import net.mullvad.mullvadvpn.ui.widget.RedeemVoucherButton
import net.mullvad.mullvadvpn.ui.widget.SitePaymentButton
+import net.mullvad.mullvadvpn.util.JobTracker
+import net.mullvad.mullvadvpn.util.UNKNOWN_STATE_DEBOUNCE_DELAY_MILLISECONDS
+import net.mullvad.mullvadvpn.util.addDebounceForUnknownState
+import net.mullvad.mullvadvpn.util.callbackFlowFromNotifier
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) {
+class AccountFragment : BaseFragment() {
// Injected dependencies
private val accountRepository: AccountRepository by inject()
private val deviceRepository: DeviceRepository by inject()
-
- override val isSecureScreen = true
+ private val serviceConnectionManager: ServiceConnectionManager by inject()
private val dateStyle = DateFormat.MEDIUM
private val timeStyle = DateFormat.SHORT
@@ -71,7 +84,20 @@ class AccountFragment : ServiceDependentFragment(OnNoService.GoBack) {
private lateinit var redeemVoucherButton: RedeemVoucherButton
private lateinit var titleController: CollapsibleTitleController
- override fun onSafelyCreateView(
+ @Deprecated("Refactor code to instead rely on Lifecycle.")
+ private val jobTracker = JobTracker()
+
+ override fun onAttach(activity: Activity) {
+ super.onAttach(activity)
+ requireMainActivity().enterSecureScreen(this)
+ }
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ lifecycleScope.launchUiSubscriptionsOnResume()
+ }
+
+ override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
@@ -79,13 +105,18 @@ class AccountFragment : ServiceDependentFragment(OnNoService.GoBack) {
val view = inflater.inflate(R.layout.account, container, false)
view.findViewById<View>(R.id.back).setOnClickListener {
- parentActivity.onBackPressed()
+ requireMainActivity().onBackPressed()
}
sitePaymentButton = view.findViewById<SitePaymentButton>(R.id.site_payment).apply {
newAccount = false
- prepare(authTokenCache, jobTracker) {
+ setOnClickAction("openAccountPageInBrowser", jobTracker) {
+ setEnabled(false)
+ serviceConnectionManager.authTokenCache()?.fetchAuthToken()?.let { token ->
+ context.openAccountPageInBrowser(token)
+ }
+ setEnabled(true)
checkForAddedTime()
}
}
@@ -109,42 +140,35 @@ class AccountFragment : ServiceDependentFragment(OnNoService.GoBack) {
return view
}
- override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
- lifecycleScope.launchUiSubscriptionsOnResume()
- }
-
- override fun onSafelyStart() {
- connectionProxy.onUiStateChange.subscribe(this) { uiState ->
- jobTracker.newUiJob("updateHasConnectivity") {
- hasConnectivity = uiState is TunnelState.Connected ||
- uiState is TunnelState.Disconnected ||
- (uiState is TunnelState.Error && !uiState.errorState.isBlocking)
- isOffline = uiState is TunnelState.Error &&
- uiState.errorState.cause is ErrorStateCause.IsOffline
- }
- }
-
- sitePaymentButton.updateAuthTokenCache(authTokenCache)
- }
-
- override fun onSafelyStop() {
+ override fun onStop() {
jobTracker.cancelAllJobs()
+ super.onStop()
}
- override fun onSafelyDestroyView() {
+ override fun onDestroyView() {
titleController.onDestroy()
+ super.onDestroyView()
+ }
+
+ override fun onDetach() {
+ requireMainActivity().leaveSecureScreen(this)
+ super.onDetach()
}
private fun CoroutineScope.launchUiSubscriptionsOnResume() = launch {
repeatOnLifecycle(Lifecycle.State.RESUMED) {
launchUpdateTextOnDeviceChanges()
launchUpdateTextOnExpiryChanges()
+ launchTunnelStateSubscription()
}
}
private fun CoroutineScope.launchUpdateTextOnDeviceChanges() {
launch {
deviceRepository.deviceState
+ .debounce {
+ it.addDebounceForUnknownState(UNKNOWN_STATE_DEBOUNCE_DELAY_MILLISECONDS)
+ }
.collect { state ->
accountNumberView.information = state.token()
deviceNameView.information =
@@ -165,6 +189,28 @@ class AccountFragment : ServiceDependentFragment(OnNoService.GoBack) {
}
}
+ private fun CoroutineScope.launchTunnelStateSubscription() {
+ launch {
+ serviceConnectionManager.connectionState
+ .flatMapLatest { state ->
+ if (state is ServiceConnectionState.ConnectedReady) {
+ callbackFlowFromNotifier(
+ state.container.connectionProxy.onUiStateChange
+ )
+ } else {
+ emptyFlow()
+ }
+ }
+ .collect { uiState ->
+ hasConnectivity = uiState is TunnelState.Connected ||
+ uiState is TunnelState.Disconnected ||
+ (uiState is TunnelState.Error && !uiState.errorState.isBlocking)
+ isOffline = uiState is TunnelState.Error &&
+ uiState.errorState.cause is ErrorStateCause.IsOffline
+ }
+ }
+ }
+
private fun checkForAddedTime() {
currentAccountExpiry?.let { expiry ->
oldAccountExpiry = expiry
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 275981bd83..12282c30a9 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
@@ -4,22 +4,45 @@ import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.lifecycleScope
+import androidx.lifecycle.repeatOnLifecycle
import androidx.recyclerview.widget.LinearLayoutManager
import java.net.InetAddress
import kotlinx.coroutines.CompletableDeferred
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.collect
+import kotlinx.coroutines.flow.emptyFlow
+import kotlinx.coroutines.flow.flatMapLatest
+import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.shareIn
+import kotlinx.coroutines.launch
import net.mullvad.mullvadvpn.R
import net.mullvad.mullvadvpn.model.Settings
import net.mullvad.mullvadvpn.ui.customdns.CustomDnsAdapter
+import net.mullvad.mullvadvpn.ui.extension.requireMainActivity
+import net.mullvad.mullvadvpn.ui.fragments.BaseFragment
import net.mullvad.mullvadvpn.ui.fragments.SplitTunnelingFragment
-import net.mullvad.mullvadvpn.ui.serviceconnection.ServiceConnectionContainer
+import net.mullvad.mullvadvpn.ui.serviceconnection.ServiceConnectionManager
+import net.mullvad.mullvadvpn.ui.serviceconnection.ServiceConnectionState
+import net.mullvad.mullvadvpn.ui.serviceconnection.customDns
+import net.mullvad.mullvadvpn.ui.serviceconnection.settingsListener
import net.mullvad.mullvadvpn.ui.widget.CellSwitch
import net.mullvad.mullvadvpn.ui.widget.CustomRecyclerView
import net.mullvad.mullvadvpn.ui.widget.MtuCell
import net.mullvad.mullvadvpn.ui.widget.NavigateCell
import net.mullvad.mullvadvpn.ui.widget.ToggleCell
import net.mullvad.mullvadvpn.util.AdapterWithHeader
+import net.mullvad.mullvadvpn.util.JobTracker
+import net.mullvad.mullvadvpn.util.callbackFlowFromNotifier
+import org.koin.android.ext.android.inject
+
+class AdvancedFragment : BaseFragment() {
+
+ // Injected dependencies
+ private val serviceConnectionManager: ServiceConnectionManager by inject()
-class AdvancedFragment : ServiceDependentFragment(OnNoService.GoBack) {
private var isAllowLanEnabled = false
// Both customDnsAdapter and customDnsToggle are nullable since onNewServiceConnection,
@@ -30,38 +53,115 @@ class AdvancedFragment : ServiceDependentFragment(OnNoService.GoBack) {
private lateinit var wireguardMtuInput: MtuCell
private lateinit var titleController: CollapsibleTitleController
- override fun onSafelyCreateView(
+ @Deprecated("Refactor code to instead rely on Lifecycle.")
+ private val jobTracker = JobTracker()
+
+ val shared = serviceConnectionManager.connectionState
+ .flatMapLatest { state ->
+ if (state is ServiceConnectionState.ConnectedReady) {
+ flowOf(state.container)
+ } else {
+ emptyFlow()
+ }
+ }
+ .map {
+ it.customDns
+ }
+ .shareIn(lifecycleScope, SharingStarted.WhileSubscribed())
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ lifecycleScope.launch {
+ repeatOnLifecycle(Lifecycle.State.RESUMED) {
+ launch {
+ serviceConnectionManager.connectionState
+ .flatMapLatest { state ->
+ if (state is ServiceConnectionState.ConnectedReady) {
+ flowOf(state.container)
+ } else {
+ emptyFlow()
+ }
+ }
+ .flatMapLatest {
+ callbackFlowFromNotifier(it.settingsListener.settingsNotifier)
+ }
+ .collect { settings ->
+ if (settings != null) {
+ updateUi(settings)
+ }
+ }
+ }
+
+ launch {
+ shared
+ .flatMapLatest {
+ callbackFlowFromNotifier(it.onEnabledChanged)
+ }
+ .collect { isEnabled ->
+ customDnsAdapter?.updateState(isEnabled)
+ jobTracker.newUiJob("updateEnabled") {
+ if (isEnabled) {
+ customDnsToggle?.state = CellSwitch.State.ON
+ } else {
+ customDnsToggle?.state = CellSwitch.State.OFF
+ }
+ }
+ }
+ }
+
+ launch {
+ shared
+ .flatMapLatest {
+ callbackFlowFromNotifier(it.onDnsServersChanged)
+ }
+ .collect { servers ->
+ customDnsAdapter?.updateServers(servers)
+ }
+ }
+ }
+ }
+ }
+
+ override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
- ): View {
+ ): View? {
val view = inflater.inflate(R.layout.advanced, container, false)
view.findViewById<View>(R.id.back).setOnClickListener {
customDnsAdapter?.stopEditing()
- parentActivity.onBackPressed()
+ requireActivity().onBackPressed()
}
titleController = CollapsibleTitleController(view, R.id.contents)
customDnsAdapter = CustomDnsAdapter(
- onAddServer = { address -> customDns.addDnsServer(address) },
- onRemoveDnsServer = { address -> customDns.removeDnsServer(address) },
+ onAddServer = { address ->
+ serviceConnectionManager.customDns()?.addDnsServer(address) ?: false
+ },
+ onRemoveDnsServer = { address ->
+ serviceConnectionManager.customDns()?.removeDnsServer(address) ?: false
+ },
onSetCustomDnsEnabled = { isEnabled ->
if (isEnabled) {
- customDns.enable()
+ serviceConnectionManager.customDns()?.enable()
} else {
- customDns.disable()
+ serviceConnectionManager.customDns()?.disable()
}
},
onReplaceDnsServer = { oldServer, newServer ->
- customDns.replaceDnsServer(oldServer, newServer)
+ serviceConnectionManager.customDns()?.replaceDnsServer(
+ oldServer,
+ newServer
+ ) ?: false
}
).also { newCustomDnsAdapter ->
+
newCustomDnsAdapter.confirmAddAddress = ::confirmAddAddress
view.findViewById<CustomRecyclerView>(R.id.contents).apply {
- layoutManager = LinearLayoutManager(parentActivity)
+ layoutManager = LinearLayoutManager(requireContext())
adapter = AdapterWithHeader(newCustomDnsAdapter, R.layout.advanced_header).apply {
onHeaderAvailable = { headerView ->
@@ -84,21 +184,21 @@ class AdvancedFragment : ServiceDependentFragment(OnNoService.GoBack) {
return view
}
- override fun onNewServiceConnection(serviceConnectionContainer: ServiceConnectionContainer) {
- super.onNewServiceConnection(serviceConnectionContainer)
- subscribeToCustomDnsChanges()
- }
-
- override fun onSafelyDestroyView() {
+ override fun onDestroyView() {
detachBackButtonHandler()
customDnsAdapter?.onDestroy()
titleController.onDestroy()
- settingsListener.settingsNotifier.unsubscribe(this)
+ super.onDestroyView()
}
private fun configureHeader(view: View) {
wireguardMtuInput = view.findViewById<MtuCell>(R.id.wireguard_mtu).apply {
- onSubmit = { mtu -> settingsListener.wireguardMtu = mtu }
+ onSubmit = { mtu ->
+ serviceConnectionManager.settingsListener()?.wireguardMtu = mtu
+ }
+ value = serviceConnectionManager.settingsListener()?.let { settingsNotifier ->
+ settingsNotifier.wireguardMtu
+ }
}
view.findViewById<NavigateCell>(R.id.split_tunneling).apply {
@@ -109,52 +209,18 @@ class AdvancedFragment : ServiceDependentFragment(OnNoService.GoBack) {
listener = { state ->
jobTracker.newBackgroundJob("toggleCustomDns") {
if (state == CellSwitch.State.ON) {
- customDns.enable()
+ serviceConnectionManager.customDns()?.enable()
} else {
- customDns.disable()
+ serviceConnectionManager.customDns()?.disable()
}
}
}
}
-
- settingsListener.settingsNotifier.subscribe(this) { maybeSettings ->
- maybeSettings?.let { settings ->
- updateUi(settings)
- }
-
- isAllowLanEnabled = maybeSettings?.allowLan ?: false
- }
-
- subscribeToCustomDnsChanges()
- }
-
- private fun subscribeToCustomDnsChanges() {
- // Ensure there are no previous subscriptions as this function might be called either when
- // there view has been created or when there is a new service connection.
- customDns.onEnabledChanged.unsubscribe(this)
- customDns.onDnsServersChanged.unsubscribe(this)
-
- customDns.onEnabledChanged.subscribe(this) { isEnabled ->
- customDnsAdapter?.updateState(isEnabled)
- jobTracker.newUiJob("updateEnabled") {
- if (isEnabled) {
- customDnsToggle?.state = CellSwitch.State.ON
- } else {
- customDnsToggle?.state = CellSwitch.State.OFF
- }
- }
- }
-
- customDns.onDnsServersChanged.subscribe(this) { servers ->
- customDnsAdapter?.updateServers(servers)
- }
}
private fun updateUi(settings: Settings) {
- jobTracker.newUiJob("updateUi") {
- if (!wireguardMtuInput.hasFocus) {
- wireguardMtuInput.value = settings.tunnelOptions.wireguard.options.mtu
- }
+ if (this::wireguardMtuInput.isInitialized && wireguardMtuInput.hasFocus == false) {
+ wireguardMtuInput.value = settings.tunnelOptions.wireguard.options.mtu
}
}
@@ -182,7 +248,7 @@ class AdvancedFragment : ServiceDependentFragment(OnNoService.GoBack) {
}
private fun attachBackButtonHandler() {
- parentActivity.backButtonHandler = {
+ requireMainActivity().backButtonHandler = {
if (customDnsAdapter?.isEditing == true) {
customDnsAdapter?.stopEditing()
}
@@ -191,6 +257,6 @@ class AdvancedFragment : ServiceDependentFragment(OnNoService.GoBack) {
}
private fun detachBackButtonHandler() {
- parentActivity.backButtonHandler = null
+ requireMainActivity().backButtonHandler = null
}
}
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 31d1c0dd9c..fdb0adaf13 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
@@ -1,5 +1,7 @@
package net.mullvad.mullvadvpn.ui
+import android.content.Intent
+import android.net.Uri
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
@@ -9,29 +11,51 @@ import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.delay
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.collect
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.emptyFlow
+import kotlinx.coroutines.flow.flatMapLatest
+import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.shareIn
import kotlinx.coroutines.launch
import net.mullvad.mullvadvpn.R
import net.mullvad.mullvadvpn.model.TunnelState
+import net.mullvad.mullvadvpn.ui.extension.requireMainActivity
+import net.mullvad.mullvadvpn.ui.fragments.BaseFragment
import net.mullvad.mullvadvpn.ui.notification.AccountExpiryNotification
import net.mullvad.mullvadvpn.ui.notification.TunnelStateNotification
import net.mullvad.mullvadvpn.ui.notification.VersionInfoNotification
import net.mullvad.mullvadvpn.ui.serviceconnection.AccountRepository
+import net.mullvad.mullvadvpn.ui.serviceconnection.LocationInfoCache
+import net.mullvad.mullvadvpn.ui.serviceconnection.RelayListListener
+import net.mullvad.mullvadvpn.ui.serviceconnection.ServiceConnectionManager
+import net.mullvad.mullvadvpn.ui.serviceconnection.ServiceConnectionState
+import net.mullvad.mullvadvpn.ui.serviceconnection.authTokenCache
+import net.mullvad.mullvadvpn.ui.serviceconnection.connectionProxy
import net.mullvad.mullvadvpn.ui.widget.HeaderBar
import net.mullvad.mullvadvpn.ui.widget.NotificationBanner
import net.mullvad.mullvadvpn.ui.widget.SwitchLocationButton
+import net.mullvad.mullvadvpn.util.JobTracker
+import net.mullvad.mullvadvpn.util.appVersionCallbackFlow
+import net.mullvad.mullvadvpn.util.callbackFlowFromNotifier
import org.joda.time.DateTime
import org.koin.android.ext.android.inject
val KEY_IS_TUNNEL_INFO_EXPANDED = "is_tunnel_info_expanded"
-class ConnectFragment :
- ServiceDependentFragment(OnNoService.GoToLaunchScreen), NavigationBarPainter {
+class ConnectFragment : BaseFragment(), NavigationBarPainter {
// Injected dependencies
private val accountRepository: AccountRepository by inject()
+ private val accountExpiryNotification: AccountExpiryNotification by inject()
+ private val serviceConnectionManager: ServiceConnectionManager by inject()
+ private val tunnelStateNotification: TunnelStateNotification by inject()
+ private val versionInfoNotification: VersionInfoNotification by inject()
private lateinit var actionButton: ConnectActionButton
private lateinit var switchLocationButton: SwitchLocationButton
@@ -42,49 +66,58 @@ class ConnectFragment :
private var isTunnelInfoExpanded = false
+ @Deprecated("Refactor code to instead rely on Lifecycle.")
+ private val jobTracker = JobTracker()
+
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
isTunnelInfoExpanded =
savedInstanceState?.getBoolean(KEY_IS_TUNNEL_INFO_EXPANDED, false) ?: false
+
+ lifecycleScope.launchUiSubscriptionsOnResume()
}
- override fun onSafelyCreateView(
+ override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
- ): View {
+ ): View? {
val view = inflater.inflate(R.layout.connect, container, false)
headerBar = view.findViewById<HeaderBar>(R.id.header_bar).apply {
tunnelState = TunnelState.Disconnected
}
+ accountExpiryNotification.onClick = {
+ serviceConnectionManager.authTokenCache()?.fetchAuthToken()?.let { token ->
+ val url = getString(R.string.account_url)
+ val ready = Uri.parse("$url?token=$token")
+ requireContext().startActivity(Intent(Intent.ACTION_VIEW, ready))
+ }
+ }
+
notificationBanner = view.findViewById<NotificationBanner>(R.id.notification_banner).apply {
notifications.apply {
- register(TunnelStateNotification(parentActivity, connectionProxy))
- register(VersionInfoNotification(parentActivity, appVersionInfoCache))
- register(
- AccountExpiryNotification(
- parentActivity,
- authTokenCache,
- accountRepository
- )
- )
+ // NOTE: The order of below notifications is significant.
+ register(tunnelStateNotification)
+ register(versionInfoNotification)
+ register(accountExpiryNotification)
}
}
- status = ConnectionStatus(view, parentActivity)
+ status = ConnectionStatus(view, requireMainActivity())
locationInfo = LocationInfo(view, requireContext())
locationInfo.isTunnelInfoExpanded = isTunnelInfoExpanded
actionButton = ConnectActionButton(view)
+
actionButton.apply {
- onConnect = { connectionProxy.connect() }
- onCancel = { connectionProxy.disconnect() }
- onReconnect = { connectionProxy.reconnect() }
- onDisconnect = { connectionProxy.disconnect() }
+ onConnect = { serviceConnectionManager.connectionProxy()?.connect() }
+ onCancel = { serviceConnectionManager.connectionProxy()?.disconnect() }
+ onReconnect = { serviceConnectionManager.connectionProxy()?.reconnect() }
+ onDisconnect = { serviceConnectionManager.connectionProxy()?.disconnect() }
}
switchLocationButton = view.findViewById<SwitchLocationButton>(R.id.switch_location).apply {
@@ -94,54 +127,10 @@ class ConnectFragment :
return view
}
- override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
- lifecycleScope.launchUiSubscriptionsOnResume()
- }
-
- override fun onSafelyStart() {
+ override fun onStart() {
+ super.onStart()
locationInfo.isTunnelInfoExpanded = isTunnelInfoExpanded
-
notificationBanner.onResume()
-
- locationInfoCache.onNewLocation = { location ->
- jobTracker.newUiJob("updateLocationInfo") {
- locationInfo.location = location
- }
- }
-
- relayListListener.onRelayListChange = { _, selectedRelayItem ->
- jobTracker.newUiJob("updateSelectedRelayItem") {
- switchLocationButton.location = selectedRelayItem
- }
- }
-
- connectionProxy.onUiStateChange.subscribe(this) { uiState ->
- jobTracker.newUiJob("updateTunnelState") {
- updateTunnelState(uiState, connectionProxy.state)
- }
- }
- }
-
- override fun onSafelyStop() {
- jobTracker.cancelAllJobs()
-
- locationInfoCache.onNewLocation = null
- relayListListener.onRelayListChange = null
-
- connectionProxy.onUiStateChange.unsubscribe(this)
-
- notificationBanner.onPause()
-
- isTunnelInfoExpanded = locationInfo.isTunnelInfoExpanded
- }
-
- override fun onSafelyDestroyView() {
- notificationBanner.onDestroy()
- }
-
- override fun onSafelySaveInstanceState(state: Bundle) {
- isTunnelInfoExpanded = locationInfo.isTunnelInfoExpanded
- state.putBoolean(KEY_IS_TUNNEL_INFO_EXPANDED, isTunnelInfoExpanded)
}
override fun onResume() {
@@ -149,9 +138,24 @@ class ConnectFragment :
paintNavigationBar(ContextCompat.getColor(requireContext(), R.color.blue))
}
+ val shared = serviceConnectionManager.connectionState
+ .flatMapLatest { state ->
+ if (state is ServiceConnectionState.ConnectedReady) {
+ flowOf(state.container)
+ } else {
+ emptyFlow()
+ }
+ }
+ .shareIn(lifecycleScope, SharingStarted.WhileSubscribed())
+
private fun CoroutineScope.launchUiSubscriptionsOnResume() = launch {
repeatOnLifecycle(Lifecycle.State.RESUMED) {
launchScheduledExpiryCheck()
+ launchLocationSubscription()
+ launchRelayLocationSubscription()
+ launchTunnelStateSubscription()
+ launchVersionInfoSubscription()
+ launchAccountExpirySubscription()
}
}
@@ -166,6 +170,61 @@ class ConnectFragment :
}
}
+ private fun CoroutineScope.launchLocationSubscription() = launch {
+ shared
+ .flatMapLatest { it.locationInfoCache.locationCallbackFlow() }
+ .collect { locationInfo.location = it }
+ }
+
+ private fun LocationInfoCache.locationCallbackFlow() = callbackFlow {
+ onNewLocation = {
+ this.trySend(it)
+ }
+ awaitClose { onNewLocation = null }
+ }
+
+ private fun CoroutineScope.launchRelayLocationSubscription() = launch {
+ shared
+ .flatMapLatest { it.relayListListener.relayListCallbackFlow() }
+ .collect { switchLocationButton.location = it }
+ }
+
+ private fun RelayListListener.relayListCallbackFlow() = callbackFlow {
+ onRelayListChange = { _, item ->
+ this.trySend(item)
+ }
+ awaitClose { onRelayListChange = null }
+ }
+
+ private fun CoroutineScope.launchTunnelStateSubscription() = launch {
+ shared
+ .flatMapLatest {
+ combine(
+ callbackFlowFromNotifier(it.connectionProxy.onUiStateChange),
+ callbackFlowFromNotifier(it.connectionProxy.onStateChange)
+ ) { uiState, realState ->
+ Pair(uiState, realState)
+ }
+ }
+ .collect { (uiState, realState) ->
+ tunnelStateNotification.updateTunnelState(uiState)
+ updateTunnelState(uiState, realState)
+ }
+ }
+
+ private fun CoroutineScope.launchVersionInfoSubscription() = launch {
+ shared
+ .flatMapLatest { it.appVersionInfoCache.appVersionCallbackFlow() }
+ .collect { versionInfo -> versionInfoNotification.updateVersionInfo(versionInfo) }
+ }
+
+ private fun CoroutineScope.launchAccountExpirySubscription() = launch {
+ accountRepository.accountExpiryState
+ .collect {
+ accountExpiryNotification.updateAccountExpiry(it.date())
+ }
+ }
+
private fun updateTunnelState(uiState: TunnelState, realState: TunnelState) {
locationInfo.state = realState
headerBar.tunnelState = realState
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 d0e6fbf2f3..18e2f08b95 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
@@ -15,16 +15,17 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch
import net.mullvad.mullvadvpn.R
+import net.mullvad.mullvadvpn.ui.extension.requireMainActivity
import net.mullvad.mullvadvpn.ui.fragments.ACCOUNT_TOKEN_ARGUMENT_KEY
+import net.mullvad.mullvadvpn.ui.fragments.BaseFragment
import net.mullvad.mullvadvpn.ui.fragments.DeviceListFragment
import net.mullvad.mullvadvpn.ui.widget.AccountLogin
import net.mullvad.mullvadvpn.ui.widget.HeaderBar
+import net.mullvad.mullvadvpn.util.JobTracker
import net.mullvad.mullvadvpn.viewmodel.LoginViewModel
import org.koin.androidx.viewmodel.ext.android.viewModel
-class LoginFragment :
- ServiceDependentFragment(OnNoService.GoToLaunchScreen),
- NavigationBarPainter {
+class LoginFragment : BaseFragment(), NavigationBarPainter {
private val loginViewModel: LoginViewModel by viewModel()
@@ -38,11 +39,19 @@ class LoginFragment :
private lateinit var background: View
private lateinit var headerBar: HeaderBar
- override fun onSafelyCreateView(
+ @Deprecated("Refactor code to instead rely on Lifecycle.")
+ private val jobTracker = JobTracker()
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ lifecycleScope.launchUiSubscriptionsOnResume()
+ }
+
+ override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
- ): View {
+ ): View? {
val view = inflater.inflate(R.layout.login, container, false)
headerBar = view.findViewById(R.id.header_bar)
@@ -74,12 +83,9 @@ class LoginFragment :
return view
}
- override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
- lifecycleScope.launchUiSubscriptionsOnResume()
- }
-
- override fun onSafelyStart() {
- parentActivity.backButtonHandler = {
+ override fun onStart() {
+ super.onStart()
+ requireMainActivity().backButtonHandler = {
if (accountLogin.hasFocus) {
background.requestFocus()
true
@@ -94,8 +100,10 @@ class LoginFragment :
paintNavigationBar(ContextCompat.getColor(requireContext(), R.color.darkBlue))
}
- override fun onSafelyStop() {
- parentActivity.backButtonHandler = null
+ override fun onStop() {
+ jobTracker.cancelAllJobs()
+ requireMainActivity().backButtonHandler = null
+ super.onStop()
}
private fun triggerAutoLoginIfAccountTokenPresent() {
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 f1831f0a88..ec31a4c706 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
@@ -27,6 +27,8 @@ import net.mullvad.mullvadvpn.ui.fragments.DeviceRevokedFragment
import net.mullvad.mullvadvpn.ui.serviceconnection.AccountRepository
import net.mullvad.mullvadvpn.ui.serviceconnection.DeviceRepository
import net.mullvad.mullvadvpn.ui.serviceconnection.ServiceConnectionManager
+import net.mullvad.mullvadvpn.util.UNKNOWN_STATE_DEBOUNCE_DELAY_MILLISECONDS
+import net.mullvad.mullvadvpn.util.addDebounceForUnknownState
import org.koin.android.ext.android.getKoin
import org.koin.core.context.loadKoinModules
@@ -159,7 +161,7 @@ open class MainActivity : FragmentActivity() {
.flowWithLifecycle(lifecycle, Lifecycle.State.RESUMED)
.debounce {
// Debounce DeviceState.Unknown to delay view transitions during reconnect.
- it.addDebounceForUnknownState()
+ it.addDebounceForUnknownState(UNKNOWN_STATE_DEBOUNCE_DELAY_MILLISECONDS)
}
.collect { newState ->
if (newState != currentState) {
@@ -178,14 +180,6 @@ open class MainActivity : FragmentActivity() {
}
}
- private fun DeviceState.addDebounceForUnknownState(): Long {
- return if (this is DeviceState.Unknown) {
- UNKNOWN_STATE_DEBOUNCE_DELAY_MILLISECONDS
- } else {
- ZERO_DEBOUNCE_DELAY_MILLISECONDS
- }
- }
-
@Suppress("DEPRECATION")
private fun requestVpnPermission() {
val intent = VpnService.prepare(this)
@@ -243,9 +237,4 @@ open class MainActivity : FragmentActivity() {
}
}
}
-
- companion object {
- private const val ZERO_DEBOUNCE_DELAY_MILLISECONDS = 0L
- private const val UNKNOWN_STATE_DEBOUNCE_DELAY_MILLISECONDS = 2000L
- }
}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/OutOfTimeFragment.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/OutOfTimeFragment.kt
index 03aa10c264..ce6c2f690f 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/OutOfTimeFragment.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/OutOfTimeFragment.kt
@@ -12,24 +12,35 @@ import kotlin.properties.Delegates.observable
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.collect
+import kotlinx.coroutines.flow.emptyFlow
+import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
import net.mullvad.mullvadvpn.R
import net.mullvad.mullvadvpn.model.TunnelState
+import net.mullvad.mullvadvpn.ui.extension.openAccountPageInBrowser
+import net.mullvad.mullvadvpn.ui.fragments.BaseFragment
import net.mullvad.mullvadvpn.ui.serviceconnection.AccountRepository
+import net.mullvad.mullvadvpn.ui.serviceconnection.ServiceConnectionManager
+import net.mullvad.mullvadvpn.ui.serviceconnection.ServiceConnectionState
+import net.mullvad.mullvadvpn.ui.serviceconnection.authTokenCache
+import net.mullvad.mullvadvpn.ui.serviceconnection.connectionProxy
import net.mullvad.mullvadvpn.ui.widget.Button
import net.mullvad.mullvadvpn.ui.widget.HeaderBar
import net.mullvad.mullvadvpn.ui.widget.RedeemVoucherButton
import net.mullvad.mullvadvpn.ui.widget.SitePaymentButton
+import net.mullvad.mullvadvpn.util.JobTracker
+import net.mullvad.mullvadvpn.util.callbackFlowFromNotifier
import net.mullvad.talpid.tunnel.ActionAfterDisconnect
import net.mullvad.talpid.tunnel.ErrorStateCause
import org.joda.time.DateTime
import org.koin.android.ext.android.inject
-class OutOfTimeFragment : ServiceDependentFragment(OnNoService.GoToLaunchScreen) {
+class OutOfTimeFragment : BaseFragment() {
// Injected dependencies
private val accountRepository: AccountRepository by inject()
+ private val serviceConnectionManager: ServiceConnectionManager by inject()
private lateinit var headerBar: HeaderBar
@@ -43,11 +54,19 @@ class OutOfTimeFragment : ServiceDependentFragment(OnNoService.GoToLaunchScreen)
headerBar.tunnelState = state
}
- override fun onSafelyCreateView(
+ @Deprecated("Refactor code to instead rely on Lifecycle.")
+ private val jobTracker = JobTracker()
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ lifecycleScope.launchUiSubscriptionsOnResume()
+ }
+
+ override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
- ): View {
+ ): View? {
val view = inflater.inflate(R.layout.out_of_time, container, false)
headerBar = view.findViewById<HeaderBar>(R.id.header_bar).apply {
@@ -55,61 +74,48 @@ class OutOfTimeFragment : ServiceDependentFragment(OnNoService.GoToLaunchScreen)
}
view.findViewById<TextView>(R.id.account_credit_has_expired).text = buildString {
- append(parentActivity.getString(R.string.account_credit_has_expired))
+ append(requireActivity().getString(R.string.account_credit_has_expired))
append(" ")
- parentActivity.getString(R.string.add_time_to_account)
+ requireActivity().getString(R.string.add_time_to_account)
}
disconnectButton = view.findViewById<Button>(R.id.disconnect).apply {
setOnClickAction("disconnect", jobTracker) {
- connectionProxy.disconnect()
+ serviceConnectionManager.connectionProxy()?.disconnect()
}
}
sitePaymentButton = view.findViewById<SitePaymentButton>(R.id.site_payment).apply {
newAccount = false
- prepare(authTokenCache, jobTracker)
+
+ setOnClickAction("openAccountPageInBrowser", jobTracker) {
+ setEnabled(false)
+ serviceConnectionManager.authTokenCache()?.fetchAuthToken()?.let { token ->
+ context.openAccountPageInBrowser(token)
+ }
+ setEnabled(true)
+ }
+
+ isEnabled = true
}
redeemButton = view.findViewById<RedeemVoucherButton>(R.id.redeem_voucher).apply {
prepare(parentFragmentManager, jobTracker)
}
- connectionProxy.onStateChange.subscribe(this) { newState ->
- jobTracker.newUiJob("updateTunnelState") {
- tunnelState = newState
- }
- }
-
return view
}
- override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
- lifecycleScope.launchUiSubscriptionsOnResume()
- }
-
- override fun onSafelyStart() {
- jobTracker.newBackgroundJob("pollAccountData") {
- while (true) {
- accountRepository.fetchAccountExpiry()
- delay(POLL_INTERVAL)
- }
- }
-
- sitePaymentButton.updateAuthTokenCache(authTokenCache)
- }
-
- override fun onSafelyStop() {
- jobTracker.cancelJob("pollAccountData")
- }
-
- override fun onSafelyDestroyView() {
- connectionProxy.onStateChange.unsubscribe(this)
+ override fun onStop() {
+ jobTracker.cancelAllJobs()
+ super.onStop()
}
private fun CoroutineScope.launchUiSubscriptionsOnResume() = launch {
repeatOnLifecycle(Lifecycle.State.RESUMED) {
launchProceedToConnectViewIfExpiryExtended()
+ launchExpiryPolling()
+ launchTunnelStateSubscription()
}
}
@@ -121,6 +127,29 @@ class OutOfTimeFragment : ServiceDependentFragment(OnNoService.GoToLaunchScreen)
}
}
+ private fun CoroutineScope.launchExpiryPolling() = launch {
+ while (true) {
+ accountRepository.fetchAccountExpiry()
+ delay(POLL_INTERVAL)
+ }
+ }
+
+ private fun CoroutineScope.launchTunnelStateSubscription() = launch {
+ serviceConnectionManager.connectionState
+ .flatMapLatest { state ->
+ if (state is ServiceConnectionState.ConnectedReady) {
+ callbackFlowFromNotifier(
+ state.container.connectionProxy.onStateChange
+ )
+ } else {
+ emptyFlow()
+ }
+ }
+ .collect { newState ->
+ tunnelState = newState
+ }
+ }
+
private fun updateDisconnectButton() {
val state = tunnelState
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/PreferencesFragment.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/PreferencesFragment.kt
index b3e85a94cc..5b76def952 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/PreferencesFragment.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/PreferencesFragment.kt
@@ -4,59 +4,107 @@ import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.lifecycleScope
+import androidx.lifecycle.repeatOnLifecycle
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.collect
+import kotlinx.coroutines.flow.emptyFlow
+import kotlinx.coroutines.flow.flatMapLatest
+import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.launch
import net.mullvad.mullvadvpn.R
import net.mullvad.mullvadvpn.model.Settings
+import net.mullvad.mullvadvpn.ui.extension.requireMainActivity
+import net.mullvad.mullvadvpn.ui.fragments.BaseFragment
+import net.mullvad.mullvadvpn.ui.serviceconnection.ServiceConnectionManager
+import net.mullvad.mullvadvpn.ui.serviceconnection.ServiceConnectionState
+import net.mullvad.mullvadvpn.ui.serviceconnection.settingsListener
import net.mullvad.mullvadvpn.ui.widget.CellSwitch
import net.mullvad.mullvadvpn.ui.widget.ToggleCell
+import net.mullvad.mullvadvpn.util.JobTracker
+import net.mullvad.mullvadvpn.util.callbackFlowFromNotifier
+import org.koin.android.ext.android.inject
+
+class PreferencesFragment : BaseFragment() {
+
+ // Injected dependencies
+ private val serviceConnectionManager: ServiceConnectionManager by inject()
-class PreferencesFragment : ServiceDependentFragment(OnNoService.GoBack) {
private lateinit var allowLanToggle: ToggleCell
private lateinit var autoConnectToggle: ToggleCell
private lateinit var titleController: CollapsibleTitleController
- override fun onSafelyCreateView(
+ @Deprecated("Refactor code to instead rely on Lifecycle.")
+ private val jobTracker = JobTracker()
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ lifecycleScope.launchUiSubscriptionsOnResume()
+ }
+
+ override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
- ): View {
+ ): View? {
val view = inflater.inflate(R.layout.preferences, container, false)
view.findViewById<View>(R.id.back).setOnClickListener {
- parentActivity.onBackPressed()
+ requireMainActivity().onBackPressed()
}
allowLanToggle = view.findViewById<ToggleCell>(R.id.allow_lan).apply {
listener = { state ->
- when (state) {
- CellSwitch.State.ON -> settingsListener.allowLan = true
- CellSwitch.State.OFF -> settingsListener.allowLan = false
+ serviceConnectionManager.settingsListener()?.allowLan = when (state) {
+ CellSwitch.State.ON -> true
+ else -> false
}
}
}
autoConnectToggle = view.findViewById<ToggleCell>(R.id.auto_connect).apply {
listener = { state ->
- when (state) {
- CellSwitch.State.ON -> settingsListener.autoConnect = true
- CellSwitch.State.OFF -> settingsListener.autoConnect = false
+ serviceConnectionManager.settingsListener()?.autoConnect = when (state) {
+ CellSwitch.State.ON -> true
+ else -> false
}
}
}
- settingsListener.settingsNotifier.subscribe(this) { maybeSettings ->
- maybeSettings?.let { settings ->
- updateUi(settings)
- }
- }
-
titleController = CollapsibleTitleController(view)
return view
}
- override fun onSafelyDestroyView() {
+ override fun onDestroyView() {
titleController.onDestroy()
- settingsListener.settingsNotifier.unsubscribe(this)
+ super.onDestroyView()
+ }
+
+ private fun CoroutineScope.launchUiSubscriptionsOnResume() = launch {
+ repeatOnLifecycle(Lifecycle.State.RESUMED) {
+ launchSettingsSubscription()
+ }
+ }
+
+ private fun CoroutineScope.launchSettingsSubscription() = launch {
+ serviceConnectionManager.connectionState
+ .flatMapLatest { state ->
+ if (state is ServiceConnectionState.ConnectedReady) {
+ flowOf(state.container)
+ } else {
+ emptyFlow()
+ }
+ }
+ .flatMapLatest {
+ callbackFlowFromNotifier(it.settingsListener.settingsNotifier)
+ }
+ .collect { settings ->
+ if (settings != null) {
+ updateUi(settings)
+ }
+ }
}
private fun updateUi(settings: Settings) {
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/SelectLocationFragment.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/SelectLocationFragment.kt
index 68df75417a..da33465ea5 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/SelectLocationFragment.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/SelectLocationFragment.kt
@@ -10,19 +10,38 @@ import android.view.animation.Animation.AnimationListener
import android.view.animation.AnimationUtils
import android.widget.ImageButton
import androidx.core.content.ContextCompat
+import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
+import androidx.lifecycle.repeatOnLifecycle
import androidx.recyclerview.widget.LinearLayoutManager
import kotlinx.coroutines.CompletableDeferred
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.collect
+import kotlinx.coroutines.flow.emptyFlow
+import kotlinx.coroutines.flow.flatMapLatest
+import kotlinx.coroutines.launch
import net.mullvad.mullvadvpn.R
import net.mullvad.mullvadvpn.relaylist.RelayItem
import net.mullvad.mullvadvpn.relaylist.RelayList
import net.mullvad.mullvadvpn.relaylist.RelayListAdapter
+import net.mullvad.mullvadvpn.ui.extension.requireMainActivity
+import net.mullvad.mullvadvpn.ui.fragments.BaseFragment
+import net.mullvad.mullvadvpn.ui.serviceconnection.ServiceConnectionManager
+import net.mullvad.mullvadvpn.ui.serviceconnection.ServiceConnectionState
+import net.mullvad.mullvadvpn.ui.serviceconnection.connectionProxy
+import net.mullvad.mullvadvpn.ui.serviceconnection.relayListListener
import net.mullvad.mullvadvpn.ui.widget.CustomRecyclerView
import net.mullvad.mullvadvpn.util.AdapterWithHeader
+import net.mullvad.mullvadvpn.util.JobTracker
+import org.koin.android.ext.android.inject
+
+class SelectLocationFragment : BaseFragment(), StatusBarPainter, NavigationBarPainter {
+
+ // Injected dependencies
+ private val serviceConnectionManager: ServiceConnectionManager by inject()
-class SelectLocationFragment :
- ServiceDependentFragment(OnNoService.GoToLaunchScreen), StatusBarPainter, NavigationBarPainter {
private enum class RelayListState {
Initializing,
Loading,
@@ -35,28 +54,32 @@ class SelectLocationFragment :
private var loadingSpinner = CompletableDeferred<View>()
private var relayListState = RelayListState.Initializing
+ @Deprecated("Refactor code to instead rely on Lifecycle.")
+ private val jobTracker = JobTracker()
+
override fun onAttach(context: Context) {
super.onAttach(context)
relayListAdapter = RelayListAdapter(context.resources).apply {
onSelect = { relayItem ->
- jobTracker.newBackgroundJob("selectRelay") {
- relayListListener.selectedRelayLocation = relayItem?.location
- connectionProxy.connect()
-
- jobTracker.newUiJob("close") {
- close()
- }
- }
+ serviceConnectionManager.relayListListener()?.selectedRelayLocation =
+ relayItem?.location
+ serviceConnectionManager.connectionProxy()?.connect()
+ close()
}
}
}
- override fun onSafelyCreateView(
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ lifecycleScope.launchUiSubscriptionsOnResume()
+ }
+
+ override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
- ): View {
+ ): View? {
val view = inflater.inflate(R.layout.select_location, container, false)
view.findViewById<ImageButton>(R.id.close).setOnClickListener { close() }
@@ -64,7 +87,7 @@ class SelectLocationFragment :
titleController = CollapsibleTitleController(view, R.id.relay_list)
view.findViewById<CustomRecyclerView>(R.id.relay_list).apply {
- layoutManager = LinearLayoutManager(parentActivity)
+ layoutManager = LinearLayoutManager(requireMainActivity())
adapter = AdapterWithHeader(relayListAdapter, R.layout.select_location_header).apply {
onHeaderAvailable = { headerView ->
@@ -83,68 +106,75 @@ class SelectLocationFragment :
return view
}
- override fun onSafelyStart() {
- // If the relay list is immediately available, setting the listener will cause it to be
- // called right away, while the state is still Initializing. In that case we can skip
- // showing the spinner animation and go directly to the Visible state.
- //
- // If it's not immediately available, then when the listener is called later the state will
- // have changed to Loading, and an animation from the spinner to the new relay items will be
- // shown.
- //
- // If the state is ready, it means that the relay list has already been shown, and we can
- // update it in place.
- relayListListener.onRelayListChange = { relayList, selectedItem ->
- when (relayListState) {
- RelayListState.Initializing -> {
- jobTracker.newUiJob("updateRelayList") {
- updateRelayList(relayList, selectedItem)
- }
-
- relayListState = RelayListState.Visible
- }
- RelayListState.Loading -> {
- jobTracker.newUiJob("updateRelayList") {
- animateRelayListInitialization(relayList, selectedItem)
- }
- }
- RelayListState.Visible -> {
- jobTracker.newUiJob("updateRelayList") {
- updateRelayList(relayList, selectedItem)
- }
- }
- }
- }
-
- if (relayListState == RelayListState.Initializing) {
- relayListState = RelayListState.Loading
- }
+ override fun onResume() {
+ super.onResume()
+ paintNavigationBar(ContextCompat.getColor(requireContext(), R.color.darkBlue))
}
- override fun onSafelyStop() {
- relayListListener.onRelayListChange = null
+ override fun onDestroyView() {
+ titleController.onDestroy()
+ super.onDestroyView()
}
- override fun onSafelyDestroyView() {
- titleController.onDestroy()
+ fun close() {
+ activity?.onBackPressed()
}
- override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
- super.onViewCreated(view, savedInstanceState)
- lifecycleScope.launchWhenResumed {
- transitionFinishedFlow.collect {
- paintStatusBar(ContextCompat.getColor(requireContext(), R.color.darkBlue))
- }
+ private fun CoroutineScope.launchUiSubscriptionsOnResume() = launch {
+ repeatOnLifecycle(Lifecycle.State.RESUMED) {
+ launchPaintStatusBarAfterTransition()
+ launchRelayListSubscription()
}
}
- override fun onResume() {
- super.onResume()
- paintNavigationBar(ContextCompat.getColor(requireContext(), R.color.darkBlue))
+ private fun CoroutineScope.launchPaintStatusBarAfterTransition() = launch {
+ transitionFinishedFlow.collect {
+ paintStatusBar(ContextCompat.getColor(requireContext(), R.color.darkBlue))
+ }
}
- fun close() {
- activity?.onBackPressed()
+ private fun CoroutineScope.launchRelayListSubscription() = launch {
+ serviceConnectionManager.connectionState
+ .flatMapLatest { state ->
+ if (state is ServiceConnectionState.ConnectedReady) {
+ callbackFlow {
+ state.container.relayListListener.onRelayListChange =
+ { list, item ->
+ this.trySend(Pair(list, item))
+ }
+
+ awaitClose {
+ state.container.relayListListener.onRelayListChange = null
+ }
+ }
+ } else {
+ emptyFlow()
+ }
+ }
+ .collect { (relayList, selectedItem) ->
+ when (relayListState) {
+ RelayListState.Initializing -> {
+ jobTracker.newUiJob("updateRelayList") {
+ updateRelayList(relayList, selectedItem)
+ }
+ relayListState = RelayListState.Visible
+ }
+ RelayListState.Loading -> {
+ jobTracker.newUiJob("updateRelayList") {
+ animateRelayListInitialization(relayList, selectedItem)
+ }
+ }
+ RelayListState.Visible -> {
+ jobTracker.newUiJob("updateRelayList") {
+ updateRelayList(relayList, selectedItem)
+ }
+ }
+ }
+
+ if (relayListState == RelayListState.Initializing) {
+ relayListState = RelayListState.Loading
+ }
+ }
}
private fun updateRelayList(relayList: RelayList, selectedItem: RelayItem?) {
@@ -181,9 +211,10 @@ class SelectLocationFragment :
override fun onAnimationRepeat(animation: Animation) {}
}
- val fadeOut = AnimationUtils.loadAnimation(parentActivity, R.anim.fade_out).apply {
- setAnimationListener(animationListener)
- }
+ val fadeOut =
+ AnimationUtils.loadAnimation(requireMainActivity(), R.anim.fade_out).apply {
+ setAnimationListener(animationListener)
+ }
loadingSpinner.await().let { spinner ->
spinner.startAnimation(fadeOut)
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
deleted file mode 100644
index 32ad70daaa..0000000000
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/ServiceAwareFragment.kt
+++ /dev/null
@@ -1,69 +0,0 @@
-package net.mullvad.mullvadvpn.ui
-
-import android.content.Context
-import net.mullvad.mullvadvpn.ui.fragments.BaseFragment
-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
-
- lateinit var parentActivity: MainActivity
- private set
-
- var serviceConnectionContainer: ServiceConnectionContainer? = null
- private set
-
- override fun onAttach(context: Context) {
- super.onAttach(context)
-
- parentActivity = context as MainActivity
-
- if (isSecureScreen) {
- parentActivity.enterSecureScreen(this)
- }
-
- serviceConnectionManager.serviceNotifier.subscribe(this) { connection ->
- configureServiceConnection(connection)
- }
- }
-
- override fun onDestroyView() {
- jobTracker.cancelAllJobs()
-
- super.onDestroyView()
- }
-
- override fun onDetach() {
- serviceConnectionManager.serviceNotifier.unsubscribe(this)
-
- if (isSecureScreen) {
- parentActivity.leaveSecureScreen(this)
- }
-
- super.onDetach()
- }
-
- abstract fun onNewServiceConnection(serviceConnectionContainer: ServiceConnectionContainer)
-
- open fun onNoServiceConnection() {
- }
-
- private fun configureServiceConnection(
- serviceConnectionContainer: ServiceConnectionContainer?
- ) {
- this.serviceConnectionContainer = serviceConnectionContainer
-
- 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
deleted file mode 100644
index be2998cfad..0000000000
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/ServiceDependentFragment.kt
+++ /dev/null
@@ -1,192 +0,0 @@
-package net.mullvad.mullvadvpn.ui
-
-import android.os.Bundle
-import android.view.LayoutInflater
-import android.view.View
-import android.view.ViewGroup
-import net.mullvad.mullvadvpn.R
-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.LocationInfoCache
-import net.mullvad.mullvadvpn.ui.serviceconnection.RelayListListener
-import net.mullvad.mullvadvpn.ui.serviceconnection.ServiceConnectionContainer
-import net.mullvad.mullvadvpn.ui.serviceconnection.SettingsListener
-import net.mullvad.mullvadvpn.ui.serviceconnection.SplitTunneling
-
-abstract class ServiceDependentFragment(private val onNoService: OnNoService) :
- ServiceAwareFragment() {
- enum class OnNoService {
- GoBack, GoToLaunchScreen
- }
-
- enum class State {
- Uninitialized,
- Initialized,
- Active,
- Stopped,
- LostConnection
- }
-
- private var state = State.Uninitialized
-
- lateinit var appVersionInfoCache: AppVersionInfoCache
- private set
-
- lateinit var authTokenCache: AuthTokenCache
- private set
-
- lateinit var connectionProxy: ConnectionProxy
- private set
-
- lateinit var customDns: CustomDns
- private set
-
- lateinit var locationInfoCache: LocationInfoCache
- private set
-
- lateinit var relayListListener: RelayListListener
- private set
-
- lateinit var settingsListener: SettingsListener
- private set
-
- lateinit var splitTunneling: SplitTunneling
- private set
-
- 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
- appVersionInfoCache = serviceConnectionContainer.appVersionInfoCache
- authTokenCache = serviceConnectionContainer.authTokenCache
- connectionProxy = serviceConnectionContainer.connectionProxy
- customDns = serviceConnectionContainer.customDns
- locationInfoCache = serviceConnectionContainer.locationInfoCache
- relayListListener = serviceConnectionContainer.relayListListener
- settingsListener = serviceConnectionContainer.settingsListener
-
- splitTunneling = serviceConnectionContainer.splitTunneling
-
- synchronized(this) {
- when (state) {
- State.Uninitialized -> state = State.Initialized
- State.Active -> {
- onSafelyStop()
- onSafelyStart()
- }
- else -> Unit
- }
- }
- }
-
- override fun onNoServiceConnection() {
- synchronized(this) {
- when (state) {
- State.Uninitialized -> {
- state = State.LostConnection
- leaveFragment()
- }
- State.Active -> {
- state = State.LostConnection
- leaveFragment()
- }
- else -> Unit
- }
- }
- }
-
- override fun onCreateView(
- inflater: LayoutInflater,
- container: ViewGroup?,
- savedInstanceState: Bundle?
- ): View {
- synchronized(this) {
- return when (state) {
- State.Initialized, State.Active, State.Stopped -> {
- onSafelyCreateView(inflater, container, savedInstanceState)
- }
- State.Uninitialized, State.LostConnection -> {
- inflater.inflate(R.layout.missing_service, container, false)
- }
- }
- }
- }
-
- override fun onStart() {
- super.onStart()
-
- synchronized(this) {
- when (state) {
- State.Initialized, State.Stopped -> {
- state = State.Active
- onSafelyStart()
- }
- else -> Unit
- }
- }
- }
-
- override fun onSaveInstanceState(instanceState: Bundle) {
- synchronized(this) {
- when (state) {
- State.Initialized, State.Stopped, State.Active -> {
- onSafelySaveInstanceState(instanceState)
- }
- else -> Unit
- }
- }
- }
-
- override fun onStop() {
- synchronized(this) {
- when (state) {
- State.Initialized, State.Active -> {
- onSafelyStop()
- state = State.Stopped
- }
- else -> Unit
- }
- }
-
- super.onStop()
- }
-
- override fun onDestroyView() {
- synchronized(this) {
- when (state) {
- State.Initialized, State.Stopped, State.Active -> onSafelyDestroyView()
- else -> Unit
- }
- }
-
- super.onDestroyView()
- }
-
- abstract fun onSafelyCreateView(
- inflater: LayoutInflater,
- container: ViewGroup?,
- savedInstanceState: Bundle?
- ): View
-
- open fun onSafelyStart() {
- }
-
- open fun onSafelySaveInstanceState(state: Bundle) {
- }
-
- open fun onSafelyStop() {
- }
-
- open fun onSafelyDestroyView() {
- }
-
- private fun leaveFragment() {
- jobTracker.newUiJob("leaveFragment") {
- when (onNoService) {
- OnNoService.GoBack -> parentActivity.onBackPressed()
- OnNoService.GoToLaunchScreen -> parentActivity.returnToLaunchScreen()
- }
- }
- }
-}
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 e0f619550c..710f894044 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
@@ -11,23 +11,35 @@ import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.collect
+import kotlinx.coroutines.flow.debounce
+import kotlinx.coroutines.flow.emptyFlow
+import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.launch
import net.mullvad.mullvadvpn.R
import net.mullvad.mullvadvpn.model.DeviceState
+import net.mullvad.mullvadvpn.ui.fragments.BaseFragment
import net.mullvad.mullvadvpn.ui.serviceconnection.AccountRepository
-import net.mullvad.mullvadvpn.ui.serviceconnection.AppVersionInfoCache
import net.mullvad.mullvadvpn.ui.serviceconnection.DeviceRepository
-import net.mullvad.mullvadvpn.ui.serviceconnection.ServiceConnectionContainer
+import net.mullvad.mullvadvpn.ui.serviceconnection.ServiceConnectionManager
+import net.mullvad.mullvadvpn.ui.serviceconnection.ServiceConnectionState
+import net.mullvad.mullvadvpn.ui.serviceconnection.appVersionInfoCache
import net.mullvad.mullvadvpn.ui.widget.AccountCell
import net.mullvad.mullvadvpn.ui.widget.AppVersionCell
import net.mullvad.mullvadvpn.ui.widget.NavigateCell
+import net.mullvad.mullvadvpn.util.JobTracker
+import net.mullvad.mullvadvpn.util.UNKNOWN_STATE_DEBOUNCE_DELAY_MILLISECONDS
+import net.mullvad.mullvadvpn.util.addDebounceForUnknownState
+import net.mullvad.mullvadvpn.util.appVersionCallbackFlow
import org.koin.android.ext.android.inject
-class SettingsFragment : ServiceAwareFragment(), StatusBarPainter, NavigationBarPainter {
+class SettingsFragment : BaseFragment(), StatusBarPainter, NavigationBarPainter {
+
+ // Injected dependencies
private val accountRepository: AccountRepository by inject()
private val deviceRepository: DeviceRepository by inject()
+ private val serviceConnectionManager: ServiceConnectionManager by inject()
private lateinit var accountMenu: AccountCell
private lateinit var appVersionMenu: AppVersionCell
@@ -35,20 +47,12 @@ class SettingsFragment : ServiceAwareFragment(), StatusBarPainter, NavigationBar
private lateinit var advancedMenu: View
private lateinit var titleController: CollapsibleTitleController
- private var active = false
-
- private var versionInfoCache: AppVersionInfoCache? = null
-
- override fun onNewServiceConnection(serviceConnectionContainer: ServiceConnectionContainer) {
- versionInfoCache = serviceConnectionContainer.appVersionInfoCache
-
- if (active) {
- configureListeners()
- }
- }
+ @Deprecated("Refactor code to instead rely on Lifecycle.")
+ private val jobTracker = JobTracker()
- override fun onNoServiceConnection() {
- versionInfoCache = null
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ lifecycleScope.launchUiSubscriptionsOnResume()
}
override fun onCreateView(
@@ -86,7 +90,7 @@ class SettingsFragment : ServiceAwareFragment(), StatusBarPainter, NavigationBar
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
- lifecycleScope.launchUiSubscriptionsOnResume()
+ initializeUiState()
}
override fun onResume() {
@@ -94,25 +98,38 @@ class SettingsFragment : ServiceAwareFragment(), StatusBarPainter, NavigationBar
paintNavigationBar(ContextCompat.getColor(requireContext(), R.color.darkBlue))
}
- override fun onStart() {
- super.onStart()
-
- configureListeners()
- active = true
- }
-
override fun onStop() {
- active = false
- versionInfoCache?.onUpdate = null
-
jobTracker.cancelAllJobs()
-
super.onStop()
}
override fun onDestroyView() {
- super.onDestroyView()
titleController.onDestroy()
+ super.onDestroyView()
+ }
+
+ private fun initializeUiState() {
+ updateLoggedInStatus(deviceRepository.deviceState.value is DeviceState.LoggedIn)
+ accountMenu.accountExpiry = accountRepository.accountExpiryState.value.date()
+ serviceConnectionManager.appVersionInfoCache().let { cache ->
+ updateVersionInfo(
+ if (cache != null) {
+ VersionInfo(
+ currentVersion = cache.version,
+ upgradeVersion = cache.upgradeVersion,
+ isOutdated = cache.isOutdated,
+ isSupported = cache.isSupported
+ )
+ } else {
+ VersionInfo(
+ currentVersion = null,
+ upgradeVersion = null,
+ isOutdated = false,
+ isSupported = true
+ )
+ }
+ )
+ }
}
private fun CoroutineScope.launchUiSubscriptionsOnResume() = launch {
@@ -120,6 +137,7 @@ class SettingsFragment : ServiceAwareFragment(), StatusBarPainter, NavigationBar
launchPaintStatusBarAfterTransition()
luanchConfigureMenuOnDeviceChanges()
launchUpdateExpiryTextOnExpiryChanges()
+ launchVersionInfoSubscription()
}
}
@@ -131,6 +149,9 @@ class SettingsFragment : ServiceAwareFragment(), StatusBarPainter, NavigationBar
private fun CoroutineScope.luanchConfigureMenuOnDeviceChanges() = launch {
deviceRepository.deviceState
+ .debounce {
+ it.addDebounceForUnknownState(UNKNOWN_STATE_DEBOUNCE_DELAY_MILLISECONDS)
+ }
.collect { device ->
updateLoggedInStatus(device is DeviceState.LoggedIn)
}
@@ -145,12 +166,18 @@ class SettingsFragment : ServiceAwareFragment(), StatusBarPainter, NavigationBar
}
}
- private fun configureListeners() {
- versionInfoCache?.onUpdate = {
- jobTracker.newUiJob("updateVersionInfo") {
- updateVersionInfo()
+ private fun CoroutineScope.launchVersionInfoSubscription() = launch {
+ serviceConnectionManager.connectionState
+ .flatMapLatest { state ->
+ if (state is ServiceConnectionState.ConnectedReady) {
+ state.container.appVersionInfoCache.appVersionCallbackFlow()
+ } else {
+ emptyFlow()
+ }
+ }
+ .collect { versionInfo ->
+ updateVersionInfo(versionInfo)
}
- }
}
private fun updateLoggedInStatus(loggedIn: Boolean) {
@@ -165,11 +192,10 @@ class SettingsFragment : ServiceAwareFragment(), StatusBarPainter, NavigationBar
advancedMenu.visibility = visibility
}
- private fun updateVersionInfo() {
- val isOutdated = versionInfoCache?.isOutdated ?: false
- val isSupported = versionInfoCache?.isSupported ?: true
-
- appVersionMenu.updateAvailable = isOutdated || !isSupported
- appVersionMenu.version = versionInfoCache?.version ?: ""
+ private fun updateVersionInfo(
+ versionInfo: VersionInfo
+ ) {
+ appVersionMenu.updateAvailable = versionInfo.isOutdated || !versionInfo.isSupported
+ appVersionMenu.version = versionInfo.currentVersion ?: ""
}
}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/VersionInfo.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/VersionInfo.kt
new file mode 100644
index 0000000000..ac52959374
--- /dev/null
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/VersionInfo.kt
@@ -0,0 +1,8 @@
+package net.mullvad.mullvadvpn.ui
+
+data class VersionInfo(
+ val currentVersion: String?,
+ val upgradeVersion: String?,
+ val isOutdated: Boolean,
+ val isSupported: Boolean
+)
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 3acf9b3eee..925fe67b43 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
@@ -15,36 +15,58 @@ import androidx.lifecycle.repeatOnLifecycle
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.collect
+import kotlinx.coroutines.flow.debounce
+import kotlinx.coroutines.flow.emptyFlow
+import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.launch
import net.mullvad.mullvadvpn.R
import net.mullvad.mullvadvpn.model.TunnelState
+import net.mullvad.mullvadvpn.ui.extension.openAccountPageInBrowser
+import net.mullvad.mullvadvpn.ui.fragments.BaseFragment
import net.mullvad.mullvadvpn.ui.serviceconnection.AccountRepository
import net.mullvad.mullvadvpn.ui.serviceconnection.DeviceRepository
+import net.mullvad.mullvadvpn.ui.serviceconnection.ServiceConnectionManager
+import net.mullvad.mullvadvpn.ui.serviceconnection.ServiceConnectionState
+import net.mullvad.mullvadvpn.ui.serviceconnection.authTokenCache
import net.mullvad.mullvadvpn.ui.widget.HeaderBar
import net.mullvad.mullvadvpn.ui.widget.RedeemVoucherButton
import net.mullvad.mullvadvpn.ui.widget.SitePaymentButton
+import net.mullvad.mullvadvpn.util.JobTracker
+import net.mullvad.mullvadvpn.util.UNKNOWN_STATE_DEBOUNCE_DELAY_MILLISECONDS
+import net.mullvad.mullvadvpn.util.addDebounceForUnknownState
+import net.mullvad.mullvadvpn.util.callbackFlowFromNotifier
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) {
+class WelcomeFragment : BaseFragment() {
// Injected dependencies
private val accountRepository: AccountRepository by inject()
private val deviceRepository: DeviceRepository by inject()
+ private val serviceConnectionManager: ServiceConnectionManager by inject()
private lateinit var accountLabel: TextView
+ private lateinit var headerBar: HeaderBar
private lateinit var sitePaymentButton: SitePaymentButton
- override fun onSafelyCreateView(
+ @Deprecated("Refactor code to instead rely on Lifecycle.")
+ private val jobTracker = JobTracker()
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ lifecycleScope.launchUiSubscriptionsOnResume()
+ }
+
+ override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
- ): View {
+ ): View? {
val view = inflater.inflate(R.layout.welcome, container, false)
- view.findViewById<HeaderBar>(R.id.header_bar).apply {
+ headerBar = view.findViewById<HeaderBar>(R.id.header_bar).apply {
tunnelState = TunnelState.Disconnected
}
@@ -53,14 +75,21 @@ class WelcomeFragment : ServiceDependentFragment(OnNoService.GoToLaunchScreen) {
}
view.findViewById<TextView>(R.id.pay_to_start_using).text = buildString {
- append(parentActivity.getString(R.string.pay_to_start_using))
+ append(requireActivity().getString(R.string.pay_to_start_using))
append(" ")
- append(parentActivity.getString(R.string.add_time_to_account))
+ append(requireActivity().getString(R.string.add_time_to_account))
}
sitePaymentButton = view.findViewById<SitePaymentButton>(R.id.site_payment).apply {
newAccount = true
- prepare(authTokenCache, jobTracker)
+
+ setOnClickAction("openAccountPageInBrowser", jobTracker) {
+ setEnabled(false)
+ serviceConnectionManager.authTokenCache()?.fetchAuthToken()?.let { token ->
+ context.openAccountPageInBrowser(token)
+ }
+ setEnabled(true)
+ }
}
view.findViewById<RedeemVoucherButton>(R.id.redeem_voucher).apply {
@@ -70,34 +99,23 @@ class WelcomeFragment : ServiceDependentFragment(OnNoService.GoToLaunchScreen) {
return view
}
- override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
- lifecycleScope.launchUiSubscriptionsOnResume()
- }
-
- override fun onSafelyStart() {
- jobTracker.newBackgroundJob("pollAccountData") {
- while (true) {
- accountRepository.fetchAccountExpiry()
- delay(POLL_INTERVAL)
- }
- }
-
- sitePaymentButton.updateAuthTokenCache(authTokenCache)
- }
-
- override fun onSafelyStop() {
- jobTracker.cancelJob("pollAccountData")
+ override fun onStop() {
+ jobTracker.cancelAllJobs()
+ super.onStop()
}
private fun CoroutineScope.launchUiSubscriptionsOnResume() = launch {
repeatOnLifecycle(Lifecycle.State.RESUMED) {
launchUpdateAccountNumberOnDeviceChanges()
launchAdvanceToConnectViewOnExpiryExtended()
+ launchExpiryPolling()
+ launchTunnelStateSubscription()
}
}
private fun CoroutineScope.launchUpdateAccountNumberOnDeviceChanges() = launch {
deviceRepository.deviceState
+ .debounce { it.addDebounceForUnknownState(UNKNOWN_STATE_DEBOUNCE_DELAY_MILLISECONDS) }
.collect { state ->
updateAccountNumber(state.token())
}
@@ -109,6 +127,32 @@ class WelcomeFragment : ServiceDependentFragment(OnNoService.GoToLaunchScreen) {
}
}
+ private fun CoroutineScope.launchExpiryPolling() = launch {
+ while (true) {
+ accountRepository.fetchAccountExpiry()
+ delay(POLL_INTERVAL)
+ }
+ }
+
+ private fun CoroutineScope.launchTunnelStateSubscription() = launch {
+ serviceConnectionManager.connectionState
+ .flatMapLatest { state ->
+ if (state is ServiceConnectionState.ConnectedReady) {
+ callbackFlowFromNotifier(
+ state.container.connectionProxy.onStateChange
+ )
+ } else {
+ emptyFlow()
+ }
+ }
+ .collect { state -> updateUiForTunnelState(state) }
+ }
+
+ private fun updateUiForTunnelState(tunnelState: TunnelState) {
+ headerBar.tunnelState = tunnelState
+ sitePaymentButton.isEnabled = tunnelState is TunnelState.Disconnected
+ }
+
private fun updateAccountNumber(rawAccountNumber: String?) {
val accountText = rawAccountNumber?.let { account ->
addSpacesToAccountText(account)
@@ -142,9 +186,7 @@ class WelcomeFragment : ServiceDependentFragment(OnNoService.GoToLaunchScreen) {
val tomorrow = DateTime.now().plusDays(1)
if (expiry.isAfter(tomorrow)) {
- jobTracker.newUiJob("advanceToConnectScreen") {
- advanceToConnectScreen()
- }
+ advanceToConnectScreen()
}
}
}
@@ -161,7 +203,7 @@ class WelcomeFragment : ServiceDependentFragment(OnNoService.GoToLaunchScreen) {
val clipboardLabel = resources.getString(R.string.mullvad_account_number)
val toastMessage = resources.getString(R.string.copied_mullvad_account_number)
- val context = parentActivity
+ val context = requireActivity()
val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
val clipData = ClipData.newPlainText(clipboardLabel, accountToken)
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/extension/ContextExtensions.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/extension/ContextExtensions.kt
new file mode 100644
index 0000000000..6314c3eaef
--- /dev/null
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/extension/ContextExtensions.kt
@@ -0,0 +1,27 @@
+package net.mullvad.mullvadvpn.ui.extension
+
+import android.content.Context
+import android.content.Intent
+import android.net.Uri
+import androidx.fragment.app.Fragment
+import net.mullvad.mullvadvpn.R
+import net.mullvad.mullvadvpn.ui.MainActivity
+
+fun Context.openAccountPageInBrowser(authToken: String) {
+ startActivity(
+ Intent(
+ Intent.ACTION_VIEW,
+ Uri.parse(getString(R.string.account_url) + "?token=$authToken")
+ )
+ )
+}
+
+fun Fragment.requireMainActivity(): MainActivity {
+ return if (this.activity is MainActivity) {
+ this.activity as MainActivity
+ } else {
+ throw IllegalStateException(
+ "Fragment $this not attached to ${MainActivity::class.simpleName}."
+ )
+ }
+}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/notification/AccountExpiryNotification.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/notification/AccountExpiryNotification.kt
index 5db4c17996..38c8f8ed90 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/notification/AccountExpiryNotification.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/notification/AccountExpiryNotification.kt
@@ -1,18 +1,13 @@
package net.mullvad.mullvadvpn.ui.notification
import android.content.Context
-import kotlinx.coroutines.flow.collect
import net.mullvad.mullvadvpn.R
-import net.mullvad.mullvadvpn.ui.serviceconnection.AccountRepository
-import net.mullvad.mullvadvpn.ui.serviceconnection.AuthTokenCache
import net.mullvad.mullvadvpn.util.TimeLeftFormatter
import org.joda.time.DateTime
class AccountExpiryNotification(
context: Context,
- authTokenCache: AuthTokenCache,
- private val accountRepository: AccountRepository
-) : NotificationWithUrlWithToken(context, authTokenCache, R.string.account_url) {
+) : InAppNotification() {
private val timeLeftFormatter = TimeLeftFormatter(context.resources)
init {
@@ -20,19 +15,7 @@ class AccountExpiryNotification(
title = context.getString(R.string.account_credit_expires_soon)
}
- override fun onResume() {
- jobTracker.newUiJob("updateAccountExpiry") {
- accountRepository.accountExpiryState.collect { state ->
- updateAccountExpiry(state.date())
- }
- }
- }
-
- override fun onPause() {
- jobTracker.cancelJob("updateAccountExpiry")
- }
-
- private fun updateAccountExpiry(expiry: DateTime?) {
+ fun updateAccountExpiry(expiry: DateTime?) {
val threeDaysFromNow = DateTime.now().plusDays(3)
if (expiry != null && expiry.isBefore(threeDaysFromNow)) {
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/notification/InAppNotification.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/notification/InAppNotification.kt
index aa58b0bbf5..af4d34e9c1 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/notification/InAppNotification.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/notification/InAppNotification.kt
@@ -19,7 +19,6 @@ abstract class InAppNotification {
protected set
var onClick by changeMonitor.monitor<(suspend () -> Unit)?>(null)
- protected set
var showIcon by changeMonitor.monitor(false)
protected set
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/notification/TunnelStateNotification.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/notification/TunnelStateNotification.kt
index 8c26c5dc1e..10927cb5e7 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/notification/TunnelStateNotification.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/notification/TunnelStateNotification.kt
@@ -3,7 +3,6 @@ package net.mullvad.mullvadvpn.ui.notification
import android.content.Context
import net.mullvad.mullvadvpn.R
import net.mullvad.mullvadvpn.model.TunnelState
-import net.mullvad.mullvadvpn.ui.serviceconnection.ConnectionProxy
import net.mullvad.talpid.tunnel.ActionAfterDisconnect
import net.mullvad.talpid.tunnel.ErrorState
import net.mullvad.talpid.tunnel.ErrorStateCause
@@ -12,7 +11,6 @@ import net.mullvad.talpid.util.addressString
class TunnelStateNotification(
private val context: Context,
- private val connectionProxy: ConnectionProxy
) : InAppNotification() {
private val blockingTitle = context.getString(R.string.blocking_internet)
private val notBlockingTitle = context.getString(R.string.not_blocking_internet)
@@ -23,19 +21,7 @@ class TunnelStateNotification(
showIcon = false
}
- override fun onResume() {
- connectionProxy.onStateChange.subscribe(this) { tunnelState ->
- jobTracker.newUiJob("updateTunnelState") {
- updateTunnelState(tunnelState)
- }
- }
- }
-
- override fun onPause() {
- connectionProxy.onStateChange.unsubscribe(this)
- }
-
- private fun updateTunnelState(state: TunnelState) {
+ fun updateTunnelState(state: TunnelState) {
when (state) {
is TunnelState.Disconnecting -> {
when (state.actionAfterDisconnect) {
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/notification/VersionInfoNotification.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/notification/VersionInfoNotification.kt
index 8a8104290f..d85f70ca5b 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/notification/VersionInfoNotification.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/notification/VersionInfoNotification.kt
@@ -2,37 +2,20 @@ package net.mullvad.mullvadvpn.ui.notification
import android.content.Context
import net.mullvad.mullvadvpn.R
-import net.mullvad.mullvadvpn.ui.serviceconnection.AppVersionInfoCache
+import net.mullvad.mullvadvpn.ui.VersionInfo
class VersionInfoNotification(
- context: Context,
- private val versionInfoCache: AppVersionInfoCache
+ context: Context
) : NotificationWithUrl(context, R.string.download_url) {
private val unsupportedVersion = context.getString(R.string.unsupported_version)
private val updateAvailable = context.getString(R.string.update_available)
- override fun onResume() {
- versionInfoCache.onUpdate = {
- jobTracker.newUiJob("updateVersionInfo") {
- updateVersionInfo(
- versionInfoCache.isOutdated,
- versionInfoCache.isSupported,
- versionInfoCache.upgradeVersion
- )
- }
- }
- }
-
- override fun onPause() {
- versionInfoCache.onUpdate = null
- }
-
- private fun updateVersionInfo(isOutdated: Boolean, isSupported: Boolean, upgrade: String?) {
- if (isOutdated || !isSupported) {
- if (upgrade != null) {
+ fun updateVersionInfo(versionInfo: VersionInfo) {
+ if (versionInfo.isOutdated || !versionInfo.isSupported) {
+ if (versionInfo.upgradeVersion != null) {
val template: Int
- if (isSupported) {
+ if (versionInfo.isSupported) {
status = StatusLevel.Warning
title = updateAvailable
template = R.string.update_available_description
@@ -42,7 +25,7 @@ class VersionInfoNotification(
template = R.string.unsupported_version_description
}
- message = context.getString(template, upgrade)
+ message = context.getString(template, versionInfo.upgradeVersion)
} else {
status = StatusLevel.Error
title = unsupportedVersion
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/serviceconnection/AccountRepository.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/serviceconnection/AccountRepository.kt
index 4d79bfd356..6a58739aec 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/serviceconnection/AccountRepository.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/serviceconnection/AccountRepository.kt
@@ -23,9 +23,6 @@ class AccountRepository(
private val serviceConnectionManager: ServiceConnectionManager,
dispatcher: CoroutineDispatcher = Dispatchers.IO
) {
- private val dataSource
- get() = serviceConnectionManager.connectionState.value.readyContainer()?.accountDataSource
-
private val _cachedCreatedAccount = MutableStateFlow<String?>(null)
val cachedCreatedAccount = _cachedCreatedAccount.asStateFlow()
@@ -78,28 +75,28 @@ class AccountRepository(
)
fun createAccount() {
- dataSource?.createAccount()
+ serviceConnectionManager.accountDataSource()?.createAccount()
}
fun login(accountToken: String) {
- dataSource?.login(accountToken)
+ serviceConnectionManager.accountDataSource()?.login(accountToken)
}
fun logout() {
clearCreatedAccountCache()
- dataSource?.logout()
+ serviceConnectionManager.accountDataSource()?.logout()
}
fun fetchAccountExpiry() {
- dataSource?.fetchAccountExpiry()
+ serviceConnectionManager.accountDataSource()?.fetchAccountExpiry()
}
fun fetchAccountHistory() {
- dataSource?.fetchAccountHistory()
+ serviceConnectionManager.accountDataSource()?.fetchAccountHistory()
}
fun clearAccountHistory() {
- dataSource?.clearAccountHistory()
+ serviceConnectionManager.accountDataSource()?.clearAccountHistory()
}
private fun clearCreatedAccountCache() {
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 08290ef7d2..790c2404f2 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
@@ -61,20 +61,16 @@ class DeviceRepository(
.stateIn(CoroutineScope(Dispatchers.IO), SharingStarted.WhileSubscribed(), emptyList())
fun refreshDeviceState() {
- container()?.deviceDataSource?.refreshDevice()
- }
-
- private fun container(): ServiceConnectionContainer? {
- return serviceConnectionManager.connectionState.value.readyContainer()
+ serviceConnectionManager.deviceDataSource()?.refreshDevice()
}
fun removeDevice(accountToken: String, deviceId: String) {
cachedDeviceList.value = emptyList()
- container()?.deviceDataSource?.removeDevice(accountToken, deviceId)
+ serviceConnectionManager.deviceDataSource()?.removeDevice(accountToken, deviceId)
}
fun refreshDeviceList(accountToken: String) {
- container()?.deviceDataSource?.refreshDeviceList(accountToken)
+ serviceConnectionManager.deviceDataSource()?.refreshDeviceList(accountToken)
}
suspend fun getDeviceList(accountToken: String): DeviceListEvent {
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/serviceconnection/ServiceConnectionManagerExtensions.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/serviceconnection/ServiceConnectionManagerExtensions.kt
new file mode 100644
index 0000000000..392b841101
--- /dev/null
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/serviceconnection/ServiceConnectionManagerExtensions.kt
@@ -0,0 +1,25 @@
+package net.mullvad.mullvadvpn.ui.serviceconnection
+
+fun ServiceConnectionManager.accountDataSource() =
+ this.connectionState.value.readyContainer()?.accountDataSource
+
+fun ServiceConnectionManager.appVersionInfoCache() =
+ this.connectionState.value.readyContainer()?.appVersionInfoCache
+
+fun ServiceConnectionManager.authTokenCache() =
+ this.connectionState.value.readyContainer()?.authTokenCache
+
+fun ServiceConnectionManager.connectionProxy() =
+ this.connectionState.value.readyContainer()?.connectionProxy
+
+fun ServiceConnectionManager.deviceDataSource() =
+ this.connectionState.value.readyContainer()?.deviceDataSource
+
+fun ServiceConnectionManager.customDns() =
+ this.connectionState.value.readyContainer()?.customDns
+
+fun ServiceConnectionManager.relayListListener() =
+ this.connectionState.value.readyContainer()?.relayListListener
+
+fun ServiceConnectionManager.settingsListener() =
+ this.connectionState.value.readyContainer()?.settingsListener
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/widget/NavigateCell.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/widget/NavigateCell.kt
index fffe9d3003..31fd5f5095 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/widget/NavigateCell.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/widget/NavigateCell.kt
@@ -40,7 +40,7 @@ open class NavigateCell : Cell {
(context as? FragmentActivity)?.supportFragmentManager?.beginTransaction()?.apply {
setCustomAnimations(
R.anim.fragment_enter_from_right,
- R.anim.fragment_half_exit_to_left,
+ R.anim.fragment_exit_to_left,
R.anim.fragment_half_enter_from_left,
R.anim.fragment_exit_to_right
)
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/widget/SitePaymentButton.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/widget/SitePaymentButton.kt
index af936f1686..35d0c2326f 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/widget/SitePaymentButton.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/widget/SitePaymentButton.kt
@@ -20,9 +20,4 @@ class SitePaymentButton : UrlButton {
label = context.getString(R.string.buy_more_credit)
}
}
-
- init {
- url = context.getString(R.string.account_url)
- withToken = true
- }
}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/widget/UrlButton.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/widget/UrlButton.kt
index a7573fc7c4..c1b700e433 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/widget/UrlButton.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/widget/UrlButton.kt
@@ -1,119 +1,20 @@
package net.mullvad.mullvadvpn.ui.widget
import android.content.Context
-import android.content.Intent
-import android.net.Uri
import android.util.AttributeSet
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.GlobalScope
-import kotlinx.coroutines.async
import net.mullvad.mullvadvpn.R
-import net.mullvad.mullvadvpn.ui.serviceconnection.AuthTokenCache
-import net.mullvad.mullvadvpn.util.JobTracker
open class UrlButton : Button {
- private lateinit var authTokenCache: AuthTokenCache
+ constructor(context: Context) : super(context)
- private var shouldEnable = true
-
- var url: String? = null
- var withToken = false
-
- constructor(context: Context) : super(context) {}
-
- constructor(context: Context, attributes: AttributeSet) : super(context, attributes) {
- loadAttributes(attributes)
- }
+ constructor(context: Context, attributes: AttributeSet) : super(context, attributes)
constructor(context: Context, attributes: AttributeSet, defaultStyleAttribute: Int) :
- super(context, attributes, defaultStyleAttribute) {
- loadAttributes(attributes)
- }
-
- constructor(
- context: Context,
- attributes: AttributeSet,
- defaultStyleAttribute: Int,
- defaultStyleResource: Int
- ) : super(context, attributes, defaultStyleAttribute, defaultStyleResource) {
- loadAttributes(attributes)
- }
+ super(context, attributes, defaultStyleAttribute)
init {
super.setEnabled(false)
super.detailImage = context.getDrawable(R.drawable.icon_extlink)
super.showSpinner = true
}
-
- fun prepare(
- authTokenCache: AuthTokenCache,
- jobTracker: JobTracker,
- jobName: String = "fetchUrl",
- extraOnClickAction: (suspend () -> Unit)? = null
- ) {
- synchronized(this) {
- super.setEnabled(shouldEnable)
-
- this.authTokenCache = authTokenCache
-
- setOnClickAction(jobName, jobTracker) {
- super.setEnabled(false)
-
- context.startActivity(buildIntent(jobTracker))
- extraOnClickAction?.invoke()
-
- super.setEnabled(true)
- }
- }
- }
-
- fun updateAuthTokenCache(authTokenCache: AuthTokenCache) {
- synchronized(this) {
- this.authTokenCache = authTokenCache
- }
- }
-
- override fun setEnabled(enabled: Boolean) {
- synchronized(this) {
- shouldEnable = enabled
-
- if (!withToken || this::authTokenCache.isInitialized) {
- super.setEnabled(enabled)
- }
- }
- }
-
- private fun loadAttributes(attributes: AttributeSet) {
- context.theme.obtainStyledAttributes(attributes, R.styleable.Url, 0, 0).apply {
- try {
- url = getString(R.styleable.Url_url)
- } finally {
- recycle()
- }
- }
-
- context.theme.obtainStyledAttributes(attributes, R.styleable.UrlButton, 0, 0).apply {
- try {
- withToken = getBoolean(R.styleable.UrlButton_withToken, false)
- } finally {
- recycle()
- }
- }
- }
-
- private suspend fun buildIntent(jobTracker: JobTracker): Intent {
- val buildIntent = GlobalScope.async(Dispatchers.Default) {
- val uri = if (withToken) {
- Uri.parse(url + "?token=" + authTokenCache.fetchAuthToken())
- } else {
- Uri.parse(url)
- }
-
- Intent(Intent.ACTION_VIEW, uri)
- }
-
- jobTracker.newJob(buildIntent)
-
- return buildIntent.await()
- }
}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/util/CacheExtensions.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/util/CacheExtensions.kt
new file mode 100644
index 0000000000..62172231f9
--- /dev/null
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/util/CacheExtensions.kt
@@ -0,0 +1,22 @@
+package net.mullvad.mullvadvpn.util
+
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.callbackFlow
+import net.mullvad.mullvadvpn.ui.VersionInfo
+import net.mullvad.mullvadvpn.ui.serviceconnection.AppVersionInfoCache
+
+fun AppVersionInfoCache.appVersionCallbackFlow() = callbackFlow {
+ this@appVersionCallbackFlow.onUpdate = {
+ trySend(
+ VersionInfo(
+ currentVersion = this@appVersionCallbackFlow.version,
+ upgradeVersion = this@appVersionCallbackFlow.upgradeVersion,
+ isOutdated = this@appVersionCallbackFlow.isOutdated,
+ isSupported = this@appVersionCallbackFlow.isSupported,
+ )
+ )
+ }
+ awaitClose {
+ onUpdate = null
+ }
+}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/util/DeviceStateExtensions.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/util/DeviceStateExtensions.kt
new file mode 100644
index 0000000000..d41a628bec
--- /dev/null
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/util/DeviceStateExtensions.kt
@@ -0,0 +1,25 @@
+package net.mullvad.mullvadvpn.util
+
+import kotlin.reflect.KClass
+import net.mullvad.mullvadvpn.model.DeviceState
+
+const val UNKNOWN_STATE_DEBOUNCE_DELAY_MILLISECONDS = 2000L
+private const val ZERO_DEBOUNCE_DELAY_MILLISECONDS = 0L
+
+fun DeviceState.addDebounceForUnknownState(
+ delay: Long
+): Long {
+ return addDebounceForStates(delay, DeviceState.Unknown::class)
+}
+
+fun <T> DeviceState.addDebounceForStates(
+ delay: Long,
+ vararg states: KClass<T>
+): Long where T : DeviceState {
+ val result = states.any { this::class == it }
+ return if (result) {
+ delay
+ } else {
+ ZERO_DEBOUNCE_DELAY_MILLISECONDS
+ }
+}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/util/FlowUtils.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/util/FlowUtils.kt
index a0a809f8ff..de76ff5b0b 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/util/FlowUtils.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/util/FlowUtils.kt
@@ -17,6 +17,7 @@ import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.take
import net.mullvad.mullvadvpn.model.ServiceResult
import net.mullvad.mullvadvpn.ui.serviceconnection.ServiceConnectionState
+import net.mullvad.talpid.util.EventNotifier
fun <T> SendChannel<T>.safeOffer(element: T): Boolean {
return runCatching { offer(element) }.getOrDefault(false)
@@ -78,3 +79,9 @@ fun <R> Flow<ServiceConnectionState>.flatMapReadyConnectionOrDefault(
}
}
}
+
+fun <T> callbackFlowFromNotifier(notifier: EventNotifier<T>) = callbackFlow<T> {
+ val handler: (T) -> Unit = { value -> trySend(value) }
+ notifier.subscribe(this, handler)
+ awaitClose { notifier.unsubscribe(this) }
+}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/DeviceRevokedViewModel.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/DeviceRevokedViewModel.kt
index 4272a5de35..bf8db5b273 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/DeviceRevokedViewModel.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/DeviceRevokedViewModel.kt
@@ -11,8 +11,8 @@ import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import net.mullvad.mullvadvpn.compose.state.DeviceRevokedUiState
import net.mullvad.mullvadvpn.ui.serviceconnection.AccountRepository
-import net.mullvad.mullvadvpn.ui.serviceconnection.ServiceConnectionContainer
import net.mullvad.mullvadvpn.ui.serviceconnection.ServiceConnectionManager
+import net.mullvad.mullvadvpn.ui.serviceconnection.connectionProxy
import net.mullvad.talpid.util.callbackFlowFromSubscription
// TODO: Refactor ConnectionProxy to be easily injectable rather than injecting
@@ -44,15 +44,11 @@ class DeviceRevokedViewModel(
)
fun onGoToLoginClicked() {
- serviceContainer()?.let { container ->
- if (container.connectionProxy.state.isSecured()) {
- container.connectionProxy.disconnect()
+ serviceConnectionManager.connectionProxy()?.let { proxy ->
+ if (proxy.state.isSecured()) {
+ proxy.disconnect()
}
accountRepository.logout()
}
}
-
- private fun serviceContainer(): ServiceConnectionContainer? {
- return serviceConnectionManager.connectionState.value.readyContainer()
- }
}