From 8d807f16e1db907751b3e41f6f8b25772c482eaa Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 17 Aug 2025 23:20:14 +0000 Subject: [PATCH 1/8] Initial plan From a9ed7ba29564a7233ca8a620d744c1edad761174 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 17 Aug 2025 23:34:59 +0000 Subject: [PATCH 2/8] Add OnKey* lifecycle events for Android Co-authored-by: PureWeen <5375137+PureWeen@users.noreply.github.com> --- .../Android/AndroidLifecycle.cs | 6 ++ .../AndroidLifecycleBuilderExtensions.cs | 5 ++ .../MauiAppCompatActivity.Lifecycle.cs | 56 +++++++++++++++++++ 3 files changed, 67 insertions(+) diff --git a/src/Core/src/LifecycleEvents/Android/AndroidLifecycle.cs b/src/Core/src/LifecycleEvents/Android/AndroidLifecycle.cs index 948a244a06d3..5f9ccc3f030b 100644 --- a/src/Core/src/LifecycleEvents/Android/AndroidLifecycle.cs +++ b/src/Core/src/LifecycleEvents/Android/AndroidLifecycle.cs @@ -3,6 +3,7 @@ using Android.Content.PM; using Android.Content.Res; using Android.OS; +using Android.Views; namespace Microsoft.Maui.LifecycleEvents { @@ -33,6 +34,11 @@ public static class AndroidLifecycle public delegate void OnActivityResult(Activity activity, int requestCode, Result resultCode, Intent? data); public delegate bool OnBackPressed(Activity activity); public delegate void OnConfigurationChanged(Activity activity, Configuration newConfig); + public delegate bool OnKeyDown(Activity activity, Keycode keyCode, KeyEvent? e); + public delegate bool OnKeyLongPress(Activity activity, Keycode keyCode, KeyEvent? e); + public delegate bool OnKeyMultiple(Activity activity, Keycode keyCode, int repeatCount, KeyEvent? e); + public delegate bool OnKeyShortcut(Activity activity, Keycode keyCode, KeyEvent? e); + public delegate bool OnKeyUp(Activity activity, Keycode keyCode, KeyEvent? e); public delegate void OnNewIntent(Activity activity, Intent? intent); public delegate void OnRequestPermissionsResult(Activity activity, int requestCode, string[] permissions, Permission[] grantResults); public delegate void OnRestoreInstanceState(Activity activity, Bundle savedInstanceState); diff --git a/src/Core/src/LifecycleEvents/Android/AndroidLifecycleBuilderExtensions.cs b/src/Core/src/LifecycleEvents/Android/AndroidLifecycleBuilderExtensions.cs index f79e74f8bba1..c514415191b0 100644 --- a/src/Core/src/LifecycleEvents/Android/AndroidLifecycleBuilderExtensions.cs +++ b/src/Core/src/LifecycleEvents/Android/AndroidLifecycleBuilderExtensions.cs @@ -13,6 +13,11 @@ public static class AndroidLifecycleBuilderExtensions public static IAndroidLifecycleBuilder OnConfigurationChanged(this IAndroidLifecycleBuilder lifecycle, AndroidLifecycle.OnConfigurationChanged del) => lifecycle.OnEvent(del); public static IAndroidLifecycleBuilder OnCreate(this IAndroidLifecycleBuilder lifecycle, AndroidLifecycle.OnCreate del) => lifecycle.OnEvent(del); public static IAndroidLifecycleBuilder OnDestroy(this IAndroidLifecycleBuilder lifecycle, AndroidLifecycle.OnDestroy del) => lifecycle.OnEvent(del); + public static IAndroidLifecycleBuilder OnKeyDown(this IAndroidLifecycleBuilder lifecycle, AndroidLifecycle.OnKeyDown del) => lifecycle.OnEvent(del); + public static IAndroidLifecycleBuilder OnKeyLongPress(this IAndroidLifecycleBuilder lifecycle, AndroidLifecycle.OnKeyLongPress del) => lifecycle.OnEvent(del); + public static IAndroidLifecycleBuilder OnKeyMultiple(this IAndroidLifecycleBuilder lifecycle, AndroidLifecycle.OnKeyMultiple del) => lifecycle.OnEvent(del); + public static IAndroidLifecycleBuilder OnKeyShortcut(this IAndroidLifecycleBuilder lifecycle, AndroidLifecycle.OnKeyShortcut del) => lifecycle.OnEvent(del); + public static IAndroidLifecycleBuilder OnKeyUp(this IAndroidLifecycleBuilder lifecycle, AndroidLifecycle.OnKeyUp del) => lifecycle.OnEvent(del); public static IAndroidLifecycleBuilder OnNewIntent(this IAndroidLifecycleBuilder lifecycle, AndroidLifecycle.OnNewIntent del) => lifecycle.OnEvent(del); public static IAndroidLifecycleBuilder OnPause(this IAndroidLifecycleBuilder lifecycle, AndroidLifecycle.OnPause del) => lifecycle.OnEvent(del); public static IAndroidLifecycleBuilder OnPostCreate(this IAndroidLifecycleBuilder lifecycle, AndroidLifecycle.OnPostCreate del) => lifecycle.OnEvent(del); diff --git a/src/Core/src/Platform/Android/MauiAppCompatActivity.Lifecycle.cs b/src/Core/src/Platform/Android/MauiAppCompatActivity.Lifecycle.cs index a63b868ffe10..584a692ee186 100644 --- a/src/Core/src/Platform/Android/MauiAppCompatActivity.Lifecycle.cs +++ b/src/Core/src/Platform/Android/MauiAppCompatActivity.Lifecycle.cs @@ -4,6 +4,7 @@ using Android.Content.PM; using Android.Content.Res; using Android.OS; +using Android.Views; using Microsoft.Maui.Devices; using Microsoft.Maui.LifecycleEvents; @@ -89,5 +90,60 @@ protected override void OnRestoreInstanceState(Bundle savedInstanceState) IPlatformApplication.Current?.Services?.InvokeLifecycleEvents(del => del(this, savedInstanceState)); } + + public override bool OnKeyDown(Keycode keyCode, KeyEvent? e) + { + var handled = false; + IPlatformApplication.Current?.Services?.InvokeLifecycleEvents(del => + { + handled = del(this, keyCode, e) || handled; + }); + + return handled || base.OnKeyDown(keyCode, e); + } + + public override bool OnKeyLongPress(Keycode keyCode, KeyEvent? e) + { + var handled = false; + IPlatformApplication.Current?.Services?.InvokeLifecycleEvents(del => + { + handled = del(this, keyCode, e) || handled; + }); + + return handled || base.OnKeyLongPress(keyCode, e); + } + + public override bool OnKeyMultiple(Keycode keyCode, int repeatCount, KeyEvent? e) + { + var handled = false; + IPlatformApplication.Current?.Services?.InvokeLifecycleEvents(del => + { + handled = del(this, keyCode, repeatCount, e) || handled; + }); + + return handled || base.OnKeyMultiple(keyCode, repeatCount, e); + } + + public override bool OnKeyShortcut(Keycode keyCode, KeyEvent? e) + { + var handled = false; + IPlatformApplication.Current?.Services?.InvokeLifecycleEvents(del => + { + handled = del(this, keyCode, e) || handled; + }); + + return handled || base.OnKeyShortcut(keyCode, e); + } + + public override bool OnKeyUp(Keycode keyCode, KeyEvent? e) + { + var handled = false; + IPlatformApplication.Current?.Services?.InvokeLifecycleEvents(del => + { + handled = del(this, keyCode, e) || handled; + }); + + return handled || base.OnKeyUp(keyCode, e); + } } } \ No newline at end of file From 91562b73130b7bf04e3304cbb46a3718c54a7d05 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 17 Aug 2025 23:46:21 +0000 Subject: [PATCH 3/8] Add comprehensive tests and demo for OnKey* lifecycle events Co-authored-by: PureWeen <5375137+PureWeen@users.noreply.github.com> --- .../Controls.Sample.Sandbox/MainPage.xaml | 54 +++++ .../Controls.Sample.Sandbox/MauiProgram.cs | 53 ++++- .../LifecycleEvents/LifecycleEventsTests.cs | 225 ++++++++++++++++++ 3 files changed, 331 insertions(+), 1 deletion(-) diff --git a/src/Controls/samples/Controls.Sample.Sandbox/MainPage.xaml b/src/Controls/samples/Controls.Sample.Sandbox/MainPage.xaml index 7363d18deadd..fc73fc4ec631 100644 --- a/src/Controls/samples/Controls.Sample.Sandbox/MainPage.xaml +++ b/src/Controls/samples/Controls.Sample.Sandbox/MainPage.xaml @@ -2,4 +2,58 @@ xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" x:Class="Maui.Controls.Sample.MainPage" xmlns:local="clr-namespace:Maui.Controls.Sample"> + + + + + + + + \ No newline at end of file diff --git a/src/Controls/samples/Controls.Sample.Sandbox/MauiProgram.cs b/src/Controls/samples/Controls.Sample.Sandbox/MauiProgram.cs index d4941633bbd8..462716046ab7 100644 --- a/src/Controls/samples/Controls.Sample.Sandbox/MauiProgram.cs +++ b/src/Controls/samples/Controls.Sample.Sandbox/MauiProgram.cs @@ -1,4 +1,6 @@ -namespace Maui.Controls.Sample; +using Microsoft.Maui.LifecycleEvents; + +namespace Maui.Controls.Sample; public static class MauiProgram { @@ -9,6 +11,55 @@ public static MauiApp CreateMauiApp() => .UseMauiMaps() #endif .UseMauiApp() + .ConfigureLifecycleEvents(events => + { +#if ANDROID + events.AddAndroid(android => + { + android.OnKeyDown((activity, keyCode, keyEvent) => + { + System.Diagnostics.Debug.WriteLine($"OnKeyDown: {keyCode}"); + // Handle volume keys specifically for demonstration + if (keyCode == Android.Views.Keycode.VolumeUp) + { + System.Diagnostics.Debug.WriteLine("Volume Up key handled by lifecycle event!"); + return true; // Prevent default handling + } + return false; // Allow default handling for other keys + }); + + android.OnKeyUp((activity, keyCode, keyEvent) => + { + System.Diagnostics.Debug.WriteLine($"OnKeyUp: {keyCode}"); + return false; // Allow default handling + }); + + android.OnKeyLongPress((activity, keyCode, keyEvent) => + { + System.Diagnostics.Debug.WriteLine($"OnKeyLongPress: {keyCode}"); + // Handle back button long press + if (keyCode == Android.Views.Keycode.Back) + { + System.Diagnostics.Debug.WriteLine("Back button long press handled!"); + return true; // Prevent default handling + } + return false; + }); + + android.OnKeyMultiple((activity, keyCode, repeatCount, keyEvent) => + { + System.Diagnostics.Debug.WriteLine($"OnKeyMultiple: {keyCode}, repeat: {repeatCount}"); + return false; + }); + + android.OnKeyShortcut((activity, keyCode, keyEvent) => + { + System.Diagnostics.Debug.WriteLine($"OnKeyShortcut: {keyCode}"); + return false; + }); + }); +#endif + }) .ConfigureFonts(fonts => { fonts.AddFont("Dokdo-Regular.ttf", "Dokdo"); diff --git a/src/Core/tests/UnitTests/LifecycleEvents/LifecycleEventsTests.cs b/src/Core/tests/UnitTests/LifecycleEvents/LifecycleEventsTests.cs index a15c4916e544..ae08390f4e03 100644 --- a/src/Core/tests/UnitTests/LifecycleEvents/LifecycleEventsTests.cs +++ b/src/Core/tests/UnitTests/LifecycleEvents/LifecycleEventsTests.cs @@ -3,6 +3,10 @@ using Microsoft.Maui.LifecycleEvents; using Xunit; +#if ANDROID +using Android.Views; +#endif + namespace Microsoft.Maui.UnitTests.LifecycleEvents { [Category(TestCategory.Core, TestCategory.Lifecycle)] @@ -157,6 +161,227 @@ public void CanAddMultipleEventsViaBuilder() Assert.Equal(1, event2Fired); } +#if ANDROID + [Fact] + public void CanAddAndroidOnKeyDownLifecycleEvent() + { + var eventFired = false; + Keycode receivedKeyCode = default; + KeyEvent? receivedKeyEvent = null; + + var mauiApp = MauiApp.CreateBuilder() + .ConfigureLifecycleEvents(builder => + { + builder.AddAndroid(android => + { + android.OnKeyDown((activity, keyCode, keyEvent) => + { + eventFired = true; + receivedKeyCode = keyCode; + receivedKeyEvent = keyEvent; + return false; // Allow default handling + }); + }); + }) + .Build(); + + var service = mauiApp.Services.GetRequiredService(); + + Assert.True(service.ContainsEvent(nameof(AndroidLifecycle.OnKeyDown))); + + var testKeyCode = Keycode.VolumeUp; + service.InvokeEvents(nameof(AndroidLifecycle.OnKeyDown), del => + { + del(null!, testKeyCode, null); + }); + + Assert.True(eventFired); + Assert.Equal(testKeyCode, receivedKeyCode); + Assert.Null(receivedKeyEvent); + } + + [Fact] + public void CanAddAndroidOnKeyUpLifecycleEvent() + { + var eventFired = false; + var handledEvent = false; + + var mauiApp = MauiApp.CreateBuilder() + .ConfigureLifecycleEvents(builder => + { + builder.AddAndroid(android => + { + android.OnKeyUp((activity, keyCode, keyEvent) => + { + eventFired = true; + return keyCode == Keycode.VolumeDown; // Handle only volume down + }); + }); + }) + .Build(); + + var service = mauiApp.Services.GetRequiredService(); + + Assert.True(service.ContainsEvent(nameof(AndroidLifecycle.OnKeyUp))); + + // Test with volume down (should be handled) + service.InvokeEvents(nameof(AndroidLifecycle.OnKeyUp), del => + { + handledEvent = del(null!, Keycode.VolumeDown, null); + }); + + Assert.True(eventFired); + Assert.True(handledEvent); + + // Reset and test with volume up (should not be handled) + eventFired = false; + handledEvent = false; + + service.InvokeEvents(nameof(AndroidLifecycle.OnKeyUp), del => + { + handledEvent = del(null!, Keycode.VolumeUp, null); + }); + + Assert.True(eventFired); + Assert.False(handledEvent); + } + + [Fact] + public void CanAddAndroidOnKeyLongPressLifecycleEvent() + { + var eventFired = false; + + var mauiApp = MauiApp.CreateBuilder() + .ConfigureLifecycleEvents(builder => + { + builder.AddAndroid(android => + { + android.OnKeyLongPress((activity, keyCode, keyEvent) => + { + eventFired = true; + return true; // Handle the event + }); + }); + }) + .Build(); + + var service = mauiApp.Services.GetRequiredService(); + + Assert.True(service.ContainsEvent(nameof(AndroidLifecycle.OnKeyLongPress))); + + service.InvokeEvents(nameof(AndroidLifecycle.OnKeyLongPress), del => + { + del(null!, Keycode.Menu, null); + }); + + Assert.True(eventFired); + } + + [Fact] + public void CanAddAndroidOnKeyMultipleLifecycleEvent() + { + var eventFired = false; + var receivedRepeatCount = 0; + + var mauiApp = MauiApp.CreateBuilder() + .ConfigureLifecycleEvents(builder => + { + builder.AddAndroid(android => + { + android.OnKeyMultiple((activity, keyCode, repeatCount, keyEvent) => + { + eventFired = true; + receivedRepeatCount = repeatCount; + return false; + }); + }); + }) + .Build(); + + var service = mauiApp.Services.GetRequiredService(); + + Assert.True(service.ContainsEvent(nameof(AndroidLifecycle.OnKeyMultiple))); + + service.InvokeEvents(nameof(AndroidLifecycle.OnKeyMultiple), del => + { + del(null!, Keycode.A, 5, null); + }); + + Assert.True(eventFired); + Assert.Equal(5, receivedRepeatCount); + } + + [Fact] + public void CanAddAndroidOnKeyShortcutLifecycleEvent() + { + var eventFired = false; + + var mauiApp = MauiApp.CreateBuilder() + .ConfigureLifecycleEvents(builder => + { + builder.AddAndroid(android => + { + android.OnKeyShortcut((activity, keyCode, keyEvent) => + { + eventFired = true; + return true; + }); + }); + }) + .Build(); + + var service = mauiApp.Services.GetRequiredService(); + + Assert.True(service.ContainsEvent(nameof(AndroidLifecycle.OnKeyShortcut))); + + service.InvokeEvents(nameof(AndroidLifecycle.OnKeyShortcut), del => + { + del(null!, Keycode.C, null); + }); + + Assert.True(eventFired); + } + + [Fact] + public void AndroidOnKeyEventsCanBeHandledByMultipleListeners() + { + var event1Fired = false; + var event2Fired = false; + var totalHandled = 0; + + var mauiApp = MauiApp.CreateBuilder() + .ConfigureLifecycleEvents(builder => + { + builder.AddAndroid(android => + { + android.OnKeyDown((activity, keyCode, keyEvent) => + { + event1Fired = true; + return false; // Don't handle + }); + android.OnKeyDown((activity, keyCode, keyEvent) => + { + event2Fired = true; + return true; // Handle the event + }); + }); + }) + .Build(); + + var service = mauiApp.Services.GetRequiredService(); + + service.InvokeEvents(nameof(AndroidLifecycle.OnKeyDown), del => + { + var handled = del(null!, Keycode.Back, null); + if (handled) totalHandled++; + }); + + Assert.True(event1Fired); + Assert.True(event2Fired); + Assert.Equal(1, totalHandled); // Only one handler returned true + } +#endif + delegate void SimpleDelegate(); delegate void OtherSimpleDelegate(); From 7fb5dc7ded43eafa4a2a474c2818ecc9ab453fd1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 18 Aug 2025 00:07:29 +0000 Subject: [PATCH 4/8] Add OnKey* lifecycle events to modal dialog CustomComponentDialog Co-authored-by: PureWeen <5375137+PureWeen@users.noreply.github.com> --- .../ModalNavigationManager.Android.cs | 70 +++++++++++++++++++ 1 file changed, 70 insertions(+) diff --git a/src/Controls/src/Core/Platform/ModalNavigationManager/ModalNavigationManager.Android.cs b/src/Controls/src/Core/Platform/ModalNavigationManager/ModalNavigationManager.Android.cs index 579691c45bf7..c9c1e5ffd2a4 100644 --- a/src/Controls/src/Core/Platform/ModalNavigationManager/ModalNavigationManager.Android.cs +++ b/src/Controls/src/Core/Platform/ModalNavigationManager/ModalNavigationManager.Android.cs @@ -398,6 +398,76 @@ public CustomComponentDialog(Context context, int themeResId) : base(context, th this.OnBackPressedDispatcher.AddCallback(new CallBack(true, this)); } + public override bool OnKeyDown(Keycode keyCode, KeyEvent? e) + { + if (Context.GetActivity() is not global::Android.App.Activity activity) + return e is not null ? base.OnKeyDown(keyCode, e) : false; + + var handled = false; + IPlatformApplication.Current?.Services?.InvokeLifecycleEvents(del => + { + handled = del(activity, keyCode, e) || handled; + }); + + return handled || (e is not null && base.OnKeyDown(keyCode, e)); + } + + public override bool OnKeyLongPress(Keycode keyCode, KeyEvent? e) + { + if (Context.GetActivity() is not global::Android.App.Activity activity) + return e is not null ? base.OnKeyLongPress(keyCode, e) : false; + + var handled = false; + IPlatformApplication.Current?.Services?.InvokeLifecycleEvents(del => + { + handled = del(activity, keyCode, e) || handled; + }); + + return handled || (e is not null && base.OnKeyLongPress(keyCode, e)); + } + + public override bool OnKeyMultiple(Keycode keyCode, int repeatCount, KeyEvent? e) + { + if (Context.GetActivity() is not global::Android.App.Activity activity) + return e is not null ? base.OnKeyMultiple(keyCode, repeatCount, e) : false; + + var handled = false; + IPlatformApplication.Current?.Services?.InvokeLifecycleEvents(del => + { + handled = del(activity, keyCode, repeatCount, e) || handled; + }); + + return handled || (e is not null && base.OnKeyMultiple(keyCode, repeatCount, e)); + } + + public override bool OnKeyShortcut(Keycode keyCode, KeyEvent? e) + { + if (Context.GetActivity() is not global::Android.App.Activity activity) + return e is not null ? base.OnKeyShortcut(keyCode, e) : false; + + var handled = false; + IPlatformApplication.Current?.Services?.InvokeLifecycleEvents(del => + { + handled = del(activity, keyCode, e) || handled; + }); + + return handled || (e is not null && base.OnKeyShortcut(keyCode, e)); + } + + public override bool OnKeyUp(Keycode keyCode, KeyEvent? e) + { + if (Context.GetActivity() is not global::Android.App.Activity activity) + return e is not null ? base.OnKeyUp(keyCode, e) : false; + + var handled = false; + IPlatformApplication.Current?.Services?.InvokeLifecycleEvents(del => + { + handled = del(activity, keyCode, e) || handled; + }); + + return handled || (e is not null && base.OnKeyUp(keyCode, e)); + } + sealed class CallBack : OnBackPressedCallback { WeakReference _customComponentDialog; From a1fb7ec56f46babfa4755a2c8629aa999c043438 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 18 Aug 2025 19:18:42 +0000 Subject: [PATCH 5/8] Revert sandbox project changes as requested Co-authored-by: PureWeen <5375137+PureWeen@users.noreply.github.com> --- .../Controls.Sample.Sandbox/MainPage.xaml | 54 ------------------- .../Controls.Sample.Sandbox/MauiProgram.cs | 53 +----------------- 2 files changed, 1 insertion(+), 106 deletions(-) diff --git a/src/Controls/samples/Controls.Sample.Sandbox/MainPage.xaml b/src/Controls/samples/Controls.Sample.Sandbox/MainPage.xaml index fc73fc4ec631..7363d18deadd 100644 --- a/src/Controls/samples/Controls.Sample.Sandbox/MainPage.xaml +++ b/src/Controls/samples/Controls.Sample.Sandbox/MainPage.xaml @@ -2,58 +2,4 @@ xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" x:Class="Maui.Controls.Sample.MainPage" xmlns:local="clr-namespace:Maui.Controls.Sample"> - - - - - - - - \ No newline at end of file diff --git a/src/Controls/samples/Controls.Sample.Sandbox/MauiProgram.cs b/src/Controls/samples/Controls.Sample.Sandbox/MauiProgram.cs index 462716046ab7..d4941633bbd8 100644 --- a/src/Controls/samples/Controls.Sample.Sandbox/MauiProgram.cs +++ b/src/Controls/samples/Controls.Sample.Sandbox/MauiProgram.cs @@ -1,6 +1,4 @@ -using Microsoft.Maui.LifecycleEvents; - -namespace Maui.Controls.Sample; +namespace Maui.Controls.Sample; public static class MauiProgram { @@ -11,55 +9,6 @@ public static MauiApp CreateMauiApp() => .UseMauiMaps() #endif .UseMauiApp() - .ConfigureLifecycleEvents(events => - { -#if ANDROID - events.AddAndroid(android => - { - android.OnKeyDown((activity, keyCode, keyEvent) => - { - System.Diagnostics.Debug.WriteLine($"OnKeyDown: {keyCode}"); - // Handle volume keys specifically for demonstration - if (keyCode == Android.Views.Keycode.VolumeUp) - { - System.Diagnostics.Debug.WriteLine("Volume Up key handled by lifecycle event!"); - return true; // Prevent default handling - } - return false; // Allow default handling for other keys - }); - - android.OnKeyUp((activity, keyCode, keyEvent) => - { - System.Diagnostics.Debug.WriteLine($"OnKeyUp: {keyCode}"); - return false; // Allow default handling - }); - - android.OnKeyLongPress((activity, keyCode, keyEvent) => - { - System.Diagnostics.Debug.WriteLine($"OnKeyLongPress: {keyCode}"); - // Handle back button long press - if (keyCode == Android.Views.Keycode.Back) - { - System.Diagnostics.Debug.WriteLine("Back button long press handled!"); - return true; // Prevent default handling - } - return false; - }); - - android.OnKeyMultiple((activity, keyCode, repeatCount, keyEvent) => - { - System.Diagnostics.Debug.WriteLine($"OnKeyMultiple: {keyCode}, repeat: {repeatCount}"); - return false; - }); - - android.OnKeyShortcut((activity, keyCode, keyEvent) => - { - System.Diagnostics.Debug.WriteLine($"OnKeyShortcut: {keyCode}"); - return false; - }); - }); -#endif - }) .ConfigureFonts(fonts => { fonts.AddFont("Dokdo-Regular.ttf", "Dokdo"); From 6f62ec28d1ebd1a6ac94726653972cae6268d72d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 18 Aug 2025 19:45:24 +0000 Subject: [PATCH 6/8] Fix KeyEvent nullability and generalize OnKey* lifecycle event implementation Co-authored-by: PureWeen <5375137+PureWeen@users.noreply.github.com> --- .../ModalNavigationManager.Android.cs | 45 +++++++------------ .../Android/AndroidLifecycle.cs | 10 ++--- .../LifecycleEvents/LifecycleEventsTests.cs | 14 +++--- 3 files changed, 27 insertions(+), 42 deletions(-) diff --git a/src/Controls/src/Core/Platform/ModalNavigationManager/ModalNavigationManager.Android.cs b/src/Controls/src/Core/Platform/ModalNavigationManager/ModalNavigationManager.Android.cs index c9c1e5ffd2a4..79c462f7c920 100644 --- a/src/Controls/src/Core/Platform/ModalNavigationManager/ModalNavigationManager.Android.cs +++ b/src/Controls/src/Core/Platform/ModalNavigationManager/ModalNavigationManager.Android.cs @@ -398,74 +398,59 @@ public CustomComponentDialog(Context context, int themeResId) : base(context, th this.OnBackPressedDispatcher.AddCallback(new CallBack(true, this)); } - public override bool OnKeyDown(Keycode keyCode, KeyEvent? e) + public override bool OnKeyDown(Keycode keyCode, KeyEvent e) { - if (Context.GetActivity() is not global::Android.App.Activity activity) - return e is not null ? base.OnKeyDown(keyCode, e) : false; - var handled = false; IPlatformApplication.Current?.Services?.InvokeLifecycleEvents(del => { - handled = del(activity, keyCode, e) || handled; + handled = del(this, keyCode, e) || handled; }); - return handled || (e is not null && base.OnKeyDown(keyCode, e)); + return handled || base.OnKeyDown(keyCode, e); } - public override bool OnKeyLongPress(Keycode keyCode, KeyEvent? e) + public override bool OnKeyLongPress(Keycode keyCode, KeyEvent e) { - if (Context.GetActivity() is not global::Android.App.Activity activity) - return e is not null ? base.OnKeyLongPress(keyCode, e) : false; - var handled = false; IPlatformApplication.Current?.Services?.InvokeLifecycleEvents(del => { - handled = del(activity, keyCode, e) || handled; + handled = del(this, keyCode, e) || handled; }); - return handled || (e is not null && base.OnKeyLongPress(keyCode, e)); + return handled || base.OnKeyLongPress(keyCode, e); } - public override bool OnKeyMultiple(Keycode keyCode, int repeatCount, KeyEvent? e) + public override bool OnKeyMultiple(Keycode keyCode, int repeatCount, KeyEvent e) { - if (Context.GetActivity() is not global::Android.App.Activity activity) - return e is not null ? base.OnKeyMultiple(keyCode, repeatCount, e) : false; - var handled = false; IPlatformApplication.Current?.Services?.InvokeLifecycleEvents(del => { - handled = del(activity, keyCode, repeatCount, e) || handled; + handled = del(this, keyCode, repeatCount, e) || handled; }); - return handled || (e is not null && base.OnKeyMultiple(keyCode, repeatCount, e)); + return handled || base.OnKeyMultiple(keyCode, repeatCount, e); } - public override bool OnKeyShortcut(Keycode keyCode, KeyEvent? e) + public override bool OnKeyShortcut(Keycode keyCode, KeyEvent e) { - if (Context.GetActivity() is not global::Android.App.Activity activity) - return e is not null ? base.OnKeyShortcut(keyCode, e) : false; - var handled = false; IPlatformApplication.Current?.Services?.InvokeLifecycleEvents(del => { - handled = del(activity, keyCode, e) || handled; + handled = del(this, keyCode, e) || handled; }); - return handled || (e is not null && base.OnKeyShortcut(keyCode, e)); + return handled || base.OnKeyShortcut(keyCode, e); } - public override bool OnKeyUp(Keycode keyCode, KeyEvent? e) + public override bool OnKeyUp(Keycode keyCode, KeyEvent e) { - if (Context.GetActivity() is not global::Android.App.Activity activity) - return e is not null ? base.OnKeyUp(keyCode, e) : false; - var handled = false; IPlatformApplication.Current?.Services?.InvokeLifecycleEvents(del => { - handled = del(activity, keyCode, e) || handled; + handled = del(this, keyCode, e) || handled; }); - return handled || (e is not null && base.OnKeyUp(keyCode, e)); + return handled || base.OnKeyUp(keyCode, e); } sealed class CallBack : OnBackPressedCallback diff --git a/src/Core/src/LifecycleEvents/Android/AndroidLifecycle.cs b/src/Core/src/LifecycleEvents/Android/AndroidLifecycle.cs index 5f9ccc3f030b..31158f1a3f6b 100644 --- a/src/Core/src/LifecycleEvents/Android/AndroidLifecycle.cs +++ b/src/Core/src/LifecycleEvents/Android/AndroidLifecycle.cs @@ -34,11 +34,11 @@ public static class AndroidLifecycle public delegate void OnActivityResult(Activity activity, int requestCode, Result resultCode, Intent? data); public delegate bool OnBackPressed(Activity activity); public delegate void OnConfigurationChanged(Activity activity, Configuration newConfig); - public delegate bool OnKeyDown(Activity activity, Keycode keyCode, KeyEvent? e); - public delegate bool OnKeyLongPress(Activity activity, Keycode keyCode, KeyEvent? e); - public delegate bool OnKeyMultiple(Activity activity, Keycode keyCode, int repeatCount, KeyEvent? e); - public delegate bool OnKeyShortcut(Activity activity, Keycode keyCode, KeyEvent? e); - public delegate bool OnKeyUp(Activity activity, Keycode keyCode, KeyEvent? e); + public delegate bool OnKeyDown(object context, Keycode keyCode, KeyEvent? e); + public delegate bool OnKeyLongPress(object context, Keycode keyCode, KeyEvent? e); + public delegate bool OnKeyMultiple(object context, Keycode keyCode, int repeatCount, KeyEvent? e); + public delegate bool OnKeyShortcut(object context, Keycode keyCode, KeyEvent? e); + public delegate bool OnKeyUp(object context, Keycode keyCode, KeyEvent? e); public delegate void OnNewIntent(Activity activity, Intent? intent); public delegate void OnRequestPermissionsResult(Activity activity, int requestCode, string[] permissions, Permission[] grantResults); public delegate void OnRestoreInstanceState(Activity activity, Bundle savedInstanceState); diff --git a/src/Core/tests/UnitTests/LifecycleEvents/LifecycleEventsTests.cs b/src/Core/tests/UnitTests/LifecycleEvents/LifecycleEventsTests.cs index ae08390f4e03..d0ec5d1f677c 100644 --- a/src/Core/tests/UnitTests/LifecycleEvents/LifecycleEventsTests.cs +++ b/src/Core/tests/UnitTests/LifecycleEvents/LifecycleEventsTests.cs @@ -174,7 +174,7 @@ public void CanAddAndroidOnKeyDownLifecycleEvent() { builder.AddAndroid(android => { - android.OnKeyDown((activity, keyCode, keyEvent) => + android.OnKeyDown((context, keyCode, keyEvent) => { eventFired = true; receivedKeyCode = keyCode; @@ -211,7 +211,7 @@ public void CanAddAndroidOnKeyUpLifecycleEvent() { builder.AddAndroid(android => { - android.OnKeyUp((activity, keyCode, keyEvent) => + android.OnKeyUp((context, keyCode, keyEvent) => { eventFired = true; return keyCode == Keycode.VolumeDown; // Handle only volume down @@ -256,7 +256,7 @@ public void CanAddAndroidOnKeyLongPressLifecycleEvent() { builder.AddAndroid(android => { - android.OnKeyLongPress((activity, keyCode, keyEvent) => + android.OnKeyLongPress((context, keyCode, keyEvent) => { eventFired = true; return true; // Handle the event @@ -288,7 +288,7 @@ public void CanAddAndroidOnKeyMultipleLifecycleEvent() { builder.AddAndroid(android => { - android.OnKeyMultiple((activity, keyCode, repeatCount, keyEvent) => + android.OnKeyMultiple((context, keyCode, repeatCount, keyEvent) => { eventFired = true; receivedRepeatCount = repeatCount; @@ -321,7 +321,7 @@ public void CanAddAndroidOnKeyShortcutLifecycleEvent() { builder.AddAndroid(android => { - android.OnKeyShortcut((activity, keyCode, keyEvent) => + android.OnKeyShortcut((context, keyCode, keyEvent) => { eventFired = true; return true; @@ -354,12 +354,12 @@ public void AndroidOnKeyEventsCanBeHandledByMultipleListeners() { builder.AddAndroid(android => { - android.OnKeyDown((activity, keyCode, keyEvent) => + android.OnKeyDown((context, keyCode, keyEvent) => { event1Fired = true; return false; // Don't handle }); - android.OnKeyDown((activity, keyCode, keyEvent) => + android.OnKeyDown((context, keyCode, keyEvent) => { event2Fired = true; return true; // Handle the event From 1873074ebfafb28a6855696edbea9c4b93a61cb9 Mon Sep 17 00:00:00 2001 From: Shane Neuville Date: Mon, 18 Aug 2025 15:23:49 -0500 Subject: [PATCH 7/8] - fix publicapi --- .../net-android/PublicAPI.Unshipped.txt | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/src/Core/src/PublicAPI/net-android/PublicAPI.Unshipped.txt b/src/Core/src/PublicAPI/net-android/PublicAPI.Unshipped.txt index c33fbf044b8b..b4dd7e0f9f32 100644 --- a/src/Core/src/PublicAPI/net-android/PublicAPI.Unshipped.txt +++ b/src/Core/src/PublicAPI/net-android/PublicAPI.Unshipped.txt @@ -53,6 +53,11 @@ Microsoft.Maui.ITimePicker.IsOpen.set -> void Microsoft.Maui.ITimePicker.Time.get -> System.TimeSpan? Microsoft.Maui.IWebRequestInterceptingWebView Microsoft.Maui.IWebRequestInterceptingWebView.WebResourceRequested(Microsoft.Maui.WebResourceRequestedEventArgs! args) -> bool +Microsoft.Maui.LifecycleEvents.AndroidLifecycle.OnKeyDown +Microsoft.Maui.LifecycleEvents.AndroidLifecycle.OnKeyLongPress +Microsoft.Maui.LifecycleEvents.AndroidLifecycle.OnKeyMultiple +Microsoft.Maui.LifecycleEvents.AndroidLifecycle.OnKeyShortcut +Microsoft.Maui.LifecycleEvents.AndroidLifecycle.OnKeyUp Microsoft.Maui.Platform.MauiHorizontalScrollView Microsoft.Maui.Platform.MauiHorizontalScrollView.MauiHorizontalScrollView(Android.Content.Context? context, Android.Util.IAttributeSet? attrs) -> void Microsoft.Maui.Platform.MauiHorizontalScrollView.MauiHorizontalScrollView(Android.Content.Context? context, Android.Util.IAttributeSet? attrs, int defStyleAttr) -> void @@ -191,6 +196,11 @@ override Microsoft.Maui.Handlers.OpenWindowRequest.GetHashCode() -> int override Microsoft.Maui.Handlers.OpenWindowRequest.ToString() -> string! override Microsoft.Maui.Handlers.RefreshViewHandler.SetVirtualView(Microsoft.Maui.IView! view) -> void override Microsoft.Maui.Handlers.TimePickerHandler.ConnectHandler(Microsoft.Maui.Platform.MauiTimePicker! platformView) -> void +override Microsoft.Maui.MauiAppCompatActivity.OnKeyDown(Android.Views.Keycode keyCode, Android.Views.KeyEvent? e) -> bool +override Microsoft.Maui.MauiAppCompatActivity.OnKeyLongPress(Android.Views.Keycode keyCode, Android.Views.KeyEvent? e) -> bool +override Microsoft.Maui.MauiAppCompatActivity.OnKeyMultiple(Android.Views.Keycode keyCode, int repeatCount, Android.Views.KeyEvent? e) -> bool +override Microsoft.Maui.MauiAppCompatActivity.OnKeyShortcut(Android.Views.Keycode keyCode, Android.Views.KeyEvent? e) -> bool +override Microsoft.Maui.MauiAppCompatActivity.OnKeyUp(Android.Views.Keycode keyCode, Android.Views.KeyEvent? e) -> bool override Microsoft.Maui.Platform.MauiDatePicker.DefaultMovementMethod.get -> Android.Text.Method.IMovementMethod? override Microsoft.Maui.Platform.MauiHorizontalScrollView.Draw(Android.Graphics.Canvas? canvas) -> void override Microsoft.Maui.Platform.MauiHorizontalScrollView.HorizontalScrollBarEnabled.get -> bool @@ -246,6 +256,11 @@ static Microsoft.Maui.Handlers.OpenWindowRequest.operator ==(Microsoft.Maui.Hand static Microsoft.Maui.Handlers.RefreshViewHandler.MapIsEnabled(Microsoft.Maui.Handlers.IRefreshViewHandler! handler, Microsoft.Maui.IRefreshView! refreshView) -> void static Microsoft.Maui.Handlers.SearchBarHandler.MapReturnType(Microsoft.Maui.Handlers.ISearchBarHandler! handler, Microsoft.Maui.ISearchBar! searchBar) -> void static Microsoft.Maui.Hosting.AppHostBuilderExtensions.ConfigureEnvironmentVariables(this Microsoft.Maui.Hosting.MauiAppBuilder! builder) -> Microsoft.Maui.Hosting.MauiAppBuilder! +static Microsoft.Maui.LifecycleEvents.AndroidLifecycleBuilderExtensions.OnKeyDown(this Microsoft.Maui.LifecycleEvents.IAndroidLifecycleBuilder! lifecycle, Microsoft.Maui.LifecycleEvents.AndroidLifecycle.OnKeyDown! del) -> Microsoft.Maui.LifecycleEvents.IAndroidLifecycleBuilder! +static Microsoft.Maui.LifecycleEvents.AndroidLifecycleBuilderExtensions.OnKeyLongPress(this Microsoft.Maui.LifecycleEvents.IAndroidLifecycleBuilder! lifecycle, Microsoft.Maui.LifecycleEvents.AndroidLifecycle.OnKeyLongPress! del) -> Microsoft.Maui.LifecycleEvents.IAndroidLifecycleBuilder! +static Microsoft.Maui.LifecycleEvents.AndroidLifecycleBuilderExtensions.OnKeyMultiple(this Microsoft.Maui.LifecycleEvents.IAndroidLifecycleBuilder! lifecycle, Microsoft.Maui.LifecycleEvents.AndroidLifecycle.OnKeyMultiple! del) -> Microsoft.Maui.LifecycleEvents.IAndroidLifecycleBuilder! +static Microsoft.Maui.LifecycleEvents.AndroidLifecycleBuilderExtensions.OnKeyShortcut(this Microsoft.Maui.LifecycleEvents.IAndroidLifecycleBuilder! lifecycle, Microsoft.Maui.LifecycleEvents.AndroidLifecycle.OnKeyShortcut! del) -> Microsoft.Maui.LifecycleEvents.IAndroidLifecycleBuilder! +static Microsoft.Maui.LifecycleEvents.AndroidLifecycleBuilderExtensions.OnKeyUp(this Microsoft.Maui.LifecycleEvents.IAndroidLifecycleBuilder! lifecycle, Microsoft.Maui.LifecycleEvents.AndroidLifecycle.OnKeyUp! del) -> Microsoft.Maui.LifecycleEvents.IAndroidLifecycleBuilder! static Microsoft.Maui.Platform.ButtonExtensions.UpdateRippleColor(this Google.Android.Material.Button.MaterialButton! platformView, Microsoft.Maui.Graphics.Color? rippleColor) -> void static Microsoft.Maui.Platform.EditTextExtensions.GetCursorPosition(this Android.Widget.EditText! editText, int cursorOffset = 0) -> int static Microsoft.Maui.Platform.EditTextExtensions.GetSelectedTextLength(this Android.Widget.EditText! editText) -> int @@ -309,6 +324,11 @@ virtual Microsoft.Maui.LifecycleEvents.AndroidLifecycle.OnBackPressed.Invoke(And virtual Microsoft.Maui.LifecycleEvents.AndroidLifecycle.OnConfigurationChanged.Invoke(Android.App.Activity! activity, Android.Content.Res.Configuration! newConfig) -> void virtual Microsoft.Maui.LifecycleEvents.AndroidLifecycle.OnCreate.Invoke(Android.App.Activity! activity, Android.OS.Bundle? savedInstanceState) -> void virtual Microsoft.Maui.LifecycleEvents.AndroidLifecycle.OnDestroy.Invoke(Android.App.Activity! activity) -> void +virtual Microsoft.Maui.LifecycleEvents.AndroidLifecycle.OnKeyDown.Invoke(object! context, Android.Views.Keycode keyCode, Android.Views.KeyEvent? e) -> bool +virtual Microsoft.Maui.LifecycleEvents.AndroidLifecycle.OnKeyLongPress.Invoke(object! context, Android.Views.Keycode keyCode, Android.Views.KeyEvent? e) -> bool +virtual Microsoft.Maui.LifecycleEvents.AndroidLifecycle.OnKeyMultiple.Invoke(object! context, Android.Views.Keycode keyCode, int repeatCount, Android.Views.KeyEvent? e) -> bool +virtual Microsoft.Maui.LifecycleEvents.AndroidLifecycle.OnKeyShortcut.Invoke(object! context, Android.Views.Keycode keyCode, Android.Views.KeyEvent? e) -> bool +virtual Microsoft.Maui.LifecycleEvents.AndroidLifecycle.OnKeyUp.Invoke(object! context, Android.Views.Keycode keyCode, Android.Views.KeyEvent? e) -> bool virtual Microsoft.Maui.LifecycleEvents.AndroidLifecycle.OnNewIntent.Invoke(Android.App.Activity! activity, Android.Content.Intent? intent) -> void virtual Microsoft.Maui.LifecycleEvents.AndroidLifecycle.OnPause.Invoke(Android.App.Activity! activity) -> void virtual Microsoft.Maui.LifecycleEvents.AndroidLifecycle.OnPostCreate.Invoke(Android.App.Activity! activity, Android.OS.Bundle? savedInstanceState) -> void From bcff03e53c7b1dfc3295ff33df3ffbd04a47e37e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 19 Aug 2025 11:43:48 +0000 Subject: [PATCH 8/8] Revert changes to LifecycleEventsTests.cs as requested Co-authored-by: PureWeen <5375137+PureWeen@users.noreply.github.com> --- .../LifecycleEvents/LifecycleEventsTests.cs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/Core/tests/UnitTests/LifecycleEvents/LifecycleEventsTests.cs b/src/Core/tests/UnitTests/LifecycleEvents/LifecycleEventsTests.cs index d0ec5d1f677c..ae08390f4e03 100644 --- a/src/Core/tests/UnitTests/LifecycleEvents/LifecycleEventsTests.cs +++ b/src/Core/tests/UnitTests/LifecycleEvents/LifecycleEventsTests.cs @@ -174,7 +174,7 @@ public void CanAddAndroidOnKeyDownLifecycleEvent() { builder.AddAndroid(android => { - android.OnKeyDown((context, keyCode, keyEvent) => + android.OnKeyDown((activity, keyCode, keyEvent) => { eventFired = true; receivedKeyCode = keyCode; @@ -211,7 +211,7 @@ public void CanAddAndroidOnKeyUpLifecycleEvent() { builder.AddAndroid(android => { - android.OnKeyUp((context, keyCode, keyEvent) => + android.OnKeyUp((activity, keyCode, keyEvent) => { eventFired = true; return keyCode == Keycode.VolumeDown; // Handle only volume down @@ -256,7 +256,7 @@ public void CanAddAndroidOnKeyLongPressLifecycleEvent() { builder.AddAndroid(android => { - android.OnKeyLongPress((context, keyCode, keyEvent) => + android.OnKeyLongPress((activity, keyCode, keyEvent) => { eventFired = true; return true; // Handle the event @@ -288,7 +288,7 @@ public void CanAddAndroidOnKeyMultipleLifecycleEvent() { builder.AddAndroid(android => { - android.OnKeyMultiple((context, keyCode, repeatCount, keyEvent) => + android.OnKeyMultiple((activity, keyCode, repeatCount, keyEvent) => { eventFired = true; receivedRepeatCount = repeatCount; @@ -321,7 +321,7 @@ public void CanAddAndroidOnKeyShortcutLifecycleEvent() { builder.AddAndroid(android => { - android.OnKeyShortcut((context, keyCode, keyEvent) => + android.OnKeyShortcut((activity, keyCode, keyEvent) => { eventFired = true; return true; @@ -354,12 +354,12 @@ public void AndroidOnKeyEventsCanBeHandledByMultipleListeners() { builder.AddAndroid(android => { - android.OnKeyDown((context, keyCode, keyEvent) => + android.OnKeyDown((activity, keyCode, keyEvent) => { event1Fired = true; return false; // Don't handle }); - android.OnKeyDown((context, keyCode, keyEvent) => + android.OnKeyDown((activity, keyCode, keyEvent) => { event2Fired = true; return true; // Handle the event