Skip to content

Commit

Permalink
refactor(custom-study): Kotlinize buildInputDialog
Browse files Browse the repository at this point in the history
  • Loading branch information
david-allison committed Dec 18, 2024
1 parent c653dfb commit 1bd0d66
Show file tree
Hide file tree
Showing 3 changed files with 96 additions and 48 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -21,15 +21,14 @@ 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
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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -221,65 +221,43 @@ 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<TextView>(R.id.custom_study_details_text1)
val textView2 = v.findViewById<TextView>(R.id.custom_study_details_text2)
val editText = v.findViewById<EditText>(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<TextView>(R.id.custom_study_details_text1).apply {
text = text1
}
v.findViewById<TextView>(R.id.custom_study_details_text2).apply {
text = text2
}
val editText =
v.findViewById<EditText>(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
.Builder(requireActivity())
.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)
Expand Down Expand Up @@ -397,6 +375,7 @@ class CustomStudyDialog(
-> ""
}

/** Line 2 of the number entry dialog */
private val text2: String
get() {
val res = resources
Expand All @@ -411,6 +390,8 @@ class CustomStudyDialog(
-> ""
}
}

/** Initial value of the number entry dialog */
private val defaultValue: String
get() {
val prefs = requireActivity().sharedPrefs()
Expand Down
8 changes: 8 additions & 0 deletions AnkiDroid/src/main/java/com/ichi2/utils/EditTextUtils.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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()
59 changes: 59 additions & 0 deletions AnkiDroid/src/test/java/com/ichi2/utils/EditTextUtilsTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
/*
* Copyright (c) 2024 David Allison <[email protected]>
*
* 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 <http://www.gnu.org/licenses/>.
*/

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())
}
}

0 comments on commit 1bd0d66

Please sign in to comment.