summaryrefslogtreecommitdiffhomepage
path: root/android
diff options
context:
space:
mode:
authorJanito Vaqueiro Ferreira Filho <janito@mullvad.net>2020-09-02 10:37:47 -0300
committerJanito Vaqueiro Ferreira Filho <janito@mullvad.net>2020-09-02 10:37:47 -0300
commite01c04bd2c8c9ad3d09b449d5cd0a74bb6275948 (patch)
treefd65f3cfe6ee8f4f2dd737539fa9052b0cb53334 /android
parentb66faec8a39beaf9ed54b4545462abd983ef7fff (diff)
parent8e01b0d90c130cedac792c110c8a42ef19768b57 (diff)
downloadmullvadvpn-e01c04bd2c8c9ad3d09b449d5cd0a74bb6275948.tar.xz
mullvadvpn-e01c04bd2c8c9ad3d09b449d5cd0a74bb6275948.zip
Merge branch 'create-account-input-widget'
Diffstat (limited to 'android')
-rw-r--r--android/src/main/kotlin/net/mullvad/mullvadvpn/ui/AccountInput.kt215
-rw-r--r--android/src/main/kotlin/net/mullvad/mullvadvpn/ui/AccountInputController.kt117
-rw-r--r--android/src/main/kotlin/net/mullvad/mullvadvpn/ui/LoginFragment.kt4
-rw-r--r--android/src/main/kotlin/net/mullvad/mullvadvpn/ui/widget/AccountInput.kt160
-rw-r--r--android/src/main/res/layout/account_input.xml24
-rw-r--r--android/src/main/res/layout/login.xml32
6 files changed, 308 insertions, 244 deletions
diff --git a/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/AccountInput.kt b/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/AccountInput.kt
deleted file mode 100644
index 76ac2a86cf..0000000000
--- a/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/AccountInput.kt
+++ /dev/null
@@ -1,215 +0,0 @@
-package net.mullvad.mullvadvpn.ui
-
-import android.content.Context
-import android.text.Editable
-import android.text.TextWatcher
-import android.text.style.MetricAffectingSpan
-import android.view.MotionEvent
-import android.view.View
-import android.view.View.OnTouchListener
-import android.widget.ArrayAdapter
-import android.widget.ImageButton
-import android.widget.ListView
-import android.widget.TextView
-import net.mullvad.mullvadvpn.R
-import net.mullvad.mullvadvpn.ui.AccountInputContainer.BorderState
-
-const val MIN_ACCOUNT_TOKEN_LENGTH = 10
-
-class AccountInput(val parentView: View, context: Context) {
- private val disabledBackgroundColor = context.getColor(R.color.white20)
- private val disabledTextColor = context.getColor(R.color.white)
- private val enabledBackgroundColor = context.getColor(R.color.white)
- private val enabledTextColor = context.getColor(R.color.blue)
- private val errorTextColor = context.getColor(R.color.red)
-
- private var inputHasFocus = false
- set(value) {
- field = value
- updateBorder()
- if (value == true) {
- shouldShowAccountHistory = true
- }
- }
-
- private var usingErrorColor = false
- set(value) {
- field = value
- updateBorder()
- }
-
- var state = LoginState.Initial
- set(value) {
- when (value) {
- LoginState.Initial -> initialState()
- LoginState.InProgress -> loggingInState()
- LoginState.Success -> successState()
- LoginState.Failure -> failureState()
- }
- }
-
- val container: AccountInputContainer = parentView.findViewById(R.id.account_input_container)
- val input: TextView = parentView.findViewById(R.id.account_input)
- val button: ImageButton = parentView.findViewById(R.id.login_button)
- val accountHistoryList: ListView = parentView.findViewById(R.id.account_history_list)
-
- var accountHistory: ArrayList<String>? = null
- set(value) {
- synchronized(this) {
- field = value
- updateAccountHistory()
- }
- }
-
- private var shouldShowAccountHistory = false
- set(value) {
- synchronized(this) {
- field = value
- updateAccountHistory()
- }
- }
-
- var onLogin: ((String) -> Unit)? = null
-
- init {
- button.setOnClickListener {
- onLogin?.invoke(input.text.toString())
- }
- setButtonEnabled(false)
-
- input.apply {
- addTextChangedListener(InputWatcher())
- setOnTouchListener(
- OnTouchListener {
- _, event ->
- if (MotionEvent.ACTION_UP == event.getAction()) {
- shouldShowAccountHistory = true
- }
- false
- }
- )
- }
-
- container.setOnClickListener { shouldShowAccountHistory = true }
- }
-
- private fun initialState() {
- setButtonEnabled(input.text.length >= MIN_ACCOUNT_TOKEN_LENGTH)
- button.visibility = View.VISIBLE
-
- input.apply {
- setTextColor(enabledTextColor)
- setEnabled(true)
- visibility = View.VISIBLE
- }
- }
-
- private fun loggingInState() {
- setButtonEnabled(false)
- button.visibility = View.GONE
-
- input.apply {
- setTextColor(disabledTextColor)
- setEnabled(false)
- visibility = View.VISIBLE
- clearFocus()
- }
- accountHistoryList.visibility = View.INVISIBLE
- }
-
- private fun successState() {
- setButtonEnabled(false)
- button.visibility = View.GONE
- input.visibility = View.GONE
- container.visibility = View.INVISIBLE
- }
-
- private fun failureState() {
- setButtonEnabled(false)
- button.visibility = View.VISIBLE
-
- input.apply {
- findFocus()
- setTextColor(errorTextColor)
- setEnabled(true)
- visibility = View.VISIBLE
- }
-
- usingErrorColor = true
- }
-
- private fun setButtonEnabled(enabled: Boolean) {
- button.apply {
- if (enabled != isEnabled()) {
- setEnabled(enabled)
- setClickable(enabled)
- setFocusable(enabled)
- }
- }
- }
-
- private fun updateAccountHistory() {
- accountHistory?.let { history ->
- accountHistoryList.apply {
- setAdapter(
- ArrayAdapter(
- context,
- R.layout.account_history_entry,
- R.id.account_history_entry_text_view,
- history
- )
- )
-
- setOnItemClickListener { _, _, idx, _ ->
- val accountNumber = history[idx]
-
- input.setText(accountNumber)
- accountHistoryList.visibility = View.GONE
- onLogin?.invoke(accountNumber)
- }
- }
-
- if (shouldShowAccountHistory && accountHistoryList.visibility != View.VISIBLE) {
- accountHistoryList.visibility = View.VISIBLE
- accountHistoryList.translationY = -accountHistoryList.height.toFloat()
- accountHistoryList.animate().translationY(0.0F).setDuration(350).start()
- }
- }
- }
-
- private fun updateBorder() {
- if (usingErrorColor) {
- container.borderState = BorderState.ERROR
- } else if (inputHasFocus) {
- container.borderState = BorderState.FOCUSED
- } else {
- container.borderState = BorderState.UNFOCUSED
- }
- }
-
- private fun leaveErrorState() {
- if (usingErrorColor) {
- input.setTextColor(enabledTextColor)
- usingErrorColor = false
- }
- }
-
- private fun removeFormattingSpans(text: Editable) {
- for (span in text.getSpans(0, text.length, MetricAffectingSpan::class.java)) {
- text.removeSpan(span)
- }
- }
-
- inner class InputWatcher : TextWatcher {
- override fun beforeTextChanged(text: CharSequence, start: Int, count: Int, after: Int) {}
-
- override fun onTextChanged(text: CharSequence, start: Int, before: Int, count: Int) {}
-
- override fun afterTextChanged(text: Editable) {
- inputHasFocus = true
- removeFormattingSpans(text)
- setButtonEnabled(text.length >= MIN_ACCOUNT_TOKEN_LENGTH)
- leaveErrorState()
- }
- }
-}
diff --git a/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/AccountInputController.kt b/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/AccountInputController.kt
new file mode 100644
index 0000000000..4c690537ba
--- /dev/null
+++ b/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/AccountInputController.kt
@@ -0,0 +1,117 @@
+package net.mullvad.mullvadvpn.ui
+
+import android.content.Context
+import android.view.View
+import android.widget.ArrayAdapter
+import android.widget.ListView
+import kotlin.properties.Delegates.observable
+import net.mullvad.mullvadvpn.R
+import net.mullvad.mullvadvpn.ui.AccountInputContainer.BorderState
+import net.mullvad.mullvadvpn.ui.widget.AccountInput
+
+class AccountInputController(val parentView: View, context: Context) {
+ private var inputHasFocus by observable(false) { _, _, hasFocus ->
+ updateBorder()
+
+ if (hasFocus) {
+ shouldShowAccountHistory = true
+ }
+ }
+
+ var state: LoginState by observable(LoginState.Initial) { _, _, newState ->
+ input.loginState = newState
+
+ updateBorder()
+
+ when (newState) {
+ LoginState.Initial -> {}
+ LoginState.InProgress -> loggingInState()
+ LoginState.Success -> successState()
+ LoginState.Failure -> {}
+ }
+ }
+
+ val container: AccountInputContainer = parentView.findViewById(R.id.account_input_container)
+ val accountHistoryList: ListView = parentView.findViewById(R.id.account_history_list)
+
+ val input = parentView.findViewById<AccountInput>(R.id.account_input).apply {
+ onFocusChanged.subscribe(this) { hasFocus ->
+ inputHasFocus = hasFocus
+ }
+
+ onTextChanged.subscribe(this) { _ ->
+ if (state == LoginState.Failure) {
+ state = LoginState.Initial
+ }
+ }
+ }
+
+ var accountHistory: ArrayList<String>? = null
+ set(value) {
+ synchronized(this) {
+ field = value
+ updateAccountHistory()
+ }
+ }
+
+ private var shouldShowAccountHistory = false
+ set(value) {
+ synchronized(this) {
+ field = value
+ updateAccountHistory()
+ }
+ }
+
+ var onLogin: ((String) -> Unit)?
+ get() = input.onLogin
+ set(value) { input.onLogin = value }
+
+ fun onDestroy() {
+ input.onFocusChanged.unsubscribe(this)
+ input.onTextChanged.unsubscribe(this)
+ }
+
+ private fun loggingInState() {
+ accountHistoryList.visibility = View.INVISIBLE
+ }
+
+ private fun successState() {
+ container.visibility = View.INVISIBLE
+ }
+
+ private fun updateAccountHistory() {
+ accountHistory?.let { history ->
+ accountHistoryList.apply {
+ setAdapter(
+ ArrayAdapter(
+ context,
+ R.layout.account_history_entry,
+ R.id.account_history_entry_text_view,
+ history
+ )
+ )
+
+ setOnItemClickListener { _, _, idx, _ ->
+ input.loginWith(history[idx])
+ accountHistoryList.visibility = View.GONE
+ }
+ }
+
+ if (shouldShowAccountHistory && accountHistoryList.visibility != View.VISIBLE) {
+ accountHistoryList.visibility = View.VISIBLE
+ accountHistoryList.translationY = -accountHistoryList.height.toFloat()
+ accountHistoryList.animate().translationY(0.0F).setDuration(350).start()
+ }
+ }
+ }
+
+ private fun updateBorder() {
+ if (state == LoginState.Failure) {
+ container.borderState = BorderState.ERROR
+ } else if (inputHasFocus) {
+ container.borderState = BorderState.FOCUSED
+ } else {
+ container.borderState = BorderState.UNFOCUSED
+ }
+ }
+}
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 2f3efbf9b0..643e5137cd 100644
--- a/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/LoginFragment.kt
+++ b/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/LoginFragment.kt
@@ -28,7 +28,7 @@ class LoginFragment : ServiceDependentFragment(OnNoService.GoToLaunchScreen) {
private lateinit var loggingInStatus: View
private lateinit var loggedInStatus: View
private lateinit var loginFailStatus: View
- private lateinit var accountInput: AccountInput
+ private lateinit var accountInput: AccountInputController
private lateinit var scrollArea: ScrollView
private val loggedIn = CompletableDeferred<LoginResult>()
@@ -46,7 +46,7 @@ class LoginFragment : ServiceDependentFragment(OnNoService.GoToLaunchScreen) {
loggedInStatus = view.findViewById(R.id.logged_in_status)
loginFailStatus = view.findViewById(R.id.login_fail_status)
- accountInput = AccountInput(view, parentActivity)
+ accountInput = AccountInputController(view, parentActivity)
accountInput.onLogin = { accountToken -> login(accountToken) }
view.findViewById<Button>(R.id.create_account)
diff --git a/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/widget/AccountInput.kt b/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/widget/AccountInput.kt
new file mode 100644
index 0000000000..fd1c5a1e96
--- /dev/null
+++ b/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/widget/AccountInput.kt
@@ -0,0 +1,160 @@
+package net.mullvad.mullvadvpn.ui.widget
+
+import android.content.Context
+import android.text.Editable
+import android.text.TextWatcher
+import android.text.style.MetricAffectingSpan
+import android.util.AttributeSet
+import android.view.LayoutInflater
+import android.view.View
+import android.view.View.OnFocusChangeListener
+import android.widget.ImageButton
+import android.widget.LinearLayout
+import android.widget.TextView
+import kotlin.properties.Delegates.observable
+import net.mullvad.mullvadvpn.R
+import net.mullvad.mullvadvpn.ui.LoginState
+import net.mullvad.talpid.util.EventNotifier
+
+const val MIN_ACCOUNT_TOKEN_LENGTH = 10
+
+class AccountInput : LinearLayout {
+ private val disabledTextColor = context.getColor(R.color.white)
+ private val enabledTextColor = context.getColor(R.color.blue)
+ private val errorTextColor = context.getColor(R.color.red)
+
+ private val container =
+ context.getSystemService(Context.LAYOUT_INFLATER_SERVICE).let { service ->
+ val inflater = service as LayoutInflater
+
+ inflater.inflate(R.layout.account_input, this)
+ }
+
+ private val inputWatcher = object : TextWatcher {
+ override fun beforeTextChanged(text: CharSequence, start: Int, count: Int, after: Int) {}
+
+ override fun onTextChanged(text: CharSequence, start: Int, before: Int, count: Int) {}
+
+ override fun afterTextChanged(text: Editable) {
+ removeFormattingSpans(text)
+ setButtonEnabled(text.length >= MIN_ACCOUNT_TOKEN_LENGTH)
+ onTextChanged.notify(Unit)
+ }
+ }
+
+ private val input = container.findViewById<TextView>(R.id.login_input).apply {
+ addTextChangedListener(inputWatcher)
+
+ onFocusChangeListener = OnFocusChangeListener { view, inputHasFocus ->
+ hasFocus = inputHasFocus && view.isEnabled
+ }
+ }
+
+ private val button = container.findViewById<ImageButton>(R.id.login_button).apply {
+ setOnClickListener {
+ onLogin?.invoke(input.text.toString())
+ }
+ }
+
+ val onFocusChanged = EventNotifier(false)
+ private var hasFocus by onFocusChanged.notifiable()
+
+ val onTextChanged = EventNotifier(Unit)
+
+ var loginState by observable(LoginState.Initial) { _, _, state ->
+ when (state) {
+ LoginState.Initial -> initialState()
+ LoginState.InProgress -> loggingInState()
+ LoginState.Success -> successState()
+ LoginState.Failure -> failureState()
+ }
+ }
+
+ var onLogin: ((String) -> Unit)? = null
+
+ constructor(context: Context) : super(context) {}
+
+ constructor(context: Context, attributes: AttributeSet) : super(context, attributes) {}
+
+ constructor(context: Context, attributes: AttributeSet, defaultStyleAttribute: Int) :
+ super(context, attributes, defaultStyleAttribute) {}
+
+ constructor(
+ context: Context,
+ attributes: AttributeSet,
+ defaultStyleAttribute: Int,
+ defaultStyleResource: Int
+ ) : super(context, attributes, defaultStyleAttribute, defaultStyleResource) {
+ }
+
+ init {
+ orientation = HORIZONTAL
+
+ setButtonEnabled(false)
+ }
+
+ fun loginWith(accountNumber: String) {
+ input.text = accountNumber
+ onLogin?.invoke(accountNumber)
+ }
+
+ private fun initialState() {
+ input.apply {
+ setTextColor(enabledTextColor)
+ setEnabled(true)
+ setFocusableInTouchMode(true)
+ visibility = View.VISIBLE
+ }
+
+ button.visibility = View.VISIBLE
+ setButtonEnabled(input.text.length >= MIN_ACCOUNT_TOKEN_LENGTH)
+ }
+
+ private fun loggingInState() {
+ input.apply {
+ setTextColor(disabledTextColor)
+ setEnabled(false)
+ setFocusable(false)
+ visibility = View.VISIBLE
+ }
+
+ button.visibility = View.GONE
+ setButtonEnabled(false)
+ }
+
+ private fun successState() {
+ button.visibility = View.GONE
+ setButtonEnabled(false)
+
+ input.visibility = View.GONE
+ }
+
+ private fun failureState() {
+ button.visibility = View.VISIBLE
+ setButtonEnabled(false)
+
+ input.apply {
+ setTextColor(errorTextColor)
+ setEnabled(true)
+ setFocusableInTouchMode(true)
+ visibility = View.VISIBLE
+ requestFocus()
+ }
+ }
+
+ private fun setButtonEnabled(enabled: Boolean) {
+ button.apply {
+ if (enabled != isEnabled()) {
+ setEnabled(enabled)
+ setClickable(enabled)
+ setFocusable(enabled)
+ }
+ }
+ }
+
+ private fun removeFormattingSpans(text: Editable) {
+ for (span in text.getSpans(0, text.length, MetricAffectingSpan::class.java)) {
+ text.removeSpan(span)
+ }
+ }
+}
diff --git a/android/src/main/res/layout/account_input.xml b/android/src/main/res/layout/account_input.xml
new file mode 100644
index 0000000000..1c23583c42
--- /dev/null
+++ b/android/src/main/res/layout/account_input.xml
@@ -0,0 +1,24 @@
+<merge xmlns:android="http://schemas.android.com/apk/res/android">
+ <EditText android:id="@+id/login_input"
+ android:digits="0123456789"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_weight="1"
+ android:paddingHorizontal="12dp"
+ android:background="@drawable/account_input_background"
+ android:inputType="number"
+ android:singleLine="true"
+ android:imeOptions="flagNoPersonalizedLearning"
+ android:textCursorDrawable="@drawable/text_input_cursor"
+ android:hint="@string/login_hint"
+ android:textColorHint="@color/blue40"
+ android:textColor="@color/blue"
+ android:textSize="@dimen/text_medium_plus"
+ android:textStyle="bold" />
+ <ImageButton android:id="@+id/login_button"
+ android:layout_width="48dp"
+ android:layout_height="match_parent"
+ android:layout_weight="0"
+ android:background="@drawable/login_button_background"
+ android:src="@drawable/login_button_arrow" />
+</merge>
diff --git a/android/src/main/res/layout/login.xml b/android/src/main/res/layout/login.xml
index 87e240928c..de5f6d0f2d 100644
--- a/android/src/main/res/layout/login.xml
+++ b/android/src/main/res/layout/login.xml
@@ -67,33 +67,11 @@
<net.mullvad.mullvadvpn.ui.AccountInputContainer android:id="@+id/account_input_container"
android:layout_width="match_parent"
android:layout_height="48dp">
- <LinearLayout android:layout_width="match_parent"
- android:layout_height="48dp"
- android:layout_alignParentTop="true"
- android:orientation="horizontal">
- <EditText android:id="@+id/account_input"
- android:digits="0123456789"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:layout_weight="1"
- android:paddingHorizontal="12dp"
- android:background="@drawable/account_input_background"
- android:inputType="number"
- android:singleLine="true"
- android:imeOptions="flagNoPersonalizedLearning"
- android:textCursorDrawable="@drawable/text_input_cursor"
- android:hint="@string/login_hint"
- android:textColorHint="@color/blue40"
- android:textColor="@color/blue"
- android:textSize="@dimen/text_medium_plus"
- android:textStyle="bold" />
- <ImageButton android:id="@+id/login_button"
- android:layout_width="48dp"
- android:layout_height="match_parent"
- android:layout_weight="0"
- android:background="@drawable/login_button_background"
- android:src="@drawable/login_button_arrow" />
- </LinearLayout>
+ <net.mullvad.mullvadvpn.ui.widget.AccountInput android:id="@+id/account_input"
+ android:layout_width="match_parent"
+ android:layout_height="48dp"
+ android:layout_alignParentTop="true"
+ android:orientation="horizontal" />
</net.mullvad.mullvadvpn.ui.AccountInputContainer>
<ListView android:id="@+id/account_history_list"
android:layout_width="fill_parent"