committed by
GitHub
3 changed files with 170 additions and 88 deletions
@ -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
|
|||
} |
|||
} |
|||
@ -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…
Reference in new issue