Android notifications using MAUI (Part 5 of 10)

So the last couple of posts started to look at using styles within the notifications. We’re actually going to continue in that vein by looking at the message style and also direct replies, i.e. a style where we get a text entry control in the notification. This cover the tutorial Notifications Tutorial Part 5 – MESSAGING STYLE + DIRECT REPLY – Android Studio Tutorial and unlike the previous couple of posts where the style only partially worked or didn’t work at all. This one worked as expected.

Overview

What we’re aiming to implement here is…

Imagine a chat application which ofcourse might go into the background and yet we want to notify the user when a message appears and allow them to reply to that message via the notification.

We’ll start by adding our message type and our database to store the messages (okay a simple collection, not a database).

First off, add a new class named Message to the Platforms/Android folder/namespace. Ofcourse, as I’ve mentioned previously, this is just the simplest way to do this, obviously we’d have this as a service etc. in a real world application, anyway the Message class looks like this

public class Message
{
    public Message(string text, string sender)
    {
        Text = text;
        Sender = sender;
        // The Timestamp is required for the NotificationCompat.MessagingStyle.Message object, so we'll just generate here
        Timestamp = DateTime.Now.Millisecond; // prob. doesn't do the same as the Java example, need to check System.currentTimeMillis()
    }

    public string Text { get; }
    public long Timestamp { get; }
    public string Sender { get; }
}

It should be self-explanatory apart from the Timestamp, this is required later in our NotificationCompat.MessageStyle.Message – we could create it when that’s called or when the message is created.

We’re going to be a little naughty here (again to keep things simple) by making our SendOnChannel1 a static method. The reason we’re doing this is that we need to create a BroadcastReceiver to allow us to reply to messages and it needs to call the notification channel to update it. So, let’s jlook at the current state of this method, but first let’s add our pretend database to the MainActivity like this

public static List<Message> Messages = new List<Message>();

We now want to just prepopulate our messages, so in the MainActivity constructor add

Messages.Add(new Message("Good morning!", "Jim"));
// null will be from us, and hence will use the "Me" from the messaging style
Messages.Add(new Message("Hello", null)); 
Messages.Add(new Message("Ji!", "Jenny"));

and now to the SendOnChannel1 changes (well I’ll just show the whole method as it’s almost all changed)

public static void SendOnChannel1(Context context)
{
  var activityIntent = new Intent(context, typeof(MainActivity));
  var contentIntent = PendingIntent.GetActivity(context, 0, activityIntent, 0);

  var remoteInput = new RemoteInput.Builder("key_text_reply")
    .SetLabel("Your answer...")
    .Build();

  PendingIntent replyPendingIntent = null;

  if (Build.VERSION.SdkInt >= BuildVersionCodes.N)
  {
    var replyIntent = new Intent(context, typeof(DirectReplyReceiver));
    replyPendingIntent = PendingIntent.GetBroadcast(context, 0, replyIntent, 0);
  }
  else
  {
    // older versions of Android 
    // start activity instead PendingIntent.GetActivity()
    // cancel notification with notificationManagerCompat.Cancel(id)
  }

  var replyAction = new NotificationCompat.Action.Builder(Resource.Drawable.AppIcon, "Reply", replyPendingIntent)
    .AddRemoteInput(remoteInput)
    .Build();

  var messagingStyle = new NotificationCompat.MessagingStyle("Me");
  messagingStyle.SetConversationTitle("Group Chat");

  foreach(var chatMessage in Messages)
  {
    var notificationMessage =
      new NotificationCompat.MessagingStyle.Message(
        chatMessage.Text, 
        chatMessage.Timestamp,
        chatMessage.Sender);
    messagingStyle.AddMessage(notificationMessage);
  }

  var notification = new NotificationCompat.Builder(context, MainApplication.Channel1Id)
    // mandatory
    .SetSmallIcon(Resource.Drawable.abc_ab_share_pack_mtrl_alpha)
    .SetStyle(messagingStyle)
    .AddAction(replyAction)
    .SetColor(Colors.Blue.ToInt())
    .SetPriority(NotificationCompat.PriorityHigh)
    .SetCategory(NotificationCompat.CategoryMessage)
    .SetContentIntent(contentIntent)
    // when we tap the notification it will close
    .SetAutoCancel(true)
    // only show/update first time
    .SetOnlyAlertOnce(true)
    .Build();

  var notificationManager = NotificationManagerCompat.From(context);
  notificationManager.Notify(1, notification);
}

There’s a lot to take in there. The first obvious different (other than the method going static) is the use of RemoteInputBuilder. The remote builder takes a key (a string) which we use later to retrieve the input from. The “Your answer…” text is what will be displayed as a hint in the reply text entry that we’ll being implementing.

Next we have some code to ensure the correct version of Android is being targeted (I don’t have code for a previous version, so I assume if the correct version or above is not being used then this feature is not available). In here we create the received for replies via our notification. So you can see we create a broadcast intent which we pass into our replyAction. Notices how in the previous RemoteInputBuilder code we also have a key key_text_reply this is used in the reciever, as we’ll see later.

We then create an the replyAction which will become our action when the user replies to a message.

Next, we supply the current messages to the notification, i.e. to pre-populate and update the list of messages. Then NotificationCompat.Builder is probably self-explanatory now.

Oh I almost forgot, in the code above we also have the usage of typeof(DirectReplyReceiver) this will handle the reply text etc. So create yourself a class named DirectReplyReceiver which should look like this

[BroadcastReceiver(Enabled = true, Exported = false)]
public class DirectReplyReceiver : BroadcastReceiver
{
    public override void OnReceive(Context context, Intent intent)
    {
        var remoteInput = RemoteInput.GetResultsFromIntent(intent);
        if (remoteInput != null)
        {
            var replyText = remoteInput.GetCharSequence("key_text_reply");
            var answer = new Message(replyText, null);
            MainActivity.Messages.Add(answer);

            // without calling this the message will get added to the Messages but left in limbo
            // the reply will look like it's stuck sending a message (i.e. spinning progress bar and no updates)
            MainActivity.SendOnChannel1(context);
        }
    }
}

In the OnReceive we get the intent value using the key_text_reply key to get the text the user entered then for our demo we add it to the messages collection to update the notification. We need to then call SendOnChannel1 again to get it to complete updating of the notifications.

Code

Code for this an subsequent posts is found on my blog project.

Android notifications using MAUI (Part 4 of 10)

Extending on what we did in “Android notifications using MAUI (Part 3 of 10)” we’re going to look an nig text style and inbox style as per Notifications Tutorial Part 4 – BIG PICTURE STYLE + MEDIA STYLE – Android Studio Tutorial.

Overview

This is not a very useful post as I again found that either I’m missing something or there’s issues in MAUI, for the sake of argument I’ll assume it’s my fault. Anyway I’m still going to show how (I think) you can display a larger bitmap as well as apply the MediaStyle which has the ability to handle up to five actions
(obviously useful for play, pause, back, forward etc.) as well as three actions in a collapsed state.

In the SendOnChannel1 method from our previous posts, we’ll add a BigPictureStyle to our NotificationCompat.Builder like this, to begin with I’ve just renamed the variable as per the tutorial video

var picture = BitmapFactory.DecodeResource(Android.App.Application.Context.Resources, Resource.Drawable.AppIcon);

Now we add the style like this

.SetStyle(new NotificationCompat
  .BigPictureStyle()
  .BigPicture(picture)
  .BigLargeIcon(null))

Note: Again I’m having trouble with the bitmap side of things, i.e. not displaying on the emulator. I will update here if I find it’s something I’ve done incorrectly. What you will see is a larger notification when you expand the notifications via the status bar, presumably to accommodate my picture

Let’s now create a pretend media play or at least the actions for one on channel 2 notifications. As such I added PNG’s to Platforms/Android/Resources/drawable for like, dislike, next, pause, previous actions.

We need to update our SendOnChannel2 method to add the following actions

.AddAction(Resource.Drawable.dislike, "Dislike", null)
.AddAction(Resource.Drawable.previous, "Previous", null)
.AddAction(Resource.Drawable.pause, "Pause", null)
.AddAction(Resource.Drawable.next, "Next", null)
.AddAction(Resource.Drawable.like, "Like", null)

Nothing too much different there apart from I’m not bothering to set intents for the actions (i.e. they do nothing). Now we need to set the style to MediaStyle and here we supply three indexes (zero-based) into our actions to denote the actions available when the notification is not expanded.

.SetStyle(new AndroidX.Media.App.NotificationCompat.MediaStyle()
  // the id's for the actions listed as actions
  .SetShowActionsInCompactView(1, 2, 3) 
  /*.SetMediaSession(_mediaSession.SessionToken)*/) 

As you can I’ve commented out the SetMediaSession (which is shown in the code listed at the end of this post) – I wasn’t able to get this to work as per the tutorial video, when declaring it in the constructor I was getting a JNI failure – again this might be something I’ve done wrong, so best to take a look at the source code on my repos. and decide what to do with this.

Code

Code for this an subsequent posts is found on my blog project.

Android notifications using MAUI (Part 3 of 10)

Extending on what we did in “Android notifications using MAUI (Part 2 of 10)” we’re going to look at adding text style and inbox style as per Notifications Tutorial Part 3 – BIG TEXT STYLE + INBOX STYLE – Android Studio Tutorial.

Overview

We’re going to continue using the code we implemented in the last part of this set of posts. We’re going to add a large icon to the notification as well using some built-in styling to style our notification. We’ll also use another style for our channel 2 notifications called the InboxStyle which will display messages more like a list.

Before we get into this the code for setting the large icon does not appear to work correctly and as mentioned in part 1 of this series of posts, some styles don’t seem to work as expected in general, but we’ll go through the process of writing the code and maybe I can return to in the future if I find a way to get things to work.

Let’s get started

To add an icon to the Android resources, go to Platforms/Android/Resources and add a folder named drawables. Within this add a .PNG. I’m using my application’s icon which I exported/saved to a .PNG. My file is named AppIcon.png.

Now, in the MainActivity.SendOnChannel1 method, before we create the notification add the following line

var largeIcon = BitmapFactory.DecodeResource(
   Android.App.Application.Context.Resources, Resource.Drawable.AppIcon
);

Notice how our AppIcon is accessible (or will be when you build the project) from Resource.Drawable.AppIcon in other words the filename excluding the extension becomes our resource id. To use this icon we need to pass a bitmap to the SetLargeIcon method, hence we’re using the BitmapFactory to decode the resource.

Now, if we add the following to the notification builder we should see a large icon displayed alongside the message

.SetLargeIcon(largeIcon)

Remember that a small icon is required for a notification. The large icon is not required. Let’s also change our small icon to use the same icon, so change SetSmallIcon to this

.SetSmallIcon(Resource.Drawable.AppIcon)

Next we want to change our notification style. This is done through SetStyle and we can supply different built-in styles, so the code for this looks like this (added to the NotificationCompat.Builder)

.SetStyle(new NotificationCompat
                .BigTextStyle()
                .BigText("Some Big Text")
                .SetBigContentTitle("Big Content Title")
                .SetSummaryText("Summary Text"))

Lastly we’ll display the messages in channel 2 using the InboxStyle, which will allows us to add up to seven lines, like a list. To add this go to SendOnChannel2 and add the following code

.SetStyle(new NotificationCompat
  .InboxStyle()
    .AddLine("This is line 1")
    .AddLine("This is line 2")
    .AddLine("This is line 3")
    .SetBigContentTitle("Big Content Title")
    .SetSummaryText("Summary Text"))

Code

Code for this an subsequent posts is found on my blog project.

Android notifications using MAUI (Part 2 of 10)

In “Android notifications using MAUI (Part 1 of 10)” we created a simple MAUI application and implemented the Android specific code for notifications. In part 2 we mirror Notifications Tutorial Part 2 – ACTION BUTTONS & COLOR – Android Studio Tutorial by adding colour to our notifications along with action buttons.

Overview

We can customize our notifications by changing some parts of the notification’s foreground colour, we can also add buttons (and more) to the notification.

In some cases, when using a foreground service (for example) our application may not no longer be visible, but we want the user to be able to still interact with our application via the notifications. So let’s imagine a stopwatch or better still don’t imagine one, instead go and look at the Android Clock applet, when it goes into the background the notification and foreground service are made available, from here we can see the stop watch running but also press buttons to pause and reset the stop watch, these are action buttons and if your application is designed well, then these can carry out functionality within the foreground service that will appear in the main application when it reappears in the foreground.

Let’s get started

To ensure we’re overwriting our previous application/notification channels etc. uninstall the application it’s it’s on your emulator.

Let’s begin by editing the MainActivity SendOnChannel1 method from the previous post. Here’s what the code should look like

var activityIntent = new Intent(this, typeof(MainActivity));
var contentIntent = PendingIntent.GetActivity(this, 0, activityIntent, 0);

var broadcastIntent = new Intent(this, typeof(NotificationReceiver));
broadcastIntent.PutExtra(MainApplication.ToastMessage, message);
var actionIntent = PendingIntent.GetBroadcast(this, 0, broadcastIntent, PendingIntentFlags.UpdateCurrent);

var notification = new NotificationCompat.Builder(this, MainApplication.Channel1Id)
  .SetSmallIcon(Resource.Drawable.abc_ab_share_pack_mtrl_alpha)
  .SetContentTitle(title)
  .SetContentText(message)
  .SetPriority(NotificationCompat.PriorityHigh)
  .SetCategory(NotificationCompat.CategoryMessage)
  // set the fore colour for the button etc.
  .SetColor(Colors.Red.ToInt())
  .SetContentIntent(contentIntent)
  // when we tap the notification it will close
  .SetAutoCancel(true)
  // only show/update first time
  .SetOnlyAlertOnce(true)
  // can add upto three action buttons
  .AddAction(Resource.Drawable.abc_edit_text_material, "Toast", actionIntent)
  .Build();

_notificationManager.Notify(1, notification);

The first change (the first couple of lines) is that when the user clicks on a notification it does nothing in the original code, but by adding the content intent (as per the code above) we essentially tell the notification that when clicked go to this activity. In our case we create an intent for the MainActivity, but the notification requires a PendingIntent so we create that using PendingIntent.GetActivity. So basically clicking on the notification will bring the application to the foreground.

We’re going to also add a button to our notification, which you can see being set via AddAction (we’re again just reusing an existing resource here), the action/button name is “Toast” and we need to supply an intent for when it’s clicked (null will just not do anything). So we set the actionIntent to the one we created at the top of the method.

The actionIntent is going to be a BroadcastReceiver. So, we first create an intent for typeof(NotificationReceiver) (we’ll look at this type soon) and we add a key/value pair. The key is currently just a const in MainApplication (again for a real world application you’d probably have a class specific for these), it looks like this in MainApplication

public static readonly string ToastMessage = "toastMessage";

Back to the SendOnChannel1 code, we pass the message along with the ToastMessage key, so as you’d probably expect we’re going to popup up a toast message in Android with our message when the action button “Toast” is clicked.

The other changes to this code include setting the foreground colour. Actually this only seems to set the colour of progress bars and actions etc. not the main title and message. We SetAutoCancel to true to close the notification when tapped, we also add SetOnlyAlertOnce so only the first message of high importance on this channel causes the sound to be played and popup display, subsequent messages are just sent to the notification – the is less intrusive especially if you have potentially lots of notification updates on a high priority channel.

Oh, and I almost forgot, we use SetColor to set our colour. Now I’m using the MAUI colours, but ofcourse you may have a colour set as a resource i.e. Resource.Color.notificationColour taken from your Platform/Andourse/Resources/values/colors.xml file.

Adding a BroadcastReceiver

Android would normally require that we add the receiver to the application’s AndroindManifest.xml (application section). But MAUI allows us to declare the received using attributes, so create a new class named NotificationReceiver and it should look like this

[BroadcastReceiver(Enabled = true, Exported = false)]
public class NotificationReceiver : BroadcastReceiver
{
    public override void OnReceive(Context context, Intent intent)
    {
        var message = intent.GetStringExtra(MainApplication.ToastMessage);
        Toast.MakeText(context, message, ToastLength.Short).Show();
    }
}

Beautifully simple. We need to override the OnReceive method, which as the name suggests, receives messages. We get the value for the given key (ToastMessage) and then use the Android Toast code to display a popup message at the bottom of the Android screen). The only other thing to point out is that, as mentioned, we use the BroadcastReceiver attribute to register our receiver.

Code

Code for this an subsequent posts is found on my blog project.

Android notifications using MAUI (Part 1 of 10)

In my previous post Android Foreground Service using MAUI we looked at implementing a foreground service for Android using MAUI. We touched on notification channels and notifications in general but I decided to go a bit deeper into learning about what we can do with notifications…

I found a good way to learn what Android had to offer was by going through the excellent set of tutorials on the “Coding in Flow” channel on youtube (starting with Notifications Tutorial Part 1 – NOTIFICATION CHANNELS – Android Studio Tutorial).

We’re going to follow through those tutorials in the this and the next nine posts. The intention is to recreate the tutorials but using MAUI and C#, however I know (as I’ve already tried the code out) that there are several areas I was not able to get things to work 100% the same way as the tutorials. Now, this may be due to my code or the current lack of support in MAUI or simply that things have changed in the four years since those tutorials were posted – anyway so upfront, I had problems with some of the notification styles and also with the large icon – if I do figure a way to implement the same functionality I will update this and the subsequent post.

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(int Channel, string Title, string Message);
    

    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 use code-behind (again to keep things simple), so in the MainPage.xaml, within the ContentPage content, add the following
    <VerticalStackLayout>
       <Entry Text="Title" x:Name="Title" Margin="10" />
       <Entry Text="Message" x:Name="Message" Margin="10" />
    
       <Button Text="Send on Channel 1" Clicked="Channel1_OnClicked" Margin="10" />
       <Button Text="Send on Channel 2" Clicked="Channel2_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 Channel1_OnClicked(object sender, EventArgs e)
    {
       _messenger.Send(new MessageData(1, Title.Text, Message.Text));
    }
    
    private void Channel2_OnClicked(object sender, EventArgs e)
    {
       _messenger.Send(new MessageData(2, Title.Text, Message.Text));
    }
    

At this point we have a basic test application with ways to enter a title, a message and send to notification channel 1 and/or channel 2 (we’ll discuss these in the next section).

Android platform specific code

So the MAUI shared code is complete, but now we need to start writing code specific to the Android platform. Note that there are alternatives to writing the code (as already suggests) such as creating platform specific services, but for this post we’re going to do things as simple as possible and reduce things (hopefully) to the bare minimum.

Go to the Platforms/Android folder and in MainApplication.cs add the OnCreate method and again for simplicity, we’ll just have this method set up our channels. So our code looks like this

public override void OnCreate()
{
  base.OnCreate();

  if (Build.VERSION.SdkInt >= BuildVersionCodes.O)
  {
#pragma warning disable CA1416
    var channel1 = new NotificationChannel(Channel1Id, "Channel 1", NotificationImportance.High)
    {
      Description = "This is Channel 1"
    };

    var channel2 = new NotificationChannel(Channel2Id, "Channel 2", NotificationImportance.Low)
    {
      Description = "This is Channel 2"
    };

    if (GetSystemService(NotificationService) is NotificationManager manager)
    {
      manager.CreateNotificationChannel(channel1);
      manager.CreateNotificationChannel(channel2);
    }
#pragma warning restore CA1416
  }
}

We’ll also need to add the const values for the channel id’s, so add the following to the MainApplication class

public static readonly string Channel1Id = "channel1";
public static readonly string Channel2Id = "channel2";

These are public to make them accessible from our MainActivity, in a real world application you might prefer to have these in a separate class, but we’re not going to worry too much about such things here.

With the OnCreate method we need to check that we’re using API >= 21 (or Oreo) to use notification channels and I’m just disabling warnings from the compiler with the pragma’s.

We create two channels, the first we give a high importance, which means that by default when it receives a message it will make a sound and popup the message, unless the user changes their settings. If the user does change the settings, either they need to be reinstated or you need to uninstall and reinstall the application to get them back to default – you may find yourself doing this through the post to reset the application in the emulator.

The code to create the notification channels is fairly simple and hopefully self-explanatory.

Now, you don’t need to create the channels at start-up. If your application (as the one I’m working on that prompted the necessity to learn this stuff) only needs the channels when it goes into the background or when a button is clicked, then you can create the channels as and when required.

Finally for this section, we need to amend the MainActivity.cs to both receive messages via our MAUI UI and to then send the message through to the appropriate notification channel. We will add code to the constructor which simply routes messages from IMessenger like this

public MainActivity()
{
  var messenger = MauiApplication.Current.Services.GetService<IMessenger>();

  messenger.Register<MessageData>(this, (recipient, message) =>
  {
    if (message.Channel == 1)
    {
      SendOnChannel1(message.Title, message.Message);
    }
    else
    {
      SendOnChannel2(message.Title, message.Message);
    }
  });
}

We’re also going to need access to the NotificationManagerCompat so declare the following in the MainActivity class and override the OnCreate to set it, like this

private NotificationManagerCompat _notificationManager;

protected override void OnCreate(Bundle savedInstanceState)
{
  base.OnCreate(savedInstanceState);

  _notificationManager = NotificationManagerCompat.From(this);
}

Finally we’ll need the code for the SendOnChannel1 and SendOnChannel2 methods, which look like this

private void SendOnChannel1(string title, string message)
{
  var notification = new NotificationCompat.Builder(this, MainApplication.Channel1Id)
    .SetSmallIcon(Resource.Drawable.abc_ab_share_pack_mtrl_alpha)
    .SetContentTitle(title)
    .SetContentText(message)
    .SetPriority(NotificationCompat.PriorityHigh)
    .SetCategory(NotificationCompat.CategoryMessage)
    .Build();

    _notificationManager.Notify(1, notification);
}

private void SendOnChannel2(string title, string message)
{
  var notification = new NotificationCompat.Builder(this, MainApplication.Channel2Id)
    .SetSmallIcon(Resource.Drawable.abc_btn_check_material)
    .SetContentTitle(title)
    .SetContentText(message)
    .SetPriority(NotificationCompat.PriorityLow)
    .Build();

    _notificationManager.Notify(2, notification);
}

The two methods are currently pretty much the same, each Builder is supplied with a unique channel id (we’re reusing ours from the MainApplication). A notification requires a small icon, so we’re just reusing built in icons here (which may not show up very well) but feel free to add your own icon (which we will do in a subsequent post if you’re not sure how to). We’re setting priorities differently. If I recall these are duplicated from the code where we create the channels due to different versions of Android API support. So, we’re setting a category on channel 1 which just creates a category association with the channel – we’ll see more on this in a later post. Lastly we essentially add the notification to the NotificationManager with a unique id and we’re done.

Running our application

Now if you run your application, I’m using the Pixel 3 emulator and click the “Send on Channel 1” button, you should see a popup with the title you supplied (or defaults to Title in the text entry) and message you supplied (again defaulted to Message in the text entry) and an audible (annoying) sound, if you click it again you’ll get the same again. If, on the other hand you now click “Send on Channel 2” you’ll see another icon on the status bar appear for the channel 2 messages, but no popup or sound, this is because anything above low importance makes the sound (again unless disabled by the user). Low priority just displays the icon and you can view the message(s) by dragging down the status bar to view notifications.

So, that’s it for part 1. We now have the core MAUI code, we can pass messages around which are received by the platform specific code and which turns those messages from the UI into notifications for Android to display.

Code

Code for this an subsequent posts is found on my blog project.

Android Foreground Service using MAUI

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.

ImageButton or Image with TapGestureRecognizer in MAUI ?

MAUI includes a Button (as you’d expect) as well as an ImageButton which allows us to display (as I’m sure you guessed) and image instead of text. Sadly (at least in the current version of .NET 7.0) this does not work nicely on Windows and I want my MAUI app to deploy to iOS, Android and Windows.

An Image Button takes and image source and we can supply a command etc. like this

<ImageButton 
   Source="start.png" 
   Command="{Binding StartCommand}" />

I’ve not tested this yet on a device, but on an Android emulator the image button doesn’t have the pressed, ripple effect that a standard button has, so it’s not that obvious that it’s been clicked. However it displays the image correctly and with transparency. Much to my surprise though, on Windows, the transparency is lost and instead we get a button with some percentage of opacity, however on Windows it does look more like a button – so good and bad points.

So, at this time, the best way to display an image and respond to tap events appears to still be using an Image and TapGestureRecognizer, like this

<Image Source="start.png">
   <Image.GestureRecognizers>
      <TapGestureRecognizer Command="{Binding StartStopCommand}"/>
   </Image.GestureRecognizers>
</Image>

This still lacks the button-like feedback (such as a ripple). Using something like the AlohaKit.Animations NuGet package we could add a Tapped event to our TapGestureRecognizer and in code behind have something like

private void TapGestureRecognizer_OnTapped(object? sender, TappedEventArgs e)
{
   if (sender is View view)
   {
      view.Animate(new StoryBoard(new List<AnimationBase>
      {
         new ScaleToAnimation { Scale = 1.2, Duration = "200" },
         new ScaleToAnimation { Scale = 1, Duration = "100" }
      }));
   }
}

Whilst it’s not a button ripple effect, it does give some feedback on the image being tapped – obviously it would be worth looking at alternative animations to best suit your needs.

Unit testing your MAUI project

Note: I found this didn’t work correctly on Visual Studio for Mac, I’ll update the post further if I do get it working.

This post is pretty much a duplicate of Adding xUnit Test to your .NET MAUI Project but just simplified for me to quickly repeat the steps which allow me to write unit tests against my MAUI project code.

So, you want to unit test some code in your MAUI project. It’s not quite as simple as just creating a test project then referencing the MAUI project. Here are the steps to create an NUnit test project with .NET 7 (as my MAUI project has been updated to .NET 7).

  • Add a new NUnit Test Project to the solution via right mouse click on the solution, Add | Project and select NUnit Test Project
  • Open the MAUI project (csproj) file and prepend the net7.0 to the TargetFrameworks so it looks like this
    <TargetFrameworks>net7.0;net7.0-android;net7.0-ios;net7.0-maccatalyst</TargetFrameworks>
    
  • Replace <OutputType>Exe</OutputType> with the following
    <OutputType Condition="'$(TargetFramework)' != 'net7.0'">Exe</OutputType>
    

    Yes, this is TargetFramework singular. You may need to reload the project.

  • Now in your unit test project you can reference this MAUI project and write your tests

So, why did we carry out these steps?

Our test project was targeting .NET 7.0 but our MAUI project was targeting different platform implementations of the .NET 7 frameworks, i.e those for Android etc. We need the MAUI project to build a .NET 7.0 compatible version hence added the net7.0 to the TargetFrameworks.

The change to add the OutputType ensures that we only build an EXE output for those other frameworks, and therefore for .NET 7.0 we’ll have a DLL to reference instead in our tests.

Now we can build and run our unit tests.

Swift’s @discardableResult

When we’re writing Swift code we might have a function that returns a value as part of its invocation. For example, maybe we’ve a function which sends some data to a server and then returns a boolean to denote success or failure, here’s a mock up

func serviceCall() -> Bool {
    // do something real here
    true
}

We call this method but maybe we don't care what the result is, so we simply call it like this

[code]
serviceCall()

If we build this, swift will report warning: result of call to ‘serviceCall()’ is unused serviceCall(). We can get around this using a discard, for example

let _ = serviceCall()
// OR
_ = serviceCall()

but there’s another way to tell the compiler to ignore this warning. We mark the function with the @discardableResult attribute, i.e.

@discardableResult
func serviceCall() -> Bool {
    // do something real here
    true
}

Admittedly this seems a little odd, that the function being called should say, “hey you can just ignore the result”. This is something that should probably be used with care, although if it’s known that the function’s result is likely to be ignored, then it’s better than having lots of discards all over the place to ignore the result of the function.

Conditional Compilation with Swift

Occasional you’ll need to write code blocks that are specific to an OS (as I’ve found Swift on Linux and on Mac OS is not always 100% in sync with features).

In which case we can use conditional compilation blocks, such as

#if os(Linux)
// code specific to Linux
#elseif os(Windows)
// code specific to Windows
#endif

The os() condition accepts, macOS, iOS, watchOS, tvOS, Linux and Windows.

Other conditions include architecture arch() with options i386, x86_64, arm and arm64.

Swift also allows supports canImport which can be used to check if a module is available, like this

#if canImport(FoundationNetworking)
import FoundationNetworking
#endif

See Conditional Compilation Block from the Swift language manual for a full list of conditions.