feat(core): back button event on Android, closes #8142 (#14133)

* feat(core): back button event and exit on Android, closes #8142

I've used https://github.com/ionic-team/capacitor-plugins/blob/main/app/android/src/main/java/com/capacitorjs/plugins/app/AppPlugin.java as a reference here, checking if there's a back button event handler with a default of webview's goBack implementation

* missing change file

* remove exit impl

* fmt

* update wry

* fix default back press

* add remove_listener
This commit is contained in:
Lucas Fernandes Nogueira
2025-10-15 20:50:15 -03:00
committed by GitHub
parent 3b4fac2017
commit 3397fd9bfe
13 changed files with 167 additions and 12 deletions

View File

@@ -0,0 +1,5 @@
---
"tauri": minor:feat
---
Added mobile app plugin to support exit and back button press event.

View File

@@ -0,0 +1,5 @@
---
"@tauri-apps/api": minor:feat
---
Added `app > onBackButtonPress` for Android back button handling.

4
Cargo.lock generated
View File

@@ -10947,9 +10947,9 @@ checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51"
[[package]]
name = "wry"
version = "0.53.2"
version = "0.53.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3b6763512fe4b51c80b3ce9b50939d682acb4de335dfabbdb20d7a2642199b7"
checksum = "6d78ec082b80fa088569a970d043bb3050abaabf4454101d44514ee8d9a8c9f6"
dependencies = [
"base64 0.22.1",
"block2 0.6.0",

View File

@@ -17,7 +17,7 @@ rustc-args = ["--cfg", "docsrs"]
rustdoc-args = ["--cfg", "docsrs"]
[dependencies]
wry = { version = "0.53.2", default-features = false, features = [
wry = { version = "0.53.4", default-features = false, features = [
"drag-drop",
"protocol",
"os-webview",

View File

@@ -164,6 +164,8 @@ const PLUGINS: &[(&str, &[(&str, bool)])] = &[
("set_app_theme", false),
("set_dock_visibility", false),
("bundle_type", true),
("register_listener", true),
("remove_listener", true),
],
),
(

View File

@@ -12,6 +12,7 @@ import app.tauri.plugin.PluginManager
abstract class TauriActivity : WryActivity() {
var pluginManager: PluginManager = PluginManager(this)
override val handleBackNavigation: Boolean = false
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)

View File

@@ -0,0 +1,54 @@
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
package app.tauri
import android.app.Activity
import android.webkit.WebView
import androidx.activity.OnBackPressedCallback
import androidx.appcompat.app.AppCompatActivity
import app.tauri.annotation.Command
import app.tauri.annotation.TauriPlugin
import app.tauri.plugin.Plugin
import app.tauri.plugin.Invoke
import app.tauri.plugin.JSObject
@TauriPlugin
class AppPlugin(private val activity: Activity): Plugin(activity) {
private val BACK_BUTTON_EVENT = "back-button"
private var webView: WebView? = null
override fun load(webView: WebView) {
this.webView = webView
}
init {
val callback = object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
if (!hasListener(BACK_BUTTON_EVENT)) {
if (this@AppPlugin.webView?.canGoBack() == true) {
this@AppPlugin.webView!!.goBack()
} else {
this.isEnabled = false
this@AppPlugin.activity.onBackPressed()
this.isEnabled = true
}
} else {
val data = JSObject().apply {
put("canGoBack", this@AppPlugin.webView?.canGoBack() ?: false)
}
trigger(BACK_BUTTON_EVENT, data)
}
}
}
(activity as AppCompatActivity).onBackPressedDispatcher.addCallback(activity, callback)
}
@Command
fun exit(invoke: Invoke) {
invoke.resolve()
activity.finish()
}
}

View File

@@ -8,7 +8,6 @@ import android.app.Activity
import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri
import android.os.Bundle
import android.webkit.WebView
import androidx.activity.result.IntentSenderRequest
import androidx.core.app.ActivityCompat
@@ -22,7 +21,6 @@ import app.tauri.annotation.InvokeArg
import app.tauri.annotation.PermissionCallback
import app.tauri.annotation.TauriPlugin
import com.fasterxml.jackson.databind.ObjectMapper
import org.json.JSONException
import java.util.*
import java.util.concurrent.CopyOnWriteArrayList
@@ -148,6 +146,10 @@ abstract class Plugin(private val activity: Activity) {
}
}
fun hasListener(event: String): Boolean {
return !listeners[event].isNullOrEmpty()
}
@Command
open fun registerListener(invoke: Invoke) {
val args = invoke.parseArgs(RegisterListenerArgs::class.java)

View File

@@ -9,6 +9,8 @@ Default permissions for the plugin.
- `allow-tauri-version`
- `allow-identifier`
- `allow-bundle-type`
- `allow-register-listener`
- `allow-remove-listener`
## Permission Table
@@ -204,6 +206,32 @@ Denies the name command without any pre-configured scope.
<tr>
<td>
`core:app:allow-register-listener`
</td>
<td>
Enables the register_listener command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`core:app:deny-register-listener`
</td>
<td>
Denies the register_listener command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`core:app:allow-remove-data-store`
</td>
@@ -230,6 +258,32 @@ Denies the remove_data_store command without any pre-configured scope.
<tr>
<td>
`core:app:allow-remove-listener`
</td>
<td>
Enables the remove_listener command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`core:app:deny-remove-listener`
</td>
<td>
Denies the remove_listener command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`core:app:allow-set-app-theme`
</td>

File diff suppressed because one or more lines are too long

View File

@@ -132,5 +132,16 @@ pub fn init<R: Runtime>() -> TauriPlugin<R> {
set_dock_visibility,
bundle_type,
])
.setup(|_app, _api| {
#[cfg(target_os = "android")]
{
let handle = _api.register_android_plugin("app.tauri", "AppPlugin")?;
_app.manage(AppPlugin(handle));
}
Ok(())
})
.build()
}
#[cfg(target_os = "android")]
pub(crate) struct AppPlugin<R: Runtime>(pub crate::plugin::PluginHandle<R>);

View File

@@ -4,10 +4,7 @@
use super::Result;
use crate::{plugin::PluginHandle, Runtime};
use std::{
ffi::OsStr,
path::{Path, PathBuf},
};
use std::path::{Path, PathBuf};
/// A helper class to access the mobile path APIs.
pub struct PathResolver<R: Runtime>(pub(crate) PluginHandle<R>);

View File

@@ -2,7 +2,7 @@
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
import { invoke } from './core'
import { addPluginListener, invoke, PluginListener } from './core'
import { Image } from './image'
import { Theme } from './window'
@@ -252,6 +252,28 @@ async function getBundleType(): Promise<BundleType> {
return invoke('plugin:app|bundle_type')
}
/**
* Payload for the onBackButtonPress event.
*/
type OnBackButtonPressPayload = {
/** Whether the webview canGoBack property is true. */
canGoBack: boolean
}
/**
* Listens to the backButton event on Android.
* @param handler
*/
async function onBackButtonPress(
handler: (payload: OnBackButtonPressPayload) => void
): Promise<PluginListener> {
return addPluginListener<OnBackButtonPressPayload>(
'app',
'back-button',
handler
)
}
export {
getName,
getVersion,
@@ -264,5 +286,7 @@ export {
fetchDataStoreIdentifiers,
removeDataStore,
setDockVisibility,
getBundleType
getBundleType,
type OnBackButtonPressPayload,
onBackButtonPress
}