diff --git a/src/Controls/src/Core/Platform/ModalNavigationManager/ModalNavigationManager.Android.cs b/src/Controls/src/Core/Platform/ModalNavigationManager/ModalNavigationManager.Android.cs index 579691c45bf7..79c462f7c920 100644 --- a/src/Controls/src/Core/Platform/ModalNavigationManager/ModalNavigationManager.Android.cs +++ b/src/Controls/src/Core/Platform/ModalNavigationManager/ModalNavigationManager.Android.cs @@ -398,6 +398,61 @@ public CustomComponentDialog(Context context, int themeResId) : base(context, th this.OnBackPressedDispatcher.AddCallback(new CallBack(true, this)); } + 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); + } + sealed class CallBack : OnBackPressedCallback { WeakReference _customComponentDialog; diff --git a/src/Core/src/LifecycleEvents/Android/AndroidLifecycle.cs b/src/Core/src/LifecycleEvents/Android/AndroidLifecycle.cs index 948a244a06d3..31158f1a3f6b 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(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/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 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 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();