[Android] Restore OnBackButtonPressed interception support when using OnBackPressedDispatcher #35154
[Android] Restore OnBackButtonPressed interception support when using OnBackPressedDispatcher #35154Dhivya-SF4094 wants to merge 131 commits into
Conversation
There was a problem hiding this comment.
Pull request overview
Updates Android back navigation handling in MauiAppCompatActivity to restore reliable OnBackButtonPressed interception when using AndroidX OnBackPressedDispatcher, avoiding deprecated/dual-path back handling and adding a regression UI test.
Changes:
- Register a single AndroidX
OnBackPressedCallback(all API levels) and route back handling through MAUI lifecycle events. - Remove the deprecated
OnBackPressed()override and adjustHandleBackNavigation()to return whether MAUI handled the back press. - Add Issue #8680 HostApp page + UI test, and adjust Shell toolbar back handling to use Shell’s back-press pipeline.
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
| src/Core/src/PublicAPI/net-android/PublicAPI.Unshipped.txt | Records removal of OnBackPressed/OnDestroy overrides from the public API surface (but introduces a BOM inconsistency). |
| src/Core/src/Platform/Android/MauiAppCompatActivity.cs | Registers MauiOnBackPressedCallback on OnBackPressedDispatcher and removes predictive-back callback plumbing. |
| src/Core/src/Platform/Android/MauiAppCompatActivity.Lifecycle.cs | Removes OnBackPressed() override and changes HandleBackNavigation() to return a boolean. |
| src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue8680.cs | Adds a regression UI test for intercepting back navigation (but currently gated incorrectly and doesn’t simulate device back). |
| src/Controls/tests/TestCases.HostApp/Issues/Issue8680.cs | Adds the HostApp reproduction page for Issue #8680 (needs formatting cleanup). |
| src/Controls/src/Core/Compatibility/Handlers/Shell/Android/ShellToolbarTracker.cs | Routes toolbar back through _shell.SendBackButtonPressed() instead of the current Page directly. |
MauiBot
left a comment
There was a problem hiding this comment.
Expert Review — 7 findings
See inline comments for details.
MauiBot
left a comment
There was a problem hiding this comment.
Expert Review — 6 findings
See inline comments for details.
|
/review -b feature/regression-check -p android |
MauiBot
left a comment
There was a problem hiding this comment.
🤖 Automated review — alternative fix proposed
The expert-reviewer evaluation compared the PR fix against #1 automatically generated candidates and selected try-fix-1 as the strongest fix.
Why: try-fix-1 unblocks the gate (resolves RS0017) by restoring the deprecated OnBackPressed() override as a one-line shim that calls base.OnBackPressed(), routing through OnBackPressedDispatcher to the new MauiOnBackPressedCallback. It preserves the PR's underlying design (single AndroidX callback) while avoiding the publicly visible breaking-API change introduced by both the PR and try-fix-2/pr-plus-reviewer.
Please consider applying the candidate diff below (or use it as guidance). Once you push an update, this workflow will re-trigger and re-evaluate.
Candidate diff (`try-fix-1`)
diff --git a/src/Controls/tests/TestCases.HostApp/Issues/Issue8680.cs b/src/Controls/tests/TestCases.HostApp/Issues/Issue8680.cs
new file mode 100644
index 0000000000..0546cb2f3a
--- /dev/null
+++ b/src/Controls/tests/TestCases.HostApp/Issues/Issue8680.cs
@@ -0,0 +1,66 @@
+namespace Maui.Controls.Sample.Issues;
+
+[Issue(IssueTracker.Github, 8680, "Rework OnBackButtonPressed to use onBackPressedDispatcher", PlatformAffected.Android)]
+public class Issue8680 : TestNavigationPage
+{
+ protected override void Init()
+ {
+ PushAsync(new Issue8680MainPage());
+ }
+}
+
+public class Issue8680MainPage : ContentPage
+{
+ public Issue8680MainPage()
+ {
+ var navigateButton = new Button
+ {
+ Text = "Go to Intercept Page",
+ AutomationId = "NavigateButton",
+ };
+ navigateButton.Clicked += async (s, e) =>
+ {
+ await Navigation.PushAsync(new Issue8680InterceptPage());
+ };
+
+ Content = new VerticalStackLayout
+ {
+ Children =
+ {
+ new Label { Text = "Main Page", AutomationId = "MainPageLabel" },
+ navigateButton,
+ }
+ };
+ }
+}
+
+public class Issue8680InterceptPage : ContentPage
+{
+ int _backPressCount;
+ readonly Label _statusLabel;
+
+ public Issue8680InterceptPage()
+ {
+ _statusLabel = new Label
+ {
+ Text = "Back not pressed yet",
+ AutomationId = "StatusLabel",
+ };
+
+ Content = new VerticalStackLayout
+ {
+ Children =
+ {
+ _statusLabel,
+ new Label { Text = "Press device back button — it should be intercepted", AutomationId = "InterceptPageLabel" },
+ }
+ };
+ }
+
+ protected override bool OnBackButtonPressed()
+ {
+ _backPressCount++;
+ _statusLabel.Text = $"Back intercepted: {_backPressCount}";
+ return true; // true = handled, prevents navigation back
+ }
+}
diff --git a/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue8680.cs b/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue8680.cs
new file mode 100644
index 0000000000..d53179569c
--- /dev/null
+++ b/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue8680.cs
@@ -0,0 +1,39 @@
+#if ANDROID // Android-specific: OnBackButtonPressed uses onBackPressedDispatcher
+using NUnit.Framework;
+using UITest.Appium;
+using UITest.Core;
+
+namespace Microsoft.Maui.TestCases.Tests.Issues;
+
+public class Issue8680 : _IssuesUITest
+{
+ public Issue8680(TestDevice device) : base(device) { }
+
+ public override string Issue => "Rework OnBackButtonPressed to use onBackPressedDispatcher";
+
+ [Test]
+ [Category(UITestCategories.Navigation)]
+ public void BackButtonPressIsInterceptedByOnBackButtonPressed()
+ {
+ // Navigate to the intercept page
+ App.WaitForElement("NavigateButton");
+ App.Tap("NavigateButton");
+
+ // Confirm we are on the intercept page
+ App.WaitForElement("InterceptPageLabel");
+
+ // Press the device back button — should be intercepted (page stays)
+ App.Back();
+
+ // The page should still be visible because OnBackButtonPressed returned true
+ App.WaitForElement("StatusLabel");
+ Assert.That(
+ App.FindElement("StatusLabel").GetText(),
+ Is.EqualTo("Back intercepted: 1"),
+ "OnBackButtonPressed should have been called exactly once per back press (detects dual-fire regression on API 33+).");
+
+ // The intercept page should still be displayed (not popped)
+ App.WaitForElement("InterceptPageLabel");
+ }
+}
+#endif
\ No newline at end of file
diff --git a/src/Core/src/Platform/Android/MauiAppCompatActivity.cs b/src/Core/src/Platform/Android/MauiAppCompatActivity.cs
index de7ae83171..62b9b4f72c 100644
--- a/src/Core/src/Platform/Android/MauiAppCompatActivity.cs
+++ b/src/Core/src/Platform/Android/MauiAppCompatActivity.cs
@@ -1,4 +1,3 @@
-using System;
using Android.OS;
using Android.Views;
using AndroidX.Activity;
diff --git a/src/Core/src/Platform/Android/MauiAppCompatActivity.Lifecycle.cs b/src/Core/src/Platform/Android/MauiAppCompatActivity.Lifecycle.cs
index 1f813c41cd..84ce333f7f 100644
--- a/src/Core/src/Platform/Android/MauiAppCompatActivity.Lifecycle.cs
+++ b/src/Core/src/Platform/Android/MauiAppCompatActivity.Lifecycle.cs
@@ -5,7 +5,6 @@ using Android.Content.PM;
using Android.Content.Res;
using Android.OS;
using Android.Views;
-using Microsoft.Maui.Devices;
using Microsoft.Maui.LifecycleEvents;
namespace Microsoft.Maui
@@ -20,14 +19,19 @@ namespace Microsoft.Maui
IPlatformApplication.Current?.Services?.InvokeLifecycleEvents<AndroidLifecycle.OnActivityResult>(del => del(this, requestCode, resultCode, data));
}
- // TODO: Investigate whether the new AndroidX way is actually useful:
- // https://developer.android.com/reference/android/app/Activity#onBackPressed()
+ // Preserved as a thin compatibility shim for the shipped public API surface.
+ // Back navigation is fully driven by MauiOnBackPressedCallback (registered on
+ // OnBackPressedDispatcher in OnCreate). AppCompatActivity.OnBackPressed() forwards
+ // to the dispatcher, so calling base here routes through the same callback and
+ // avoids the dual-fire risk that existed when this override called
+ // HandleBackNavigation() directly while a separate PredictiveBackCallback was
+ // also registered.
[Obsolete]
#pragma warning disable 809
public override void OnBackPressed()
#pragma warning restore 809
{
- HandleBackNavigation();
+ base.OnBackPressed();
}
public override void OnConfigurationChanged(Configuration newConfig)
@@ -136,10 +140,12 @@ namespace Microsoft.Maui
}
/// <summary>
- /// Central handler used by both legacy <see cref="OnBackPressed"/> and the Android 13+ predictive back gesture callback.
- /// Implements lifecycle event invocation and default back stack propagation unless explicitly prevented.
+ /// Central handler invoked by <see cref="MauiOnBackPressedCallback"/> when the back button is pressed.
+ /// This method only checks whether MAUI lifecycle handlers intercept the back press.
+ /// Callers are responsible for invoking <see cref="AndroidX.Activity.OnBackPressedDispatcher.OnBackPressed()"/> as a
+ /// fallback when this method returns <see langword="false"/>.
/// </summary>
- void HandleBackNavigation()
+ internal void HandleBackNavigation()
{
var preventBackPropagation = false;
IPlatformApplication.Current?.Services?.InvokeLifecycleEvents<AndroidLifecycle.OnBackPressed>(del =>
|
/review -b feature/regression-check -p android |
|
/azp run maui-pr-devicetests, maui-pr-uitests |
|
Azure Pipelines successfully started running 2 pipeline(s). |
kubaflo
left a comment
There was a problem hiding this comment.
Could you please verify the failing tests?
|
/review -b feature/refactor-copilot-yml |
|
/review -b feature/regression-check -p android |
Test Failure Review
❔ Test Failure Review -
|
| Failure | Verdict | Evidence |
|---|---|---|
| Build Analysis | Insufficient data | The check is marked failed, but the context provides only the Build Analysis documentation link and no build-analysis matches, timeline issues, or log excerpts. |
maui-pr-devicetests build 1428613 |
Insufficient data | The rollup and Android CoreCLR, Android Mono, MacCatalyst Mono, Windows device-test build, and iOS Mono child checks failed, but build 1428613 was inaccessible with 404 (Not Found); failedRecords, logExcerpts, testResults, and Helix summaries are empty, so hidden device-test failures could not be verified. |
maui-pr-uitests build 1428612 |
Insufficient data | The rollup and sample-app build child checks failed, but build 1428612 was inaccessible with 404 (Not Found); no timeline records, logs, test results, or distinct extracted failures were available. |
Recommended action
Regather the review context when builds 1428612 and 1428613 are accessible, then inspect the failed timelines/logs and Helix aggregate data before attributing these failures to the PR.
Evidence details
PR #35154 targets inflight/current from fix-8680 at a42e2f99997da32392b4bc9c76a8e8c9c29ddc7a. Labels and scope point to platform/android and area-navigation; inferred platform from changed files is android.
Changed files are src/Core/src/Platform/Android/MauiAppCompatActivity.Lifecycle.cs, src/Core/src/Platform/Android/MauiAppCompatActivity.cs, src/Controls/tests/TestCases.HostApp/Issues/Issue8680.cs, and src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue8680.cs.
The gathered check rollup marks maui-pr build 1420508 successful. The failing or inconclusive checks are Build Analysis, maui-pr-devicetests, device-test child checks for Android CoreCLR, Android Mono, MacCatalyst Mono, Windows device-test build, iOS Mono, maui-pr-uitests, and UI sample-app build child checks.
Build 1428613 and build 1428612 both have accessible: false with error Response status code does not indicate success: 404 (Not Found). For both builds, failedRecords, timelineIssues, logExcerpts, testFailuresFromLogs, testResults, and recentBaseBuilds are empty. For device-test build 1428613, helix.checked is false, helix.jobIds is empty, and helix.summaries is empty.
No distinct test failures were extracted from accessible AzDO logs or test results. The context limitation notes that authenticated AzDO access used an Azure CLI bearer token for local-only data gathering, while the gh-aw workflow relies on public build/timeline/log APIs unless AZDO_TOKEN is provided by the runner environment.
Note
Are you waiting for the changes in this PR to be merged?
It would be very helpful if you could test the resulting artifacts from this PR and let us know in a comment if this change resolves your issue. Thank you!
Description
Fixes #8680
Replaces the deprecated
OnBackPressed()override and the API 33+-onlyPredictiveBackCallback(registered via nativeOnBackInvokedDispatcher) with a single unifiedMauiOnBackPressedCallbackregistered on AndroidXOnBackPressedDispatcher.What this fixes:
OnBackPressed()andPredictiveBackCallbackcould fireOnDestroycleanup —AddCallback(LifecycleOwner, callback)is lifecycle-awareWhat this does NOT change:
onBackStarted/onBackProgressed/onBackCancelled) are not surfaced to MAUI lifecycle — this was also the case before and could be a future enhancementChanges
MauiAppCompatActivity.cs— AddedMauiOnBackPressedCallback : OnBackPressedCallbackregistered inOnCreate; removedPredictiveBackCallback,using Android.Window, andusing SystemMauiAppCompatActivity.Lifecycle.cs— Removed deprecatedOnBackPressed()override; changedHandleBackNavigation()fromvoidtointernal boolKey Design Decisions
OnBackPressed()override?AppCompatActivity.OnBackPressed()internally callsOnBackPressedDispatcher.OnBackPressed()anyway. Once callbacks are registered with the dispatcher, the override is redundant and was creating a dual-path risk on API 33+.try/finallyaround the dispatcher call? When MAUI doesn't handle back, the callback temporarily disables itself and re-invokes the dispatcher, then re-enables infinally. Withoutfinally, an exception would leaveEnabled = falsepermanently.OnDestroycleanup?AddCallback(LifecycleOwner, callback)is lifecycle-aware — AndroidX removes the callback automatically atDESTROYED.