diff options
Diffstat (limited to 'android')
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 + } +} |
