3

In my .NET Maui Android app, I'm trying to use an AndroidX.Work.Worker to fire up a long running process that listens for events from the Accelerometer. Problem I'm having is that as soon as the DoWork method exits, the Worker terminates. So I need to keep the Worker running by preventing DoWork from exiting, but can't figure out the code for this.

    public class DropDetectorWorker : AndroidX.Work.Worker
    {
        public override Result DoWork()
        {
            CreateInitialNotificationChannel();
            
            var foregroundInfo = new ForegroundInfo(NotificationId++, BuildInitialNotification());
            SetForegroundAsync(foregroundInfo);
            
            InitialiseDropDetector();
            
            // TODO - prevent DoWork from exiting
            return Result.InvokeSuccess();
        }
        
        private void InitialiseDropDetector()
        {
            _dropDetector.DropDetected += DropDetector_DropDetected;
            _dropDetector.Start();
        }
        
        private void DropDetector_DropDetected(object sender, DropDetectedEventArgs e)
        {
            _lastDropDetected = SystemDate.UtcNow;
            TriggerAutoclaimNotification(e.Magnitude);
        }
        
        private void CreateInitialNotificationChannel()
        {
            NotificationChannel channel = new(INITIAL_NOTIFICATION_CHANNEL_ID, INITIAL_NOTIFICATION_CHANNEL_NAME, NotificationImportance.Default)
            {
                LightColor = Microsoft.Maui.Graphics.Color.FromRgba(0, 0, 255, 0).ToInt(),
                LockscreenVisibility = NotificationVisibility.Public
            };
        
            _notificationManager.CreateNotificationChannel(channel);
        }
        
        private void TriggerAutoclaimNotification(double magnitude)
        {
            var foregroundInfo = new ForegroundInfo(NotificationId, BuildMakeClaimNotification(magnitude));
            SetForegroundAsync(foregroundInfo);
        }
    }
Netricity
  • 2,550
  • 1
  • 22
  • 28
  • In case the listening is done through out the app's lifetime, it should be inside a foreground service. A worker is at most long living enough to support a download, but that's it: https://developer.android.com/guide/background/persistent – o_w Mar 19 '23 at 07:06
  • @o_w, I did start this project with a foreground svc only to discover that Android restricts what a foreground svc can do if auto-started from device boot (which is what my app needs to do), that's why I went with the Worker approach. Also, according to the documentation, it is fine to use a Worker for a task that runs for longer periods, as the call to SetForegroundAsync in DoWork starts and manages a foreground svc internally: https://developer.android.com/guide/background/persistent/how-to/long-running – Netricity Mar 20 '23 at 09:08
  • The problem here is not that the worker starts the foreground service, its that you can't register to event in DoWork, because returning Result.InvokeSuccess(); will terminate the worker. So this is not the way. The documentation specifies downloading, not a worker that stays alive forever. Please reconsider this approach. – o_w Mar 20 '23 at 09:38
  • It would be straightforward to **launch a long-running, event-driven task** e.g. as a *`Task`*. Is there something in particular you're gaining by using `Worker.DoWork` that makes it a requirement? In other words, would answers that take different approaches still be a fit for what you're looking for? – IVSoftware Mar 20 '23 at 11:04
  • @IVSoftware, my app needs to run in the background (without having to open the UI) while it listens for accelerometer events. The only ways to achieve this on Android that I know of are either with an Android.App.Service (aka Foreground Service) or an AndroidX.Work.Worker. Using the Worker approach requires preventing the DoWork method from completing, because when it does, the Worker terminates. I thought about using a ```while (true) { Thread.Sleep(1000); }``` but this feels wrong. – Netricity Mar 20 '23 at 11:15
  • Here's what I'm wondering. Could the same approach that keeps a `Task` from completing be adapted and used in your `DropDetectorWorker` constuctor? I'll show what I was thinking which may or may not be a good fit. – IVSoftware Mar 20 '23 at 11:24
  • @o_w, I've been able to overcome the Android OS Foreground svc restrictions (have to start App at least once via its home screen icon and also accept Allow Notifications permissions request), so now the Foreground svc is working as I want and no need for the Worker. Thanks for pushing me back that way! – Netricity Mar 20 '23 at 15:56

2 Answers2

3

Your question is about how to launch a long-running, event-driven task.

Could the same approach that keeps a Task from completing be adapted and used in your DropDetectorWorker constuctor? I'll show what I was thinking which may or may not be a good fit.

This sample keeps it simple for demonstration purposes. A clean, minimal example of such a task would be a clock updater that (without blocking the UI thread) samples the current time ~10x a second and fires an event when the second changes. This code snippet shows how one might launch this task in the App constructor, letting it run in the background.

This general approach could be easily adapted to the Accelerometer monitoring you want your app to do.


public partial class App : Application
{
    public App()
    {
        InitializeComponent();

        MainPage = new AppShell();
        _ = Task.Run(() => longRunningEventDrivenTask(LongRunningTskCanceller.Token));
    }
    CancellationTokenSource LongRunningTskCanceller { get; } = new CancellationTokenSource();

    private async Task longRunningEventDrivenTask(CancellationToken token)
    {
        int prevSecond = -1;
        while(!token.IsCancellationRequested)
        {
            var now = DateTime.Now;
            if (now.Second != prevSecond)
            {
                OnSecondChanged(new SecondChangedEventArgs(timestamp: now));
                prevSecond= now.Second;
            }
            await Task.Delay(TimeSpan.FromSeconds(0.1), token);
        }
    }
    public event SecondChangedEventHandler SecondChanged;
    protected virtual void OnSecondChanged(SecondChangedEventArgs e)
    {
        SecondChanged?.Invoke(this, e);
    }
}

Event

public delegate void SecondChangedEventHandler(Object sender, SecondChangedEventArgs e);
public class SecondChangedEventArgs : EventArgs
{
    public SecondChangedEventArgs(DateTime timestamp)
    {
        Timestamp = timestamp;
    }
    public DateTime Timestamp { get; }
}
IVSoftware
  • 5,732
  • 2
  • 12
  • 23
  • This looks like it could be a good approach, so thanks for the suggestion. However, I have reverted to the Foreground service approach and have now found out how to overcome the Android OS restrictions it has (have to start App at least once via its home screen icon and also accept Allow Notifications permissions request). – Netricity Mar 20 '23 at 15:54
  • 1
    Glad things got worked out! Just a thought - consider posting a [Self-Answer](https://stackoverflow.com/help/self-answer) showing how you ended up implementing the foreground service (if that can be sampled in a minimal way). – IVSoftware Mar 20 '23 at 16:02
1

Ultimately I just needed to start a long-running task on Android. I had gone the Worker approach as I'd had problems with the Foreground Service approach not having permissions to create a Notification. I've since discovered how to get round these restrictions:

  • Android needs the user to open the app at least once using the app's home screen icon before Notifications can be created.
  • When prompted, the user will must allow Notifications; this happens the first time the app calls NotificationManager.CreateNotificationChannel

once these have been satisfied, the Foreground service fires up and I can run my accelerometer watcher.

Netricity
  • 2,550
  • 1
  • 22
  • 28
  • 1
    Make sure to add permission check before using the mentioned usage. Users can deny permission after giving it(From Android apps settings). And you'll have to either ask again permission from the user, or block the usage(with a warning message toward the user, in app or otherwise) – o_w Mar 21 '23 at 06:37
  • Glad that you solved it. You could mark your answer that may help others with same issue. – Zack Mar 21 '23 at 09:26