From 413ff78ebb6b9204e122fb10d007f1c12bfe75b9 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Thu, 25 Jul 2024 21:48:02 -0700 Subject: [PATCH] Fix android uri activation (#16477) * Make SetContentView call delayed * Make it possible to run secondary activity for OpenUri activation * Add activation DataSchemeActivity example --- .../ControlCatalog.Android/MainActivity.cs | 21 +++++- .../Avalonia.Android/AvaloniaActivity.cs | 32 ++++++--- .../Avalonia.Android/AvaloniaMainActivity.cs | 18 ++--- .../Platform/AndroidActivatableLifetime.cs | 65 +++++++++++++++---- 4 files changed, 100 insertions(+), 36 deletions(-) diff --git a/samples/ControlCatalog.Android/MainActivity.cs b/samples/ControlCatalog.Android/MainActivity.cs index dcc2066149..c54b616b17 100644 --- a/samples/ControlCatalog.Android/MainActivity.cs +++ b/samples/ControlCatalog.Android/MainActivity.cs @@ -1,5 +1,6 @@ using Android.App; using Android.Content.PM; +using Android.OS; using Avalonia; using Avalonia.Android; using static Android.Content.Intent; @@ -10,10 +11,9 @@ using static Android.Content.Intent; namespace ControlCatalog.Android { - [Activity(Label = "ControlCatalog.Android", Theme = "@style/MyTheme.NoActionBar", Icon = "@drawable/icon", MainLauncher = true, Exported = true, ConfigurationChanges = ConfigChanges.Orientation | ConfigChanges.ScreenSize | ConfigChanges.UiMode)] - // CategoryBrowsable and DataScheme are required for Protocol activation. + [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, CategoryBrowsable, CategoryLeanbackLauncher }, DataScheme = "avln" )] + [IntentFilter(new [] { ActionView }, Categories = new [] { CategoryDefault, CategoryLeanbackLauncher })] public class MainActivity : AvaloniaMainActivity { protected override AppBuilder CustomizeAppBuilder(AppBuilder builder) @@ -25,4 +25,19 @@ namespace ControlCatalog.Android }); } } + + /// + /// Special activity to handle OpenUri activation. + /// `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")] + public class DataSchemeActivity : AvaloniaActivity + { + protected override void OnCreate(Bundle? savedInstanceState) + { + base.OnCreate(savedInstanceState); + Finish(); + } + } } diff --git a/src/Android/Avalonia.Android/AvaloniaActivity.cs b/src/Android/Avalonia.Android/AvaloniaActivity.cs index 4e7917494b..7117595787 100644 --- a/src/Android/Avalonia.Android/AvaloniaActivity.cs +++ b/src/Android/Avalonia.Android/AvaloniaActivity.cs @@ -8,6 +8,8 @@ using Android.OS; using Android.Runtime; using Android.Views; using AndroidX.AppCompat.App; +using Avalonia.Platform; +using Avalonia.Android.Platform; using Avalonia.Android.Platform.Storage; using Avalonia.Controls.ApplicationLifetimes; @@ -22,6 +24,7 @@ public class AvaloniaActivity : AppCompatActivity, IAvaloniaActivity private EventHandler? _onActivated, _onDeactivated; private GlobalLayoutListener? _listener; private object? _content; + private bool _contentViewSet; internal AvaloniaView? _view; public Action? ActivityResult { get; set; } @@ -39,6 +42,17 @@ public class AvaloniaActivity : AppCompatActivity, IAvaloniaActivity _content = value; if (_view is not null) { + if (!_contentViewSet) + { + _contentViewSet = true; + + SetContentView(_view); + + _listener = new GlobalLayoutListener(_view); + + _view.ViewTreeObserver?.AddOnGlobalLayoutListener(_listener); + } + _view.Content = _content; } } @@ -76,14 +90,13 @@ public class AvaloniaActivity : AppCompatActivity, IAvaloniaActivity base.OnCreate(savedInstanceState); - SetContentView(_view); - - _listener = new GlobalLayoutListener(_view); - - _view.ViewTreeObserver?.AddOnGlobalLayoutListener(_listener); + if (Avalonia.Application.Current?.TryGetFeature() + is AndroidActivatableLifetime activatableLifetime) + { + activatableLifetime.CurrentIntendActivity = this; + } - // TODO: we probably don't need to create AvaloniaView, if it's just a protocol activation, and main activity is already created. - if (Intent?.Data is {} androidUri + if (Intent?.Data is { } androidUri && androidUri.IsAbsolute && Uri.TryCreate(androidUri.ToString(), UriKind.Absolute, out var uri)) { @@ -131,8 +144,11 @@ public class AvaloniaActivity : AppCompatActivity, IAvaloniaActivity { if (_view is not null) { + if (_listener is not null) + { + _view.ViewTreeObserver?.RemoveOnGlobalLayoutListener(_listener); + } _view.Content = null; - _view.ViewTreeObserver?.RemoveOnGlobalLayoutListener(_listener); _view.Dispose(); _view = null; } diff --git a/src/Android/Avalonia.Android/AvaloniaMainActivity.cs b/src/Android/Avalonia.Android/AvaloniaMainActivity.cs index 5ecbee0414..639082ed02 100644 --- a/src/Android/Avalonia.Android/AvaloniaMainActivity.cs +++ b/src/Android/Avalonia.Android/AvaloniaMainActivity.cs @@ -10,18 +10,6 @@ public class AvaloniaMainActivity : AvaloniaActivity { private protected static SingleViewLifetime? Lifetime; - public override void OnCreate(Bundle? savedInstanceState, PersistableBundle? persistentState) - { - // Global IActivatableLifetime expects a main activity, so we need to replace it on each OnCreate. - if (Avalonia.Application.Current?.TryGetFeature() - is AndroidActivatableLifetime activatableLifetime) - { - activatableLifetime.Activity = this; - } - - base.OnCreate(savedInstanceState, persistentState); - } - private protected override void InitializeAvaloniaView(object? initialContent) { // Android can run OnCreate + InitializeAvaloniaView multiple times per process lifetime. @@ -55,6 +43,12 @@ public class AvaloniaMainActivity : AvaloniaActivity if (_view is null) throw new InvalidOperationException("Unknown error: AvaloniaView initialization has failed."); } + + if (Avalonia.Application.Current?.TryGetFeature() + is AndroidActivatableLifetime activatableLifetime) + { + activatableLifetime.CurrentMainActivity = this; + } } protected virtual AppBuilder CreateAppBuilder() => AppBuilder.Configure().UseAndroid(); diff --git a/src/Android/Avalonia.Android/Platform/AndroidActivatableLifetime.cs b/src/Android/Avalonia.Android/Platform/AndroidActivatableLifetime.cs index a09c0ea88a..9679d39d5e 100644 --- a/src/Android/Avalonia.Android/Platform/AndroidActivatableLifetime.cs +++ b/src/Android/Avalonia.Android/Platform/AndroidActivatableLifetime.cs @@ -5,32 +5,71 @@ namespace Avalonia.Android.Platform; internal class AndroidActivatableLifetime : ActivatableLifetimeBase { - private IAvaloniaActivity? _activity; + private IAvaloniaActivity? _mainActivity, _intendActivity; - public IAvaloniaActivity? Activity + /// + /// While we primarily handle main activity lifecycle events. + /// Any secondary activity might send protocol or file activation. + /// + public IAvaloniaActivity? CurrentIntendActivity { - get => _activity; + get => _intendActivity; set { - if (_activity is not null) + if (_intendActivity is not null) { - _activity.Activated -= ActivityOnActivated; - _activity.Deactivated -= ActivityOnDeactivated; + _intendActivity.Activated -= IntendActivityOnActivated; } - _activity = value; + _intendActivity = value; - if (_activity is not null) + if (_intendActivity is not null) { - _activity.Activated += ActivityOnActivated; - _activity.Deactivated += ActivityOnDeactivated; + _intendActivity.Activated += IntendActivityOnActivated; + } + } + } + + public IAvaloniaActivity? CurrentMainActivity + { + get => _mainActivity; + set + { + if (_mainActivity is not null) + { + _mainActivity.Activated -= MainActivityOnActivated; + _mainActivity.Deactivated -= MainActivityOnDeactivated; + } + + _mainActivity = value; + + if (_mainActivity is not null) + { + _mainActivity.Activated += MainActivityOnActivated; + _mainActivity.Deactivated += MainActivityOnDeactivated; } } } - public override bool TryEnterBackground() => (_activity as Activity)?.MoveTaskToBack(true) == true; + public override bool TryEnterBackground() => (_mainActivity as Activity)?.MoveTaskToBack(true) == true; - private void ActivityOnDeactivated(object? sender, ActivatedEventArgs e) => OnDeactivated(e); + private void MainActivityOnDeactivated(object? sender, ActivatedEventArgs e) => OnDeactivated(e); + + private void MainActivityOnActivated(object? sender, ActivatedEventArgs e) + { + if (!IsIntendActivation(e.Kind)) + { + OnActivated(e); + } + } + + private void IntendActivityOnActivated(object? sender, ActivatedEventArgs e) + { + if (IsIntendActivation(e.Kind)) + { + OnActivated(e); + } + } - private void ActivityOnActivated(object? sender, ActivatedEventArgs e) => OnActivated(e); + private static bool IsIntendActivation(ActivationKind kind) => kind is ActivationKind.File or ActivationKind.OpenUri; }