summaryrefslogtreecommitdiffhomepage
path: root/android
diff options
context:
space:
mode:
Diffstat (limited to 'android')
-rw-r--r--android/src/main/kotlin/net/mullvad/mullvadvpn/service/AccountCache.kt44
-rw-r--r--android/src/main/kotlin/net/mullvad/mullvadvpn/ui/AccountFragment.kt75
-rw-r--r--android/src/main/kotlin/net/mullvad/mullvadvpn/ui/RedeemVoucherDialogFragment.kt34
-rw-r--r--android/src/main/kotlin/net/mullvad/mullvadvpn/ui/widget/UrlButton.kt8
-rw-r--r--android/src/main/res/layout/account.xml29
5 files changed, 170 insertions, 20 deletions
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 4231ff1e40..f63ca84f58 100644
--- a/android/src/main/kotlin/net/mullvad/mullvadvpn/service/AccountCache.kt
+++ b/android/src/main/kotlin/net/mullvad/mullvadvpn/service/AccountCache.kt
@@ -8,6 +8,13 @@ import net.mullvad.talpid.util.EventNotifier
import org.joda.time.DateTime
import org.joda.time.format.DateTimeFormat
+// Number of retry attempts to check for a changed expiry before giving up.
+// Current value will force the cache to keep fetching for about four minutes or until a new expiry
+// value is received.
+// This is only used if the expiry was invalidated and fetching a new expiry returns the same value
+// as before the invalidation.
+const val MAX_INVALIDATED_RETRIES = 7
+
class AccountCache(val daemon: MullvadDaemon, val settingsListener: SettingsListener) {
companion object {
public val EXPIRY_FORMAT = DateTimeFormat.forPattern("YYYY-MM-dd HH:mm:ss z")
@@ -27,6 +34,8 @@ class AccountCache(val daemon: MullvadDaemon, val settingsListener: SettingsList
onAccountExpiryChange.notify(value)
}
+ private var oldAccountExpiry: DateTime? = null
+
val onAccountNumberChange = EventNotifier<String?>(null)
val onAccountExpiryChange = EventNotifier<DateTime?>(null)
@@ -46,8 +55,9 @@ class AccountCache(val daemon: MullvadDaemon, val settingsListener: SettingsList
val result = daemon.getAccountData(account)
if (result is GetAccountDataResult.Ok) {
- handleNewExpiry(account, result.accountData.expiry)
- break
+ if (handleNewExpiry(account, result.accountData.expiry, retryAttempt)) {
+ break
+ }
} else if (result is GetAccountDataResult.InvalidAccount) {
break
}
@@ -60,6 +70,15 @@ class AccountCache(val daemon: MullvadDaemon, val settingsListener: SettingsList
}
}
+ fun invalidateAccountExpiry(accountExpiryToInvalidate: DateTime) {
+ synchronized(this) {
+ if (accountExpiry == accountExpiryToInvalidate) {
+ oldAccountExpiry = accountExpiryToInvalidate
+ fetchAccountExpiry()
+ }
+ }
+ }
+
fun onDestroy() {
settingsListener.accountNumberNotifier.unsubscribe(this)
jobTracker.cancelAllJobs()
@@ -74,11 +93,26 @@ class AccountCache(val daemon: MullvadDaemon, val settingsListener: SettingsList
}
}
- private fun handleNewExpiry(accountNumberUsedForFetch: String, expiryString: String) {
+ private fun handleNewExpiry(
+ accountNumberUsedForFetch: String,
+ expiryString: String,
+ retryAttempt: Int
+ ): Boolean {
synchronized(this) {
- if (accountNumber === accountNumberUsedForFetch) {
- accountExpiry = DateTime.parse(expiryString, EXPIRY_FORMAT)
+ if (accountNumber !== accountNumberUsedForFetch) {
+ return true
+ }
+
+ val newAccountExpiry = DateTime.parse(expiryString, EXPIRY_FORMAT)
+
+ if (newAccountExpiry != oldAccountExpiry || retryAttempt >= MAX_INVALIDATED_RETRIES) {
+ accountExpiry = newAccountExpiry
+ oldAccountExpiry = null
+
+ return true
}
+
+ return false
}
}
diff --git a/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/AccountFragment.kt b/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/AccountFragment.kt
index 3787d2bafe..f7606ea737 100644
--- a/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/AccountFragment.kt
+++ b/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/AccountFragment.kt
@@ -7,8 +7,11 @@ import android.view.View
import android.view.ViewGroup
import java.text.DateFormat
import net.mullvad.mullvadvpn.R
+import net.mullvad.mullvadvpn.model.TunnelState
+import net.mullvad.mullvadvpn.ui.widget.Button
import net.mullvad.mullvadvpn.ui.widget.CopyableInformationView
import net.mullvad.mullvadvpn.ui.widget.InformationView
+import net.mullvad.mullvadvpn.ui.widget.UrlButton
import org.joda.time.DateTime
class AccountFragment : ServiceDependentFragment(OnNoService.GoBack) {
@@ -16,8 +19,30 @@ class AccountFragment : ServiceDependentFragment(OnNoService.GoBack) {
private val timeStyle = DateFormat.SHORT
private val expiryFormatter = DateFormat.getDateTimeInstance(dateStyle, timeStyle)
+ private var oldAccountExpiry: DateTime? = null
+
+ private var currentAccountExpiry: DateTime? = null
+ set(value) {
+ field = value
+
+ synchronized(this) {
+ if (value != oldAccountExpiry) {
+ oldAccountExpiry = null
+ }
+ }
+ }
+
+ private var hasConnectivity = true
+ set(value) {
+ field = value
+ buyCreditButton.setEnabled(value)
+ redeemVoucherButton.setEnabled(value)
+ }
+
private lateinit var accountExpiryView: InformationView
private lateinit var accountNumberView: CopyableInformationView
+ private lateinit var buyCreditButton: Button
+ private lateinit var redeemVoucherButton: Button
override fun onSafelyCreateView(
inflater: LayoutInflater,
@@ -30,7 +55,21 @@ class AccountFragment : ServiceDependentFragment(OnNoService.GoBack) {
parentActivity.onBackPressed()
}
- view.findViewById<View>(R.id.logout).setOnClickListener { logout() }
+ buyCreditButton = view.findViewById<UrlButton>(R.id.buy_credit).apply {
+ prepare(daemon, jobTracker) {
+ checkForAddedTime()
+ }
+ }
+
+ redeemVoucherButton = view.findViewById<Button>(R.id.redeem_voucher).apply {
+ setOnClickAction("redeem", jobTracker) {
+ showRedeemVoucherDialog()
+ }
+ }
+
+ view.findViewById<Button>(R.id.logout).setOnClickAction("logout", jobTracker) {
+ logout()
+ }
accountNumberView = view.findViewById<CopyableInformationView>(R.id.account_number).apply {
displayFormatter = { rawAccountNumber -> addSpacesToAccountNumber(rawAccountNumber) }
@@ -50,9 +89,22 @@ class AccountFragment : ServiceDependentFragment(OnNoService.GoBack) {
accountCache.onAccountExpiryChange.subscribe(this) { accountExpiry ->
jobTracker.newUiJob("updateAccountExpiry") {
+ currentAccountExpiry = accountExpiry
updateAccountExpiry(accountExpiry)
}
}
+
+ connectionProxy.onUiStateChange.subscribe(this) { uiState ->
+ jobTracker.newUiJob("updateHasConnectivity") {
+ hasConnectivity = uiState is TunnelState.Connected ||
+ uiState is TunnelState.Disconnected ||
+ (uiState is TunnelState.Error && !uiState.errorState.isBlocking)
+ }
+ }
+
+ oldAccountExpiry?.let { expiry ->
+ accountCache.invalidateAccountExpiry(expiry)
+ }
}
override fun onSafelyPause() {
@@ -60,6 +112,13 @@ class AccountFragment : ServiceDependentFragment(OnNoService.GoBack) {
accountCache.onAccountExpiryChange.unsubscribe(this)
}
+ private fun checkForAddedTime() {
+ currentAccountExpiry?.let { expiry ->
+ oldAccountExpiry = expiry
+ accountCache.invalidateAccountExpiry(expiry)
+ }
+ }
+
private fun updateAccountExpiry(accountExpiry: DateTime?) {
if (accountExpiry != null) {
accountExpiryView.information = expiryFormatter.format(accountExpiry.toDate())
@@ -69,14 +128,22 @@ class AccountFragment : ServiceDependentFragment(OnNoService.GoBack) {
}
}
- private fun logout() {
+ private fun showRedeemVoucherDialog() {
+ val transaction = fragmentManager?.beginTransaction()
+
+ transaction?.addToBackStack(null)
+
+ RedeemVoucherDialogFragment().show(transaction, null)
+ }
+
+ private suspend fun logout() {
clearAccountNumber()
clearBackStack()
goToLoginScreen()
}
- private fun clearAccountNumber() {
- jobTracker.newBackgroundJob("clearAccountNumber") {
+ private suspend fun clearAccountNumber() {
+ jobTracker.runOnBackground {
daemon.setAccount(null)
}
}
diff --git a/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/RedeemVoucherDialogFragment.kt b/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/RedeemVoucherDialogFragment.kt
index d583684ad1..1a300e1fec 100644
--- a/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/RedeemVoucherDialogFragment.kt
+++ b/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/RedeemVoucherDialogFragment.kt
@@ -15,10 +15,12 @@ import android.widget.EditText
import android.widget.TextView
import net.mullvad.mullvadvpn.R
import net.mullvad.mullvadvpn.model.VoucherSubmissionResult
+import net.mullvad.mullvadvpn.service.AccountCache
import net.mullvad.mullvadvpn.service.MullvadDaemon
import net.mullvad.mullvadvpn.ui.widget.Button
import net.mullvad.mullvadvpn.util.JobTracker
import net.mullvad.mullvadvpn.util.SegmentedInputFormatter
+import org.joda.time.DateTime
const val FULL_VOUCHER_CODE_LENGTH = "XXXX-XXXX-XXXX-XXXX".length
@@ -29,6 +31,9 @@ class RedeemVoucherDialogFragment : DialogFragment() {
private lateinit var errorMessage: TextView
private lateinit var voucherInput: EditText
+ private var accountCache: AccountCache? = null
+ private var accountExpiry: DateTime? = null
+ private var accountExpiryListener: Int? = null
private var redeemButton: Button? = null
private var daemon: MullvadDaemon? = null
@@ -50,6 +55,15 @@ class RedeemVoucherDialogFragment : DialogFragment() {
parentActivity.serviceNotifier.subscribe(this) { connection ->
daemon = connection?.daemon
+
+ accountCache?.onAccountExpiryChange?.unsubscribe(this@RedeemVoucherDialogFragment)
+
+ accountCache = connection?.accountCache?.apply {
+ onAccountExpiryChange
+ .subscribe(this@RedeemVoucherDialogFragment) { newAccountExpiry ->
+ accountExpiry = newAccountExpiry
+ }
+ }
}
}
@@ -112,6 +126,10 @@ class RedeemVoucherDialogFragment : DialogFragment() {
override fun onDetach() {
parentActivity.serviceNotifier.unsubscribe(this)
+ accountExpiryListener?.let { id ->
+ accountCache?.onAccountExpiryChange?.unsubscribe(id)
+ }
+
super.onDetach()
}
@@ -129,11 +147,7 @@ class RedeemVoucherDialogFragment : DialogFragment() {
}
when (result) {
- is VoucherSubmissionResult.Ok -> {
- if (result.submission.timeAdded > 0) {
- dismiss()
- }
- }
+ is VoucherSubmissionResult.Ok -> handleAddedTime(result.submission.timeAdded)
is VoucherSubmissionResult.InvalidVoucher -> showError(R.string.invalid_voucher)
is VoucherSubmissionResult.VoucherAlreadyUsed -> {
showError(R.string.voucher_already_used)
@@ -142,6 +156,16 @@ class RedeemVoucherDialogFragment : DialogFragment() {
}
}
+ private fun handleAddedTime(timeAdded: Long) {
+ if (timeAdded > 0) {
+ accountExpiry?.let { oldAccountExpiry ->
+ accountCache?.invalidateAccountExpiry(oldAccountExpiry)
+ }
+
+ dismiss()
+ }
+ }
+
private fun showError(message: Int) {
errorMessage.apply {
setText(message)
diff --git a/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/widget/UrlButton.kt b/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/widget/UrlButton.kt
index 3215e6f616..c2a7839748 100644
--- a/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/widget/UrlButton.kt
+++ b/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/widget/UrlButton.kt
@@ -45,7 +45,12 @@ class UrlButton : Button {
super.showSpinner = true
}
- fun prepare(daemon: MullvadDaemon, jobTracker: JobTracker, jobName: String = "fetchUrl") {
+ fun prepare(
+ daemon: MullvadDaemon,
+ jobTracker: JobTracker,
+ jobName: String = "fetchUrl",
+ extraOnClickAction: (suspend () -> Unit)? = null
+ ) {
synchronized(this) {
super.setEnabled(shouldEnable)
@@ -55,6 +60,7 @@ class UrlButton : Button {
super.setEnabled(false)
context.startActivity(buildIntent(jobTracker))
+ extraOnClickAction?.invoke()
super.setEnabled(true)
}
diff --git a/android/src/main/res/layout/account.xml b/android/src/main/res/layout/account.xml
index fb18b09c04..6356d3ae70 100644
--- a/android/src/main/res/layout/account.xml
+++ b/android/src/main/res/layout/account.xml
@@ -54,10 +54,29 @@
android:paddingVertical="12dp"
mullvad:description="@string/paid_until"
mullvad:whenMissing="hide" />
- <Button android:id="@+id/logout"
- android:layout_marginTop="12dp"
- android:layout_marginHorizontal="24dp"
- android:text="@string/log_out"
- style="@style/RedButton" />
+ <net.mullvad.mullvadvpn.ui.widget.UrlButton android:id="@+id/buy_credit"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="12dp"
+ android:layout_marginHorizontal="24dp"
+ mullvad:showSpinner="true"
+ mullvad:url="@string/account_url"
+ mullvad:withToken="true"
+ mullvad:text="@string/buy_more_credit"
+ mullvad:buttonColor="green" />
+ <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"
+ android:layout_marginHorizontal="24dp"
+ mullvad:text="@string/redeem_voucher"
+ mullvad:buttonColor="green" />
+ <net.mullvad.mullvadvpn.ui.widget.Button android:id="@+id/logout"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="24dp"
+ android:layout_marginHorizontal="24dp"
+ mullvad:text="@string/log_out"
+ mullvad:buttonColor="red" />
</LinearLayout>
</LinearLayout>