Skip to content

Commit

Permalink
Sync enabled in the reviewer when the deck picker was not used
Browse files Browse the repository at this point in the history
Widgets and shortcuts allows to directly review a deck.
This means that automated sync, or even manual sync, is never
available. Except if the user close the reviewer and then manually
open ankidroid. This create a lot of friction on a very important
feature.

This commit enable the sync feature in the reviewer, but hides it
unless the intent actually requested it.

In my opinion, it should be always available. It's available in anki
upstream from the reviewer, and anki allows automated save every 10
minutes. Which means even if your device suddenly dies during a
streak, you only lose up to 10 minutes of review.

Still, this feature at least improve the case that was the most
dangerous for users (and frustrating for me)

I don't know the code of Reviewer2/ReviewerFragment. So this is only
enabled in the legacy reviewer. I hope the code is simple enough for
the feature to be easily ported to the other reviewers now that the
entire sync part of the code was decoupled from the DeckPicker.

Manually tested with:
* signin in (when no account present)
* full sync (requested locally and requested by ankiweb)
* normal sync
* changing the selected deck on the other device

To test it you should use a widget or a shortcut.

One point amy need to be changed. The little icon that state that
there is a not-uploaded change. While it's okay in the deckpicker, it
may not be ideal to have it as soon as we have the first review done.
  • Loading branch information
Arthur-Milchior committed Dec 11, 2024
1 parent 7f3d83b commit f93096a
Show file tree
Hide file tree
Showing 6 changed files with 235 additions and 4 deletions.
5 changes: 5 additions & 0 deletions AnkiDroid/src/main/java/com/ichi2/anki/IntentHandler.kt
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import androidx.annotation.VisibleForTesting
import androidx.core.app.TaskStackBuilder
import androidx.core.content.FileProvider
import androidx.core.content.IntentCompat
import com.ichi2.anki.Reviewer.Companion.ENABLE_SYNC
import com.ichi2.anki.dialogs.DialogHandler.Companion.storeMessage
import com.ichi2.anki.dialogs.DialogHandlerMessage
import com.ichi2.anki.preferences.sharedPrefs
Expand Down Expand Up @@ -153,6 +154,9 @@ class IntentHandler : AbstractIntentHandler() {
} else {
Intent(this, Reviewer::class.java)
}
if (intent.extras?.getBoolean(ENABLE_SYNC) == true) {
reviewIntent.putExtra(ENABLE_SYNC, true)
}
CollectionManager.getColUnsafe().decks.select(deckId)
startActivity(reviewIntent)
finish()
Expand Down Expand Up @@ -440,6 +444,7 @@ class IntentHandler : AbstractIntentHandler() {
setAction(Intent.ACTION_VIEW)
putExtra(ReminderService.EXTRA_DECK_ID, deckId)
addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
putExtra(ENABLE_SYNC, true)
}
}
}
102 changes: 101 additions & 1 deletion AnkiDroid/src/main/java/com/ichi2/anki/Reviewer.kt
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import android.os.Bundle
import android.os.Handler
import android.os.Message
import android.os.Parcelable
import android.os.PersistableBundle
import android.text.SpannableString
import android.text.style.UnderlineSpan
import android.view.KeyEvent
Expand All @@ -41,6 +42,8 @@ import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.RelativeLayout
import android.widget.TextView
import androidx.activity.result.ActivityResult
import androidx.activity.result.ActivityResultCallback
import androidx.activity.result.contract.ActivityResultContracts
import androidx.annotation.CheckResult
import androidx.annotation.DrawableRes
Expand All @@ -51,6 +54,7 @@ import androidx.appcompat.view.menu.MenuBuilder
import androidx.appcompat.widget.Toolbar
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import androidx.core.view.MenuItemCompat
import androidx.lifecycle.lifecycleScope
import androidx.vectordrawable.graphics.drawable.VectorDrawableCompat
import anki.frontend.SetSchedulingStatesRequest
Expand All @@ -62,6 +66,8 @@ import com.ichi2.anki.Whiteboard.Companion.createInstance
import com.ichi2.anki.Whiteboard.OnPaintColorChangeListener
import com.ichi2.anki.cardviewer.Gesture
import com.ichi2.anki.cardviewer.ViewerCommand
import com.ichi2.anki.dialogs.MediaCheckDialog
import com.ichi2.anki.dialogs.SyncErrorDialog.SyncErrorDialogListenerProvider
import com.ichi2.anki.multimedia.audio.AudioRecordingController
import com.ichi2.anki.multimedia.audio.AudioRecordingController.Companion.generateTempAudioFile
import com.ichi2.anki.multimedia.audio.AudioRecordingController.Companion.isAudioRecordingSaved
Expand Down Expand Up @@ -100,6 +106,7 @@ import com.ichi2.libanki.Card
import com.ichi2.libanki.CardId
import com.ichi2.libanki.Collection
import com.ichi2.libanki.Consts
import com.ichi2.libanki.MediaCheckResult
import com.ichi2.libanki.sched.Counts
import com.ichi2.libanki.sched.CurrentQueueState
import com.ichi2.libanki.undoableOp
Expand Down Expand Up @@ -132,6 +139,8 @@ import kotlin.coroutines.resume
@NeedsTest("#14709: Timebox shouldn't appear instantly when the Reviewer is opened")
open class Reviewer :
AbstractFlashcardViewer(),
SyncHandlerDelegate,
SyncErrorDialogListenerProvider,
ReviewerUi {
private var queueState: CurrentQueueState? = null
private val customSchedulingKey = TimeManager.time.intTimeMS().toString()
Expand Down Expand Up @@ -165,6 +174,15 @@ open class Reviewer :
private lateinit var answerTimer: AnswerTimer
private var prefHideDueCount = false

/**
* Whether to offer the user to sync.
* Normally, only the deck picker deal with syncing. But direct intent could avoid the deckPicker.
* In this case, the reviewer deals with syncing.
*/
private var syncIsEnabled = false

val syncHandler: SyncHandler = SyncHandler(this, this)

// Whiteboard
var prefWhiteboard = false

Expand Down Expand Up @@ -206,6 +224,12 @@ open class Reviewer :

private val flagItemIds = mutableSetOf<Int>()

// flag keeping track of when the app has been paused
var activityPaused = false
private set

override fun activityPaused() = activityPaused

override fun onCreate(savedInstanceState: Bundle?) {
if (showedActivityFailedScreen(savedInstanceState)) {
return
Expand All @@ -214,6 +238,9 @@ open class Reviewer :
if (!ensureStoragePermissions()) {
return
}

syncIsEnabled = intent.extras?.getBoolean(ENABLE_SYNC, false) == true

colorPalette = findViewById(R.id.whiteboard_editor)
answerTimer = AnswerTimer(findViewById(R.id.card_time))
textBarNew = findViewById(R.id.new_number)
Expand All @@ -232,12 +259,21 @@ open class Reviewer :
registerOnForgetHandler { listOf(currentCardId!!) }
}

override fun onSaveInstanceState(
outState: Bundle,
outPersistentState: PersistableBundle,
) {
outPersistentState.putBoolean(ENABLE_SYNC, syncIsEnabled)
}

override fun onPause() {
activityPaused = true
answerTimer.pause()
super.onPause()
}

override fun onResume() {
activityPaused = false
when {
stopTimerOnAnswer && isDisplayingAnswer -> {}
else -> launchCatchingTask { answerTimer.resume() }
Expand Down Expand Up @@ -409,6 +445,18 @@ open class Reviewer :
Timber.i("Reviewer:: Home button pressed")
closeReviewer(RESULT_OK)
}
R.id.action_sync -> {
Timber.i("DeckPicker:: Sync button pressed")
val actionProvider = MenuItemCompat.getActionProvider(item) as? SyncActionProvider
if (actionProvider?.isProgressShown == true) {
launchCatchingTask {
syncHandler.monitorMediaSync()
}
} else {
syncHandler.sync()
}
return true
}
R.id.action_undo -> {
Timber.i("Reviewer:: Undo button pressed")
if (showWhiteboard && whiteboard != null && !whiteboard!!.undoEmpty()) {
Expand Down Expand Up @@ -871,6 +919,14 @@ open class Reviewer :
voicePlaybackIcon.setTitle(R.string.menu_enable_voice_playback)
}

menu.findItem(R.id.action_sync).isVisible = syncIsEnabled
// Ensures the previous state is set back to avoid flickering.
syncHandler.updateSyncIconFromState(menu)
launchCatchingTask {
syncHandler.updateMenuState()
syncHandler.updateSyncIconFromState(menu)
}

increaseHorizontalPaddingOfOverflowMenuIcons(menu)
tintOverflowMenuIcons(menu, skipIf = { isFlagItem(it) })

Expand Down Expand Up @@ -924,7 +980,20 @@ open class Reviewer :
if (processor.onKeyUp(keyCode, event)) {
true
} else {
super.onKeyUp(keyCode, event)
when (keyCode) {
KeyEvent.KEYCODE_Y ->
if (syncIsEnabled) {
Timber.i("Sync from keypress")
syncHandler.sync()
true
} else {
false
}

else -> {
super.onKeyUp(keyCode, event)
}
}
}

override fun onGenericMotionEvent(event: MotionEvent?): Boolean {
Expand Down Expand Up @@ -1657,12 +1726,43 @@ open class Reviewer :
return cardDataForJsAPI
}

override fun onSyncStart() {
// Nothing to do during sync start
}

override fun refreshState() {
launchCatchingTask {
updateCardAndRedraw()
}
}

override fun syncCallback(callback: (ActivityResult) -> Unit) =
object : ActivityResultCallback<ActivityResult> {
override fun onActivityResult(result: ActivityResult) {
callback(result)
}
}

override fun showMediaCheckDialog(
dialogType: Int,
checkList: MediaCheckResult,
) {
showAsyncDialogFragment(MediaCheckDialog.newInstance(dialogType, checkList))
}

override fun requireSyncErrorDialogListener() = syncHandler

companion object {
/**
* Bundle key for the deck id to review.
*/
const val EXTRA_DECK_ID = "deckId"

/**
* Key of a bundle entry stating whether sync is enabled. This is the case when the reviewer was not opened through the deck picker.
*/
const val ENABLE_SYNC = "enableSync"

private const val REQUEST_AUDIO_PERMISSION = 0
private const val ANIMATION_DURATION = 200
private const val TRANSPARENCY = 0.90f
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,15 @@
*/
package com.ichi2.anki.previewer

import android.R.attr.fragment
import android.content.Context
import android.content.Intent
import android.os.Bundle
import androidx.activity.enableEdgeToEdge
import androidx.fragment.app.Fragment
import com.ichi2.anki.R
import com.ichi2.anki.SingleFragmentActivity
import com.ichi2.anki.dialogs.SyncErrorDialog
import com.ichi2.anki.utils.navBarNeedsScrim
import com.ichi2.themes.Themes
import kotlin.reflect.KClass
Expand All @@ -31,7 +33,9 @@ import kotlin.reflect.jvm.jvmName
* @see PreviewerFragment
* @see TemplatePreviewerFragment
*/
class CardViewerActivity : SingleFragmentActivity() {
class CardViewerActivity :
SingleFragmentActivity(),
SyncErrorDialog.SyncErrorDialogListenerProvider {
@Suppress("deprecation", "API35 properly handle edge-to-edge")
override fun onCreate(savedInstanceState: Bundle?) {
enableEdgeToEdge() // TODO assess moving this to SingleFragmentActivity
Expand All @@ -44,6 +48,12 @@ class CardViewerActivity : SingleFragmentActivity() {
}
}

/**
* Returns the fragment, in charge of dealing with the sync error. Fails if the fragment don't offer sync.
*/
override fun requireSyncErrorDialogListener() =
(fragment as SyncErrorDialog.SyncErrorDialogListenerProvider).requireSyncErrorDialogListener()

companion object {
fun getIntent(
context: Context,
Expand Down
Loading

0 comments on commit f93096a

Please sign in to comment.