Browse Source

Implement IDispatcherImpl for Android based on Looper and MessageQueue (#18975)

pull/18990/head
Nikita Tsukanov 1 year ago
committed by GitHub
parent
commit
bc5816d61a
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 168
      src/Android/Avalonia.Android/AndroidDispatcherImpl.cs
  2. 3
      src/Android/Avalonia.Android/AndroidPlatform.cs
  3. 87
      src/Android/Avalonia.Android/AndroidThreadingInterface.cs

168
src/Android/Avalonia.Android/AndroidDispatcherImpl.cs

@ -0,0 +1,168 @@
using System;
using System.Diagnostics;
using Android.OS;
using Avalonia.Controls.Documents;
using Avalonia.Threading;
using Java.Lang;
using App = Android.App.Application;
using Object = Java.Lang.Object;
namespace Avalonia.Android
{
internal sealed class AndroidDispatcherImpl : IDispatcherImplWithExplicitBackgroundProcessing,
IDispatcherImplWithPendingInput
{
[ThreadStatic] private static bool? s_isUIThread;
private readonly Looper _mainLooper;
private readonly Handler _handler;
private readonly Runnable _signaler;
private readonly Runnable _timerSignaler;
private readonly Runnable _wakeupSignaler;
private readonly MessageQueue _queue;
private readonly object _lock = new();
private bool _signaled;
private bool _backgroundProcessingRequested;
public AndroidDispatcherImpl()
{
_mainLooper = App.Context.MainLooper ??
throw new InvalidOperationException(
"Application.Context.MainLooper was not expected to be null.");
if (!CurrentThreadIsLoopThread)
throw new InvalidOperationException("This class should be instanciated from the UI thread");
_handler = new Handler(_mainLooper);
_signaler = new Runnable(OnSignaled);
_timerSignaler = new Runnable(OnTimer);
_wakeupSignaler = new Runnable(() => { });
_queue = Looper.MyQueue();
Looper.MyQueue().AddIdleHandler(new IdleHandler(this));
CanQueryPendingInput = OperatingSystem.IsAndroidVersionAtLeast(23);
}
public event Action? Timer;
private void OnTimer() => Timer?.Invoke();
public event Action? Signaled;
private void OnSignaled()
{
lock (_lock)
_signaled = false;
Signaled?.Invoke();
}
public bool CurrentThreadIsLoopThread
{
get
{
if (s_isUIThread.HasValue)
return s_isUIThread.Value;
var uiThread = OperatingSystem.IsAndroidVersionAtLeast(23)
? _mainLooper.IsCurrentThread
: _mainLooper.Thread.Equals(Java.Lang.Thread.CurrentThread());
s_isUIThread = uiThread;
return uiThread;
}
}
public void Signal()
{
lock (_lock)
{
if(_signaled)
return;
_signaled = true;
_handler.Post(_signaler);
}
}
readonly Stopwatch _clock = Stopwatch.StartNew();
public long Now => _clock.ElapsedMilliseconds;
public void UpdateTimer(long? dueTimeInMs)
{
_handler.RemoveCallbacks(_timerSignaler);
if (dueTimeInMs.HasValue)
{
var delay = dueTimeInMs.Value - Now;
if (delay > 0)
_handler.PostDelayed(_timerSignaler, delay);
else
_handler.Post(_timerSignaler);
}
}
class IdleHandler : Object, MessageQueue.IIdleHandler
{
private readonly AndroidDispatcherImpl _parent;
public IdleHandler(AndroidDispatcherImpl parent)
{
_parent = parent;
}
public bool QueueIdle()
{
_parent.OnIdle();
return true;
}
}
public event Action? ReadyForBackgroundProcessing;
public void RequestBackgroundProcessing()
{
_backgroundProcessingRequested = true;
}
void OnIdle()
{
tailCall:
if (_backgroundProcessingRequested)
{
_backgroundProcessingRequested = false;
ReadyForBackgroundProcessing?.Invoke();
}
if (_backgroundProcessingRequested)
{
// Dispatcher requested background processing again, however if the queue is empty and we
// just return here, Android's Looper will go to sleep and won't call us again and we'll have
// "background" jobs not being processed
// So we need to examine the queue state to prevent that scenario
lock (_lock)
{
// There are higher priority jobs enqueued, we'll be called again
if (_signaled)
return;
}
if (CanQueryPendingInput)
{
if (!HasPendingInput)
// There are no events in the queue, so if we just return here, Looper will go to sleep,
// so just run our logic again
goto tailCall;
// Nothing to do otherwise, we'll be called again after higher priority events get processed
}
else
{
// On this API level we can't check if there is pending input,
// so we explicitly wake up the Looper to make sure that it will call idle hooks again
// before going to sleep
_handler.Post(_wakeupSignaler);
}
}
}
public bool CanQueryPendingInput { get; }
// See check in ctor
#pragma warning disable CA1416
public bool HasPendingInput => !_queue.IsIdle;
#pragma warning restore CA1416
}
}

3
src/Android/Avalonia.Android/AndroidPlatform.cs

@ -12,6 +12,7 @@ using Avalonia.OpenGL.Egl;
using Avalonia.Platform;
using Avalonia.Rendering;
using Avalonia.Rendering.Composition;
using Avalonia.Threading;
using Avalonia.Vulkan;
namespace Avalonia
@ -84,7 +85,7 @@ namespace Avalonia.Android
.Bind<IWindowingPlatform>().ToConstant(new WindowingPlatformStub())
.Bind<IKeyboardDevice>().ToSingleton<AndroidKeyboardDevice>()
.Bind<IPlatformSettings>().ToSingleton<AndroidPlatformSettings>()
.Bind<IPlatformThreadingInterface>().ToConstant(new AndroidThreadingInterface())
.Bind<IDispatcherImpl>().ToConstant(new AndroidDispatcherImpl())
.Bind<IPlatformIconLoader>().ToSingleton<PlatformIconLoaderStub>()
.Bind<IRenderTimer>().ToConstant(new ChoreographerTimer())
.Bind<PlatformHotkeyConfiguration>().ToSingleton<PlatformHotkeyConfiguration>()

87
src/Android/Avalonia.Android/AndroidThreadingInterface.cs

@ -1,87 +0,0 @@
using System;
using System.Threading;
using Android.OS;
using Avalonia.Platform;
using Avalonia.Reactive;
using Avalonia.Threading;
using App = Android.App.Application;
namespace Avalonia.Android
{
internal sealed class AndroidThreadingInterface : IPlatformThreadingInterface
{
private Handler _handler;
private static Thread? s_uiThread;
public AndroidThreadingInterface()
{
_handler = new Handler(App.Context.MainLooper
?? throw new InvalidOperationException("Application.Context.MainLooper was not expected to be null."));
}
public IDisposable StartTimer(DispatcherPriority priority, TimeSpan interval, Action tick)
{
if (interval.TotalMilliseconds < 10)
interval = TimeSpan.FromMilliseconds(10);
var stopped = false;
Timer? timer = null;
timer = new Timer(_ =>
{
if (stopped)
return;
EnsureInvokeOnMainThread(() =>
{
try
{
tick();
}
finally
{
if (!stopped)
timer!.Change(interval, Timeout.InfiniteTimeSpan);
}
});
},
null, interval, Timeout.InfiniteTimeSpan);
return Disposable.Create(() =>
{
stopped = true;
timer.Dispose();
});
}
private void EnsureInvokeOnMainThread(Action action) => _handler.Post(action);
public void Signal(DispatcherPriority prio)
{
EnsureInvokeOnMainThread(() => Signaled?.Invoke(null));
}
public bool CurrentThreadIsLoopThread
{
get
{
if (s_uiThread != null)
return s_uiThread == Thread.CurrentThread;
var isOnMainThread = OperatingSystem.IsAndroidVersionAtLeast(23)
? Looper.MainLooper?.IsCurrentThread
: Looper.MainLooper?.Thread.Equals(Java.Lang.Thread.CurrentThread());
if (isOnMainThread == true)
{
s_uiThread = Thread.CurrentThread;
return true;
}
return false;
}
}
public event Action<DispatcherPriority?>? Signaled;
}
}
Loading…
Cancel
Save