Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ import android.app.Dialog
import android.content.res.Resources
import android.os.Bundle
import android.os.Parcelable
import android.text.Editable
import android.text.InputFilter
import android.text.Spanned
import android.util.TypedValue
import android.view.WindowManager
import android.view.inputmethod.EditorInfo
Expand Down Expand Up @@ -88,6 +91,7 @@ import com.ichi2.utils.setPaddingRelative
import com.ichi2.utils.textAsIntOrNull
import com.ichi2.utils.title
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.parcelize.Parcelize
import net.ankiweb.rsdroid.BackendException
Expand Down Expand Up @@ -331,14 +335,28 @@ class CustomStudyDialog : AnalyticsDialogFragment() {
// Give EditText focus and show keyboard
setSelectAllOnFocus(true)
requestFocus()
inputType = EditorInfo.TYPE_CLASS_NUMBER
// a user may enter a negative value when extending limits
if (contextMenuOption == EXTEND_NEW || contextMenuOption == EXTEND_REV) {
inputType = EditorInfo.TYPE_CLASS_NUMBER or EditorInfo.TYPE_NUMBER_FLAG_SIGNED
}
if (contextMenuOption == STUDY_AHEAD) {
// UI safeguard: prevent excessively long numeric input
filters = arrayOf(InputFilter.LengthFilter(5), NoLeadingZeroFilter())
val initialValue = defaultValue.toIntOrNull() ?: 1
binding.detailsEditText2Layout.suffixText =
resources.getQuantityString(
R.plurals.set_due_date_label_suffix,
initialValue,
initialValue,
)
}
}
val positiveBtnLabel =
if (contextMenuOption == STUDY_TAGS) {
TR.customStudyChooseTags().toSentenceCase(R.string.sentence_choose_tags)
} else if (contextMenuOption == STUDY_AHEAD) {
getString(R.string.dialog_positive_create)
} else {
getString(R.string.dialog_ok)
}
Expand Down Expand Up @@ -423,13 +441,99 @@ class CustomStudyDialog : AnalyticsDialogFragment() {

binding.detailsEditText2.doAfterTextChanged {
dialog.positiveButton.isEnabled = userInputValue != null && userInputValue != 0
val value = it?.toString()?.toIntOrNull()

// Prevent invalid inputs like leading zeros (e.g. "01") by normalizing the value.
if (replaceZeroWithNextNumber(it)) return@doAfterTextChanged
studyAheadCase(contextMenuOption, dialog, value)
}

// Show soft keyboard
dialog.window?.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE)
return dialog
}

private fun studyAheadCase(
contextMenuOption: ContextMenuOption,
dialog: AlertDialog,
value: Int?,
) {
if (contextMenuOption != STUDY_AHEAD) return

if (userInputValue == null) {
binding.detailsEditText2Layout.error = null
dialog.positiveButton.isEnabled = false
return
}

if (userInputValue == 0) {
binding.detailsEditText2Layout.error =
getString(R.string.custom_study_ahead_prevent_leading_zeros)
dialog.positiveButton.isEnabled = false
return
}

val safeValue = value ?: return

binding.detailsEditText2Layout.suffixText =
resources.getQuantityString(
R.plurals.set_due_date_label_suffix,
safeValue,
safeValue,
)

val currentInput = userInputValue
lifecycleScope.launch {
val hasCards = hasMatchingCards(contextMenuOption, userInputValue)

if (currentInput != userInputValue) return@launch

if (hasCards) {
binding.detailsEditText2Layout.error = null
dialog.positiveButton.isEnabled = true
} else {
binding.detailsEditText2Layout.error =
TR.customStudyNoCardsMatchedTheCriteriaYou()
dialog.positiveButton.isEnabled = false
}
}
}

private fun replaceZeroWithNextNumber(editable: Editable?): Boolean {
val text = editable?.toString() ?: return true

if (text.length > 1 && text.startsWith("0")) {
val newText = text.trimStart('0')

val finalText = newText.ifEmpty { "0" }

binding.detailsEditText2.setText(finalText)
binding.detailsEditText2.setSelection(finalText.length)
return true
}
return false
}

@SuppressLint("CheckResult")
private suspend fun hasMatchingCards(
option: ContextMenuOption,
input: Int?,
): Boolean =
try {
withCol {
val currentDeckName = decks.name(viewModel.deckId)
val query =
when (option) {
STUDY_AHEAD -> "deck:\"$currentDeckName\" prop:due<=$input"
else -> "deck:\"$currentDeckName\""
}
findCards(query).isNotEmpty()
}
} catch (e: Exception) {
Timber.e(e)
true
}
Comment thread
Galal-20 marked this conversation as resolved.

// TODO cram kind and the included/excluded tags lists are only relevant for STUDY_TAGS and
// should be included in the option to not leak in the method's api
private suspend fun customStudy(
Expand Down Expand Up @@ -508,8 +612,8 @@ class CustomStudyDialog : AnalyticsDialogFragment() {
when (selectedSubDialog) {
EXTEND_NEW -> deferredDefaults.getCompleted().labelForNewQueueAvailable()
EXTEND_REV -> deferredDefaults.getCompleted().labelForReviewQueueAvailable()
STUDY_AHEAD -> TR.customStudyReviewAhead()
STUDY_FORGOT,
STUDY_AHEAD,
STUDY_PREVIEW,
STUDY_TAGS,
null,
Expand Down Expand Up @@ -560,6 +664,31 @@ class CustomStudyDialog : AnalyticsDialogFragment() {
}
}

class NoLeadingZeroFilter : InputFilter {
override fun filter(
source: CharSequence?,
start: Int,
end: Int,
dest: Spanned?,
dstart: Int,
dend: Int,
): CharSequence? {
val newText = dest?.replaceRange(dstart, dend, source?.subSequence(start, end) ?: "")

if (dest?.toString() == "0") return null

return if (
newText != null &&
newText.length > 1 &&
newText.startsWith("0")
) {
""
Comment thread
Galal-20 marked this conversation as resolved.
} else {
null
}
}
}

/**
* Represents actions for managing custom study sessions and extending study limits.
* These actions are passed between fragments and activities via the FragmentResult API.
Expand Down
10 changes: 7 additions & 3 deletions AnkiDroid/src/main/res/layout/fragment_custom_study.xml
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,9 @@
android:layout_marginRight="4dip"
android:gravity="start"
android:text=""
android:textSize="16sp" />
android:textSize="18sp"
android:textStyle="bold"
/>

<com.ichi2.ui.FixedTextView
android:id="@+id/details_text_2"
Expand All @@ -55,13 +57,15 @@
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/details_edit_text_2_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content">
android:layout_height="wrap_content"
app:errorEnabled="true"
app:suffixTextAppearance="@style/TextAppearance.MaterialComponents.Body1"
>

<com.google.android.material.textfield.TextInputEditText
android:id="@+id/details_edit_text_2"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
android:inputType="number|numberSigned"
tools:text="1"
/>
Expand Down
3 changes: 2 additions & 1 deletion AnkiDroid/src/main/res/values/03-dialogs.xml
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,8 @@
<string name="custom_study_new_extend">Increase/decrease (“-3”) today’s new card limit by</string>
<string name="custom_study_rev_extend">Increase/decrease (“-3”) today’s review limit by</string>
<string name="custom_study_forgotten">Review cards forgotten in last x days:</string>
<string name="custom_study_ahead">Review ahead by x days:</string>
<string name="custom_study_ahead">Review cards due in the next:</string>
<string name="custom_study_ahead_prevent_leading_zeros">Value must be greater than 0</string>
<string name="custom_study_preview">Preview new cards added in the last x days:</string>
<string name="custom_study_tags">Select x cards from the deck:</string>

Expand Down
Loading