diff options
| author | Janito Vaqueiro Ferreira Filho <janito@mullvad.net> | 2020-05-12 14:52:04 -0300 |
|---|---|---|
| committer | Janito Vaqueiro Ferreira Filho <janito@mullvad.net> | 2020-05-12 14:52:04 -0300 |
| commit | 39f028115a338c99efd23d8179831fa3d951ddea (patch) | |
| tree | 5d8aa016e6385fefa3140ef6fb2063c3beb1b7c5 | |
| parent | ae4bc29d63367f56d14ff501ba25bfe9b5fe2c19 (diff) | |
| parent | 6b2cea81d07f93039ff62b33f7024616ce1c16fa (diff) | |
| download | mullvadvpn-39f028115a338c99efd23d8179831fa3d951ddea.tar.xz mullvadvpn-39f028115a338c99efd23d8179831fa3d951ddea.zip | |
Merge branch 'add-out-of-time-screen'
9 files changed, 321 insertions, 11 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index 185a897e16..7df618c731 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,6 +30,7 @@ Line wrap the file at 100 chars. Th - Add possibility to create account from the login screen. - Add welcome screen for newly created accounts. - Allow submitting voucher codes to add time to the account. +- Add Out Of Time screen for user to add more time to account once it expires. ### Changed - Move location of the account data (including the WireGuard keys), so that it isn't lost when the diff --git a/android/src/main/kotlin/net/mullvad/mullvadvpn/service/AccountCache.kt b/android/src/main/kotlin/net/mullvad/mullvadvpn/service/AccountCache.kt index c11bf2d770..07fa7911fc 100644 --- a/android/src/main/kotlin/net/mullvad/mullvadvpn/service/AccountCache.kt +++ b/android/src/main/kotlin/net/mullvad/mullvadvpn/service/AccountCache.kt @@ -5,9 +5,11 @@ import net.mullvad.mullvadvpn.util.JobTracker import org.joda.time.DateTime import org.joda.time.format.DateTimeFormat -val EXPIRY_FORMAT = DateTimeFormat.forPattern("YYYY-MM-dd HH:mm:ss z") - class AccountCache(val daemon: MullvadDaemon, val settingsListener: SettingsListener) { + companion object { + public val EXPIRY_FORMAT = DateTimeFormat.forPattern("YYYY-MM-dd HH:mm:ss z") + } + private val jobTracker = JobTracker() private val subscriptionId = settingsListener.accountNumberNotifier.subscribe { accountNumber -> handleNewAccountNumber(accountNumber) diff --git a/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/ConnectFragment.kt b/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/ConnectFragment.kt index c59f1b192d..22960d5e73 100644 --- a/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/ConnectFragment.kt +++ b/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/ConnectFragment.kt @@ -8,14 +8,19 @@ import android.widget.ImageButton import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.Job +import kotlinx.coroutines.delay import kotlinx.coroutines.launch import net.mullvad.mullvadvpn.R import net.mullvad.mullvadvpn.model.KeygenEvent import net.mullvad.mullvadvpn.model.TunnelState +import net.mullvad.mullvadvpn.util.JobTracker +import org.joda.time.DateTime val KEY_IS_TUNNEL_INFO_EXPANDED = "is_tunnel_info_expanded" class ConnectFragment : ServiceDependentFragment(OnNoService.GoToLaunchScreen) { + private val jobTracker = JobTracker() + private lateinit var actionButton: ConnectActionButton private lateinit var switchLocationButton: SwitchLocationButton private lateinit var headerBar: HeaderBar @@ -98,9 +103,18 @@ class ConnectFragment : ServiceDependentFragment(OnNoService.GoToLaunchScreen) { updateTunnelStateJob?.cancel() updateTunnelStateJob = updateTunnelState(uiState, connectionProxy.state) } + + accountCache.onAccountDataChange = { _, expiry -> + if (expiry?.isBeforeNow() ?: false) { + openOutOfTimeScreen() + } else if (expiry != null) { + scheduleNextAccountExpiryCheck(expiry) + } + } } override fun onSafelyPause() { + accountCache.onAccountDataChange = null keyStatusListener.onKeyStatusChange = null locationInfoCache.onNewLocation = null relayListListener.onRelayListChange = null @@ -117,6 +131,7 @@ class ConnectFragment : ServiceDependentFragment(OnNoService.GoToLaunchScreen) { } override fun onSafelyDestroyView() { + jobTracker.cancelAllJobs() switchLocationButton.onDestroy() } @@ -153,4 +168,29 @@ class ConnectFragment : ServiceDependentFragment(OnNoService.GoToLaunchScreen) { commit() } } + + private fun openOutOfTimeScreen() { + jobTracker.newUiJob("openOutOfTimeScreen") { + fragmentManager?.beginTransaction()?.apply { + replace(R.id.main_fragment, OutOfTimeFragment()) + commit() + } + } + } + + private fun scheduleNextAccountExpiryCheck(expiration: DateTime) { + jobTracker.newBackgroundJob("refetchAccountExpiry") { + val millisUntilExpiration = expiration.millis - DateTime.now().millis + + delay(millisUntilExpiration) + accountCache.fetchAccountExpiry() + + // If the account ran out of time but is still connected, fetching the expiry again will + // fail. Therefore, after a timeout of 5 seconds the app will assume the account time + // really expired and move to the out of time screen. However, if fetching the expiry + // succeeds, this job is cancelled and replaced with a new scheduled check. + delay(5_000) + openOutOfTimeScreen() + } + } } diff --git a/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/LoginFragment.kt b/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/LoginFragment.kt index 959431b85a..d59393f2a1 100644 --- a/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/LoginFragment.kt +++ b/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/LoginFragment.kt @@ -13,12 +13,15 @@ import kotlinx.coroutines.delay import kotlinx.coroutines.launch import net.mullvad.mullvadvpn.R import net.mullvad.mullvadvpn.model.GetAccountDataResult +import net.mullvad.mullvadvpn.service.AccountCache import net.mullvad.mullvadvpn.ui.widget.Button import net.mullvad.mullvadvpn.util.JobTracker +import org.joda.time.DateTime class LoginFragment : ServiceDependentFragment(OnNoService.GoToLaunchScreen) { enum class LoginResult { - ExistingAccount, + ExistingAccountWithTime, + ExistingAccountOutOfTime, NewAccount; } @@ -61,7 +64,8 @@ class LoginFragment : ServiceDependentFragment(OnNoService.GoToLaunchScreen) { override fun onSafelyResume() { jobTracker.newUiJob("advanceToNextScreen") { when (loggedIn.await()) { - LoginResult.ExistingAccount -> openNextScreen(ConnectFragment()) + LoginResult.ExistingAccountWithTime -> openNextScreen(ConnectFragment()) + LoginResult.ExistingAccountOutOfTime -> openNextScreen(OutOfTimeFragment()) LoginResult.NewAccount -> openNextScreen(WelcomeFragment()) } } @@ -121,20 +125,32 @@ class LoginFragment : ServiceDependentFragment(OnNoService.GoToLaunchScreen) { private fun performLogin(accountToken: String) = GlobalScope.launch(Dispatchers.Main) { jobTracker.newUiJob("login") { - val loginSucceeded = jobTracker.runOnBackground { + val loginResult = jobTracker.runOnBackground { val accountDataResult = daemon.getAccountData(accountToken) when (accountDataResult) { - is GetAccountDataResult.Ok, is GetAccountDataResult.RpcError -> { + is GetAccountDataResult.Ok -> { daemon.setAccount(accountToken) - true + + val expiryString = accountDataResult.accountData.expiry + val expiry = DateTime.parse(expiryString, AccountCache.EXPIRY_FORMAT) + + if (expiry.isAfterNow()) { + LoginResult.ExistingAccountWithTime + } else { + LoginResult.ExistingAccountOutOfTime + } + } + is GetAccountDataResult.RpcError -> { + daemon.setAccount(accountToken) + LoginResult.ExistingAccountWithTime } - else -> false + else -> null } } - if (loginSucceeded) { - loggedIn("", LoginResult.ExistingAccount) + if (loginResult != null) { + loggedIn("", loginResult) } else { loginFailure(R.string.login_fail_description) } diff --git a/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/OutOfTimeFragment.kt b/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/OutOfTimeFragment.kt new file mode 100644 index 0000000000..007589f13f --- /dev/null +++ b/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/OutOfTimeFragment.kt @@ -0,0 +1,153 @@ +package net.mullvad.mullvadvpn.ui + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import kotlinx.coroutines.delay +import net.mullvad.mullvadvpn.R +import net.mullvad.mullvadvpn.model.TunnelState +import net.mullvad.mullvadvpn.ui.widget.Button +import net.mullvad.mullvadvpn.ui.widget.UrlButton +import net.mullvad.mullvadvpn.util.JobTracker +import net.mullvad.talpid.tunnel.ActionAfterDisconnect +import org.joda.time.DateTime + +class OutOfTimeFragment : ServiceDependentFragment(OnNoService.GoToLaunchScreen) { + private val jobTracker = JobTracker() + + private lateinit var headerBar: HeaderBar + + private lateinit var buyCreditButton: UrlButton + private lateinit var disconnectButton: Button + private lateinit var redeemButton: Button + + private var tunnelStateListener: Int? = null + + private var tunnelState: TunnelState = TunnelState.Disconnected() + set(value) { + field = value + updateDisconnectButton() + updateBuyButtons() + headerBar.setState(value) + } + + override fun onSafelyCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + val view = inflater.inflate(R.layout.out_of_time, container, false) + + view.findViewById<View>(R.id.settings).setOnClickListener { + parentActivity.openSettings() + } + + headerBar = HeaderBar(view, resources) + + disconnectButton = view.findViewById<Button>(R.id.disconnect).apply { + setOnClickAction("disconnect", jobTracker) { + connectionProxy.disconnect() + } + } + + buyCreditButton = view.findViewById<UrlButton>(R.id.buy_credit).apply { + prepare(daemon, jobTracker) + } + + redeemButton = view.findViewById<Button>(R.id.redeem_voucher).apply { + setOnClickAction("openRedeemVoucherDialog", jobTracker) { + showRedeemVoucherDialog() + } + } + + tunnelStateListener = connectionProxy.onStateChange.subscribe() { newState -> + jobTracker.newUiJob("updateTunnelState") { + tunnelState = newState + } + } + + return view + } + + override fun onSafelyResume() { + accountCache.onAccountDataChange = { _, expiry -> + checkExpiry(expiry) + } + + jobTracker.newBackgroundJob("pollAccountData") { + while (true) { + accountCache.fetchAccountExpiry() + delay(POLL_INTERVAL) + } + } + } + + override fun onSafelyPause() { + accountCache.onAccountDataChange = null + jobTracker.cancelJob("pollAccountData") + } + + override fun onSafelyDestroyView() { + jobTracker.cancelAllJobs() + + tunnelStateListener?.let { id -> + connectionProxy.onStateChange.unsubscribe(id) + } + } + + private fun showRedeemVoucherDialog() { + val transaction = fragmentManager?.beginTransaction() + + transaction?.addToBackStack(null) + + RedeemVoucherDialogFragment().show(transaction, null) + } + + 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 + } + is TunnelState.Error -> state.errorState.isBlocking + } + + disconnectButton.apply { + if (showButton) { + setEnabled(true) + visibility = View.VISIBLE + } else { + setEnabled(false) + visibility = View.GONE + } + } + } + + private fun updateBuyButtons() { + val hasConnectivity = tunnelState is TunnelState.Disconnected + + buyCreditButton.setEnabled(hasConnectivity) + redeemButton.setEnabled(hasConnectivity) + } + + private fun checkExpiry(maybeExpiry: DateTime?) { + maybeExpiry?.let { expiry -> + if (expiry.isAfterNow()) { + jobTracker.newUiJob("advanceToConnectScreen") { + advanceToConnectScreen() + } + } + } + } + + private fun advanceToConnectScreen() { + fragmentManager?.beginTransaction()?.apply { + replace(R.id.main_fragment, ConnectFragment()) + commit() + } + } +} diff --git a/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/widget/Button.kt b/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/widget/Button.kt index ac510d0735..0dbed7eb19 100644 --- a/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/widget/Button.kt +++ b/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/widget/Button.kt @@ -13,13 +13,15 @@ import net.mullvad.mullvadvpn.util.JobTracker open class Button : FrameLayout { enum class ButtonColor { Blue, - Green; + Green, + Red; companion object { internal fun fromCode(code: Int): ButtonColor { when (code) { 0 -> return Blue 1 -> return Green + 2 -> return Red else -> throw Exception("Invalid buttonColor attribute value") } } @@ -49,6 +51,7 @@ open class Button : FrameLayout { val backgroundResource = when (value) { ButtonColor.Blue -> R.drawable.blue_button_background ButtonColor.Green -> R.drawable.green_button_background + ButtonColor.Red -> R.drawable.red_button_background } button.setBackgroundResource(backgroundResource) diff --git a/android/src/main/res/layout/out_of_time.xml b/android/src/main/res/layout/out_of_time.xml new file mode 100644 index 0000000000..f1ae5b06a6 --- /dev/null +++ b/android/src/main/res/layout/out_of_time.xml @@ -0,0 +1,90 @@ +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:mullvad="http://schemas.android.com/apk/res-auto" + android:id="@+id/main_fragment" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:orientation="vertical"> + <LinearLayout android:id="@+id/header_bar" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_weight="0" + android:orientation="horizontal" + android:gravity="center_vertical" + android:background="@color/red" + android:elevation="0.5dp"> + <ImageView android:layout_width="50dp" + android:layout_height="50dp" + android:layout_marginLeft="12dp" + android:layout_marginVertical="12dp" + android:layout_weight="0" + android:src="@drawable/logo_icon" /> + <TextView android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginHorizontal="8dp" + android:layout_marginVertical="12dp" + android:layout_weight="1" + android:textColor="@color/white80" + android:textSize="24sp" + android:textStyle="bold" + android:text="@string/app_name" + android:textAllCaps="true" /> + <ImageButton android:id="@+id/settings" + android:layout_width="wrap_content" + android:layout_height="match_parent" + android:layout_weight="0" + android:paddingHorizontal="12dp" + android:background="?android:attr/selectableItemBackground" + android:src="@drawable/icon_settings" /> + </LinearLayout> + <ImageView android:layout_width="60dp" + android:layout_height="60dp" + android:layout_gravity="center" + android:layout_marginTop="24dp" + android:layout_marginBottom="18dp" + android:src="@drawable/icon_fail" /> + <TextView android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginHorizontal="24dp" + android:textColor="@color/white" + android:textSize="32sp" + android:textStyle="bold" + android:text="@string/out_of_time" /> + <TextView android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginHorizontal="24dp" + android:layout_marginTop="8dp" + android:layout_marginBottom="11dp" + android:textColor="@color/white" + android:textSize="13sp" + android:text="@string/no_more_vpn_time_left" /> + <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:padding="24dp" + 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_marginBottom="24dp" + android:visibility="gone" + mullvad:buttonColor="red" + mullvad:text="@string/disconnect" /> + <net.mullvad.mullvadvpn.ui.widget.UrlButton android:id="@+id/buy_credit" + android:layout_width="match_parent" + android:layout_height="wrap_content" + mullvad:buttonColor="green" + mullvad:text="@string/buy_more_credit" + mullvad:url="@string/account_url" + mullvad:withToken="true" /> + <net.mullvad.mullvadvpn.ui.widget.Button android:id="@+id/redeem_voucher" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginTop="24dp" + mullvad:buttonColor="green" + mullvad:text="@string/redeem_voucher" /> + </LinearLayout> +</LinearLayout> diff --git a/android/src/main/res/values/attrs.xml b/android/src/main/res/values/attrs.xml index 362af43680..449db0efd7 100644 --- a/android/src/main/res/values/attrs.xml +++ b/android/src/main/res/values/attrs.xml @@ -6,6 +6,8 @@ value="0" /> <enum name="green" value="1" /> + <enum name="red" + value="2" /> </attr> <attr name="detailImage" format="reference" /> diff --git a/android/src/main/res/values/strings.xml b/android/src/main/res/values/strings.xml index 27d7350a0c..86cca30416 100644 --- a/android/src/main/res/values/strings.xml +++ b/android/src/main/res/values/strings.xml @@ -29,6 +29,7 @@ <string name="pay_to_start_using">To start using the app, you first need to add time to your account. Either buy credit on our website or redeem a voucher.</string> <string name="buy_credit">Buy credit</string> + <string name="buy_more_credit">Buy more credit</string> <string name="redeem_voucher">Redeem voucher</string> <string name="enter_voucher_code">Enter voucher code</string> <string name="voucher_hint">XXXX-XXXX-XXXX-XXXX</string> @@ -41,6 +42,8 @@ <string name="less_than_a_day_left">less than a day left</string> <string name="less_than_a_minute_ago">less than a minute ago</string> <string name="out_of_time">Out of time</string> + <string name="no_more_vpn_time_left">You have no more VPN time left on this account. Either buy + credit on our website or redeem a voucher.</string> <string name="settings_preferences">Preferences</string> <string name="settings_advanced">Advanced</string> <string name="app_version">App version</string> |
