From 25406151c191c838bd00be2f395b1f23ba60a8e8 Mon Sep 17 00:00:00 2001 From: Thomas Klingbeil <64434904+tklingbeil@users.noreply.github.com> Date: Mon, 8 Jun 2020 17:56:38 +0200 Subject: [PATCH] Extend teleTAN to 10 characters + input validation (re-submit) (#270) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Change teleTAN from 7 to 10 characters # Conflicts: # Corona-Warn-App/src/main/res/values/dimens.xml * adjust TAN input to updated design # Conflicts: # Corona-Warn-App/src/main/res/values/dimens.xml * move TAN to upper case centrally * styling of filled/empty TAN input spaces * replace gradient at TAN entry with solid shape * fix TAN input dash style * add teleTAN input validation * ensure valid characters are entered * block further input if character is invalid * mark invalid characters accordingly * show error message if character is invalid * calculate checksum * show error and prevent the user from clicking „next“ if checksum is invalid * code formatting * Add include to TAN fragment * Update teleTAN entry info text * remove code smells * Change detection of valid characters in TAN to whitelist * add tests for TanHelper * add tests for SubmissionTanViewModel * use ROOT locale and specific charset for string operations * extend tests for submissionTanViewModel * override synced translations Co-authored-by: Jakob Möller <jakob.moeller@sap.com> --- .../ui/submission/SubmissionTanViewModel.kt | 17 +++- .../ui/submission/TanConstants.kt | 2 +- .../coronawarnapp/ui/submission/TanInput.kt | 43 ++++++++- .../de/rki/coronawarnapp/util/TanHelper.kt | 35 ++++++++ .../formatter/FormatterSubmissionHelper.kt | 5 ++ .../src/main/res/drawable/tan_input_digit.xml | 29 +++--- .../res/drawable/tan_input_digit_entered.xml | 6 ++ .../res/drawable/tan_input_digit_error.xml | 20 +++++ .../res/layout/fragment_submission_tan.xml | 28 ++++++ .../src/main/res/layout/view_tan_input.xml | 52 ++++++++++- .../src/main/res/values-de/strings.xml | 2 +- .../src/main/res/values-en/strings.xml | 2 +- .../src/main/res/values/dimens.xml | 6 +- .../src/main/res/values/strings.xml | 6 +- .../src/main/res/values/styles.xml | 7 ++ .../submission/SubmissionTanViewModelTest.kt | 61 +++++++++++++ .../rki/coronawarnapp/util/TanHelperTest.kt | 88 +++++++++++++++++++ 17 files changed, 385 insertions(+), 24 deletions(-) create mode 100644 Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/TanHelper.kt create mode 100644 Corona-Warn-App/src/main/res/drawable/tan_input_digit_entered.xml create mode 100644 Corona-Warn-App/src/main/res/drawable/tan_input_digit_error.xml create mode 100644 Corona-Warn-App/src/test/java/de/rki/coronawarnapp/ui/submission/SubmissionTanViewModelTest.kt create mode 100644 Corona-Warn-App/src/test/java/de/rki/coronawarnapp/util/TanHelperTest.kt diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/SubmissionTanViewModel.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/SubmissionTanViewModel.kt index c18d49a01..cae07743b 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/SubmissionTanViewModel.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/SubmissionTanViewModel.kt @@ -5,6 +5,7 @@ import androidx.lifecycle.MutableLiveData import androidx.lifecycle.Transformations import androidx.lifecycle.ViewModel import de.rki.coronawarnapp.storage.SubmissionRepository +import de.rki.coronawarnapp.util.TanHelper class SubmissionTanViewModel : ViewModel() { @@ -16,7 +17,21 @@ class SubmissionTanViewModel : ViewModel() { val isValidTanFormat = Transformations.map(tan) { - it != null && it.length == TanConstants.MAX_LENGTH + it != null && + it.length == TanConstants.MAX_LENGTH && + TanHelper.isChecksumValid(it) && + TanHelper.allCharactersValid(it) + } + + val tanChecksumValid = + Transformations.map(tan) { + ((it !== null && it.trim().length == TanConstants.MAX_LENGTH) && + TanHelper.isChecksumValid(it).not()).not() + } + + val tanCharactersValid = + Transformations.map(tan) { + !((it != null) && TanHelper.allCharactersValid(it).not()) } fun storeTeletan() { diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/TanConstants.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/TanConstants.kt index b6be13be8..644cf1ee0 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/TanConstants.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/TanConstants.kt @@ -1,6 +1,6 @@ package de.rki.coronawarnapp.ui.submission object TanConstants { - const val MAX_LENGTH = 7 + const val MAX_LENGTH = 10 val ALPHA_NUMERIC_CHARS = ('a'..'z').plus('A'..'Z').plus('0'..'9') } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/TanInput.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/TanInput.kt index 70cc48b68..b7b02a825 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/TanInput.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/TanInput.kt @@ -8,6 +8,9 @@ import android.view.inputmethod.InputMethodManager import android.widget.FrameLayout import androidx.core.widget.doOnTextChanged import de.rki.coronawarnapp.R +import de.rki.coronawarnapp.ui.submission.TanConstants.MAX_LENGTH +import de.rki.coronawarnapp.util.TanHelper +import java.util.Locale import kotlinx.android.synthetic.main.view_tan_input.view.tan_input_edittext import kotlinx.android.synthetic.main.view_tan_input.view.tan_input_textview_1 import kotlinx.android.synthetic.main.view_tan_input.view.tan_input_textview_2 @@ -16,6 +19,11 @@ import kotlinx.android.synthetic.main.view_tan_input.view.tan_input_textview_4 import kotlinx.android.synthetic.main.view_tan_input.view.tan_input_textview_5 import kotlinx.android.synthetic.main.view_tan_input.view.tan_input_textview_6 import kotlinx.android.synthetic.main.view_tan_input.view.tan_input_textview_7 +import kotlinx.android.synthetic.main.view_tan_input.view.tan_input_textview_8 +import kotlinx.android.synthetic.main.view_tan_input.view.tan_input_textview_9 +import kotlinx.android.synthetic.main.view_tan_input.view.tan_input_textview_10 +import kotlinx.android.synthetic.main.view_tan_input.view.dash_view_1 +import kotlinx.android.synthetic.main.view_tan_input.view.dash_view_2 class TanInput(context: Context, attrs: AttributeSet) : FrameLayout(context, attrs) { @@ -30,7 +38,7 @@ class TanInput(context: Context, attrs: AttributeSet) : FrameLayout(context, att TanConstants.ALPHA_NUMERIC_CHARS.contains(it) } } - private val lengthFilter = InputFilter.LengthFilter(TanConstants.MAX_LENGTH) + private var lengthFilter = InputFilter.LengthFilter(MAX_LENGTH) var listener: ((String?) -> Unit)? = null @@ -41,6 +49,9 @@ class TanInput(context: Context, attrs: AttributeSet) : FrameLayout(context, att tan_input_edittext.filters = arrayOf(whitespaceFilter, alphaNumericFilter, lengthFilter) + dash_view_1.text = "-" + dash_view_2.text = "-" + // register listener tan_input_edittext.doOnTextChanged { text, _, _, _ -> updateTan(text) } setOnClickListener { showKeyboard() } @@ -56,9 +67,20 @@ class TanInput(context: Context, attrs: AttributeSet) : FrameLayout(context, att } } + private fun limitLength(length: Int?) { + lengthFilter = InputFilter.LengthFilter(if (length != null) length else MAX_LENGTH) + tan_input_edittext.filters = arrayOf(whitespaceFilter, alphaNumericFilter, lengthFilter) + } + private fun updateTan(text: CharSequence?) { - this.tan = text?.toString() + this.tan = text?.toString()?.toUpperCase(Locale.ROOT) updateDigits() + tan?.let { + limitLength( + if (TanHelper.allCharactersValid(it)) null + else it.length + ) + } notifyListener() } @@ -71,9 +93,24 @@ class TanInput(context: Context, attrs: AttributeSet) : FrameLayout(context, att tan_input_textview_4, tan_input_textview_5, tan_input_textview_6, - tan_input_textview_7 + tan_input_textview_7, + tan_input_textview_8, + tan_input_textview_9, + tan_input_textview_10 ).forEachIndexed { i, tanDigit -> tanDigit.text = digitAtIndex(i) + tanDigit.background = + if (digitAtIndex(i) == "") + resources.getDrawable(R.drawable.tan_input_digit, null) + else if (TanHelper.isTanCharacterValid(digitAtIndex(i))) + resources.getDrawable(R.drawable.tan_input_digit_entered, null) + else resources.getDrawable(R.drawable.tan_input_digit_error, null) + + tanDigit.setTextColor( + if (TanHelper.isTanCharacterValid(digitAtIndex(i))) + resources.getColor(R.color.colorTextSemanticNeutral, null) + else resources.getColor(R.color.colorTextSemanticRed, null) + ) } private fun digitAtIndex(index: Int): String = tan?.getOrNull(index)?.toString() ?: "" diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/TanHelper.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/TanHelper.kt new file mode 100644 index 000000000..7fa1562c7 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/TanHelper.kt @@ -0,0 +1,35 @@ +package de.rki.coronawarnapp.util + +import de.rki.coronawarnapp.ui.submission.TanConstants.MAX_LENGTH +import java.nio.charset.StandardCharsets +import java.security.MessageDigest +import java.util.Locale + +object TanHelper { + private const val VALID_CHARACTERS = "23456789ABCDEFGHJKMNPQRSTUVWXYZ" + + fun isChecksumValid(tan: String): Boolean { + if (tan.trim().length != MAX_LENGTH) + return false + val subTan = tan.substring(0, MAX_LENGTH - 1).toUpperCase(Locale.ROOT) + val tanDigest = MessageDigest.getInstance("SHA-256") + .digest(subTan.toByteArray(StandardCharsets.US_ASCII)) + var checkChar = "%02x".format(tanDigest[0])[0] + if (checkChar == '0') checkChar = 'G' + if (checkChar == '1') checkChar = 'H' + + return checkChar.toUpperCase() == tan.last().toUpperCase() + } + + fun allCharactersValid(tan: String): Boolean { + for (character in tan) { + if (!isTanCharacterValid(character.toString())) + return false + } + return true + } + + fun isTanCharacterValid(character: String): Boolean { + return VALID_CHARACTERS.contains(character) + } +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/formatter/FormatterSubmissionHelper.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/formatter/FormatterSubmissionHelper.kt index 22f1b6b6a..7f16f3ed7 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/formatter/FormatterSubmissionHelper.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/formatter/FormatterSubmissionHelper.kt @@ -193,3 +193,8 @@ fun formatShowRiskStatusCard(deviceUiState: DeviceUIState?): Int = deviceUiState != DeviceUIState.PAIRED_POSITIVE_TELETAN && deviceUiState != DeviceUIState.SUBMITTED_FINAL ) + +fun formatShowTanCharacterError( + charactersValid: Boolean, + checksumValid: Boolean +): Int = formatVisibility(checksumValid && !charactersValid) diff --git a/Corona-Warn-App/src/main/res/drawable/tan_input_digit.xml b/Corona-Warn-App/src/main/res/drawable/tan_input_digit.xml index d06cdb920..48e3d0c51 100644 --- a/Corona-Warn-App/src/main/res/drawable/tan_input_digit.xml +++ b/Corona-Warn-App/src/main/res/drawable/tan_input_digit.xml @@ -1,11 +1,20 @@ <?xml version="1.0" encoding="utf-8"?> -<shape xmlns:android="http://schemas.android.com/apk/res/android" - android:shape="rectangle"> - <corners android:radius="@dimen/submission_tan_input_digit_radius" /> - <gradient - android:angle="90" - android:centerColor="@color/colorSurface2" - android:centerX="0.02" - android:endColor="@color/colorSurface2" - android:startColor="@color/colorTextSemanticNeutral" /> -</shape> \ No newline at end of file +<layer-list xmlns:android="http://schemas.android.com/apk/res/android"> +<item> + <shape xmlns:android="http://schemas.android.com/apk/res/android" + android:shape="rectangle"> + <corners android:radius="@dimen/submission_tan_input_digit_radius" /> + <solid android:color="@color/colorSurface2" /> + </shape> +</item> +<item + android:height="1.5dp" + android:gravity="bottom"> + <shape xmlns:android="http://schemas.android.com/apk/res/android" + android:shape="rectangle"> + <corners android:bottomLeftRadius="@dimen/submission_tan_input_digit_radius" /> + <corners android:bottomRightRadius="@dimen/submission_tan_input_digit_radius" /> + <solid android:color="@color/colorTextSemanticNeutral" /> + </shape> +</item> +</layer-list> \ No newline at end of file diff --git a/Corona-Warn-App/src/main/res/drawable/tan_input_digit_entered.xml b/Corona-Warn-App/src/main/res/drawable/tan_input_digit_entered.xml new file mode 100644 index 000000000..0550164c0 --- /dev/null +++ b/Corona-Warn-App/src/main/res/drawable/tan_input_digit_entered.xml @@ -0,0 +1,6 @@ +<?xml version="1.0" encoding="utf-8"?> +<shape xmlns:android="http://schemas.android.com/apk/res/android" + android:shape="rectangle"> + <corners android:radius="@dimen/submission_tan_input_digit_radius" /> + <solid android:color="@color/colorSurface2" /> +</shape> \ No newline at end of file diff --git a/Corona-Warn-App/src/main/res/drawable/tan_input_digit_error.xml b/Corona-Warn-App/src/main/res/drawable/tan_input_digit_error.xml new file mode 100644 index 000000000..01780eeb0 --- /dev/null +++ b/Corona-Warn-App/src/main/res/drawable/tan_input_digit_error.xml @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="utf-8"?> +<layer-list xmlns:android="http://schemas.android.com/apk/res/android"> + <item> + <shape xmlns:android="http://schemas.android.com/apk/res/android" + android:shape="rectangle"> + <corners android:radius="@dimen/submission_tan_input_digit_radius" /> + <solid android:color="@color/colorSurface2" /> + </shape> + </item> + <item + android:height="1.5dp" + android:gravity="bottom"> + <shape xmlns:android="http://schemas.android.com/apk/res/android" + android:shape="rectangle"> + <corners android:bottomLeftRadius="@dimen/submission_tan_input_digit_radius" /> + <corners android:bottomRightRadius="@dimen/submission_tan_input_digit_radius" /> + <solid android:color="@color/colorTextSemanticRed" /> + </shape> + </item> +</layer-list> diff --git a/Corona-Warn-App/src/main/res/layout/fragment_submission_tan.xml b/Corona-Warn-App/src/main/res/layout/fragment_submission_tan.xml index c940d7044..4337a0b5e 100644 --- a/Corona-Warn-App/src/main/res/layout/fragment_submission_tan.xml +++ b/Corona-Warn-App/src/main/res/layout/fragment_submission_tan.xml @@ -5,6 +5,7 @@ <data> + <import type="de.rki.coronawarnapp.util.formatter.FormatterHelper" /> <import type="de.rki.coronawarnapp.util.formatter.FormatterSubmissionHelper" /> <variable @@ -54,6 +55,33 @@ app:layout_constraintStart_toStartOf="@+id/guideline_start" app:layout_constraintTop_toBottomOf="@+id/submission_tan_body" /> + <TextView + android:id="@+id/submission_tan_character_error" + style="@style/subtitle" + android:layout_width="@dimen/match_constraint" + android:layout_height="wrap_content" + android:layout_marginTop="@dimen/spacing_small" + android:visibility="@{FormatterSubmissionHelper.formatShowTanCharacterError(viewmodel.tanCharactersValid, viewmodel.tanChecksumValid)}" + android:text="@string/submission_tan_character_error" + android:textColor="@color/colorTextSemanticRed" + app:layout_constraintEnd_toStartOf="@+id/guideline_end" + app:layout_constraintStart_toStartOf="@+id/guideline_start" + app:layout_constraintTop_toBottomOf="@id/submission_tan_input" /> + + <TextView + android:id="@+id/submission_tan_error" + style="@style/subtitle" + android:layout_width="@dimen/match_constraint" + android:layout_height="wrap_content" + android:layout_marginTop="@dimen/spacing_small" + android:visibility="@{FormatterHelper.formatVisibility(!viewmodel.tanChecksumValid)}" + android:text="@string/submission_tan_error" + android:textColor="@color/colorTextSemanticRed" + app:layout_constraintEnd_toStartOf="@+id/guideline_end" + app:layout_constraintStart_toStartOf="@+id/guideline_start" + app:layout_constraintTop_toBottomOf="@id/submission_tan_input" /> + + <Button android:id="@+id/submission_tan_button_enter" style="@style/buttonPrimary" diff --git a/Corona-Warn-App/src/main/res/layout/view_tan_input.xml b/Corona-Warn-App/src/main/res/layout/view_tan_input.xml index 213e9ff71..9083b5628 100644 --- a/Corona-Warn-App/src/main/res/layout/view_tan_input.xml +++ b/Corona-Warn-App/src/main/res/layout/view_tan_input.xml @@ -36,17 +36,26 @@ android:id="@+id/tan_input_textview_3" style="@style/tanInputDigit" app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintEnd_toStartOf="@+id/tan_input_textview_4" + app:layout_constraintEnd_toStartOf="@+id/dash_view_1" app:layout_constraintStart_toEndOf="@+id/tan_input_textview_2" app:layout_constraintTop_toTopOf="parent" tools:text="X" /> + <TextView + android:id="@+id/dash_view_1" + style="@style/tanInputDash" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintEnd_toStartOf="@+id/tan_input_textview_4" + app:layout_constraintStart_toEndOf="@+id/tan_input_textview_3" + app:layout_constraintTop_toTopOf="parent" + tools:text="-" /> + <TextView android:id="@+id/tan_input_textview_4" style="@style/tanInputDigit" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toStartOf="@+id/tan_input_textview_5" - app:layout_constraintStart_toEndOf="@+id/tan_input_textview_3" + app:layout_constraintStart_toEndOf="@+id/dash_view_1" app:layout_constraintTop_toTopOf="parent" tools:text="X" /> @@ -63,19 +72,54 @@ android:id="@+id/tan_input_textview_6" style="@style/tanInputDigit" app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintEnd_toStartOf="@+id/tan_input_textview_7" + app:layout_constraintEnd_toStartOf="@+id/dash_view_2" app:layout_constraintStart_toEndOf="@+id/tan_input_textview_5" app:layout_constraintTop_toTopOf="parent" tools:text="X" /> + <TextView + android:id="@+id/dash_view_2" + style="@style/tanInputDash" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintEnd_toStartOf="@+id/tan_input_textview_7" + app:layout_constraintStart_toEndOf="@+id/tan_input_textview_6" + app:layout_constraintTop_toTopOf="parent" + tools:text="-" /> + <TextView android:id="@+id/tan_input_textview_7" style="@style/tanInputDigit" app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintStart_toEndOf="@+id/tan_input_textview_6" + app:layout_constraintEnd_toStartOf="@+id/tan_input_textview_8" + app:layout_constraintStart_toEndOf="@+id/dash_view_2" app:layout_constraintTop_toTopOf="parent" tools:text="X" /> + <TextView + android:id="@+id/tan_input_textview_8" + style="@style/tanInputDigit" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintEnd_toStartOf="@+id/tan_input_textview_9" + app:layout_constraintStart_toEndOf="@+id/tan_input_textview_7" + app:layout_constraintTop_toTopOf="parent" + tools:text="X" /> + + <TextView + android:id="@+id/tan_input_textview_9" + style="@style/tanInputDigit" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintEnd_toStartOf="@+id/tan_input_textview_10" + app:layout_constraintStart_toEndOf="@+id/tan_input_textview_8" + app:layout_constraintTop_toTopOf="parent" + tools:text="X" /> + + <TextView + android:id="@+id/tan_input_textview_10" + style="@style/tanInputDigit" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toEndOf="@+id/tan_input_textview_9" + app:layout_constraintTop_toTopOf="parent" + tools:text="X" /> </androidx.constraintlayout.widget.ConstraintLayout> </layout> diff --git a/Corona-Warn-App/src/main/res/values-de/strings.xml b/Corona-Warn-App/src/main/res/values-de/strings.xml index bb5abba0b..9555649d9 100644 --- a/Corona-Warn-App/src/main/res/values-de/strings.xml +++ b/Corona-Warn-App/src/main/res/values-de/strings.xml @@ -591,7 +591,7 @@ <!-- XHED: Page title for TAN submission pge --> <string name="submission_tan_title">"TAN-Eingabe"</string> <!-- YTXT: Body text for the tan submission page --> - <string name="submission_tan_body">"Die TAN ist 7-stellig und Groß- und Kleinschreibung muss nicht beachtet werden.\n\nGeben Sie bitte die Ihnen mitgeteilte TAN ein:"</string> + <string name="submission_tan_body">"Geben Sie bitte die 10 Stellen der TAN ein, die Ihnen mitgeteilt wurde."</string> <!-- XBUT: Submit TAN button --> <string name="submission_tan_button_text">"Weiter"</string> diff --git a/Corona-Warn-App/src/main/res/values-en/strings.xml b/Corona-Warn-App/src/main/res/values-en/strings.xml index a52287993..139deb406 100644 --- a/Corona-Warn-App/src/main/res/values-en/strings.xml +++ b/Corona-Warn-App/src/main/res/values-en/strings.xml @@ -593,7 +593,7 @@ <!-- XHED: Page title for TAN submission pge --> <string name="submission_tan_title">"TAN entry"</string> <!-- YTXT: Body text for the tan submission page --> - <string name="submission_tan_body">"The TAN has 7 characters and is case-insensitive.\n\nPlease enter the TAN you were given:"</string> + <string name="submission_tan_body">"Please enter the 10 characters of the TAN you were given:"</string> <!-- XBUT: Submit TAN button --> <string name="submission_tan_button_text">"Next"</string> diff --git a/Corona-Warn-App/src/main/res/values/dimens.xml b/Corona-Warn-App/src/main/res/values/dimens.xml index e23305dc4..5b3c0277c 100644 --- a/Corona-Warn-App/src/main/res/values/dimens.xml +++ b/Corona-Warn-App/src/main/res/values/dimens.xml @@ -82,8 +82,10 @@ <!-- Submission Tan Input --> <dimen name="submission_tan_input_edittext_size">1dp</dimen> <dimen name="submission_tan_input_digit_radius">2dp</dimen> - <dimen name="submission_tan_input_digit_width">24dp</dimen> - <dimen name="submission_tan_input_digit_height">32dp</dimen> + <dimen name="submission_tan_input_digit_width">22dp</dimen> + <dimen name="submission_tan_input_digit_height">40dp</dimen> + <dimen name="submission_tan_input_dash_width">11dp</dimen> + <dimen name="submission_tan_input_dash_height">40dp</dimen> <!-- Submission QR Code Scan --> <dimen name="submission_scan_qr_code_viewfinder_size">240dp</dimen> diff --git a/Corona-Warn-App/src/main/res/values/strings.xml b/Corona-Warn-App/src/main/res/values/strings.xml index 0b6468412..ead09a21a 100644 --- a/Corona-Warn-App/src/main/res/values/strings.xml +++ b/Corona-Warn-App/src/main/res/values/strings.xml @@ -2664,9 +2664,13 @@ as modifying the License. <!-- XHED: Page title for TAN submission pge --> <string name="submission_tan_title">TAN Eingabe</string> <!-- YTXT: Body text for the tan submission page --> - <string name="submission_tan_body">Die TAN ist 7-stellig und Groß- und Kleinschreibung muss beachtet werden.\n\nGeben Sie bitte die Ihnen mitgeteilte TAN ein:</string> + <string name="submission_tan_body">Geben Sie bitte die 10 Stellen der TAN ein, die Ihnen mitgeteilt wurde.</string> <!-- XBUT: Submit TAN button --> <string name="submission_tan_button_text">Weiter</string> + <!-- YTXT: Error text for the tan submission page --> + <string name="submission_tan_error">Ungültige TAN, bitte überprüfen Sie Ihre Eingabe.</string> + <!-- YTXT: Error text for the tan submission page (wrong characters) --> + <string name="submission_tan_character_error">Ungültige Eingabe, bitte überprüfen Sie das Zeichen.</string> <!-- Submission Intro --> <!-- XHED: Page title for menu at the start of the submission process --> diff --git a/Corona-Warn-App/src/main/res/values/styles.xml b/Corona-Warn-App/src/main/res/values/styles.xml index 8ea177f88..403916355 100644 --- a/Corona-Warn-App/src/main/res/values/styles.xml +++ b/Corona-Warn-App/src/main/res/values/styles.xml @@ -219,6 +219,13 @@ <item name="android:gravity">center</item> </style> + <style name="tanInputDash" parent="headline6"> + <item name="android:layout_width">@dimen/submission_tan_input_dash_width</item> + <item name="android:layout_height">@dimen/submission_tan_input_dash_height</item> + <item name="android:background">@null</item> + <item name="android:gravity">center</item> + </style> + <style name="tanInputEdittext"> <item name="android:layout_width">@dimen/submission_tan_input_edittext_size</item> <item name="android:layout_height">@dimen/submission_tan_input_edittext_size</item> diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/ui/submission/SubmissionTanViewModelTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/ui/submission/SubmissionTanViewModelTest.kt new file mode 100644 index 000000000..fe08e5de2 --- /dev/null +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/ui/submission/SubmissionTanViewModelTest.kt @@ -0,0 +1,61 @@ +package de.rki.coronawarnapp.ui.submission + +import com.google.android.gms.nearby.exposurenotification.ExposureSummary +import de.rki.coronawarnapp.storage.SubmissionRepository +import de.rki.coronawarnapp.util.TanHelper +import io.mockk.* +import org.junit.Assert.* +import org.junit.Test + +class SubmissionTanViewModelTest { + private var viewModel: SubmissionTanViewModel = SubmissionTanViewModel() + + @Test + fun allCharactersValid() { + viewModel.tan.postValue("ABCD") + viewModel.tanCharactersValid.value?.let { assertTrue(it) } + + viewModel.tan.postValue("ABCD0") + viewModel.tanCharactersValid.value?.let { assertFalse(it) } + + } + + @Test + fun tanFormatValid() { + viewModel.tan.postValue("ZWFPC7NG47") + viewModel.isValidTanFormat.value?.let { assertTrue(it) } + + viewModel.tan.postValue("ABC") + viewModel.isValidTanFormat.value?.let { assertFalse(it) } + + viewModel.tan.postValue("ZWFPC7NG48") + viewModel.isValidTanFormat.value?.let { assertFalse(it) } + + viewModel.tan.postValue("ZWFPC7NG4A") + viewModel.isValidTanFormat.value?.let { assertFalse(it) } + } + + @Test + fun checksumValid() { + viewModel.tan.postValue("ZWFPC7NG47") + viewModel.tanChecksumValid.value?.let { assertTrue(it) } + + viewModel.tan.postValue("ZWFPC7NG48") + viewModel.tanChecksumValid.value?.let { assertFalse(it) } + } + + @Test + fun testTanStorage() { + val sr = mockk<SubmissionRepository> { + every { setTeletan(any()) } just Runs + } + val tan = "ZWFPC7NG47"; + sr.setTeletan(tan) + + verify (exactly = 1) { sr.setTeletan( + withArg { + assertEquals(it, tan) + }) + } + } +} \ No newline at end of file diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/util/TanHelperTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/util/TanHelperTest.kt new file mode 100644 index 000000000..d3c4a1120 --- /dev/null +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/util/TanHelperTest.kt @@ -0,0 +1,88 @@ +package de.rki.coronawarnapp.util + +import de.rki.coronawarnapp.service.submission.SubmissionService +import org.hamcrest.CoreMatchers +import org.hamcrest.MatcherAssert +import org.junit.Test + +class TanHelperTest { + + @Test + fun isValidCharacter() { + // valid + val validCharacters = arrayOf( + "2","3","4","5","6","7","8","9","A","B","C","D","E","F","G","H", + "J","K","M","N","P","Q","R","S","T","U","V","W","X","Y","Z" + ) + for (character in validCharacters) { + MatcherAssert.assertThat( + TanHelper.isTanCharacterValid(character), + CoreMatchers.equalTo(true) + ) + } + + // invalid + val invalidCharacters = arrayOf( + "0","1","O","L","I","Ö","*","&","-","a","b", + "c","ö","ß","é","."," ","€","(",")",";","," + ) + for (character in invalidCharacters) { + MatcherAssert.assertThat( + TanHelper.isTanCharacterValid(character), + CoreMatchers.equalTo(false) + ) + } + } + + @Test + fun areCharactersValid() { + // valid input strings (not necessarily valid TANs) + val validStrings = arrayOf( + "ABCD", "2345", "PTPHM35RP4", "AAAAAAAAAA", "BBBBB") + for (text in validStrings) { + MatcherAssert.assertThat( + TanHelper.allCharactersValid(text), + CoreMatchers.equalTo(true) + ) + } + + // invalid input strings + val invalidStrings = arrayOf( + "ABCDÖ", "01234", "PTPHM15RP4", "AAAAAA AAA", "BB.BBB") + for (text in invalidStrings) { + MatcherAssert.assertThat( + TanHelper.allCharactersValid(text), + CoreMatchers.equalTo(false) + ) + } + } + + @Test + fun isChecksumValid() { + // valid + val validTans = arrayOf( + "9A3B578UMG", "DEU7TKSV3H", "PTPHM35RP4", "V923D59AT8", "H9NC5CQ34E") + for (tan in validTans) { + MatcherAssert.assertThat( + TanHelper.isChecksumValid(tan), + CoreMatchers.equalTo(true) + ) + } + + // invalid + val invalidTans = arrayOf( + "DEU7TKSV32", "DEU7TKSV33", "DEU7TKSV34", "DEU7TKSV35", + "DEU7TKSV36", "DEU7TKSV37", "DEU7TKSV38", "DEU7TKSV39", + "DEU7TKSV3A", "DEU7TKSV3B", "DEU7TKSV3C", "DEU7TKSV3D", + "DEU7TKSV3E", "DEU7TKSV3F", "DEU7TKSV3G", + " QV5FQ38MA", + "9A3B578UM0", "DEU7TKSV31", "Q4XBJCB43", "929B96CA8" + ) + for (tan in invalidTans) { + MatcherAssert.assertThat( + TanHelper.isChecksumValid(tan), + CoreMatchers.equalTo(false) + ) + } + } +} \ No newline at end of file -- GitLab