From 842608796e44ee33bf719d3071d774818c7beff6 Mon Sep 17 00:00:00 2001 From: David Allison <62114487+david-allison@users.noreply.github.com> Date: Mon, 16 Dec 2024 23:13:13 +0000 Subject: [PATCH] refactor(custom-study): Kotlinize `buildInputDialog` --- .../dialogs/customstudy/CustomStudyDialog.kt | 77 +++++++------------ .../java/com/ichi2/utils/EditTextUtils.kt | 8 ++ .../java/com/ichi2/utils/EditTextUtilsTest.kt | 59 ++++++++++++++ 3 files changed, 96 insertions(+), 48 deletions(-) create mode 100644 AnkiDroid/src/test/java/com/ichi2/utils/EditTextUtilsTest.kt diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/dialogs/customstudy/CustomStudyDialog.kt b/AnkiDroid/src/main/java/com/ichi2/anki/dialogs/customstudy/CustomStudyDialog.kt index bc0cac9a24f6..3dd9bf5cd743 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/dialogs/customstudy/CustomStudyDialog.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/dialogs/customstudy/CustomStudyDialog.kt @@ -21,8 +21,6 @@ import android.annotation.SuppressLint import android.app.Dialog import android.content.res.Resources import android.os.Bundle -import android.text.Editable -import android.text.TextWatcher import android.view.WindowManager import android.view.inputmethod.EditorInfo import android.widget.EditText @@ -30,6 +28,7 @@ import android.widget.TextView import androidx.annotation.VisibleForTesting import androidx.appcompat.app.AlertDialog import androidx.core.content.edit +import androidx.core.widget.doAfterTextChanged import anki.scheduler.CustomStudyDefaultsResponse import anki.scheduler.CustomStudyRequestKt import anki.scheduler.customStudyRequest @@ -70,6 +69,7 @@ import com.ichi2.utils.customView import com.ichi2.utils.listItems import com.ichi2.utils.negativeButton import com.ichi2.utils.positiveButton +import com.ichi2.utils.textAsIntOrNull import com.ichi2.utils.title import net.ankiweb.rsdroid.exceptions.BackendDeckIsFilteredException import org.json.JSONObject @@ -221,19 +221,24 @@ class CustomStudyDialog( // Show input dialog for an individual custom study dialog @SuppressLint("InflateParams") val v = requireActivity().layoutInflater.inflate(R.layout.styled_custom_study_details_dialog, null) - val textView1 = v.findViewById(R.id.custom_study_details_text1) - val textView2 = v.findViewById(R.id.custom_study_details_text2) - val editText = v.findViewById(R.id.custom_study_details_edittext2) - // Set the text - textView1.text = text1 - textView2.text = text2 - editText.setText(defaultValue) - // Give EditText focus and show keyboard - editText.setSelectAllOnFocus(true) - editText.requestFocus() - if (contextMenuOption == STUDY_NEW || contextMenuOption == STUDY_REV) { - editText.inputType = EditorInfo.TYPE_CLASS_NUMBER or EditorInfo.TYPE_NUMBER_FLAG_SIGNED + v.findViewById(R.id.custom_study_details_text1).apply { + text = text1 } + v.findViewById(R.id.custom_study_details_text2).apply { + text = text2 + } + val editText = + v.findViewById(R.id.custom_study_details_edittext2).apply { + setText(defaultValue) + // Give EditText focus and show keyboard + setSelectAllOnFocus(true) + requestFocus() + // a user may enter a negative value when extending limits + if (contextMenuOption == STUDY_NEW || contextMenuOption == STUDY_REV) { + inputType = EditorInfo.TYPE_CLASS_NUMBER or EditorInfo.TYPE_NUMBER_FLAG_SIGNED + } + } + // Set material dialog parameters val dialog = AlertDialog @@ -241,45 +246,18 @@ class CustomStudyDialog( .customView(view = v, paddingLeft = 64, paddingRight = 64, paddingTop = 32, paddingBottom = 32) .positiveButton(R.string.dialog_ok) { // Get the value selected by user - val n: Int = - try { - editText.text.toString().toInt() - } catch (e: Exception) { - Timber.w(e) - // This should never happen because we disable positive button for non-parsable inputs - return@positiveButton + val n = + editText.textAsIntOrNull() ?: return@positiveButton Unit.also { + Timber.w("Non-numeric user input was provided") + Timber.d("value: %s", editText.text.toString()) } requireActivity().launchCatchingTask { customStudy(contextMenuOption, n) } }.negativeButton(R.string.dialog_cancel) { requireActivity().dismissAllDialogFragments() }.create() // Added .create() because we wanted to access alertDialog positive button enable state - editText.addTextChangedListener( - object : TextWatcher { - override fun beforeTextChanged( - charSequence: CharSequence, - i: Int, - i1: Int, - i2: Int, - ) {} - - override fun onTextChanged( - charSequence: CharSequence, - i: Int, - i1: Int, - i2: Int, - ) {} - - override fun afterTextChanged(editable: Editable) { - try { - editText.text.toString().toInt() - dialog.positiveButton.isEnabled = true - } catch (e: Exception) { - Timber.w(e) - dialog.positiveButton.isEnabled = false - } - } - }, - ) + editText.doAfterTextChanged { + dialog.positiveButton.isEnabled = editText.textAsIntOrNull() != null + } // Show soft keyboard dialog.window?.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE) @@ -397,6 +375,7 @@ class CustomStudyDialog( -> "" } + /** Line 2 of the number entry dialog */ private val text2: String get() { val res = resources @@ -411,6 +390,8 @@ class CustomStudyDialog( -> "" } } + + /** Initial value of the number entry dialog */ private val defaultValue: String get() { val prefs = requireActivity().sharedPrefs() diff --git a/AnkiDroid/src/main/java/com/ichi2/utils/EditTextUtils.kt b/AnkiDroid/src/main/java/com/ichi2/utils/EditTextUtils.kt index 8a55a0c057f9..00bb3b3bfe03 100644 --- a/AnkiDroid/src/main/java/com/ichi2/utils/EditTextUtils.kt +++ b/AnkiDroid/src/main/java/com/ichi2/utils/EditTextUtils.kt @@ -20,3 +20,11 @@ import android.widget.EditText /** Moves the cursor to the end of the [EditText] */ fun EditText.moveCursorToEnd() = setSelection(text?.length ?: 0) + +/** + * Parses the [text][EditText.text] as an [Int] and returns the result, or `null` if the + * string is not a valid representation of an [Int]. + * + * note: "1.0" returns `null` + */ +fun EditText.textAsIntOrNull() = this.text.toString().toIntOrNull() diff --git a/AnkiDroid/src/test/java/com/ichi2/utils/EditTextUtilsTest.kt b/AnkiDroid/src/test/java/com/ichi2/utils/EditTextUtilsTest.kt new file mode 100644 index 000000000000..4f9586280cc4 --- /dev/null +++ b/AnkiDroid/src/test/java/com/ichi2/utils/EditTextUtilsTest.kt @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2024 David Allison + * + * This program is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License as published by the Free Software + * Foundation; either version 3 of the License, or (at your option) any later + * version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * PARTICULAR PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with + * this program. If not, see . + */ + +package com.ichi2.utils + +import android.widget.EditText +import androidx.test.espresso.matcher.ViewMatchers.assertThat +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.ichi2.testutils.AndroidTest +import com.ichi2.testutils.EmptyApplication +import com.ichi2.testutils.targetContext +import org.hamcrest.CoreMatchers.equalTo +import org.hamcrest.CoreMatchers.nullValue +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.annotation.Config + +@RunWith(AndroidJUnit4::class) +@Config(application = EmptyApplication::class) +class EditTextUtilsTest : AndroidTest { + @Test + fun `textAsIntOrNull test`() { + fun textAsIntOrNull(value: String) = + EditText(targetContext) + .apply { setText(value) } + .textAsIntOrNull() + + fun textAsIntOrNull(value: Long) = textAsIntOrNull(value.toString()) + + fun textAsIntOrNull(value: Int) = textAsIntOrNull(value.toString()) + + assertThat("1", textAsIntOrNull("1"), equalTo(1)) + assertThat("1.0 => null", textAsIntOrNull("1.0"), nullValue()) + assertThat("1.1 => null", textAsIntOrNull("1.1"), nullValue()) + assertThat("-1", textAsIntOrNull("-1"), equalTo(-1)) + assertThat("2147483647", textAsIntOrNull(Int.MAX_VALUE), equalTo(2147483647)) + assertThat("-2147483648", textAsIntOrNull(Int.MIN_VALUE), equalTo(-2147483648)) + + assertThat("MIN_VALUE - 1 => null", textAsIntOrNull(Int.MIN_VALUE - 1L), nullValue()) + assertThat("MAX_VALUE + 1 => null", textAsIntOrNull(Int.MAX_VALUE + 1L), nullValue()) + + assertThat("empty string => null", textAsIntOrNull(""), nullValue()) + assertThat("text => null", textAsIntOrNull("non-int text"), nullValue()) + assertThat("too long => null", textAsIntOrNull(Long.MAX_VALUE), nullValue()) + } +}