From 306f37d518581b494bde765a8415c569310841b0 Mon Sep 17 00:00:00 2001 From: Emmanuel Hansen Date: Fri, 9 May 2025 08:30:01 -0400 Subject: [PATCH] Move android app initialization to custom application class (#18756) * move android app initialization to custom application class * fix tests * update api suppression * address comments --- api/Avalonia.Android.nupkg.xml | 6 +++ samples/ControlCatalog.Android/Application.cs | 30 +++++++++++++ .../ControlCatalog.Android/MainActivity.cs | 15 ++----- .../Properties/AndroidManifest.xml | 2 +- samples/SafeAreaDemo.Android/Application.cs | 14 ++++++ samples/SafeAreaDemo.Android/MainActivity.cs | 2 +- .../Properties/AndroidManifest.xml | 2 +- .../Platforms/Android/AndroidManifest.xml | 2 +- .../Platforms/Android/Application.cs | 14 ++++++ .../Platforms/Android/MainActivity.cs | 7 +-- .../AvaloniaAndroidApplication.cs | 45 +++++++++++++++++++ .../AvaloniaMainActivity.App.cs | 7 --- .../Avalonia.Android/AvaloniaMainActivity.cs | 33 +++----------- .../BuildTests.Android/Application.cs | 14 ++++++ .../BuildTests.Android/MainActivity.cs | 2 +- .../Properties/AndroidManifest.xml | 3 +- 16 files changed, 139 insertions(+), 59 deletions(-) create mode 100644 samples/ControlCatalog.Android/Application.cs create mode 100644 samples/SafeAreaDemo.Android/Application.cs create mode 100644 samples/SingleProjectSandbox/Platforms/Android/Application.cs create mode 100644 src/Android/Avalonia.Android/AvaloniaAndroidApplication.cs delete mode 100644 src/Android/Avalonia.Android/AvaloniaMainActivity.App.cs create mode 100644 tests/BuildTests/BuildTests.Android/Application.cs diff --git a/api/Avalonia.Android.nupkg.xml b/api/Avalonia.Android.nupkg.xml index da33e03f2c..deed9db4de 100644 --- a/api/Avalonia.Android.nupkg.xml +++ b/api/Avalonia.Android.nupkg.xml @@ -1,6 +1,12 @@  + + CP0001 + T:Avalonia.Android.AvaloniaMainActivity`1 + baseline/net8.0-android34.0/Avalonia.Android.dll + target/net8.0-android34.0/Avalonia.Android.dll + CP0002 M:Avalonia.Android.AndroidViewControlHandle.get_HandleDescriptor diff --git a/samples/ControlCatalog.Android/Application.cs b/samples/ControlCatalog.Android/Application.cs new file mode 100644 index 0000000000..1bdb61e5e3 --- /dev/null +++ b/samples/ControlCatalog.Android/Application.cs @@ -0,0 +1,30 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Android.App; +using Android.Runtime; +using Avalonia; +using Avalonia.Android; + +namespace ControlCatalog.Android +{ + [Application] + public class Application : AvaloniaAndroidApplication + { + protected Application(nint javaReference, JniHandleOwnership transfer) : base(javaReference, transfer) + { + } + + + protected override AppBuilder CustomizeAppBuilder(AppBuilder builder) + { + return base.CustomizeAppBuilder(builder) + .AfterSetup(_ => + { + Pages.EmbedSample.Implementation = new EmbedSampleAndroid(); + }); + } + } +} diff --git a/samples/ControlCatalog.Android/MainActivity.cs b/samples/ControlCatalog.Android/MainActivity.cs index c54b616b17..4963d12997 100644 --- a/samples/ControlCatalog.Android/MainActivity.cs +++ b/samples/ControlCatalog.Android/MainActivity.cs @@ -1,7 +1,6 @@ using Android.App; using Android.Content.PM; using Android.OS; -using Avalonia; using Avalonia.Android; using static Android.Content.Intent; @@ -13,17 +12,9 @@ namespace ControlCatalog.Android { [Activity(Name = "com.Avalonia.ControlCatalog.MainActivity", Label = "ControlCatalog.Android", Theme = "@style/MyTheme.NoActionBar", Icon = "@drawable/icon", MainLauncher = true, Exported = true, ConfigurationChanges = ConfigChanges.Orientation | ConfigChanges.ScreenSize | ConfigChanges.UiMode)] // CategoryLeanbackLauncher is required for Android TV. - [IntentFilter(new [] { ActionView }, Categories = new [] { CategoryDefault, CategoryLeanbackLauncher })] - public class MainActivity : AvaloniaMainActivity + [IntentFilter(new[] { ActionView }, Categories = new[] { CategoryDefault, CategoryLeanbackLauncher })] + public class MainActivity : AvaloniaMainActivity { - protected override AppBuilder CustomizeAppBuilder(AppBuilder builder) - { - return base.CustomizeAppBuilder(builder) - .AfterSetup(_ => - { - Pages.EmbedSample.Implementation = new EmbedSampleAndroid(); - }); - } } /// @@ -31,7 +22,7 @@ namespace ControlCatalog.Android /// `AvaloniaActivity` internally will redirect parameters to the Avalonia Application. /// [Activity(NoHistory = true, LaunchMode = LaunchMode.SingleTop, Exported = true, Theme = "@android:style/Theme.NoDisplay")] - [IntentFilter(new[] {ActionView}, Categories = new[] {CategoryDefault, CategoryBrowsable}, DataScheme = "avln")] + [IntentFilter(new[] { ActionView }, Categories = new[] { CategoryDefault, CategoryBrowsable }, DataScheme = "avln")] public class DataSchemeActivity : AvaloniaActivity { protected override void OnCreate(Bundle? savedInstanceState) diff --git a/samples/ControlCatalog.Android/Properties/AndroidManifest.xml b/samples/ControlCatalog.Android/Properties/AndroidManifest.xml index ec07a94b22..95529679ec 100644 --- a/samples/ControlCatalog.Android/Properties/AndroidManifest.xml +++ b/samples/ControlCatalog.Android/Properties/AndroidManifest.xml @@ -1,6 +1,6 @@  - + diff --git a/samples/SafeAreaDemo.Android/Application.cs b/samples/SafeAreaDemo.Android/Application.cs new file mode 100644 index 0000000000..33893a68d8 --- /dev/null +++ b/samples/SafeAreaDemo.Android/Application.cs @@ -0,0 +1,14 @@ +using Android.App; +using Android.Runtime; +using Avalonia.Android; + +namespace SafeAreaDemo.Android +{ + [Application] + public class Application : AvaloniaAndroidApplication + { + protected Application(nint javaReference, JniHandleOwnership transfer) : base(javaReference, transfer) + { + } + } +} diff --git a/samples/SafeAreaDemo.Android/MainActivity.cs b/samples/SafeAreaDemo.Android/MainActivity.cs index 1df575eb4d..6ee9b35809 100644 --- a/samples/SafeAreaDemo.Android/MainActivity.cs +++ b/samples/SafeAreaDemo.Android/MainActivity.cs @@ -5,7 +5,7 @@ using Avalonia.Android; namespace SafeAreaDemo.Android { [Activity(Label = "SafeAreaDemo.Android", Theme = "@style/MyTheme.NoActionBar", Icon = "@drawable/icon", MainLauncher = true, ConfigurationChanges = ConfigChanges.Orientation | ConfigChanges.ScreenSize | ConfigChanges.UiMode)] - public class MainActivity : AvaloniaMainActivity + public class MainActivity : AvaloniaMainActivity { } } diff --git a/samples/SafeAreaDemo.Android/Properties/AndroidManifest.xml b/samples/SafeAreaDemo.Android/Properties/AndroidManifest.xml index b6a5777e03..c352c2d17d 100644 --- a/samples/SafeAreaDemo.Android/Properties/AndroidManifest.xml +++ b/samples/SafeAreaDemo.Android/Properties/AndroidManifest.xml @@ -1,5 +1,5 @@  - + diff --git a/samples/SingleProjectSandbox/Platforms/Android/AndroidManifest.xml b/samples/SingleProjectSandbox/Platforms/Android/AndroidManifest.xml index 449734f268..6ce631b18b 100644 --- a/samples/SingleProjectSandbox/Platforms/Android/AndroidManifest.xml +++ b/samples/SingleProjectSandbox/Platforms/Android/AndroidManifest.xml @@ -1,5 +1,5 @@  - + diff --git a/samples/SingleProjectSandbox/Platforms/Android/Application.cs b/samples/SingleProjectSandbox/Platforms/Android/Application.cs new file mode 100644 index 0000000000..251f466e51 --- /dev/null +++ b/samples/SingleProjectSandbox/Platforms/Android/Application.cs @@ -0,0 +1,14 @@ +using Android.App; +using Android.Runtime; +using Avalonia.Android; + +namespace SingleProjectSandbox.Android +{ + [Application] + public class Application : AvaloniaAndroidApplication + { + protected Application(nint javaReference, JniHandleOwnership transfer) : base(javaReference, transfer) + { + } + } +} diff --git a/samples/SingleProjectSandbox/Platforms/Android/MainActivity.cs b/samples/SingleProjectSandbox/Platforms/Android/MainActivity.cs index a075f90ee7..dc39525c01 100644 --- a/samples/SingleProjectSandbox/Platforms/Android/MainActivity.cs +++ b/samples/SingleProjectSandbox/Platforms/Android/MainActivity.cs @@ -1,15 +1,10 @@ using Android.App; using Android.Content.PM; -using Avalonia; using Avalonia.Android; namespace SingleProjectSandbox; [Activity(Label = "SingleProjectSandbox.Android", Theme = "@style/Theme.AppCompat.NoActionBar", MainLauncher = true, ConfigurationChanges = ConfigChanges.Orientation | ConfigChanges.ScreenSize)] -public class MainActivity : AvaloniaMainActivity -{ - protected override AppBuilder CreateAppBuilder() +public class MainActivity : AvaloniaMainActivity { - return App.BuildAvaloniaApp().UseAndroid(); - } } diff --git a/src/Android/Avalonia.Android/AvaloniaAndroidApplication.cs b/src/Android/Avalonia.Android/AvaloniaAndroidApplication.cs new file mode 100644 index 0000000000..5f59e63dcd --- /dev/null +++ b/src/Android/Avalonia.Android/AvaloniaAndroidApplication.cs @@ -0,0 +1,45 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Android.Runtime; + +namespace Avalonia.Android +{ + internal interface IAndroidApplication + { + SingleViewLifetime? Lifetime { get; set; } + } + + public class AvaloniaAndroidApplication : global::Android.App.Application, IAndroidApplication + where TApp : Application, new() + { + SingleViewLifetime? IAndroidApplication.Lifetime { get; set; } + + protected AvaloniaAndroidApplication(nint javaReference, JniHandleOwnership transfer) : base(javaReference, transfer) + { + } + + public override void OnCreate() + { + base.OnCreate(); + InitializeAppLifetime(); + } + + private void InitializeAppLifetime() + { + var builder = CreateAppBuilder(); + builder = CustomizeAppBuilder(builder); + + var lifetime = new SingleViewLifetime(); + + ((IAndroidApplication)this).Lifetime = lifetime; + + builder.SetupWithLifetime(lifetime); + } + + protected virtual AppBuilder CreateAppBuilder() => AppBuilder.Configure().UseAndroid(); + protected virtual AppBuilder CustomizeAppBuilder(AppBuilder builder) => builder; + } +} diff --git a/src/Android/Avalonia.Android/AvaloniaMainActivity.App.cs b/src/Android/Avalonia.Android/AvaloniaMainActivity.App.cs deleted file mode 100644 index c3116fe232..0000000000 --- a/src/Android/Avalonia.Android/AvaloniaMainActivity.App.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace Avalonia.Android; - -public class AvaloniaMainActivity : AvaloniaMainActivity - where TApp : Application, new() -{ - protected override AppBuilder CreateAppBuilder() => AppBuilder.Configure().UseAndroid(); -} diff --git a/src/Android/Avalonia.Android/AvaloniaMainActivity.cs b/src/Android/Avalonia.Android/AvaloniaMainActivity.cs index 63097b22a1..975b4b7883 100644 --- a/src/Android/Avalonia.Android/AvaloniaMainActivity.cs +++ b/src/Android/Avalonia.Android/AvaloniaMainActivity.cs @@ -8,41 +8,18 @@ namespace Avalonia.Android; public class AvaloniaMainActivity : AvaloniaActivity { - private protected static SingleViewLifetime? Lifetime; - private protected override void InitializeAvaloniaView(object? initialContent) { - // Android can run OnCreate + InitializeAvaloniaView multiple times per process lifetime. - // On each call we need to create new AvaloniaView, but we can't recreate Avalonia nor Avalonia controls. - // So, if lifetime was already created previously - recreate AvaloniaView. - // If not, initialize Avalonia, and create AvaloniaView inside of AfterSetup callback. - // We need this AfterSetup callback to match iOS/Browser behavior and ensure that view/toplevel is available in custom AfterSetup calls. - if (Lifetime is not null) + if (Application is IAndroidApplication application && application.Lifetime is { } lifetime) { - initialContent ??= Lifetime.MainView; + initialContent ??= lifetime.MainView; _view = new AvaloniaView(this) { Content = initialContent }; - Lifetime.Activity = this; + lifetime.Activity = this; } - else - { - var builder = CreateAppBuilder(); - builder = CustomizeAppBuilder(builder); - Lifetime = new SingleViewLifetime(); - Lifetime.Activity = this; - - builder - .AfterApplicationSetup(_ => - { - _view = new AvaloniaView(this) { Content = initialContent }; - }) - .SetupWithLifetime(Lifetime); - - // AfterPlatformServicesSetup should always be called. If it wasn't, we have an unusual problem. - if (_view is null) - throw new InvalidOperationException("Unknown error: AvaloniaView initialization has failed."); - } + if (_view is null) + throw new InvalidOperationException("Unknown error: AvaloniaView initialization has failed."); if (Avalonia.Application.Current?.TryGetFeature() is AndroidActivatableLifetime activatableLifetime) diff --git a/tests/BuildTests/BuildTests.Android/Application.cs b/tests/BuildTests/BuildTests.Android/Application.cs new file mode 100644 index 0000000000..4754d4b939 --- /dev/null +++ b/tests/BuildTests/BuildTests.Android/Application.cs @@ -0,0 +1,14 @@ +using Android.App; +using Android.Runtime; +using Avalonia.Android; + +namespace BuildTests.Android +{ + [Application] + public class Application : AvaloniaAndroidApplication + { + protected Application(nint javaReference, JniHandleOwnership transfer) : base(javaReference, transfer) + { + } + } +} diff --git a/tests/BuildTests/BuildTests.Android/MainActivity.cs b/tests/BuildTests/BuildTests.Android/MainActivity.cs index 046dca9a47..e672584969 100644 --- a/tests/BuildTests/BuildTests.Android/MainActivity.cs +++ b/tests/BuildTests/BuildTests.Android/MainActivity.cs @@ -8,4 +8,4 @@ namespace BuildTests.Android; Label = "BuildTests.Android", MainLauncher = true, ConfigurationChanges = ConfigChanges.Orientation | ConfigChanges.ScreenSize | ConfigChanges.UiMode)] -public class MainActivity : AvaloniaMainActivity; +public class MainActivity : AvaloniaMainActivity; diff --git a/tests/BuildTests/BuildTests.Android/Properties/AndroidManifest.xml b/tests/BuildTests/BuildTests.Android/Properties/AndroidManifest.xml index c02ce0d24b..550d777b31 100644 --- a/tests/BuildTests/BuildTests.Android/Properties/AndroidManifest.xml +++ b/tests/BuildTests/BuildTests.Android/Properties/AndroidManifest.xml @@ -1,5 +1,6 @@  - +