diff --git a/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardDetailsFragment.kt b/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardDetailsFragment.kt index 5fa8002..3b6a99d 100644 --- a/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardDetailsFragment.kt +++ b/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardDetailsFragment.kt @@ -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() } -} \ No newline at end of file + + 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 { + 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 \ No newline at end of file diff --git a/app-common/src/main/res/drawable/ic_format_number.xml b/app-common/src/main/res/drawable/ic_format_number.xml new file mode 100644 index 0000000..bed33e3 --- /dev/null +++ b/app-common/src/main/res/drawable/ic_format_number.xml @@ -0,0 +1,11 @@ + + + diff --git a/app-common/src/main/res/drawable/ic_format_text.xml b/app-common/src/main/res/drawable/ic_format_text.xml new file mode 100644 index 0000000..8a376a7 --- /dev/null +++ b/app-common/src/main/res/drawable/ic_format_text.xml @@ -0,0 +1,11 @@ + + + diff --git a/app-common/src/main/res/layout/fragment_download_details.xml b/app-common/src/main/res/layout/fragment_download_details.xml index 1a25075..524bd35 100644 --- a/app-common/src/main/res/layout/fragment_download_details.xml +++ b/app-common/src/main/res/layout/fragment_download_details.xml @@ -11,18 +11,18 @@ + android:layout_height="match_parent" + android:inputType="textVisiblePassword" + android:maxLines="1" + android:typeface="monospace" /> @@ -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"> + android:inputType="textVisiblePassword" + android:maxLines="1" + android:typeface="monospace" /> @@ -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"> + android:inputType="number" + android:maxLines="1" + android:typeface="monospace" /> @@ -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"> + android:inputType="number" + android:maxLines="1" + android:typeface="monospace" /> + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/download_wizard_details_title" /> diff --git a/app-common/src/main/res/values/strings.xml b/app-common/src/main/res/values/strings.xml index a45ce1f..28ed78e 100644 --- a/app-common/src/main/res/values/strings.xml +++ b/app-common/src/main/res/values/strings.xml @@ -81,6 +81,12 @@ Load from Clipboard Enter manually Input or confirm details for downloading your eSIM: + Server address is required + Server address not is URL + Incorrect Server address + Incorrect Matching ID format + Incorrect Confirmation Code format + Incorrect IMEI format Downloading your eSIM… Preparing Establishing connection to server