In some situations you’ll want to keep some part of your application running even if the main application goes to sleep. For example the Android Clock applet when started will keep running even if you essentially close the Clock applet. It will display an icon in the status bar at the top right of the Android device and also include notifications.
Note: Code for this solution is available via my blog projects repository.
Getting Started
- Create a MAUI application
- We’ll add the package CommunityToolkit.Mvvm just so we can use the WeakReferenceMessenger to send messages to our platform specific code. We could ofcourse do this using an interface and register platform specific implementations of the interface, amongst other ways. But for simplicity here, we’ll pass messages around
- Add the MessageData record
public record MessageData(string Message, bool Start);
This will just be used in the IMessenger to pass commands to our platform specific code.
- In MauiProgram.cs before builder.Build() add the following code
builder.Services.AddSingleton<MainPage>(); builder.Services.AddSingleton<IMessenger, WeakReferenceMessenger>();
- We’re going to simply use code-behind (again to keep things simple), so in the MainPage.xaml, within the ContentPage content, add the following
<VerticalStackLayout> <Entry x:Name="Input" Margin="10" /> <Button Text="Start Service" Clicked="Start_OnClicked" Margin="10" /> <Button Text="Stop Service" Clicked="Stop_OnClicked" Margin="10" /> </VerticalStackLayout>
- Now in the code behind MainPage.xaml.cs add the following code
private readonly IMessenger _messenger; public MainPage(IMessenger messenger) { InitializeComponent(); _messenger = messenger; } private void Start_OnClicked(object sender, EventArgs e) { _messenger.Send(new MessageData(Input.Text, true)); } private void Stop_OnClicked(object sender, EventArgs e) { _messenger.Send(new MessageData(Input.Text, false)); }
At this point we have a basic test application and a way to start/stop the foreground service.
Android platform specific code
The first thing we’ll need to do is edit the Platforms/Android/MainApplication.cs file. We need to start by adding a notification channel which will be displayed in the statusbar and act as the interaction point with our foreground service.
The first thing we need is a channel id, this is a unique identifier within our application to denote the notification channel we’ll want to communicate with. It’s possible for us to have multiple notification channels, but in this instance we’ll just use the one. So in MainApplication add
public static readonly string ChannelId = "exampleServiceChannel";
Next, override OnCreate so it looks like this
public override void OnCreate() { base.OnCreate(); if (Build.VERSION.SdkInt >= BuildVersionCodes.O) { #pragma warning disable CA1416 var serviceChannel = new NotificationChannel(ChannelId, "Example Service Channel", NotificationImportance.Default); if (GetSystemService(NotificationService) is NotificationManager manager) { manager.CreateNotificationChannel(serviceChannel); } #pragma warning restore CA1416 } }
As you can see, this code is targeting API >= 21 (Oreo). We create a NotificationChannel with the ChannelId we declare earlier. The notification has a name “Example Service Channel” and an importance level. This needs to be set to Low or higher. If we set it anything above Low we’ll hear a sound when the notification is sent/updated. Whilst the user can disable this, you may or may not prefer a low importance so your service is less intrusive. Ofcourse it’s all dependent upon your requirements.
Before we get to the foreground service itself, let’s update the MainActivity.cs to handler our IMessenger messages from the UI.
In the MainActivity constructor add the following
var messenger = MauiApplication.Current.Services.GetService<IMessenger>(); messenger.Register<MessageData>(this, (recipient, message) => { if (message.Start) { StartService(message.Message); } else { StopService(); } });
This is just some plumbing code, basically when we press the start service button we’ll receive a message with Start as true and a message from our entry control. Obviously if we get start true we’ll call StartService and if not, we’ll call StopService.
Here’s the start and stop service code
private void StartService(string input) { var serviceIntent = new Intent(this, typeof(ExampleService)); serviceIntent.PutExtra("inputExtra", input); StartService(serviceIntent); } private void StopService() { var serviceIntent = new Intent(this, typeof(ExampleService)); StopService(serviceIntent); }
Start service is passed the entry text and creates an intent associated with our service code (ExampleService). We add a key/value in the PutExtra method. The key is whatever we want and obviously the value is whatever we want to associated with the key. This will be handled via our service, so the key would best be a const in a real world application.
The StartService(serviceIntent) call is used on an activity that is already in the foreground – so in our case to be able to press the button the application must be in the foreground. If your application might be in the background you can call StartForegroundService(serviceIntent) to both force the application into the foreground and start the service.
Two more things before we get to the service code. The service will require a small icon, so create the folder Platforms/Android/Resources/drawable and add a .PNG into this. Mine’s named AppIcon.png as it’s a duplicate of the actual application icon.
Now we need to declare the requirement of the user permission FOREGROUND_SERVICE, so edit the AndroindManifest.xml file and add the following within the manifest section
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
The Service code
To recap, at this point we have a basic user interface, which allows us to start and stop a service. We have a notification channel created along with the code for starting and stopping a service. We now need the service. Mine’s named ExampleService.cs and is within the Platforms/Android folder alongside the MainActivity.cs etc.
Create a basic service with the following code
[Service] internal class ExampleService : Service { public override IBinder OnBind(Intent intent) { return null; } }
and here’s the usings, just in case you need to check
using Android.App; using Android.Content; using Android.OS; using AndroidX.Core.App;
We’re not using the OnBind method which is used for “Bound Services” so simply return null here. We’ll also need the Service attribute on the class to register our service.
We now need to override the OnStartCommand which will get information for the key/value we added in the MainActivity (via PutExtra). We’ll also want to create an Intent back to our MainActivity so when the user clicks on the notification it can bring the application back into the foreground. Hence we create the notificationIntent.
The NotificationCompat.Builder needs to use the same channel id that we want to send messages to and it requires a PendingIntent within the builder. In the builder we must supply the icon image via SetSmallIcon and we’ll need the content title set. You can see we’re sending the message to the SetContentText.
Finally, unless we’re aiming to reuse this notification we call Build on it and pass this along with an id to the StartForeground.
public override StartCommandResult OnStartCommand(Intent intent, StartCommandFlags flags, int startId) { var input = intent.GetStringExtra("inputExtra"); var notificationIntent = new Intent(this, typeof(MainActivity)); var pendingIntent = PendingIntent.GetActivity(this, 0, notificationIntent, 0); var notification = new NotificationCompat.Builder(this, MainApplication.ChannelId) .SetContentTitle("Example Service") .SetContentText(input) .SetSmallIcon(Resource.Drawable.AppIcon) .SetContentIntent(pendingIntent) .Build(); StartForeground(1, notification); return StartCommandResult.NotSticky; }
If we are intending to send multiple messages you would want to create the notification (without the Build call) and reuse this to update a notification channel, hence you’d call Build within StartForeground code, like this
var notification = new NotificationCompat.Builder(this, MainApplication.ChannelId) .SetContentTitle("Example Service") .SetContentText(input) .SetSmallIcon(Resource.Drawable.AppIcon) .SetContentIntent(pendingIntent) StartForeground(1, notification.Build());
If we imagine that after StartForeground we start some task, for example downloading a file, then remember this should be run on a background thread and it could then update the notification before again calling the StartForeground(1, notification.Build()) code, ofcourse assuming you’re running the process from the service.
Finally this method returns StartCommandResult.NotSticky. This tells the OS to not bother recreating the application if (for example) the device runs out of memory. StartCommandResult.Sticky is used to tell the OS to recreate the service when it has enough memory. There’s also the option StartCommandResult.RedeliverIntent which is like the NotSticky but if the service is killed before calling stopSelf() for a given intent then the intent will be re-delivered until it completes.
References
This code presented here is based on How to Start a Foreground Service in Android (With Notification Channels) but translated to C# and MAUI code.