Skip to content

Commit

Permalink
fix(deck-options): back button: close any open modals
Browse files Browse the repository at this point in the history
Previously the back button only handled navigation to the manual

Now it also closes any modals which are open
and requires another back press to close

Fixes 17196
  • Loading branch information
david-allison authored and lukstbit committed Oct 12, 2024
1 parent 17a44bc commit 5ba1e28
Show file tree
Hide file tree
Showing 2 changed files with 86 additions and 0 deletions.
83 changes: 83 additions & 0 deletions AnkiDroid/src/main/java/com/ichi2/anki/pages/DeckOptions.kt
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,14 @@ package com.ichi2.anki.pages
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.webkit.JavascriptInterface
import android.webkit.WebResourceRequest
import android.webkit.WebView
import androidx.activity.OnBackPressedCallback
import androidx.fragment.app.FragmentActivity
import anki.collection.OpChanges
import com.ichi2.anki.CollectionManager
import com.ichi2.anki.CrashReportService
import com.ichi2.anki.OnPageFinishedCallback
import com.ichi2.anki.R
import com.ichi2.anki.dialogs.DiscardChangesDialog
Expand Down Expand Up @@ -60,8 +62,62 @@ class DeckOptions : PageFragment() {
}
}

@NeedsTest("disabled by default")
@NeedsTest("enabled if a modal is displayed")
@NeedsTest("disabled if a modal is hidden")
@NeedsTest("disabled if back button is pressed: no error")
@NeedsTest("disabled if back button is pressed: with error closing modal")
private val onCloseBootstrapModalCallback = object : OnBackPressedCallback(false) {
override fun handleOnBackPressed() {
Timber.i("back button: closing displayed modal")
try {
webView.evaluateJavascript(
"""
document.getElementsByClassName("modal show")[0]
.getElementsByClassName("btn-close")[0].click()
""".trimIndent(),
{}
)
} catch (e: Exception) {
CrashReportService.sendExceptionReport(e, "DeckOptions:onCloseBootstrapModalCallback")
} finally {
// Even if we fail, disable the callback so the next call succeeds
this.isEnabled = false
}
}
}

/**
* Listens to bootstrap open and close events
*/
inner class ModalJavaScriptInterfaceListener {
@JavascriptInterface
fun onEvent(request: String) {
when (request) {
"open" -> {
Timber.d("WebVew modal opened")
onCloseBootstrapModalCallback.isEnabled = true
}
"close" -> {
Timber.d("WebView modal closed")
onCloseBootstrapModalCallback.isEnabled = false
}
else -> Timber.w("Unknown command: $request")
}
}
}

override fun onWebViewCreated(webView: WebView) {
// addJavascriptInterface needs to happen before loadUrl
webView.addJavascriptInterface(ModalJavaScriptInterfaceListener(), "ankidroid")
Timber.d("Added JS Interface: 'ankidroid")
}

@NeedsTest("going back on a manual page takes priority over closing a modal")
override fun onCreateWebViewClient(savedInstanceState: Bundle?): PageWebViewClient {
requireActivity().onBackPressedDispatcher.addCallback(this, onBackSaveCallback)
requireActivity().onBackPressedDispatcher.addCallback(this, onCloseBootstrapModalCallback)
// going back on a manual page takes priority over closing a modal
requireActivity().onBackPressedDispatcher.addCallback(this, onBackCallback)

return object : PageWebViewClient() {
Expand All @@ -83,10 +139,37 @@ class DeckOptions : PageFragment() {
onPageFinishedCallback = OnPageFinishedCallback { view ->
Timber.v("canGoBack: %b", view.canGoBack())
onBackCallback.isEnabled = view.canGoBack()
// reset the modal state on page load
// clicking a link to the online manual closes the modal and reloads the page
onCloseBootstrapModalCallback.isEnabled = false
listenToModalShowHideEvents()
}
}
}

/**
* Passes bootstrap modal show/hide events to [ModalJavaScriptInterfaceListener]
*/
private fun listenToModalShowHideEvents() {
// this function is called multiple times on one document, only register the listener once
// we use the command name as this is a valid identifier
fun getListenerJs(event: String, command: String): String =
"""
if (!document.added$command) {
console.log("listening to '$command'");
document.added$command = true
document.addEventListener("$event", () => { ankidroid.onEvent("$command"); })
}"""

// event names:
// https://github.com/ankitects/anki/blob/85f034b144ea17f90319b76d2c7d0feaa491eaa5/ts/lib/components/HelpModal.svelte
val openJs = getListenerJs("shown.bs.modal", "open")
val closeJs = getListenerJs("hidden.bs.modal", "close")

webView.evaluateJavascript(openJs, {})
webView.evaluateJavascript(closeJs, {})
}

companion object {
fun getIntent(context: Context, deckId: Long): Intent {
val title = context.getString(R.string.menu__deck_options)
Expand Down
3 changes: 3 additions & 0 deletions AnkiDroid/src/main/java/com/ichi2/anki/pages/PageFragment.kt
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ open class PageFragment(@LayoutRes contentLayoutId: Int = R.layout.page_fragment
*/
protected open fun onCreateWebViewClient(savedInstanceState: Bundle?) = PageWebViewClient()

protected open fun onWebViewCreated(webView: WebView) { }

@CallSuper
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
webView = view.findViewById<WebView>(R.id.webview).apply {
Expand All @@ -66,6 +68,7 @@ open class PageFragment(@LayoutRes contentLayoutId: Int = R.layout.page_fragment
webViewClient = onCreateWebViewClient(savedInstanceState)
webChromeClient = PageChromeClient()
}
onWebViewCreated(webView)
requireActivity().setTransparentStatusBar()
val arguments = requireArguments()
val path = requireNotNull(arguments.getString(PATH_ARG_KEY)) { "'$PATH_ARG_KEY' missing" }
Expand Down

0 comments on commit 5ba1e28

Please sign in to comment.