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
release/11.1.3
Max Katz 2 years ago
parent
commit
865e2244b8
  1. 21
      samples/ControlCatalog.Android/MainActivity.cs
  2. 32
      src/Android/Avalonia.Android/AvaloniaActivity.cs
  3. 18
      src/Android/Avalonia.Android/AvaloniaMainActivity.cs
  4. 65
      src/Android/Avalonia.Android/Platform/AndroidActivatableLifetime.cs

21
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();
}
}
}

32
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;
}

18
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();

65
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;
}

Loading…
Cancel
Save