Skip to content

Commit

Permalink
improvement(deck-picker): use back callbacks
Browse files Browse the repository at this point in the history
This uses a class and a handler to detect changes to the preference
rather than checking inside onBackPressed

Issue 14558
  • Loading branch information
david-allison authored and mikehardy committed Jan 26, 2025
1 parent 8dfff03 commit 1f9557c
Show file tree
Hide file tree
Showing 4 changed files with 122 additions and 41 deletions.
81 changes: 40 additions & 41 deletions AnkiDroid/src/main/java/com/ichi2/anki/DeckPicker.kt
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.RelativeLayout
import android.widget.TextView
import androidx.activity.OnBackPressedCallback
import androidx.activity.result.ActivityResult
import androidx.activity.result.ActivityResultCallback
import androidx.activity.result.ActivityResultLauncher
Expand Down Expand Up @@ -91,6 +92,7 @@ import com.google.android.material.snackbar.Snackbar
import com.ichi2.anki.CollectionManager.TR
import com.ichi2.anki.CollectionManager.withCol
import com.ichi2.anki.CollectionManager.withOpenColOrNull
import com.ichi2.anki.DeckPickerFloatingActionMenu.FloatingActionBarToggleListener
import com.ichi2.anki.InitialActivity.StartupFailure
import com.ichi2.anki.InitialActivity.StartupFailure.DBError
import com.ichi2.anki.InitialActivity.StartupFailure.DatabaseLocked
Expand All @@ -102,6 +104,7 @@ import com.ichi2.anki.InitialActivity.StartupFailure.WebviewFailed
import com.ichi2.anki.IntentHandler.Companion.intentToReviewDeckFromShorcuts
import com.ichi2.anki.StudyOptionsFragment.StudyOptionsListener
import com.ichi2.anki.analytics.UsageAnalytics
import com.ichi2.anki.android.back.exitViaDoubleTapBackCallback
import com.ichi2.anki.android.input.ShortcutGroup
import com.ichi2.anki.android.input.shortcut
import com.ichi2.anki.deckpicker.BITMAP_BYTES_PER_PIXEL
Expand Down Expand Up @@ -166,7 +169,6 @@ import com.ichi2.async.deleteMedia
import com.ichi2.compat.CompatHelper.Companion.getSerializableCompat
import com.ichi2.compat.CompatHelper.Companion.sdkVersion
import com.ichi2.libanki.ChangeManager
import com.ichi2.libanki.Consts
import com.ichi2.libanki.DeckId
import com.ichi2.libanki.Decks
import com.ichi2.libanki.MediaCheckResult
Expand All @@ -177,7 +179,6 @@ import com.ichi2.ui.AccessibleSearchView
import com.ichi2.ui.BadgeDrawableBuilder
import com.ichi2.utils.AdaptionUtil
import com.ichi2.utils.ClipboardUtil.IMPORT_MIME_TYPES
import com.ichi2.utils.HandlerUtils
import com.ichi2.utils.ImportUtils
import com.ichi2.utils.ImportUtils.ImportResult
import com.ichi2.utils.KotlinCleanup
Expand Down Expand Up @@ -257,7 +258,6 @@ open class DeckPicker :

// Short animation duration from system
private var shortAnimDuration = 0
private var backButtonPressedToExit = false
private lateinit var deckPickerContent: RelativeLayout

// TODO: Encapsulate ProgressDialog within a class to limit the use of deprecated functionality
Expand Down Expand Up @@ -392,6 +392,28 @@ open class DeckPicker :
},
)

private val exitAndSyncBackCallback =
object : OnBackPressedCallback(enabled = true) {
override fun handleOnBackPressed() {
// TODO: Room for improvement now we use back callbacks
// can't use launchCatchingTask because any errors
// would need to be shown in the UI
lifecycleScope
.launch {
automaticSync(runInBackground = true)
}.invokeOnCompletion {
finish()
}
}
}

private val closeFloatingActionBarBackPressCallback =
object : OnBackPressedCallback(enabled = false) {
override fun handleOnBackPressed() {
floatingActionMenu.closeFloatingActionMenu(applyRiseAndShrinkAnimation = true)
}
}

private inner class DeckPickerActivityResultCallback(
private val callback: (result: ActivityResult) -> Unit,
) : ActivityResultCallback<ActivityResult> {
Expand Down Expand Up @@ -570,8 +592,14 @@ open class DeckPicker :
pullToSyncWrapper.isEnabled = recyclerViewLayoutManager.findFirstCompletelyVisibleItemPosition() == 0
}
}
// Setup the FloatingActionButtons, should work everywhere with min API >= 15
floatingActionMenu = DeckPickerFloatingActionMenu(this, view, this)
// Setup the FloatingActionButtons
floatingActionMenu =
DeckPickerFloatingActionMenu(this, view, this).apply {
toggleListener =
FloatingActionBarToggleListener { isOpening ->
closeFloatingActionBarBackPressCallback.isEnabled = isOpening
}
}

reviewSummaryTextView = findViewById(R.id.today_stats_text_view)

Expand Down Expand Up @@ -622,6 +650,13 @@ open class DeckPicker :
setupFlows()
}

override fun setupBackPressedCallbacks() {
onBackPressedDispatcher.addCallback(this, exitAndSyncBackCallback)
onBackPressedDispatcher.addCallback(this, exitViaDoubleTapBackCallback(R.string.back_pressed_once))
onBackPressedDispatcher.addCallback(this, closeFloatingActionBarBackPressCallback)
super.setupBackPressedCallbacks()
}

@Suppress("UNUSED_PARAMETER")
private fun setupFlows() {
fun onDeckDeleted(result: DeckDeletionResult) {
Expand Down Expand Up @@ -1360,42 +1395,6 @@ open class DeckPicker :
}
}

@Deprecated("Deprecated in Java")
@Suppress("DEPRECATION")
override fun onBackPressed() {
val preferences = baseContext.sharedPrefs()
if (isDrawerOpen) {
super.onBackPressed()
} else {
Timber.i("Back key pressed")
if (floatingActionMenu.isFABOpen) {
floatingActionMenu.closeFloatingActionMenu(applyRiseAndShrinkAnimation = true)
} else {
if (!preferences.getBoolean(
"exitViaDoubleTapBack",
false,
) ||
backButtonPressedToExit
) {
// can't use launchCatchingTask because any errors
// would need to be shown in the UI
lifecycleScope
.launch {
automaticSync(runInBackground = true)
}.invokeOnCompletion {
finish()
}
} else {
showSnackbar(R.string.back_pressed_once, Snackbar.LENGTH_SHORT)
}
backButtonPressedToExit = true
HandlerUtils.executeFunctionWithDelay(Consts.SHORT_TOAST_DURATION) {
backButtonPressedToExit = false
}
}
}
}

override fun onKeyUp(
keyCode: Int,
event: KeyEvent,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,11 +55,14 @@ class DeckPickerFloatingActionMenu(

var isFABOpen = false

var toggleListener: FloatingActionBarToggleListener? = null

@Suppress("unused")
val isFragmented: Boolean
get() = studyOptionsFrame != null

private fun showFloatingActionMenu() {
toggleListener?.onBeginToggle(isOpening = true)
deckPicker.activeSnackBar?.dismiss()
linearLayout.alpha = 0.5f
studyOptionsFrame?.let { it.alpha = 0.5f }
Expand Down Expand Up @@ -141,6 +144,7 @@ class DeckPickerFloatingActionMenu(
* want to show any type of rise and shrink animation for the FAB so we put the value `false` for the parameter.
*/
fun closeFloatingActionMenu(applyRiseAndShrinkAnimation: Boolean) {
toggleListener?.onBeginToggle(isOpening = false)
if (applyRiseAndShrinkAnimation) {
linearLayout.alpha = 1f
studyOptionsFrame?.let { it.alpha = 1f }
Expand Down Expand Up @@ -423,4 +427,9 @@ class DeckPickerFloatingActionMenu(
private fun addNote() {
deckPicker.addNote()
}

fun interface FloatingActionBarToggleListener {
/** Triggered when the drawer is starting to open/close */
fun onBeginToggle(isOpening: Boolean)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
/*
* Copyright (c) 2025 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.anki.android.back

import android.content.SharedPreferences.OnSharedPreferenceChangeListener
import androidx.activity.OnBackPressedCallback
import androidx.annotation.StringRes
import androidx.preference.PreferenceManager
import com.google.android.material.snackbar.Snackbar
import com.ichi2.anki.AnkiActivity
import com.ichi2.anki.AnkiDroidApp
import com.ichi2.anki.R
import com.ichi2.anki.settings.Prefs
import com.ichi2.anki.snackbar.showSnackbar
import com.ichi2.libanki.Consts
import com.ichi2.utils.HandlerUtils
import timber.log.Timber

/**
* Note: this uses sharedPreferences via AnkiDroidApp, so must be called after
* [AnkiActivity.showedActivityFailedScreen]
*
* @see Prefs.exitViaDoubleTapBack
*/
// TODO: Convert this to a class when context parameters are usable
fun AnkiActivity.exitViaDoubleTapBackCallback(
@StringRes stringRes: Int,
): OnBackPressedCallback =
object : OnBackPressedCallback(enabled = Prefs.exitViaDoubleTapBack) {
lateinit var strongListenerReference: OnSharedPreferenceChangeListener

override fun handleOnBackPressed() {
showSnackbar(stringRes, Snackbar.LENGTH_SHORT)
this.isEnabled = false
HandlerUtils.executeFunctionWithDelay(Consts.SHORT_TOAST_DURATION) {
this.isEnabled = true
}
}
}.also { callback ->
// PreferenceManager uses weak references, so we need our own strong reference which
// will go out of scope
callback.strongListenerReference =
OnSharedPreferenceChangeListener { prefs, key ->
if (key == getString(R.string.exit_via_double_tap_back_key)) {
callback.isEnabled =
Prefs.exitViaDoubleTapBack.also {
Timber.i("exit via double tap callback -> %b", it)
}
}
}

PreferenceManager
.getDefaultSharedPreferences(AnkiDroidApp.instance)
.registerOnSharedPreferenceChangeListener(callback.strongListenerReference)
}
4 changes: 4 additions & 0 deletions AnkiDroid/src/main/java/com/ichi2/anki/settings/Prefs.kt
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,10 @@ object Prefs {
// **************************************** Settings **************************************** //
// ****************************************************************************************** //

// ****************************************** General ****************************************** //

val exitViaDoubleTapBack by booleanPref(R.string.exit_via_double_tap_back_key, false)

// ****************************************** Sync ****************************************** //

val isAutoSyncEnabled by booleanPref(R.string.automatic_sync_choice_key, false)
Expand Down

0 comments on commit 1f9557c

Please sign in to comment.