summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorAlbin <albin@mullvad.net>2022-07-07 11:34:05 +0200
committerAlbin <albin@mullvad.net>2022-07-20 09:59:02 +0200
commitd906d05dc52085cbf5684688b020cbda5c3245be (patch)
tree67fcbdf778c0e3ad7e121b31d78c0c8860bdb6d6
parent2f848635d7dc7caca4531360924f8813b65d5180 (diff)
downloadmullvadvpn-d906d05dc52085cbf5684688b020cbda5c3245be.tar.xz
mullvadvpn-d906d05dc52085cbf5684688b020cbda5c3245be.zip
Decouple ConnectFragment
-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/ConnectFragment.kt187
-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/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/util/CacheExtensions.kt22
8 files changed, 170 insertions, 123 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/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/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/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/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
+ }
+}