Skip to content

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

Open
tomerqodo wants to merge 11 commits into
coderabbit_only-issues-20260113-coderabbit_completion_base_featcore_back_button_event_on_android_closes_8142_pr30from
coderabbit_only-issues-20260113-coderabbit_completion_head_featcore_back_button_event_on_android_closes_8142_pr30
Open

feat(core): back button event on Android, closes #8142#13
tomerqodo wants to merge 11 commits into
coderabbit_only-issues-20260113-coderabbit_completion_base_featcore_back_button_event_on_android_closes_8142_pr30from
coderabbit_only-issues-20260113-coderabbit_completion_head_featcore_back_button_event_on_android_closes_8142_pr30

Conversation

@tomerqodo

@tomerqodo tomerqodo commented Jan 18, 2026

Copy link
Copy Markdown

Benchmark PR from qodo-benchmark#30

Summary by CodeRabbit

  • New Features

    • Added onBackButtonPress() API to handle Android back-button press events, reporting canGoBack state to the application
    • Introduced new app plugin commands for registering and removing event listeners
    • Implemented Android back-navigation handling with system integration
  • Chores

    • Updated wry dependency from 0.53.2 to 0.53.4 with linux-body feature support

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai

coderabbitai Bot commented Jan 18, 2026

Copy link
Copy Markdown

Walkthrough

This PR introduces Android back-button press event handling across the Tauri framework. Changes include a new Android plugin that intercepts back presses, registers listener callbacks, and emits events to the JavaScript layer. Companion updates add Rust-side plugin registration, TypeScript API bindings, permission declarations, and dependency configuration.

Changes

Cohort / File(s) Summary
Changelog entries
.changes/android-app-plugin.md, .changes/back-button-press-api.md
New changelog files documenting the addition of the mobile app plugin for handling back button events and the new onBackButtonPress API for the app module.
Android plugin implementation
crates/tauri/mobile/android-codegen/TauriActivity.kt, crates/tauri/mobile/android/src/main/java/app/tauri/AppPlugin.kt
New AppPlugin class with back-press callback registration, WebView reference management, and conditional logic: if a back-button listener exists, navigate back in WebView; otherwise, emit BACK_BUTTON_EVENT. Includes exit() command for activity termination. TauriActivity adds handleBackNavigation property.
Plugin base class enhancement
crates/tauri/mobile/android/src/main/java/app/tauri/plugin/Plugin.kt
Added hasListener(event: String): Boolean method and cleanup logic to remove empty listener entries from the map.
Rust plugin integration
crates/tauri/src/app/plugin.rs, crates/tauri/build.rs
New AppPlugin<R> struct wrapping a PluginHandle. Build configuration registers two new core:app commands: register_listener and remove_listener (both default-enabled).
TypeScript API
packages/api/src/app.ts
New OnBackButtonPressPayload type and onBackButtonPress() function for listening to back-button events from JavaScript.
Permissions and configuration
crates/tauri/permissions/app/autogenerated/reference.md, crates/tauri-runtime-wry/Cargo.toml
Permission reference documentation adds allow/deny-register-listener and allow/deny-remove-listener entries. Wry dependency updated from 0.53.2 to 0.53.4 with added linux-body feature.
Cleanup
crates/tauri/src/path/android.rs, crates/tauri/scripts/bundle.global.js
Removed unused OsStr import in android path module. Minor whitespace adjustment in bundle script.

Sequence Diagram

sequenceDiagram
    participant User as User
    participant Activity as TauriActivity
    participant AppPlugin as AppPlugin
    participant WebView as WebView
    participant Rust as Rust Runtime
    participant JS as JavaScript

    User->>Activity: Press back button
    Activity->>AppPlugin: OnBackPressedCallback triggered
    AppPlugin->>AppPlugin: Check hasListener("back-button")?
    
    alt Listener registered
        AppPlugin->>WebView: canGoBack()?
        alt WebView can go back
            WebView->>WebView: Navigate back
        else WebView cannot go back
            AppPlugin->>Activity: Temporarily disable callback
            Activity->>Activity: Call onBackPressed()
            AppPlugin->>Activity: Re-enable callback
        end
    else No listener registered
        AppPlugin->>WebView: Get canGoBack state
        AppPlugin->>Rust: Emit BACK_BUTTON_EVENT
        Rust->>JS: Dispatch onBackButtonPress event
        JS->>JS: Handler receives {canGoBack}
    end
Loading

Estimated Code Review Effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

Poem

🐰 A button pressed, the back it goes,
Through Android lands where logic flows,
The plugin hops with listener's might,
WebView dances, callbacks bright,
Back button bounces—pure delight! 🎉

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 5.26% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: introducing back button event handling on Android, with a reference to the closed issue.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Fix all issues with AI agents
In `@crates/tauri/mobile/android/src/main/java/app/tauri/AppPlugin.kt`:
- Line 46: The direct cast (activity as AppCompatActivity) in AppPlugin.kt can
throw ClassCastException if the host is not an AppCompatActivity; change this to
a safe check (e.g., use a safe cast or instanceof check) before calling
onBackPressedDispatcher.addCallback and handle the non-AppCompatActivity case
(log a warning, fall back to activity.onBackPressed()/no-op, or document the
requirement). Update the code that references activity and
onBackPressedDispatcher.addCallback and ensure callback is registered only when
the activity is an AppCompatActivity to avoid runtime crashes.
- Line 35: Replace the deprecated call to activity.onBackPressed() inside
AppPlugin with the OnBackPressedDispatcher API: invoke
activity.onBackPressedDispatcher.onBackPressed() (or dispatch through the
activity's OnBackPressedDispatcher) so the plugin delegates back handling via
the modern dispatcher; if you need the temporary self-disable/delegate pattern
keep the existing callback suspension logic but call the dispatcher instead, or
alternatively implement a simple chain-of-responsibility of callbacks in
AppPlugin to forward the back event to the next handler when appropriate.
- Around line 27-47: The back-button handling is inverted and uses an unsafe
cast; update the OnBackPressedCallback in AppPlugin so that when
hasListener(BACK_BUTTON_EVENT) is true you call trigger(BACK_BUTTON_EVENT, data)
to emit the event, and only when there are no listeners perform default
navigation (if webView?.canGoBack() == true call webView.goBack() else invoke
the platform back action). Replace the unsafe (activity as
AppCompatActivity).onBackPressedDispatcher.addCallback(...) with a safe check
for activity being a ComponentActivity (or AppCompatActivity): if activity is
ComponentActivity use its onBackPressedDispatcher to addCallback(activity,
callback); otherwise fall back to calling activity.onBackPressed() for the
default back action. Ensure you reference BACK_BUTTON_EVENT, hasListener,
trigger, webView, AppPlugin, OnBackPressedCallback, and onBackPressedDispatcher
when making these changes.
🧹 Nitpick comments (1)
packages/api/src/app.ts (1)

255-275: Enhance JSDoc with platform info, version tag, and example.

The implementation is correct and follows existing patterns. However, the JSDoc could be improved for consistency with other functions in this file:

  1. Add @since tag with the appropriate version
  2. Add @example block showing usage
  3. Document the Android-only platform constraint (similar to setTheme's platform note)
📝 Suggested JSDoc enhancement
 /**
  * Listens to the backButton event on Android.
- * `@param` handler
+ *
+ * `@example`
+ * ```typescript
+ * import { onBackButtonPress } from '@tauri-apps/api/app';
+ * const unlisten = await onBackButtonPress((payload) => {
+ *   console.log('Can go back:', payload.canGoBack);
+ * });
+ * ```
+ *
+ * #### Platform-specific
+ *
+ * - **Desktop / iOS:** Unsupported.
+ *
+ * `@param` handler - The callback to invoke when the back button is pressed.
+ * `@returns` A promise resolving to a function to remove the listener.
+ *
+ * `@since` 2.x.0
  */
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 08bda64 and 5ba1edb.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (12)
  • .changes/android-app-plugin.md
  • .changes/back-button-press-api.md
  • crates/tauri-runtime-wry/Cargo.toml
  • crates/tauri/build.rs
  • crates/tauri/mobile/android-codegen/TauriActivity.kt
  • crates/tauri/mobile/android/src/main/java/app/tauri/AppPlugin.kt
  • crates/tauri/mobile/android/src/main/java/app/tauri/plugin/Plugin.kt
  • crates/tauri/permissions/app/autogenerated/reference.md
  • crates/tauri/scripts/bundle.global.js
  • crates/tauri/src/app/plugin.rs
  • crates/tauri/src/path/android.rs
  • packages/api/src/app.ts
🧰 Additional context used
🧬 Code graph analysis (1)
crates/tauri/mobile/android/src/main/java/app/tauri/AppPlugin.kt (1)
crates/tauri/mobile/android/src/main/java/app/tauri/plugin/Plugin.kt (2)
  • hasListener (149-151)
  • trigger (129-137)
🔇 Additional comments (16)
crates/tauri-runtime-wry/Cargo.toml (1)

20-25: wry 0.53.4 feature compatibility confirmed.

The linux-body feature is valid in wry 0.53.4 and enables custom-protocol request body reading on Linux (requires WebKit2GTK v2.40+). No breaking API changes exist between 0.53.2 and 0.53.4. The update is compatible with tauri-runtime-wry.

crates/tauri/scripts/bundle.global.js (1)

1-1: LGTM - Generated bundle includes the new back-button API.

This minified bundle is auto-generated. The new onBackButtonPress function is correctly wired to use addPluginListener with the "back-button" event, which aligns with the Android plugin implementation.

crates/tauri/src/path/android.rs (1)

7-7: LGTM - Unused import removed.

The std::ffi::OsStr import was correctly removed as it's not used in this file. The remaining imports (Path, PathBuf) are all actively used.

crates/tauri/mobile/android/src/main/java/app/tauri/plugin/Plugin.kt (2)

149-151: LGTM - Clean implementation of listener presence check.

The hasListener method correctly uses Kotlin's isNullOrEmpty() idiom to safely check both map entry existence and list emptiness. This enables the AppPlugin to conditionally handle back-button behavior based on listener registration.


177-181: LGTM - Good cleanup of empty listener lists.

Removing the event key when its listener list becomes empty prevents unbounded growth of the map and aligns with good memory management practices.

.changes/back-button-press-api.md (1)

1-5: LGTM - Changelog entry is accurate.

The entry correctly categorizes this as a minor feature addition and clearly describes the Android-specific back button handling capability.

.changes/android-app-plugin.md (1)

1-5: LGTM - Changelog entry accurately describes the feature.

The entry correctly documents the mobile app plugin addition with its exit and back button press event capabilities.

crates/tauri/mobile/android-codegen/TauriActivity.kt (1)

15-15: LGTM - Enables back navigation handling by default.

This override enables the back-button handling infrastructure introduced in this PR. The default behavior (webview goBack() or app exit) aligns with standard Android user expectations.

crates/tauri/src/app/plugin.rs (2)

135-142: LGTM!

The Android-specific plugin registration is well-structured. Using underscore-prefixed parameters (_app, _api) avoids unused variable warnings on non-Android targets while keeping the setup closure available for future platform-specific extensions.


146-147: LGTM!

The AppPlugin<R> struct appropriately wraps PluginHandle<R> and is conditionally compiled only for Android. The pub(crate) visibility is appropriate for internal use.

crates/tauri/permissions/app/autogenerated/reference.md (2)

12-13: LGTM!

The default permissions correctly include the new allow-register-listener and allow-remove-listener entries, aligning with the build.rs configuration where both are enabled by default.


209-282: LGTM!

The permission table entries for register-listener and remove-listener (both allow and deny variants) follow the established documentation pattern and are consistent with other permission entries.

crates/tauri/build.rs (1)

167-168: LGTM!

The new register_listener and remove_listener commands are correctly added to the core:app plugin with default-enabled status. This aligns with the listener-based back-button API and permission definitions.

packages/api/src/app.ts (2)

5-5: LGTM!

The import statement correctly brings in addPluginListener and PluginListener from the core module for the new listener API.


289-291: LGTM!

The exports are correctly added for the new type and function.

crates/tauri/mobile/android/src/main/java/app/tauri/AppPlugin.kt (1)

49-53: LGTM!

The exit command correctly resolves the invoke and finishes the activity. The order (resolve then finish) ensures the JS side receives acknowledgment before the activity terminates.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

Comment on lines +27 to +47
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)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# First, let's understand the context by examining the AppPlugin.kt file more thoroughly
cat -n crates/tauri/mobile/android/src/main/java/app/tauri/AppPlugin.kt | head -60

Repository: qodo-benchmark/tauri-combined-coderabbit

Length of output: 2001


🏁 Script executed:

#!/bin/bash
# Search for hasListener method definition and its usage pattern
rg -n "hasListener" --type=kotlin -B2 -A2 crates/tauri/

Repository: qodo-benchmark/tauri-combined-coderabbit

Length of output: 1143


🏁 Script executed:

#!/bin/bash
# Look for BACK_BUTTON_EVENT and how it's used elsewhere
rg -n "BACK_BUTTON_EVENT" --type=kotlin -B3 -A3 crates/tauri/

Repository: qodo-benchmark/tauri-combined-coderabbit

Length of output: 2164


🏁 Script executed:

#!/bin/bash
# Check for any tests or documentation about back button behavior
rg -n -i "back.*button|back.*press" --type=md,java,kotlin crates/tauri/ | head -40

Repository: qodo-benchmark/tauri-combined-coderabbit

Length of output: 122


🏁 Script executed:

#!/bin/bash
# Search for activity.onBackPressed() usage to see if it's deprecated
rg -n "onBackPressed\(\)" --type=kotlin crates/tauri/ -B2 -A2

Repository: qodo-benchmark/tauri-combined-coderabbit

Length of output: 575


🏁 Script executed:

#!/bin/bash
# Check Android API level targets and deprecation notes
rg -n "compileSdk\|targetSdk\|OnBackPressedCallback" crates/tauri/mobile/android/ | head -20

Repository: qodo-benchmark/tauri-combined-coderabbit

Length of output: 66


🏁 Script executed:

#!/bin/bash
# Search for any related test cases or documentation
rg -n "back-button" --type=kotlin crates/tauri/

Repository: qodo-benchmark/tauri-combined-coderabbit

Length of output: 199


🏁 Script executed:

#!/bin/bash
# Look for build.gradle or manifest to check target SDK
find crates/tauri/mobile/android -name "build.gradle*" -o -name "AndroidManifest.xml" | xargs cat 2>/dev/null | grep -i "targetSdk\|compileSdk" | head -10

Repository: qodo-benchmark/tauri-combined-coderabbit

Length of output: 103


🏁 Script executed:

#!/bin/bash
# Search for Plugin base class to understand trigger() and hasListener() contract
cat -n crates/tauri/mobile/android/src/main/java/app/tauri/plugin/Plugin.kt | head -200

Repository: qodo-benchmark/tauri-combined-coderabbit

Length of output: 7524


🏁 Script executed:

#!/bin/bash
# Look for trigger() method definition to understand its semantics
rg -n "fun trigger" --type=kotlin crates/tauri/mobile/android/ -A3

Repository: qodo-benchmark/tauri-combined-coderabbit

Length of output: 1059


Critical: Back button handling logic is inverted.

When hasListener(BACK_BUTTON_EVENT) returns true (listeners are registered), the code performs default navigation (goBack/onBackPressed) instead of emitting the event. When no listeners exist, it calls trigger() which silently does nothing.

This is backwards—emit the event to registered listeners so they can handle it; only perform default behavior when no listeners are registered.

Also flag:

  • Line 35: activity.onBackPressed() is deprecated in API 33+ (compileSdk=36); use onBackPressedDispatcher instead.
  • Line 46: Unsafe cast (activity as AppCompatActivity) without null check; Plugin accepts Activity, not AppCompatActivity, risking runtime crash.
🤖 Prompt for AI Agents
In `@crates/tauri/mobile/android/src/main/java/app/tauri/AppPlugin.kt` around
lines 27 - 47, The back-button handling is inverted and uses an unsafe cast;
update the OnBackPressedCallback in AppPlugin so that when
hasListener(BACK_BUTTON_EVENT) is true you call trigger(BACK_BUTTON_EVENT, data)
to emit the event, and only when there are no listeners perform default
navigation (if webView?.canGoBack() == true call webView.goBack() else invoke
the platform back action). Replace the unsafe (activity as
AppCompatActivity).onBackPressedDispatcher.addCallback(...) with a safe check
for activity being a ComponentActivity (or AppCompatActivity): if activity is
ComponentActivity use its onBackPressedDispatcher to addCallback(activity,
callback); otherwise fall back to calling activity.onBackPressed() for the
default back action. Ensure you reference BACK_BUTTON_EVENT, hasListener,
trigger, webView, AppPlugin, OnBackPressedCallback, and onBackPressedDispatcher
when making these changes.

this@AppPlugin.webView!!.goBack()
} else {
this.isEnabled = false
this@AppPlugin.activity.onBackPressed()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🌐 Web query:

Android onBackPressed deprecated API 33 OnBackPressedDispatcher

💡 Result:

Short answer

  • Activity.onBackPressed() is deprecated starting API 33 (Android 13). Use OnBackPressedDispatcher with an OnBackPressedCallback (the AndroidX dispatcher adapts to the platform OnBackInvoked APIs for predictive back). [1][2]

Kotlin example

  • Simple lifecycle-aware callback:
    onBackPressedDispatcher.addCallback(this) {
    // handle back
    finish()
    }
    Java example
  • Using OnBackPressedCallback:
    getOnBackPressedDispatcher().addCallback(this, new OnBackPressedCallback(true) {
    @Override public void handleOnBackPressed() { finish(); }
    });

Notes

  • If you target API 33+ the platform introduces OnBackInvokedCallback; using androidx.activity’s OnBackPressedDispatcher (Activity 1.6+/androidx.activity) ensures compatibility and predictive-back behavior. [1][3]

References
[1] AndroidX OnBackPressedDispatcher API reference.
[2] Android Developers — Provide custom back navigation (migration guidance).
[3] StackOverflow — recommended usage / examples for OnBackPressedDispatcher.


🏁 Script executed:

cat -n crates/tauri/mobile/android/src/main/java/app/tauri/AppPlugin.kt | head -60

Repository: qodo-benchmark/tauri-combined-coderabbit

Length of output: 2001


🏁 Script executed:

rg "OnBackPressedDispatcher" --type java

Repository: qodo-benchmark/tauri-combined-coderabbit

Length of output: 250


activity.onBackPressed() is deprecated in Android API 33+.

The onBackPressed() method is deprecated as of Android 13. The code uses this deprecated method on line 35 within a callback that temporarily disables itself to delegate to the system's default back behavior. While this pattern works, it relies on a deprecated API. Consider implementing a chain of responsibility pattern with multiple callbacks or documenting this limitation for forward compatibility with Android 14+.

🤖 Prompt for AI Agents
In `@crates/tauri/mobile/android/src/main/java/app/tauri/AppPlugin.kt` at line 35,
Replace the deprecated call to activity.onBackPressed() inside AppPlugin with
the OnBackPressedDispatcher API: invoke
activity.onBackPressedDispatcher.onBackPressed() (or dispatch through the
activity's OnBackPressedDispatcher) so the plugin delegates back handling via
the modern dispatcher; if you need the temporary self-disable/delegate pattern
keep the existing callback suspension logic but call the dispatcher instead, or
alternatively implement a simple chain-of-responsibility of callbacks in
AppPlugin to forward the back event to the next handler when appropriate.

}
}
}
(activity as AppCompatActivity).onBackPressedDispatcher.addCallback(activity, callback)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Unsafe cast to AppCompatActivity without validation.

The cast (activity as AppCompatActivity) will throw ClassCastException if the activity is not an AppCompatActivity. Consider adding a type check or documenting this requirement.

🛡️ Suggested defensive check
-    (activity as AppCompatActivity).onBackPressedDispatcher.addCallback(activity, callback)
+    if (activity is AppCompatActivity) {
+      activity.onBackPressedDispatcher.addCallback(activity, callback)
+    }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
(activity as AppCompatActivity).onBackPressedDispatcher.addCallback(activity, callback)
if (activity is AppCompatActivity) {
activity.onBackPressedDispatcher.addCallback(activity, callback)
}
🤖 Prompt for AI Agents
In `@crates/tauri/mobile/android/src/main/java/app/tauri/AppPlugin.kt` at line 46,
The direct cast (activity as AppCompatActivity) in AppPlugin.kt can throw
ClassCastException if the host is not an AppCompatActivity; change this to a
safe check (e.g., use a safe cast or instanceof check) before calling
onBackPressedDispatcher.addCallback and handle the non-AppCompatActivity case
(log a warning, fall back to activity.onBackPressed()/no-op, or document the
requirement). Update the code that references activity and
onBackPressedDispatcher.addCallback and ensure callback is registered only when
the activity is an AppCompatActivity to avoid runtime crashes.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants