diff options
| author | Janito Vaqueiro Ferreira Filho <janito@mullvad.net> | 2019-06-17 07:20:34 -0300 |
|---|---|---|
| committer | Janito Vaqueiro Ferreira Filho <janito@mullvad.net> | 2019-06-17 07:20:34 -0300 |
| commit | b6db007471f0103ef7982ac8cc4852f62b51c494 (patch) | |
| tree | 11774361884e3e795c06b32b22e37c73b903fbd5 | |
| parent | d18c279cc25cee041bcf5b8231878769b7ae74ac (diff) | |
| parent | ff929470e7dbe3aa5aa5ceafb724c68bb64b7498 (diff) | |
| download | mullvadvpn-b6db007471f0103ef7982ac8cc4852f62b51c494.tar.xz mullvadvpn-b6db007471f0103ef7982ac8cc4852f62b51c494.zip | |
Merge branch 'account-screen-on-android'
18 files changed, 481 insertions, 3 deletions
diff --git a/android/build.gradle b/android/build.gradle index 914ee21e88..5e22c4decb 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -61,6 +61,7 @@ dependencies { implementation 'com.android.support:recyclerview-v7:28.0.0' implementation 'org.jetbrains.kotlin:kotlin-stdlib:1.3.21' implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.1.1' + implementation 'joda-time:joda-time:2.10.2' } buildscript { diff --git a/android/src/main/kotlin/net/mullvad/mullvadvpn/AccountCache.kt b/android/src/main/kotlin/net/mullvad/mullvadvpn/AccountCache.kt new file mode 100644 index 0000000000..af0e02da7a --- /dev/null +++ b/android/src/main/kotlin/net/mullvad/mullvadvpn/AccountCache.kt @@ -0,0 +1,55 @@ +package net.mullvad.mullvadvpn + +import kotlinx.coroutines.async +import kotlinx.coroutines.Deferred +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.GlobalScope + +import org.joda.time.format.DateTimeFormat +import org.joda.time.DateTime + +import net.mullvad.mullvadvpn.model.Settings + +val EXPIRY_FORMAT = DateTimeFormat.forPattern("YYYY-MM-dd HH:mm:ss z") + +class AccountCache(val parentActivity: MainActivity) { + private var daemon = parentActivity.asyncDaemon + + var settings = parentActivity.asyncSettings + set(value) { + field = value + accountNumber = fetchAccountNumber() + accountExpiry = fetchAccountExpiry() + } + + var accountNumber = fetchAccountNumber() + private set + var accountExpiry = fetchAccountExpiry() + private set + + fun onDestroy() { + accountExpiry.cancel() + accountNumber.cancel() + } + + private fun fetchAccountNumber() = GlobalScope.async(Dispatchers.Default) { + settings.await().accountToken + } + + private fun fetchAccountExpiry() = GlobalScope.async(Dispatchers.Default) { + val accountNumber = accountNumber.await() + + if (accountNumber != null) { + val accountData = daemon.await().getAccountData(accountNumber) + val accountExpiry = accountData?.expiry + + if (accountExpiry != null) { + DateTime.parse(accountExpiry, EXPIRY_FORMAT) + } else { + null + } + } else { + null + } + } +} diff --git a/android/src/main/kotlin/net/mullvad/mullvadvpn/AccountFragment.kt b/android/src/main/kotlin/net/mullvad/mullvadvpn/AccountFragment.kt new file mode 100644 index 0000000000..3dda9e2f8b --- /dev/null +++ b/android/src/main/kotlin/net/mullvad/mullvadvpn/AccountFragment.kt @@ -0,0 +1,81 @@ +package net.mullvad.mullvadvpn + +import java.text.DateFormat + +import kotlinx.coroutines.launch +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.Job + +import android.content.Context +import android.os.Bundle +import android.support.v4.app.Fragment +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.TextView + +import org.joda.time.DateTime + +class AccountFragment : Fragment() { + private lateinit var parentActivity: MainActivity + + private lateinit var accountExpiryContainer: View + private lateinit var accountExpiryDisplay: TextView + private lateinit var accountNumberContainer: View + private lateinit var accountNumberDisplay: TextView + + private var updateViewJob: Job? = null + + override fun onAttach(context: Context) { + super.onAttach(context) + + parentActivity = context as MainActivity + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + val view = inflater.inflate(R.layout.account, container, false) + + view.findViewById<View>(R.id.back).setOnClickListener { + parentActivity.onBackPressed() + } + + accountExpiryContainer = view.findViewById<View>(R.id.account_expiry_container) + accountNumberContainer = view.findViewById<View>(R.id.account_number_container) + + accountExpiryDisplay = view.findViewById<TextView>(R.id.account_expiry_display) + accountNumberDisplay = view.findViewById<TextView>(R.id.account_number_display) + + updateViewJob = updateView() + + return view + } + + private fun updateView() = GlobalScope.launch(Dispatchers.Main) { + val accountCache = parentActivity.accountCache + val accountNumber = accountCache.accountNumber.await() + + if (accountNumber != null) { + accountNumberDisplay.setText(accountCache.accountNumber.await()) + accountNumberContainer.visibility = View.VISIBLE + + val accountExpiry = accountCache.accountExpiry.await() + + if (accountExpiry != null) { + accountExpiryDisplay.setText(formatExpiry(accountExpiry)) + accountExpiryContainer.visibility = View.VISIBLE + } + } + } + + private fun formatExpiry(expiry: DateTime): String { + val expiryInstant = expiry.toDate() + val formatter = DateFormat.getDateTimeInstance() + + return formatter.format(expiryInstant) + } +} diff --git a/android/src/main/kotlin/net/mullvad/mullvadvpn/MainActivity.kt b/android/src/main/kotlin/net/mullvad/mullvadvpn/MainActivity.kt index fb2a5d6d77..71416c92c4 100644 --- a/android/src/main/kotlin/net/mullvad/mullvadvpn/MainActivity.kt +++ b/android/src/main/kotlin/net/mullvad/mullvadvpn/MainActivity.kt @@ -36,6 +36,8 @@ class MainActivity : FragmentActivity() { val settings get() = runBlocking { asyncSettings.await() } + val accountCache = AccountCache(this) + var selectedRelayItem: RelayItem? = null private val restoreSelectedRelayListItemJob = restoreSelectedRelayListItem() @@ -81,6 +83,8 @@ class MainActivity : FragmentActivity() { } override fun onDestroy() { + accountCache.onDestroy() + restoreSelectedRelayListItemJob.cancel() waitForDaemonJob?.cancel() asyncSettings.cancel() @@ -104,6 +108,13 @@ class MainActivity : FragmentActivity() { } } + fun refetchSettings() { + if (asyncSettings.isCompleted) { + asyncSettings = fetchSettings() + accountCache.settings = asyncSettings + } + } + private fun addInitialFragment() { supportFragmentManager?.beginTransaction()?.apply { add(R.id.main_fragment, LaunchFragment()) diff --git a/android/src/main/kotlin/net/mullvad/mullvadvpn/RemainingTimeLabel.kt b/android/src/main/kotlin/net/mullvad/mullvadvpn/RemainingTimeLabel.kt new file mode 100644 index 0000000000..6e772c1675 --- /dev/null +++ b/android/src/main/kotlin/net/mullvad/mullvadvpn/RemainingTimeLabel.kt @@ -0,0 +1,72 @@ +package net.mullvad.mullvadvpn + +import kotlinx.coroutines.async +import kotlinx.coroutines.launch +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.GlobalScope + +import org.joda.time.format.DateTimeFormat +import org.joda.time.DateTime +import org.joda.time.Duration +import org.joda.time.PeriodType + +import android.view.View +import android.widget.TextView + +class RemainingTimeLabel(val parentActivity: MainActivity, val view: View) { + private val accountCache = parentActivity.accountCache + + private val expiredColor = parentActivity.getColor(R.color.red) + private val normalColor = parentActivity.getColor(R.color.white60) + + private val resources = parentActivity.resources + + private val label = view.findViewById<TextView>(R.id.remaining_time) + + private var updateJob = updateLabel() + + fun onResume() { + if (updateJob.isCompleted) { + parentActivity.refetchSettings() + updateJob = updateLabel() + } + } + + fun onDestroy() { + updateJob.cancel() + } + + private fun updateLabel() = GlobalScope.launch(Dispatchers.Main) { + val expiry = accountCache.accountExpiry.await() + + if (expiry != null) { + val remainingTime = Duration(DateTime.now(), expiry) + + if (remainingTime.isShorterThan(Duration.ZERO)) { + label.setText(R.string.out_of_time) + label.setTextColor(expiredColor) + } else { + val remainingTimeInfo = + remainingTime.toPeriodTo(expiry, PeriodType.yearMonthDayTime()) + + if (remainingTimeInfo.years > 0) { + label.setText(getRemainingText(R.plurals.years_left, remainingTimeInfo.years)) + } else if (remainingTimeInfo.months > 0) { + label.setText(getRemainingText(R.plurals.months_left, remainingTimeInfo.months)) + } else if (remainingTimeInfo.days > 0) { + label.setText(getRemainingText(R.plurals.days_left, remainingTimeInfo.days)) + } else { + label.setText(R.string.less_than_a_day_left) + } + + label.setTextColor(normalColor) + } + } else { + label.text = "" + } + } + + private fun getRemainingText(pluralId: Int, quantity: Int): String { + return resources.getQuantityString(pluralId, quantity, quantity) + } +} diff --git a/android/src/main/kotlin/net/mullvad/mullvadvpn/SettingsFragment.kt b/android/src/main/kotlin/net/mullvad/mullvadvpn/SettingsFragment.kt index a958978daa..ee89ed7945 100644 --- a/android/src/main/kotlin/net/mullvad/mullvadvpn/SettingsFragment.kt +++ b/android/src/main/kotlin/net/mullvad/mullvadvpn/SettingsFragment.kt @@ -10,6 +10,15 @@ import android.widget.Button import android.widget.ImageButton class SettingsFragment : Fragment() { + private lateinit var parentActivity: MainActivity + private lateinit var remainingTimeLabel: RemainingTimeLabel + + override fun onAttach(context: Context) { + super.onAttach(context) + + parentActivity = context as MainActivity + } + override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, @@ -25,6 +34,34 @@ class SettingsFragment : Fragment() { activity?.finishAndRemoveTask() } + view.findViewById<View>(R.id.account).setOnClickListener { openAccountSettings() } + + remainingTimeLabel = RemainingTimeLabel(parentActivity, view) + return view } + + override fun onResume() { + super.onResume() + remainingTimeLabel.onResume() + } + + override fun onDestroyView() { + remainingTimeLabel.onDestroy() + super.onDestroyView() + } + + private fun openAccountSettings() { + fragmentManager?.beginTransaction()?.apply { + setCustomAnimations( + R.anim.fragment_enter_from_right, + R.anim.fragment_half_exit_to_left, + R.anim.fragment_half_enter_from_left, + R.anim.fragment_exit_to_right + ) + replace(R.id.main_fragment, AccountFragment()) + addToBackStack(null) + commit() + } + } } diff --git a/android/src/main/res/anim/fragment_enter_from_right.xml b/android/src/main/res/anim/fragment_enter_from_right.xml new file mode 100644 index 0000000000..c8e79bd2ee --- /dev/null +++ b/android/src/main/res/anim/fragment_enter_from_right.xml @@ -0,0 +1,8 @@ +<?xml version="1.0" encoding="utf-8"?> +<set xmlns:android="http://schemas.android.com/apk/res/android"> + <translate + android:fromXDelta="100%p" + android:toXDelta="0" + android:duration="450" + /> +</set> diff --git a/android/src/main/res/anim/fragment_exit_to_right.xml b/android/src/main/res/anim/fragment_exit_to_right.xml new file mode 100644 index 0000000000..9d5970e110 --- /dev/null +++ b/android/src/main/res/anim/fragment_exit_to_right.xml @@ -0,0 +1,8 @@ +<?xml version="1.0" encoding="utf-8"?> +<set xmlns:android="http://schemas.android.com/apk/res/android"> + <translate + android:fromXDelta="0" + android:toXDelta="100%p" + android:duration="450" + /> +</set> diff --git a/android/src/main/res/anim/fragment_half_enter_from_left.xml b/android/src/main/res/anim/fragment_half_enter_from_left.xml new file mode 100644 index 0000000000..178181d7fb --- /dev/null +++ b/android/src/main/res/anim/fragment_half_enter_from_left.xml @@ -0,0 +1,8 @@ +<?xml version="1.0" encoding="utf-8"?> +<set xmlns:android="http://schemas.android.com/apk/res/android"> + <translate + android:fromXDelta="-50%p" + android:toXDelta="0" + android:duration="450" + /> +</set> diff --git a/android/src/main/res/anim/fragment_half_exit_to_left.xml b/android/src/main/res/anim/fragment_half_exit_to_left.xml new file mode 100644 index 0000000000..ba05a62afe --- /dev/null +++ b/android/src/main/res/anim/fragment_half_exit_to_left.xml @@ -0,0 +1,8 @@ +<?xml version="1.0" encoding="utf-8"?> +<set xmlns:android="http://schemas.android.com/apk/res/android"> + <translate + android:fromXDelta="0%p" + android:toXDelta="-50%p" + android:duration="450" + /> +</set> diff --git a/android/src/main/res/drawable/cell_button_background.xml b/android/src/main/res/drawable/cell_button_background.xml new file mode 100644 index 0000000000..8791c01141 --- /dev/null +++ b/android/src/main/res/drawable/cell_button_background.xml @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="utf-8"?> +<selector xmlns:android="http://schemas.android.com/apk/res/android"> + <item android:state_pressed="false"> + <shape android:shape="rectangle"> + <solid android:color="@color/blue"/> + </shape> + </item> + + <item android:state_pressed="true"> + <shape android:shape="rectangle"> + <solid android:color="@color/blue80"/> + </shape> + </item> +</selector> diff --git a/android/src/main/res/drawable/icon_back.xml b/android/src/main/res/drawable/icon_back.xml new file mode 100644 index 0000000000..a314fe65bd --- /dev/null +++ b/android/src/main/res/drawable/icon_back.xml @@ -0,0 +1,13 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="24" + android:viewportHeight="24"> + <path + android:pathData="M12,24C5.3731,24 -0,18.6269 -0,12C-0,5.3731 5.3731,0 12,0C18.6269,0 24,5.3731 24,12C24,18.6269 18.6269,24 12,24ZM7.0055,11.9979C6.9755,12.2732 7.0685,12.5604 7.2852,12.7774L13.2129,18.7118C13.5936,19.0929 14.2231,19.0908 14.6233,18.7027L14.6942,18.634C15.0925,18.2478 15.1055,17.6196 14.7109,17.218L9.5805,11.9979L14.7109,6.7777C15.1055,6.3762 15.0925,5.7479 14.6942,5.3618L14.6233,5.293C14.2231,4.9049 13.5936,4.9028 13.2129,5.2839L7.2852,11.2184C7.0685,11.4353 6.9755,11.7225 7.0055,11.9979L7.0055,11.9979Z" + android:strokeWidth="1" + android:fillColor="#FFFFFF" + android:fillAlpha="0.6" + android:fillType="evenOdd" + android:strokeColor="#00000000"/> +</vector> diff --git a/android/src/main/res/drawable/icon_chevron.xml b/android/src/main/res/drawable/icon_chevron.xml index f10c8d04c7..d5b96fc6e5 100644 --- a/android/src/main/res/drawable/icon_chevron.xml +++ b/android/src/main/res/drawable/icon_chevron.xml @@ -1,12 +1,12 @@ <?xml version="1.0" encoding="UTF-8"?> <vector xmlns:android="http://schemas.android.com/apk/res/android" - android:width="24dp" + android:width="14dp" android:height="24dp" - android:viewportWidth="24.0" + android:viewportWidth="14.0" android:viewportHeight="24.0" > - <group android:translateX="8.5" android:translateY="6.0"> + <group android:translateX="5.0" android:translateY="6.0"> <path android:fillColor="#FFFFFF" android:pathData="M0.335204989,1.95371785 L4.23669259,6 L0.335204989,10.0462822 C-0.111734996,10.4932221 -0.111734996,11.217855 0.335204989,11.664795 C0.782144974,12.111735 1.49826561,12.111735 1.9452056,11.664795 L6.66818642,6.80553188 C6.88657769,6.58714061 6.99779844,6.29559541 6.99881099,6.00303766 C6.99779844,5.70440459 6.88657769,5.41285939 6.66818642,5.19446812 L1.9452056,0.335204989 C1.49826561,-0.111734996 0.782144974,-0.111734996 0.335204989,0.335204989 C-0.111734996,0.782144974 -0.111734996,1.50677786 0.335204989,1.95371785 Z" /> diff --git a/android/src/main/res/layout/account.xml b/android/src/main/res/layout/account.xml new file mode 100644 index 0000000000..2e7bba0340 --- /dev/null +++ b/android/src/main/res/layout/account.xml @@ -0,0 +1,102 @@ +<LinearLayout + xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:background="@color/darkBlue" + android:orientation="vertical" + android:gravity="left" + android:elevation="2dp" + > + <LinearLayout android:id="@+id/back" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:padding="12dp" + android:orientation="horizontal" + android:gravity="center_vertical | left" + android:clickable="true" + android:background="?android:attr/selectableItemBackground" + > + <ImageView + android:layout_width="24dp" + android:layout_height="24dp" + android:layout_marginRight="8dp" + android:src="@drawable/icon_back" + /> + <TextView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:textColor="@color/white60" + android:textSize="13sp" + android:textStyle="bold" + android:text="@string/settings" + /> + </LinearLayout> + <LinearLayout + android:layout_width="match_parent" + android:layout_height="match_parent" + android:layout_marginHorizontal="24dp" + android:layout_marginTop="4dp" + android:layout_marginBottom="24dp" + android:orientation="vertical" + > + <TextView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginBottom="24dp" + android:textColor="@color/white" + android:textSize="32sp" + android:textStyle="bold" + android:text="@string/settings_account" + /> + <LinearLayout android:id="@+id/account_number_container" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginBottom="24dp" + android:orientation="vertical" + android:visibility="invisible" + > + <TextView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginBottom="9dp" + android:textColor="@color/white60" + android:textSize="13sp" + android:textStyle="bold" + android:text="@string/account_number" + /> + <TextView android:id="@+id/account_number_display" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:textColor="@color/white" + android:textSize="16sp" + android:textStyle="bold" + android:text="" + /> + </LinearLayout> + <LinearLayout android:id="@+id/account_expiry_container" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginBottom="24dp" + android:orientation="vertical" + android:visibility="invisible" + > + <TextView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginBottom="9dp" + android:textColor="@color/white60" + android:textSize="13sp" + android:textStyle="bold" + android:text="@string/paid_until" + /> + <TextView android:id="@+id/account_expiry_display" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:textColor="@color/white" + android:textSize="16sp" + android:textStyle="bold" + android:text="" + /> + </LinearLayout> + </LinearLayout> +</LinearLayout> diff --git a/android/src/main/res/layout/settings.xml b/android/src/main/res/layout/settings.xml index 0c98ad539e..664a2dc790 100644 --- a/android/src/main/res/layout/settings.xml +++ b/android/src/main/res/layout/settings.xml @@ -27,6 +27,45 @@ android:textStyle="bold" android:text="@string/settings" /> + <LinearLayout android:id="@+id/account" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:paddingHorizontal="16dp" + android:background="@drawable/cell_button_background" + android:clickable="true" + android:gravity="center" + > + <TextView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_weight="1" + android:paddingHorizontal="8dp" + android:paddingVertical="17dp" + android:textColor="@color/white" + android:textSize="20sp" + android:textStyle="bold" + android:text="@string/settings_account" + /> + <TextView android:id="@+id/remaining_time" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_weight="1" + android:paddingHorizontal="8dp" + android:gravity="right" + android:textColor="@color/white60" + android:textSize="13sp" + android:textStyle="bold" + android:text="" + android:textAllCaps="true" + /> + <ImageView + android:layout_width="14dp" + android:layout_height="24dp" + android:layout_weight="0" + android:alpha="0.6" + android:src="@drawable/icon_chevron" + /> + </LinearLayout> <Button android:id="@+id/quit_button" android:layout_marginTop="24dp" android:layout_marginLeft="24dp" diff --git a/android/src/main/res/values/colors.xml b/android/src/main/res/values/colors.xml index 7d2ef85101..0e7b0cea4e 100644 --- a/android/src/main/res/values/colors.xml +++ b/android/src/main/res/values/colors.xml @@ -1,6 +1,7 @@ <resources> <color name="colorPrimary">#294D73</color> <color name="blue">#294D73</color> + <color name="blue80">#CC294D73</color> <color name="blue40">#66294D73</color> <color name="blue20">#33294D73</color> <color name="darkBlue">#192E45</color> diff --git a/android/src/main/res/values/plurals.xml b/android/src/main/res/values/plurals.xml new file mode 100644 index 0000000000..5395e2f4fa --- /dev/null +++ b/android/src/main/res/values/plurals.xml @@ -0,0 +1,14 @@ +<resources> + <plurals name="years_left"> + <item quantity="one">%d year left</item> + <item quantity="other">%d years left</item> + </plurals> + <plurals name="months_left"> + <item quantity="one">%d month left</item> + <item quantity="other">%d months left</item> + </plurals> + <plurals name="days_left"> + <item quantity="one">%d day left</item> + <item quantity="other">%d days left</item> + </plurals> +</resources> diff --git a/android/src/main/res/values/strings.xml b/android/src/main/res/values/strings.xml index 18dea4cd12..a3a033e747 100644 --- a/android/src/main/res/values/strings.xml +++ b/android/src/main/res/values/strings.xml @@ -13,8 +13,14 @@ <string name="login_fail_description">Invalid account number, try again</string> <string name="settings">Settings</string> + <string name="settings_account">Account</string> + <string name="less_than_a_day_left">less than a day left</string> + <string name="out_of_time">Out of time</string> <string name="quit">Quit</string> + <string name="account_number">Account number</string> + <string name="paid_until">Paid until</string> + <string name="unsecured_connection">Unsecured connection</string> <string name="creating_secure_connection">Creating secure connection</string> <string name="secure_connection">Secure connection</string> |
