Browse Source

Fix android uri activation (#16477)

* Make SetContentView call delayed

* Make it possible to run secondary activity for OpenUri activation

* Add activation DataSchemeActivity example
Max Katz 1 year ago
parent
commit
413ff78ebb

+ 18 - 3
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<App>
     {
         protected override AppBuilder CustomizeAppBuilder(AppBuilder builder)
@@ -25,4 +25,19 @@ namespace ControlCatalog.Android
                  });
         }
     }
+
+    /// <summary>
+    /// Special activity to handle OpenUri activation.
+    /// `AvaloniaActivity` internally will redirect parameters to the Avalonia Application.
+    /// </summary>
+    [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();
+        }
+    }
 }

+ 24 - 8
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<ActivatedEventArgs>? _onActivated, _onDeactivated;
     private GlobalLayoutListener? _listener;
     private object? _content;
+    private bool _contentViewSet;
     internal AvaloniaView? _view;
 
     public Action<int, Result, Intent?>? 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<IActivatableLifetime>()
+            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;
         }

+ 6 - 12
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<IActivatableLifetime>()
-            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<IActivatableLifetime>()
+            is AndroidActivatableLifetime activatableLifetime)
+        {
+            activatableLifetime.CurrentMainActivity = this;
+        }
     }
 
     protected virtual AppBuilder CreateAppBuilder() => AppBuilder.Configure<Application>().UseAndroid();

+ 52 - 13
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
+    /// <summary>
+    /// While we primarily handle main activity lifecycle events.
+    /// Any secondary activity might send protocol or file activation.
+    /// </summary>
+    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;
 }