summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorJanito Vaqueiro Ferreira Filho <janito@mullvad.net>2020-05-12 14:52:04 -0300
committerJanito Vaqueiro Ferreira Filho <janito@mullvad.net>2020-05-12 14:52:04 -0300
commit39f028115a338c99efd23d8179831fa3d951ddea (patch)
tree5d8aa016e6385fefa3140ef6fb2063c3beb1b7c5
parentae4bc29d63367f56d14ff501ba25bfe9b5fe2c19 (diff)
parent6b2cea81d07f93039ff62b33f7024616ce1c16fa (diff)
downloadmullvadvpn-39f028115a338c99efd23d8179831fa3d951ddea.tar.xz
mullvadvpn-39f028115a338c99efd23d8179831fa3d951ddea.zip
Merge branch 'add-out-of-time-screen'
-rw-r--r--CHANGELOG.md1
-rw-r--r--android/src/main/kotlin/net/mullvad/mullvadvpn/service/AccountCache.kt6
-rw-r--r--android/src/main/kotlin/net/mullvad/mullvadvpn/ui/ConnectFragment.kt40
-rw-r--r--android/src/main/kotlin/net/mullvad/mullvadvpn/ui/LoginFragment.kt32
-rw-r--r--android/src/main/kotlin/net/mullvad/mullvadvpn/ui/OutOfTimeFragment.kt153
-rw-r--r--android/src/main/kotlin/net/mullvad/mullvadvpn/ui/widget/Button.kt5
-rw-r--r--android/src/main/res/layout/out_of_time.xml90
-rw-r--r--android/src/main/res/values/attrs.xml2
-rw-r--r--android/src/main/res/values/strings.xml3
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>