diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/DeckPicker.kt b/AnkiDroid/src/main/java/com/ichi2/anki/DeckPicker.kt index 97f34e527402..bbd1b946b17a 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/DeckPicker.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/DeckPicker.kt @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 { @@ -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) @@ -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) { @@ -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, diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/DeckPickerFloatingActionMenu.kt b/AnkiDroid/src/main/java/com/ichi2/anki/DeckPickerFloatingActionMenu.kt index 30012b12a32c..c104fe753184 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/DeckPickerFloatingActionMenu.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/DeckPickerFloatingActionMenu.kt @@ -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 } @@ -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 } @@ -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) + } } diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/android/back/ExitViaDoubleTapBackBackPressCallback.kt b/AnkiDroid/src/main/java/com/ichi2/anki/android/back/ExitViaDoubleTapBackBackPressCallback.kt new file mode 100644 index 000000000000..21c085a2a74e --- /dev/null +++ b/AnkiDroid/src/main/java/com/ichi2/anki/android/back/ExitViaDoubleTapBackBackPressCallback.kt @@ -0,0 +1,69 @@ +/* + * Copyright (c) 2025 David Allison + * + * 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 . + */ + +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) + } diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/settings/Prefs.kt b/AnkiDroid/src/main/java/com/ichi2/anki/settings/Prefs.kt index 55c9b17c4386..b9803fefff0b 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/settings/Prefs.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/settings/Prefs.kt @@ -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)