Skip to content

Commit

Permalink
Add support for the V3 scheduler
Browse files Browse the repository at this point in the history
  • Loading branch information
dae committed Jul 20, 2022
1 parent 7cdbcac commit 7b94480
Show file tree
Hide file tree
Showing 5 changed files with 205 additions and 11 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2583,7 +2583,7 @@ abstract class AbstractFlashcardViewer :
override fun opExecuted(changes: OpChanges, handler: Any?) {
if ((changes.studyQueues || changes.noteText || changes.card) && handler !== this) {
// executing this only for the refresh side effects; there may be a better way
Undo().runWithHandler(
GetCard().runWithHandler(
answerCardHandler(false)
)
}
Expand Down
15 changes: 8 additions & 7 deletions AnkiDroid/src/main/java/com/ichi2/libanki/Collection.kt
Original file line number Diff line number Diff line change
Expand Up @@ -40,20 +40,21 @@ import com.ichi2.async.CollectionTask.PartialSearch
import com.ichi2.async.ProgressSender
import com.ichi2.async.TaskManager
import com.ichi2.libanki.TemplateManager.TemplateRenderContext.TemplateRenderOutput
import com.ichi2.libanki.backend.exception.BackendNotSupportedException
import com.ichi2.libanki.exception.NoSuchDeckException
import com.ichi2.libanki.exception.UnknownDatabaseVersionException
import com.ichi2.libanki.hooks.ChessFilter
import com.ichi2.libanki.sched.AbstractSched
import com.ichi2.libanki.sched.Sched
import com.ichi2.libanki.sched.SchedV2
import com.ichi2.libanki.sched.SchedV3
import com.ichi2.libanki.template.ParsedNode
import com.ichi2.libanki.template.TemplateError
import com.ichi2.libanki.utils.Time
import com.ichi2.libanki.utils.TimeManager
import com.ichi2.upgrade.Upgrade
import com.ichi2.utils.*
import net.ankiweb.rsdroid.Backend
import net.ankiweb.rsdroid.BackendFactory
import net.ankiweb.rsdroid.RustCleanup
import org.jetbrains.annotations.Contract
import timber.log.Timber
Expand Down Expand Up @@ -253,13 +254,13 @@ open class Collection(
if (ver == 1) {
sched = Sched(this)
} else if (ver == 2) {
sched = SchedV2(this)
if (!BackendFactory.defaultLegacySchema && newBackend.v3Enabled) {
sched = SchedV3(this.newBackend)
} else {
sched = SchedV2(this)
}
if (!server) {
try {
set_config("localOffset", (sched as SchedV2)._current_timezone_offset())
} catch (e: BackendNotSupportedException) {
throw e.alreadyUsingRustBackend()
}
set_config("localOffset", sched._current_timezone_offset())
}
}
}
Expand Down
9 changes: 9 additions & 0 deletions AnkiDroid/src/main/java/com/ichi2/libanki/CollectionV16.kt
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ package com.ichi2.libanki

import android.content.Context
import android.content.res.Resources
import anki.config.ConfigKey
import com.ichi2.async.CollectionTask
import com.ichi2.libanki.backend.*
import com.ichi2.libanki.backend.model.toProtoBuf
Expand Down Expand Up @@ -187,4 +188,12 @@ class CollectionV16(
val status = undoStatus()
return status.undo ?: super.undoName(res)
}

/** True if the V3 scheduled is enabled when schedVer is 2. */
var v3Enabled: Boolean
get() = backend.getConfigBool(ConfigKey.Bool.SCHED_2021)
set(value) {
backend.setConfigBool(ConfigKey.Bool.SCHED_2021, value, undoable = false)
_loadScheduler()
}
}
5 changes: 2 additions & 3 deletions AnkiDroid/src/main/java/com/ichi2/libanki/DecksV16.kt
Original file line number Diff line number Diff line change
Expand Up @@ -679,10 +679,9 @@ class DecksV16(private val col: CollectionV16) :
override fun select(did: did) {
// make sure arg is an int
// did = int(did) - code removed, logically impossible
val current = this.selected()
col.backend.setCurrentDeck(did)
val active = this.deck_and_child_ids(did)
if (current != did || active != this.active()) {
this.col.set_config(CURRENT_DECK, did)
if (active != this.active()) {
this.col.set_config(ACTIVE_DECKS, active.toJsonArray())
}
}
Expand Down
185 changes: 185 additions & 0 deletions AnkiDroid/src/main/java/com/ichi2/libanki/sched/SchedV3.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
/***************************************************************************************
* Copyright (c) 2022 Ankitects Pty Ltd <https://apps.ankiweb.net> *
* *
* 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.libanki.sched

import android.app.Activity
import anki.scheduler.*
import com.ichi2.async.CancelListener
import com.ichi2.libanki.Card
import com.ichi2.libanki.CollectionV16
import com.ichi2.libanki.utils.TimeManager.time
import java.lang.ref.WeakReference

/**
* This code currently tries to fit within the constraints of the AbstractSched API. In the
* future, it would be better for the reviewer to fetch queuedCards directly, so they only
* need to be fetched once.
*/
class SchedV3(col: CollectionV16) : AbstractSched(col) {
private var activityForLeechNotification: WeakReference<Activity>? = null

override val today: Int
get() = col.backend.schedTimingToday().daysElapsed

override fun reset() {
// backend automatically resets queues as operations are performed
}

override fun resetCounts() {
// backend automatically resets queues as operations are performed
}

override fun deferReset(undoneCard: Card?) {
// backend automatically resets queues as operations are performed
}

// could be made more efficient by constructing a native Card object from
// the backend card object, instead of doing a separate fetch
override val card: Card?
get() = queuedCards.cardsList.firstOrNull()?.card?.id?.let { col.getCard(it) }

private val queuedCards: QueuedCards
get() = col.backend.getQueuedCards(fetchLimit = 1, intradayLearningOnly = false)

override fun preloadNextCard() {
// if this proves necessary in the future, it could be implemented by increasing
// fetchLimit above
}

override fun answerCard(card: Card, ease: Int) {
val top = queuedCards.cardsList.first()
val answer = buildAnswer(card, top.nextStates, ease)
col.backend.answerCard(answer)
reps += 1
// if this were checked in the UI, there'd be no need to store an activity here
if (col.backend.stateIsLeech(answer.newState)) {
activityForLeechNotification?.get()?.let { leech(card, it) }
}
// tests assume the card was mutated
card.load()
}

fun buildAnswer(card: Card, states: NextCardStates, ease: Int): CardAnswer {
return cardAnswer {
cardId = card.id
currentState = states.current
newState = stateFromEase(states, ease)
rating = ratingFromEase(ease)
answeredAtMillis = time.intTimeMS()
millisecondsTaken = card.timeTaken()
}
}

private fun ratingFromEase(ease: Int): CardAnswer.Rating {
return when (ease) {
1 -> CardAnswer.Rating.AGAIN
2 -> CardAnswer.Rating.HARD
3 -> CardAnswer.Rating.GOOD
4 -> CardAnswer.Rating.EASY
else -> TODO("invalid ease: $ease")
}
}

private fun stateFromEase(states: NextCardStates, ease: Int): SchedulingState {
return when (ease) {
1 -> states.again
2 -> states.hard
3 -> states.good
4 -> states.easy
else -> TODO("invalid ease: $ease")
}
}

override fun counts(cancelListener: CancelListener?): Counts {
return queuedCards.let {
Counts(it.newCount, it.learningCount, it.reviewCount)
}
}

override fun counts(card: Card): Counts {
return counts(null)
}

/** Ignores provided card and uses top of queue */
override fun countIdx(card: Card): Counts.Queue {
return when (queuedCards.cardsList.first().queue) {
QueuedCards.Queue.NEW -> Counts.Queue.NEW
QueuedCards.Queue.LEARNING -> Counts.Queue.LRN
QueuedCards.Queue.REVIEW -> Counts.Queue.REV
QueuedCards.Queue.UNRECOGNIZED, null -> TODO("unrecognized queue")
}
}

override fun answerButtons(card: Card): Int {
return 4
}

override val goodNewButton: Int = 3

override fun haveBuried(did: Long): Boolean {
// Backend does not support checking bury status of an arbitrary deck. This is
// only used to decide whether to show an "unbury" option on a long press of a
// deck.
return false
}

override val name = "std3"

override var reps: Int = 0

override fun setContext(contextReference: WeakReference<Activity>) {
this.activityForLeechNotification = contextReference
}

override fun undoReview(card: Card, wasLeech: Boolean) {
// Only used by UndoTest
TODO("Not yet implemented")
}

/** Only provided for legacy unit tests. */
override fun nextIvl(card: Card, ease: Int): Long {
val states = col.backend.getNextCardStates(card.id)
val state = stateFromEase(states, ease)
return intervalForState(state)
}

private fun intervalForState(state: SchedulingState): Long {
return when (state.valueCase) {
SchedulingState.ValueCase.NORMAL -> intervalForNormalState(state.normal)
SchedulingState.ValueCase.FILTERED -> intervalForFilteredState(state.filtered)
SchedulingState.ValueCase.VALUE_NOT_SET, null -> TODO("invalid scheduling state")
}
}

private fun intervalForNormalState(normal: SchedulingState.Normal): Long {
return when (normal.valueCase) {
SchedulingState.Normal.ValueCase.NEW -> 0
SchedulingState.Normal.ValueCase.LEARNING -> normal.learning.scheduledSecs.toLong()
SchedulingState.Normal.ValueCase.REVIEW -> normal.review.scheduledDays.toLong() * 86400
SchedulingState.Normal.ValueCase.RELEARNING -> normal.relearning.learning.scheduledSecs.toLong()
SchedulingState.Normal.ValueCase.VALUE_NOT_SET, null -> TODO("invalid normal state")
}
}

private fun intervalForFilteredState(filtered: SchedulingState.Filtered): Long {
return when (filtered.valueCase) {
SchedulingState.Filtered.ValueCase.PREVIEW -> filtered.preview.scheduledSecs.toLong()
SchedulingState.Filtered.ValueCase.RESCHEDULING -> intervalForNormalState(filtered.rescheduling.originalState)
SchedulingState.Filtered.ValueCase.VALUE_NOT_SET, null -> TODO("invalid filtered state")
}
}
}

0 comments on commit 7b94480

Please sign in to comment.