diff options
| author | Jonatan Rhodin <jonatan.rhodin@mullvad.net> | 2023-09-20 14:05:15 +0200 |
|---|---|---|
| committer | Jonatan Rhodin <jonatan.rhodin@mullvad.net> | 2023-09-26 09:38:42 +0200 |
| commit | 26c734c0766beb5968e8a801df47232152da4d20 (patch) | |
| tree | 22db3e9c2d58232118b91828cf375b4099c235bc /android | |
| parent | 77b74239c60356a23546a49726a7337d84b69d7b (diff) | |
| download | mullvadvpn-26c734c0766beb5968e8a801df47232152da4d20.tar.xz mullvadvpn-26c734c0766beb5968e8a801df47232152da4d20.zip | |
Migrate out of time view to compose
Diffstat (limited to 'android')
11 files changed, 431 insertions, 333 deletions
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/extensions/UriHandlerExtensions.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/extensions/UriHandlerExtensions.kt new file mode 100644 index 0000000000..e85939c51c --- /dev/null +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/extensions/UriHandlerExtensions.kt @@ -0,0 +1,12 @@ +package net.mullvad.mullvadvpn.compose.extensions + +import androidx.compose.runtime.Composable +import androidx.compose.ui.platform.UriHandler +import androidx.compose.ui.res.stringResource +import net.mullvad.mullvadvpn.R + +@Composable +fun UriHandler.createOpenAccountPageHook(): (String) -> Unit { + val accountUrl = stringResource(id = R.string.account_url) + return { token -> this.openUri("$accountUrl?token=$token") } +} diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/OutOfTimeScreen.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/OutOfTimeScreen.kt new file mode 100644 index 0000000000..b36882ef41 --- /dev/null +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/OutOfTimeScreen.kt @@ -0,0 +1,280 @@ +package net.mullvad.mullvadvpn.compose.screen + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.defaultMinSize +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha +import androidx.compose.ui.platform.LocalUriHandler +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.sp +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.SharedFlow +import kotlinx.coroutines.flow.asSharedFlow +import net.mullvad.mullvadvpn.R +import net.mullvad.mullvadvpn.compose.button.ActionButton +import net.mullvad.mullvadvpn.compose.component.ScaffoldWithTopBar +import net.mullvad.mullvadvpn.compose.component.drawVerticalScrollbar +import net.mullvad.mullvadvpn.compose.extensions.createOpenAccountPageHook +import net.mullvad.mullvadvpn.compose.state.OutOfTimeUiState +import net.mullvad.mullvadvpn.lib.theme.AlphaDisabled +import net.mullvad.mullvadvpn.lib.theme.AlphaInactive +import net.mullvad.mullvadvpn.lib.theme.AlphaTopBar +import net.mullvad.mullvadvpn.lib.theme.AlphaVisible +import net.mullvad.mullvadvpn.lib.theme.AppTheme +import net.mullvad.mullvadvpn.lib.theme.Dimens +import net.mullvad.mullvadvpn.model.TunnelState +import net.mullvad.mullvadvpn.viewmodel.OutOfTimeViewModel +import net.mullvad.talpid.tunnel.ActionAfterDisconnect +import net.mullvad.talpid.tunnel.ErrorState +import net.mullvad.talpid.tunnel.ErrorStateCause + +@Preview +@Composable +private fun PreviewOutOfTimeScreenDisconnected() { + AppTheme { + OutOfTimeScreen( + showSitePayment = true, + uiState = OutOfTimeUiState(tunnelState = TunnelState.Disconnected), + viewActions = MutableSharedFlow<OutOfTimeViewModel.ViewAction>().asSharedFlow() + ) + } +} + +@Preview +@Composable +private fun PreviewOutOfTimeScreenConnecting() { + AppTheme { + OutOfTimeScreen( + showSitePayment = true, + uiState = OutOfTimeUiState(tunnelState = TunnelState.Connecting(null, null)), + viewActions = MutableSharedFlow<OutOfTimeViewModel.ViewAction>().asSharedFlow() + ) + } +} + +@Preview +@Composable +private fun PreviewOutOfTimeScreenError() { + AppTheme { + OutOfTimeScreen( + showSitePayment = true, + uiState = + OutOfTimeUiState( + tunnelState = + TunnelState.Error( + ErrorState(cause = ErrorStateCause.IsOffline, isBlocking = true) + ) + ), + viewActions = MutableSharedFlow<OutOfTimeViewModel.ViewAction>().asSharedFlow() + ) + } +} + +@Composable +fun OutOfTimeScreen( + showSitePayment: Boolean, + uiState: OutOfTimeUiState, + viewActions: SharedFlow<OutOfTimeViewModel.ViewAction>, + onDisconnectClick: () -> Unit = {}, + onSitePaymentClick: () -> Unit = {}, + onRedeemVoucherClick: () -> Unit = {}, + openConnectScreen: () -> Unit = {}, + onSettingsClick: () -> Unit = {}, + onAccountClick: () -> Unit = {} +) { + val openAccountPage = LocalUriHandler.current.createOpenAccountPageHook() + LaunchedEffect(key1 = Unit) { + viewActions.collect { viewAction -> + when (viewAction) { + is OutOfTimeViewModel.ViewAction.OpenAccountView -> + openAccountPage(viewAction.token) + OutOfTimeViewModel.ViewAction.OpenConnectScreen -> openConnectScreen() + } + } + } + val scrollState = rememberScrollState() + ScaffoldWithTopBar( + topBarColor = + if (uiState.tunnelState.isSecured()) { + MaterialTheme.colorScheme.inversePrimary + } else { + MaterialTheme.colorScheme.error + }, + statusBarColor = + if (uiState.tunnelState.isSecured()) { + MaterialTheme.colorScheme.inversePrimary + } else { + MaterialTheme.colorScheme.error + }, + navigationBarColor = MaterialTheme.colorScheme.background, + iconTintColor = + if (uiState.tunnelState.isSecured()) { + MaterialTheme.colorScheme.onPrimary + } else { + MaterialTheme.colorScheme.onError + } + .copy(alpha = AlphaTopBar), + onSettingsClicked = onSettingsClick, + onAccountClicked = onAccountClick + ) { + Column( + verticalArrangement = Arrangement.Bottom, + horizontalAlignment = Alignment.Start, + modifier = + Modifier.fillMaxSize() + .verticalScroll(scrollState) + .drawVerticalScrollbar(scrollState) + .background(color = MaterialTheme.colorScheme.background) + .padding(it) + ) { + Image( + painter = painterResource(id = R.drawable.icon_fail), + contentDescription = null, + modifier = + Modifier.align(Alignment.CenterHorizontally) + .padding(vertical = Dimens.screenVerticalMargin) + ) + Text( + text = stringResource(id = R.string.out_of_time), + style = MaterialTheme.typography.headlineLarge, + color = MaterialTheme.colorScheme.onPrimary, + modifier = Modifier.padding(horizontal = Dimens.sideMargin) + ) + Text( + text = + buildString { + append(stringResource(R.string.account_credit_has_expired)) + if (showSitePayment) { + append(" ") + append(stringResource(R.string.add_time_to_account)) + } + }, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onPrimary, + modifier = + Modifier.padding( + top = Dimens.mediumPadding, + start = Dimens.sideMargin, + end = Dimens.sideMargin + ) + ) + Spacer(modifier = Modifier.weight(1f).defaultMinSize(minHeight = Dimens.verticalSpace)) + // Button area + if (uiState.tunnelState.showDisconnectButton()) { + ActionButton( + onClick = onDisconnectClick, + colors = + ButtonDefaults.buttonColors( + containerColor = MaterialTheme.colorScheme.error, + contentColor = MaterialTheme.colorScheme.onError + ), + text = stringResource(id = R.string.disconnect), + modifier = + Modifier.padding( + start = Dimens.sideMargin, + end = Dimens.sideMargin, + bottom = Dimens.buttonSeparation + ) + ) + } + if (showSitePayment) { + ActionButton( + onClick = onSitePaymentClick, + modifier = + Modifier.padding( + start = Dimens.sideMargin, + end = Dimens.sideMargin, + bottom = Dimens.buttonSeparation + ), + colors = + ButtonDefaults.buttonColors( + contentColor = MaterialTheme.colorScheme.onPrimary, + containerColor = MaterialTheme.colorScheme.surface, + disabledContentColor = + MaterialTheme.colorScheme.onPrimary.copy(alpha = AlphaInactive), + disabledContainerColor = + MaterialTheme.colorScheme.surface.copy(alpha = AlphaDisabled) + ), + isEnabled = uiState.tunnelState.enableSitePaymentButton() + ) { + Box(modifier = Modifier.fillMaxSize()) { + Text( + text = stringResource(id = R.string.buy_more_credit), + textAlign = TextAlign.Center, + fontSize = 18.sp, + fontWeight = FontWeight.Bold, + modifier = Modifier.align(Alignment.Center) + ) + Image( + painter = painterResource(id = R.drawable.icon_extlink), + contentDescription = null, + modifier = + Modifier.align(Alignment.CenterEnd) + .padding(horizontal = Dimens.smallPadding) + .alpha( + if (uiState.tunnelState.enableSitePaymentButton()) + AlphaVisible + else AlphaDisabled + ) + ) + } + } + } + ActionButton( + text = stringResource(id = R.string.redeem_voucher), + onClick = onRedeemVoucherClick, + modifier = + Modifier.padding( + start = Dimens.sideMargin, + end = Dimens.sideMargin, + bottom = Dimens.screenVerticalMargin + ), + colors = + ButtonDefaults.buttonColors( + contentColor = MaterialTheme.colorScheme.onPrimary, + containerColor = MaterialTheme.colorScheme.surface, + disabledContentColor = + MaterialTheme.colorScheme.onPrimary.copy(alpha = AlphaInactive), + disabledContainerColor = + MaterialTheme.colorScheme.surface.copy(alpha = AlphaDisabled) + ), + isEnabled = uiState.tunnelState.enableRedeemButton() + ) + } + } +} + +private fun TunnelState.showDisconnectButton(): Boolean = + when (this) { + is TunnelState.Disconnected -> false + is TunnelState.Connecting, + is TunnelState.Connected -> true + is TunnelState.Disconnecting -> { + this.actionAfterDisconnect != ActionAfterDisconnect.Nothing + } + is TunnelState.Error -> this.errorState.isBlocking + } + +private fun TunnelState.enableSitePaymentButton(): Boolean = this is TunnelState.Disconnected + +private fun TunnelState.enableRedeemButton(): Boolean = + !(this is TunnelState.Error && this.errorState.cause is ErrorStateCause.IsOffline) diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/OutOfTimeUiState.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/OutOfTimeUiState.kt new file mode 100644 index 0000000000..cc19ac7ca8 --- /dev/null +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/OutOfTimeUiState.kt @@ -0,0 +1,5 @@ +package net.mullvad.mullvadvpn.compose.state + +import net.mullvad.mullvadvpn.model.TunnelState + +data class OutOfTimeUiState(val tunnelState: TunnelState = TunnelState.Disconnected) 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 3c4315ecb9..987a55b45f 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 @@ -24,6 +24,7 @@ import net.mullvad.mullvadvpn.viewmodel.ConnectViewModel import net.mullvad.mullvadvpn.viewmodel.DeviceListViewModel import net.mullvad.mullvadvpn.viewmodel.DeviceRevokedViewModel import net.mullvad.mullvadvpn.viewmodel.LoginViewModel +import net.mullvad.mullvadvpn.viewmodel.OutOfTimeViewModel import net.mullvad.mullvadvpn.viewmodel.PrivacyDisclaimerViewModel import net.mullvad.mullvadvpn.viewmodel.SelectLocationViewModel import net.mullvad.mullvadvpn.viewmodel.SettingsViewModel @@ -86,6 +87,7 @@ val uiModule = module { viewModel { SettingsViewModel(get(), get()) } viewModel { VpnSettingsViewModel(get(), get(), get(), get()) } viewModel { WelcomeViewModel(get(), get(), get()) } + viewModel { OutOfTimeViewModel(get(), get()) } } const val SELF_PACKAGE_NAME = "SELF_PACKAGE_NAME" diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/fragment/OutOfTimeFragment.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/fragment/OutOfTimeFragment.kt index c5a5ee7634..8d3bf00010 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/fragment/OutOfTimeFragment.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/fragment/OutOfTimeFragment.kt @@ -4,197 +4,49 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import android.widget.TextView -import androidx.core.view.isVisible -import androidx.lifecycle.Lifecycle -import androidx.lifecycle.lifecycleScope -import androidx.lifecycle.repeatOnLifecycle -import kotlin.properties.Delegates.observable -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.emptyFlow -import kotlinx.coroutines.flow.flatMapLatest -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.launch +import androidx.compose.runtime.collectAsState +import androidx.compose.ui.platform.ComposeView import net.mullvad.mullvadvpn.R -import net.mullvad.mullvadvpn.constant.ACCOUNT_EXPIRY_POLL_INTERVAL +import net.mullvad.mullvadvpn.compose.screen.OutOfTimeScreen import net.mullvad.mullvadvpn.constant.IS_PLAY_BUILD -import net.mullvad.mullvadvpn.lib.common.util.JobTracker -import net.mullvad.mullvadvpn.lib.common.util.openAccountPageInBrowser -import net.mullvad.mullvadvpn.model.TunnelState -import net.mullvad.mullvadvpn.repository.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.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 +import net.mullvad.mullvadvpn.lib.theme.AppTheme +import net.mullvad.mullvadvpn.ui.MainActivity +import net.mullvad.mullvadvpn.viewmodel.OutOfTimeViewModel +import org.koin.androidx.viewmodel.ext.android.viewModel class OutOfTimeFragment : BaseFragment() { - // Injected dependencies - private val accountRepository: AccountRepository by inject() - private val serviceConnectionManager: ServiceConnectionManager by inject() - - private lateinit var headerBar: HeaderBar - - private lateinit var sitePaymentButton: SitePaymentButton - private lateinit var disconnectButton: Button - private lateinit var redeemButton: RedeemVoucherButton - - private var tunnelState by - observable<TunnelState>(TunnelState.Disconnected) { _, _, state -> - updateDisconnectButton() - updateBuyButtons() - headerBar.tunnelState = state - } - - @Deprecated("Refactor code to instead rely on Lifecycle.") private val jobTracker = JobTracker() - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - lifecycleScope.launchUiSubscriptionsOnResume() - } + private val vm by viewModel<OutOfTimeViewModel>() override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? { - val view = inflater.inflate(R.layout.out_of_time, container, false) - - headerBar = - view.findViewById<HeaderBar>(R.id.header_bar).apply { - tunnelState = this@OutOfTimeFragment.tunnelState - } - - view.findViewById<TextView>(R.id.account_credit_has_expired).text = buildString { - append(requireActivity().getString(R.string.account_credit_has_expired)) - if (IS_PLAY_BUILD.not()) { - append(" ") - append(requireActivity().getString(R.string.add_time_to_account)) - } - } - - disconnectButton = - view.findViewById<Button>(R.id.disconnect).apply { - setOnClickAction("disconnect", jobTracker) { - serviceConnectionManager.connectionProxy()?.disconnect() - } - } - - sitePaymentButton = - view.findViewById<SitePaymentButton>(R.id.site_payment).apply { - newAccount = false - - setOnClickAction("openAccountPageInBrowser", jobTracker) { - isEnabled = false - serviceConnectionManager.authTokenCache()?.fetchAuthToken()?.let { token -> - context.openAccountPageInBrowser(token) - } - isEnabled = true - } - - isEnabled = true - } - - sitePaymentButton.isVisible = IS_PLAY_BUILD.not() - - redeemButton = - view.findViewById<RedeemVoucherButton>(R.id.redeem_voucher).apply { - prepare(parentFragmentManager, jobTracker) - } - - return view - } - - override fun onStop() { - jobTracker.cancelAllJobs() - super.onStop() - } - - private fun CoroutineScope.launchUiSubscriptionsOnResume() = launch { - repeatOnLifecycle(Lifecycle.State.RESUMED) { - launchProceedToConnectViewIfExpiryExtended() - launchExpiryPolling() - launchTunnelStateSubscription() - } - } - - private fun CoroutineScope.launchProceedToConnectViewIfExpiryExtended() = launch { - accountRepository.accountExpiryState - .map { state -> state.date() } - .collect { expiryDate -> checkExpiry(expiryDate) } - } - - private fun CoroutineScope.launchExpiryPolling() = launch { - while (true) { - accountRepository.fetchAccountExpiry() - delay(ACCOUNT_EXPIRY_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 - - val showButton = - when (state) { - is TunnelState.Disconnected -> false - is TunnelState.Connecting, - is TunnelState.Connected -> true - is TunnelState.Disconnecting -> { - state.actionAfterDisconnect != ActionAfterDisconnect.Nothing + return inflater.inflate(R.layout.fragment_compose, container, false).apply { + findViewById<ComposeView>(R.id.compose_view).setContent { + AppTheme { + val state = vm.uiState.collectAsState().value + OutOfTimeScreen( + showSitePayment = IS_PLAY_BUILD.not(), + uiState = state, + viewActions = vm.viewActions, + onSitePaymentClick = vm::onSitePaymentClick, + onRedeemVoucherClick = ::openRedeemVoucherFragment, + onSettingsClick = ::openSettingsView, + onAccountClick = ::openAccountView, + openConnectScreen = ::advanceToConnectScreen, + onDisconnectClick = vm::onDisconnectClick + ) } - is TunnelState.Error -> state.errorState.isBlocking - } - - disconnectButton.apply { - if (showButton) { - isEnabled = true - visibility = View.VISIBLE - } else { - isEnabled = false - visibility = View.GONE } } } - private fun updateBuyButtons() { - val currentState = tunnelState - val hasConnectivity = currentState is TunnelState.Disconnected - sitePaymentButton.isEnabled = hasConnectivity - - val isOffline = - currentState is TunnelState.Error && - currentState.errorState.cause is ErrorStateCause.IsOffline - redeemButton.isEnabled = !isOffline - } - - private fun checkExpiry(maybeExpiry: DateTime?) { - maybeExpiry?.let { expiry -> - if (expiry.isAfterNow) { - jobTracker.newUiJob("advanceToConnectScreen") { advanceToConnectScreen() } - } - } + private fun openRedeemVoucherFragment() { + val transaction = parentFragmentManager.beginTransaction() + transaction.addToBackStack(null) + RedeemVoucherDialogFragment().show(transaction, null) } private fun advanceToConnectScreen() { @@ -203,4 +55,12 @@ class OutOfTimeFragment : BaseFragment() { commitAllowingStateLoss() } } + + private fun openSettingsView() { + (context as? MainActivity)?.openSettings() + } + + private fun openAccountView() { + (context as? MainActivity)?.openAccount() + } } diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/widget/RedeemVoucherButton.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/widget/RedeemVoucherButton.kt deleted file mode 100644 index b6d5ddb88d..0000000000 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/widget/RedeemVoucherButton.kt +++ /dev/null @@ -1,33 +0,0 @@ -package net.mullvad.mullvadvpn.ui.widget - -import android.content.Context -import android.util.AttributeSet -import androidx.fragment.app.FragmentManager -import net.mullvad.mullvadvpn.lib.common.util.JobTracker -import net.mullvad.mullvadvpn.ui.fragment.RedeemVoucherDialogFragment - -class RedeemVoucherButton : Button { - constructor(context: Context) : super(context) - - constructor(context: Context, attributes: AttributeSet) : super(context, attributes) - - constructor( - context: Context, - attributes: AttributeSet, - defaultStyleAttribute: Int - ) : super(context, attributes, defaultStyleAttribute) - - fun prepare( - fragmentManager: FragmentManager?, - jobTracker: JobTracker, - jobName: String = "openRedeemVoucherDialog" - ) { - setOnClickAction(jobName, jobTracker) { - fragmentManager?.beginTransaction()?.let { transaction -> - transaction.addToBackStack(null) - - RedeemVoucherDialogFragment().show(transaction, null) - } - } - } -} 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 deleted file mode 100644 index 9fbe71337e..0000000000 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/widget/SitePaymentButton.kt +++ /dev/null @@ -1,27 +0,0 @@ -package net.mullvad.mullvadvpn.ui.widget - -import android.content.Context -import android.util.AttributeSet -import kotlin.properties.Delegates.observable -import net.mullvad.mullvadvpn.R - -class SitePaymentButton : UrlButton { - constructor(context: Context) : super(context) - - constructor(context: Context, attributes: AttributeSet) : super(context, attributes) - - constructor( - context: Context, - attributes: AttributeSet, - defaultStyleAttribute: Int - ) : super(context, attributes, defaultStyleAttribute) - - var newAccount by - observable(false) { _, _, isNewAccount -> - if (isNewAccount) { - label = context.getString(R.string.buy_credit) - } else { - label = context.getString(R.string.buy_more_credit) - } - } -} 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 deleted file mode 100644 index f6090bdafa..0000000000 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/widget/UrlButton.kt +++ /dev/null @@ -1,24 +0,0 @@ -package net.mullvad.mullvadvpn.ui.widget - -import android.content.Context -import android.util.AttributeSet -import androidx.appcompat.content.res.AppCompatResources -import net.mullvad.mullvadvpn.R - -open class UrlButton : Button { - constructor(context: Context) : super(context) - - constructor(context: Context, attributes: AttributeSet) : super(context, attributes) - - constructor( - context: Context, - attributes: AttributeSet, - defaultStyleAttribute: Int - ) : super(context, attributes, defaultStyleAttribute) - - init { - super.setEnabled(false) - super.detailImage = AppCompatResources.getDrawable(context, R.drawable.icon_extlink) - super.showSpinner = true - } -} diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/OutOfTimeViewModel.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/OutOfTimeViewModel.kt new file mode 100644 index 0000000000..00f3850777 --- /dev/null +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/OutOfTimeViewModel.kt @@ -0,0 +1,97 @@ +package net.mullvad.mullvadvpn.viewmodel + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.FlowPreview +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.emptyFlow +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.launch +import net.mullvad.mullvadvpn.compose.state.OutOfTimeUiState +import net.mullvad.mullvadvpn.constant.ACCOUNT_EXPIRY_POLL_INTERVAL +import net.mullvad.mullvadvpn.model.TunnelState +import net.mullvad.mullvadvpn.repository.AccountRepository +import net.mullvad.mullvadvpn.ui.serviceconnection.ConnectionProxy +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.util.callbackFlowFromNotifier +import org.joda.time.DateTime + +@OptIn(FlowPreview::class) +class OutOfTimeViewModel( + private val accountRepository: AccountRepository, + private val serviceConnectionManager: ServiceConnectionManager, + private val pollAccountExpiry: Boolean = true +) : ViewModel() { + + private val _viewActions = MutableSharedFlow<ViewAction>(extraBufferCapacity = 1) + val viewActions = _viewActions.asSharedFlow() + + val uiState = + serviceConnectionManager.connectionState + .flatMapLatest { state -> + if (state is ServiceConnectionState.ConnectedReady) { + flowOf(state.container) + } else { + emptyFlow() + } + } + .flatMapLatest { serviceConnection -> + serviceConnection.connectionProxy.tunnelStateFlow() + } + .map { tunnelState -> OutOfTimeUiState(tunnelState = tunnelState) } + .stateIn(viewModelScope, SharingStarted.WhileSubscribed(), OutOfTimeUiState()) + + init { + viewModelScope.launch { + accountRepository.accountExpiryState.collectLatest { accountExpiry -> + accountExpiry.date()?.let { expiry -> + val tomorrow = DateTime.now().plusHours(20) + + if (expiry.isAfter(tomorrow)) { + _viewActions.tryEmit(ViewAction.OpenConnectScreen) + } + } + } + } + viewModelScope.launch { + while (pollAccountExpiry) { + accountRepository.fetchAccountExpiry() + delay(ACCOUNT_EXPIRY_POLL_INTERVAL) + } + } + } + + private fun ConnectionProxy.tunnelStateFlow(): Flow<TunnelState> = + callbackFlowFromNotifier(this.onStateChange) + + fun onSitePaymentClick() { + viewModelScope.launch { + _viewActions.tryEmit( + ViewAction.OpenAccountView( + serviceConnectionManager.authTokenCache()?.fetchAuthToken() ?: "" + ) + ) + } + } + + fun onDisconnectClick() { + viewModelScope.launch { serviceConnectionManager.connectionProxy()?.disconnect() } + } + + sealed interface ViewAction { + data class OpenAccountView(val token: String) : ViewAction + + data object OpenConnectScreen : ViewAction + } +} diff --git a/android/app/src/main/res/layout/out_of_time.xml b/android/app/src/main/res/layout/out_of_time.xml deleted file mode 100644 index 791b2d8a77..0000000000 --- a/android/app/src/main/res/layout/out_of_time.xml +++ /dev/null @@ -1,59 +0,0 @@ -<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" - xmlns:mullvad="http://schemas.android.com/apk/res-auto" - android:layout_width="match_parent" - android:layout_height="match_parent"> - <net.mullvad.mullvadvpn.ui.widget.HeaderBar android:id="@+id/header_bar" - android:layout_width="match_parent" - android:layout_height="wrap_content" /> - <ScrollView android:layout_width="match_parent" - android:layout_height="match_parent" - android:layout_alignParentBottom="true" - android:layout_below="@id/header_bar" - android:fillViewport="true"> - <LinearLayout android:layout_width="match_parent" - android:layout_height="match_parent" - android:orientation="vertical"> - <ImageView android:layout_width="60dp" - android:layout_height="60dp" - android:layout_gravity="center" - android:layout_marginTop="@dimen/screen_vertical_margin" - android:layout_marginBottom="18dp" - android:src="@drawable/icon_fail" /> - <TextView android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:layout_marginHorizontal="@dimen/side_margin" - android:textColor="@color/white" - android:textSize="@dimen/text_huge" - android:textStyle="bold" - android:text="@string/out_of_time" /> - <TextView android:id="@+id/account_credit_has_expired" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:layout_marginHorizontal="@dimen/side_margin" - android:layout_marginTop="8dp" - android:layout_marginBottom="@dimen/vertical_space" - android:textColor="@color/white" - android:textSize="@dimen/text_small" /> - <Space android:layout_width="match_parent" - android:layout_height="0dp" - android:layout_weight="1" /> - <LinearLayout android:layout_width="match_parent" - android:layout_height="wrap_content" - android:layout_weight="0" - android:orientation="vertical" - android:paddingTop="@dimen/button_separation" - android:paddingBottom="@dimen/screen_vertical_margin" - android:background="@color/darkBlue"> - <net.mullvad.mullvadvpn.ui.widget.Button android:id="@+id/disconnect" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:layout_marginHorizontal="@dimen/side_margin" - android:layout_marginBottom="@dimen/button_separation" - android:visibility="gone" - mullvad:buttonColor="red" - mullvad:text="@string/disconnect" /> - <include layout="@layout/payment_buttons" /> - </LinearLayout> - </LinearLayout> - </ScrollView> -</RelativeLayout> diff --git a/android/app/src/main/res/layout/payment_buttons.xml b/android/app/src/main/res/layout/payment_buttons.xml deleted file mode 100644 index c617bb1571..0000000000 --- a/android/app/src/main/res/layout/payment_buttons.xml +++ /dev/null @@ -1,15 +0,0 @@ -<merge xmlns:android="http://schemas.android.com/apk/res/android" - xmlns:mullvad="http://schemas.android.com/apk/res-auto"> - <net.mullvad.mullvadvpn.ui.widget.SitePaymentButton android:id="@+id/site_payment" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:layout_marginHorizontal="@dimen/side_margin" - mullvad:buttonColor="green" /> - <net.mullvad.mullvadvpn.ui.widget.RedeemVoucherButton android:id="@+id/redeem_voucher" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:layout_marginTop="@dimen/button_separation" - android:layout_marginHorizontal="@dimen/side_margin" - mullvad:buttonColor="green" - mullvad:text="@string/redeem_voucher" /> -</merge> |
