refactor: download wizard details form #163

Closed
septs wants to merge 20 commits from septs:stricted-imei into master
5 changed files with 147 additions and 30 deletions

View file

@ -1,10 +1,13 @@
package im.angry.openeuicc.ui.wizard
import android.os.Bundle
import android.util.Patterns
import android.text.InputType
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.inputmethod.EditorInfo
import androidx.annotation.DrawableRes
import androidx.core.content.ContextCompat
import androidx.core.widget.addTextChangedListener
import com.google.android.material.textfield.TextInputLayout
import im.angry.openeuicc.common.R
@ -48,8 +51,13 @@ class DownloadWizardDetailsFragment : DownloadWizardActivity.DownloadWizardStepF
matchingId = view.requireViewById(R.id.profile_download_code)
confirmationCode = view.requireViewById(R.id.profile_download_confirmation_code)
imei = view.requireViewById(R.id.profile_download_imei)
smdp.editText!!.addTextChangedListener {
updateInputCompleteness()
smdp.editText!!.addTextChangedListener { updateInputCompleteness() }
matchingId.editText!!.addTextChangedListener { updateInputCompleteness() }
confirmationCode.editText!!.addTextChangedListener { updateInputCompleteness() }
imei.editText!!.addTextChangedListener { updateInputCompleteness() }
confirmationCode.setEndIconOnClickListener {
onConfirmationCodeEndIconClick(confirmationCode)
validate()
}
return view
}
@ -69,7 +77,84 @@ class DownloadWizardDetailsFragment : DownloadWizardActivity.DownloadWizardStepF
}
private fun updateInputCompleteness() {
inputComplete = Patterns.DOMAIN_NAME.matcher(smdp.editText!!.text).matches()
validate()
val layouts = arrayOf(smdp, matchingId, confirmationCode, imei)
for (layout in layouts) layout.isErrorEnabled = layout.error != null
inputComplete = layouts.all { it.error == null }
refreshButtons()
}
private fun validate() {
smdp.error = smdp.editText!!.text?.let {
if (it.isEmpty()) return@let getString(R.string.download_wizard_details_error_address_required)
if (it.contains("://")) return@let getString(R.string.download_wizard_details_error_cannot_url)
val (host, port) = splitHostPort(it)
if (isFQDN(host) && port in 1..65535) return@let null
getString(R.string.download_wizard_details_error_address_incorrect_format)
}
matchingId.error = matchingId.editText!!.text?.let {
if (isMatchingID(it)) return@let null
getString(R.string.download_wizard_details_error_matching_id_incorrect_format)
}
confirmationCode.error = confirmationCode.editText!!.let {
if (it.text.isEmpty()) return@let null
val passed = when (it.inputType and EditorInfo.TYPE_MASK_CLASS) {
EditorInfo.TYPE_CLASS_NUMBER -> it.text.all(Char::isDigit)
EditorInfo.TYPE_CLASS_TEXT -> true
else -> return@let null
}
if (passed) return@let null
getString(R.string.download_wizard_details_error_confirmation_code_incorrect_format)
}
imei.error = imei.editText!!.text?.let {
if (it.isEmpty()) return@let null
if (it.length == 15 && it.all(Char::isDigit) && luhnValid(it)) return@let null
getString(R.string.download_wizard_details_error_imei_incorrect_format)
}
}
private fun onConfirmationCodeEndIconClick(layout: TextInputLayout) {
val editText = layout.editText ?: return
fun getDrawable(@DrawableRes resId: Int) =
ContextCompat.getDrawable(requireActivity(), resId)
editText.inputType = when (editText.inputType and EditorInfo.TYPE_MASK_CLASS) {
EditorInfo.TYPE_CLASS_NUMBER -> InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD
EditorInfo.TYPE_CLASS_TEXT -> InputType.TYPE_CLASS_NUMBER
else -> EditorInfo.TYPE_NULL
}
layout.endIconDrawable = when (editText.inputType and EditorInfo.TYPE_MASK_CLASS) {
EditorInfo.TYPE_CLASS_NUMBER -> getDrawable(R.drawable.ic_format_number)
EditorInfo.TYPE_CLASS_TEXT -> getDrawable(R.drawable.ic_format_text)
else -> null
}
}
}
private fun splitHostPort(input: CharSequence): Pair<CharSequence, Int?> {
val portIndex = input.lastIndexOf(':')
if (portIndex == -1) return Pair(input, 443)
return Pair(
input.slice(0 until portIndex),
input.slice(portIndex + 1 until input.length).toString().toIntOrNull()
)
}
private fun isFQDN(input: CharSequence): Boolean {
if (input.isEmpty() || input.length > 255) return false
if (!input.contains('.')) return false
for (label in input.split('.')) {
if (label.isEmpty() || label.length > 63) return false
if (label.all { it.isLetterOrDigit() || it == '-' }) continue
return false
}
return true
}
private fun isMatchingID(input: CharSequence) =
input.isEmpty() || input.all { it.isLetterOrDigit() || it == '-' }
private fun luhnValid(input: CharSequence) = input
.map(Char::digitToInt)
.mapIndexed { index, digit -> if (index % 2 == 0) digit else digit * 2 }
.sumOf { if (it > 9) it - 9 else it }
.rem(10) == 0

View file

@ -0,0 +1,11 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:alpha="0.8"
android:tint="@android:color/white"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M7 15H5.5v-4.5H4V9h3v6zm6.5-1.5h-3v-1h2c0.55 0 1-0.45 1-1V10c0-0.55-0.45-1-1-1H9v1.5h3v1h-2c-0.55 0-1 0.45-1 1V15h4.5v-1.5zm6 0.5v-4c0-0.55-0.45-1-1-1H15v1.5h3v1h-2v1h2v1h-3V15h3.5c0.55 0 1-0.45 1-1z" />
</vector>

View file

@ -0,0 +1,11 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:alpha="0.8"
android:tint="@android:color/white"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M21 11h-1.5v-0.5h-2v3h2V13H21v1c0 0.55-0.45 1-1 1h-3c-0.55 0-1-0.45-1-1v-4c0-0.55 0.45-1 1-1h3c0.55 0 1 0.45 1 1v1zM8 10v5H6.5v-1.5h-2V15H3v-5c0-0.55 0.45-1 1-1h3c0.55 0 1 0.45 1 1zm-1.5 0.5h-2V12h2v-1.5zm7 1.5c0.55 0 1 0.45 1 1v1c0 0.55-0.45 1-1 1h-4V9h4c0.55 0 1 0.45 1 1v1c0 0.55-0.45 1-1 1zM11 10.5v0.75h2V10.5h-2zm2 2.25h-2v0.75h2v-0.75z" />
</vector>

View file

@ -11,18 +11,18 @@
<TextView
android:id="@+id/download_wizard_details_title"
android:text="@string/download_wizard_details"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center_horizontal"
android:textSize="20sp"
android:layout_marginTop="20dp"
android:layout_marginBottom="20dp"
android:layout_marginStart="60dp"
android:layout_marginTop="20dp"
android:layout_marginEnd="60dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:layout_marginBottom="20dp"
android:gravity="center_horizontal"
android:text="@string/download_wizard_details"
android:textSize="20sp"
app:layout_constrainedWidth="true"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<com.google.android.material.textfield.TextInputLayout
@ -32,10 +32,11 @@
android:hint="@string/profile_download_server">
<com.google.android.material.textfield.TextInputEditText
android:maxLines="1"
android:inputType="text"
android:layout_width="match_parent"
android:layout_height="match_parent" />
android:layout_height="match_parent"
android:inputType="textVisiblePassword"
android:maxLines="1"
android:typeface="monospace" />
</com.google.android.material.textfield.TextInputLayout>
@ -43,14 +44,14 @@
android:id="@+id/profile_download_code"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:hint="@string/profile_download_code"
app:passwordToggleEnabled="true">
android:hint="@string/profile_download_code">
<com.google.android.material.textfield.TextInputEditText
android:maxLines="1"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:inputType="textPassword" />
android:inputType="textVisiblePassword"
android:maxLines="1"
android:typeface="monospace" />
</com.google.android.material.textfield.TextInputLayout>
@ -59,13 +60,16 @@
android:layout_width="0dp"
android:layout_height="wrap_content"
android:hint="@string/profile_download_confirmation_code"
app:passwordToggleEnabled="true">
app:endIconCheckable="true"
app:endIconDrawable="@drawable/ic_format_number"
app:endIconMode="custom">
<com.google.android.material.textfield.TextInputEditText
android:maxLines="1"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:inputType="textPassword" />
android:inputType="number"
android:maxLines="1"
android:typeface="monospace" />
</com.google.android.material.textfield.TextInputLayout>
@ -75,29 +79,29 @@
android:layout_height="wrap_content"
android:layout_marginTop="15dp"
android:layout_marginBottom="6dp"
android:hint="@string/profile_download_imei"
app:passwordToggleEnabled="true">
android:hint="@string/profile_download_imei">
<com.google.android.material.textfield.TextInputEditText
android:maxLines="1"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:inputType="numberPassword" />
android:inputType="number"
android:maxLines="1"
android:typeface="monospace" />
</com.google.android.material.textfield.TextInputLayout>
<androidx.constraintlayout.helper.widget.Flow
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_marginHorizontal="20dp"
android:orientation="vertical"
app:constraint_referenced_ids="profile_download_server,profile_download_code,profile_download_confirmation_code,profile_download_imei"
app:flow_verticalGap="16dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/download_wizard_details_title"
app:layout_constrainedWidth="true"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constrainedWidth="true" />
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/download_wizard_details_title" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -81,6 +81,12 @@
<string name="download_wizard_method_clipboard">Load from Clipboard</string>
<string name="download_wizard_method_manual">Enter manually</string>
<string name="download_wizard_details">Input or confirm details for downloading your eSIM:</string>
<string name="download_wizard_details_error_address_required">Server address is required</string>
<string name="download_wizard_details_error_cannot_url">Server address not is URL</string>
<string name="download_wizard_details_error_address_incorrect_format">Incorrect Server address</string>
<string name="download_wizard_details_error_matching_id_incorrect_format">Incorrect Matching ID format</string>
<string name="download_wizard_details_error_confirmation_code_incorrect_format">Incorrect Confirmation Code format</string>
<string name="download_wizard_details_error_imei_incorrect_format">Incorrect IMEI format</string>
<string name="download_wizard_progress">Downloading your eSIM…</string>
<string name="download_wizard_progress_step_preparing">Preparing</string>
<string name="download_wizard_progress_step_connecting">Establishing connection to server</string>