Monthly Archives: January 2023

Android notifications using MAUI (Part 10 of 10)

In this post we’re going to cover the tutorial Notifications Tutorial Part 10 – DELETE NOTIFICATION CHANNELS – Android Studio Tutorial and we’re going to be look at deleting notification channels.

Overwrite

We can delete notification channels if they’re no longer required, so let’s create some code by changing the UI slightly and updating with a new message – ofcourse in a real-world scenario, it’s more likely you’re either deleting a channel that’s been dynamically created or deleting a legacy channel.

Implementation

Go to to MainPage.xaml and add

<Button Text="Delete Channels" Clicked="DeleteChannel_OnClick" Margin="10" />

In the code behind MainPage.xaml.cs add the following

private void DeleteChannel_OnClick(object sender, EventArgs e)
{
  _messenger.Send(new DeleteChannelData());
}

As you can see we’ve added a new class DeleteChannelData to be sent to anything that wishes to listen to tell them to delete a channel. The code for this is simply

public record DeleteChannelData;

(It’s just really the equivalent of a command).

In the Android MainActivity constructor, add

messenger.Register<DeleteChannelData>(this, (recipient, message) =>
{
  if (Build.VERSION.SdkInt >= BuildVersionCodes.O)
  {
    if (GetSystemService(NotificationService) is NotificationManager manager)
    {
      // hard coded to delete channel 3
      //manager.DeleteNotificationChannel(MainApplication.Channel3Id);
      manager.DeleteNotificationChannelGroup(MainApplication.Group1Id);
    }
  }
});

So this receives the DeletChannelData message and deletes either channel 3 (commented out) or a given group via the NotificationManager. Notice this is NOT the NotificationManagerCompat.

Now if you run this (after running the version prior to this) and go to the settings page for your application’s notifications (Settings | Apps & notifications) you’ll noticed it says something like 2 categories deleted if you’re deleting Group1Id group. This is telling the user that something deleted the channels.

Code

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

Android notifications using MAUI (Part 9 of 10)

In this post we’re going to cover the tutorial Notifications Tutorial Part 9 – NOTIFICATION CHANNEL SETTINGS – Android Studio Tutorial and we’re going to be notification channel settings.

Overwrite

In a previous post we looked at the fact that we cannot change the notification settings once created, we need to uninstall and reinstall. This is ofcourse not a lot of help if, whilst our application is running it determines that the user should be given the opportunity to change a setting. For example let’s assume the user blocked a channel and now wants our application to notify them when something occurs within the application.

Ofcourse we could popup an alert saying “Go to the channel settings and unblock it” or better still we can alert them then display the settings.

Implementation

We’re going to change our MainActivity.SendOnChannel1 method to check if notifications are enabled and whether they’re blocked. So let’s first look at how we check if notifications are enabled.

We need access to the NotificationManagerCompat and we use it like this

if (!notificationManager.AreNotificationsEnabled())
{
  OpenNotificationSettings(context);
  return;
}

In this case we’re not even bothering to try to send notifications if they’re disabled, but we use our new method OpenNotificationSettings to show the settings screen (in a real world app we’d probably display and alert to asking them if they wish to change the settings etc.

[RequiresApi(Api = 26)]
private static void OpenNotificationSettings(Context context)
{
  // api 26
  if (Build.VERSION.SdkInt >= BuildVersionCodes.O)
  {
    var intent = new Intent(Settings.ActionAppNotificationSettings);
    intent.PutExtra(Settings.ExtraAppPackage, context.PackageName);
    context.StartActivity(intent);
  }
  else
  {
    var intent = new Intent(Settings.ActionApplicationDetailsSettings);
    intent.SetData(Uri.Parse($"package:{context.PackageName}"));
    context.StartActivity(intent);
  }
}

Next in SendOnChannel1 we’re execute this code to check if the specific channel is blocked and again alert/open settings for the user if it is

if (Build.VERSION.SdkInt >= BuildVersionCodes.O && IsChannelBlocked(context, MainApplication.Channel1Id))
{
  OpenChannelSettings(context, MainApplication.Channel1Id);
  return;
}

IsChannelBlocked looks like this

[RequiresApi(Api = 26)]
private static bool IsChannelBlocked(Context context, string channelId)
{
  if (context.GetSystemService(NotificationService) is NotificationManager manager)
  {
    var channel = manager.GetNotificationChannel(channelId);
    return channel is { Importance: NotificationImportance.None };
  }

  return false;
}

Note that both these methods require API 21 or above.

Code

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

Android notifications using MAUI (Part 8 of 10)

In this post we’re going to cover the tutorial Notifications Tutorial Part 8 – NOTIFICATION CHANNEL GROUPS – Android Studio Tutorial and we’re going to be notification channel groups

Overwrite

In the previous post we looked at notifications being grouped into summaries. We also have the concept of grouping notification channels themselves. These are ways of, for example, grouping your channels themselves by some business or logical grouping – maybe you group channels by importance or by business process or multiple user accounts etc.

Basically this allows us to fine grain our channels allowing the user to change settings to those groups.

Implementation

In MainApplication.cs just update our channel id’s etc. to look like this

public const string Group1Id = "group1";
public const string Group2Id = "group2";
public const string Channel1Id = "channel1";
public const string Channel2Id = "channel2";
public const string Channel3Id = "channel3";
public const string Channel4Id = "channel4";

We’ve got a couple of extra channels as well as some group id’s. Now change our OnCreate method to look like this

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

  if (Build.VERSION.SdkInt >= BuildVersionCodes.O)
  {
#pragma warning disable CA1416
    var group1 = new NotificationChannelGroup(Group1Id, "Group 1");
    var group2 = new NotificationChannelGroup(Group2Id, "Group 2");

    var channel1 = new NotificationChannel(Channel1Id, "Channel 1", NotificationImportance.High);
    channel1.Description = "This is Channel 1";
    channel1.Group = Group1Id;

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

    var channel3 = new NotificationChannel(Channel3Id, "Channel 3", NotificationImportance.High);
    channel3.Description = "This is Channel 3";
    channel3.Group = Group2Id;

    var channel4 = new NotificationChannel(Channel4Id, "Channel 4", NotificationImportance.Low);
    channel4.Description = "This is Channel 4";    
    // purposefully no group

    if (GetSystemService(NotificationService) is NotificationManager manager)
    {
      manager.CreateNotificationChannelGroup(group1);
      manager.CreateNotificationChannelGroup(group2);

      manager.CreateNotificationChannel(channel1);
      manager.CreateNotificationChannel(channel2);
      manager.CreateNotificationChannel(channel3);
      manager.CreateNotificationChannel(channel4);
    }
#pragma warning restore CA1416
  }
}

As you can see, we’ve create two group channels and then assigned these, where required, to our notification channels (i.e. .Group = Group1Id etc.). That’s it.

Now if we run this application, go to the Android Settings | App & notifications click on our application name then select Notifications we’re see our channels grouped together in Group 1, Group 2 and channel 4 is not grouped, so become “Other”.

Code

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

Android notifications using MAUI (Part 7 of 10)

In this post we’re going to cover the tutorial Notifications Tutorial Part 7 – NOTIFICATION GROUPS – Android Studio Tutorial and we’re going to be adding a notification groups to our channel 2 code.

Overview

In this code we’re going to send notifications to a group. They will initially appear as different notifications but will then get grouped together.

Implementation

We’re going to overwrite/reuse our SendOnChannel2 method, first we’ll create two separate notifications but assign them to the same group, like this

var notification1 = new NotificationCompat.Builder(this, MainApplication.Channel2Id)
  .SetSmallIcon(Resource.Drawable.abc_btn_check_material)
  .SetContentTitle("Title 1")
  .SetContentText("Message 1")
  .SetPriority(NotificationCompat.PriorityLow)
  .SetGroup("example_group")
  .Build();

var notification2 = new NotificationCompat.Builder(this, MainApplication.Channel2Id)
  .SetSmallIcon(Resource.Drawable.abc_btn_check_material)
  .SetContentTitle("Title 2")
  .SetContentText("Message 2")
  .SetPriority(NotificationCompat.PriorityLow)
  .SetGroup("example_group")
  .Build();

We’re created the example_group but other than that you should be familiar with the way we create notifications.

Next we need a notification to become the summary notification (i.e. displays altogether in the same group) as these notification currently will simply be displays as two distinct notifications. So we add another notification like this

var summaryNotification = new NotificationCompat.Builder(this, MainApplication.Channel2Id)
  .SetSmallIcon(Resource.Drawable.abc_btn_colored_material)
  .SetStyle(new NotificationCompat.InboxStyle()
    .AddLine("Title 2 Message 2")
    .AddLine("Title 1 Message 1")
    .SetBigContentTitle("2 new messages")
    .SetSummaryText("user@example.com"))
  .SetPriority(NotificationCompat.PriorityLow)
  .SetGroup("example_group")
  .SetGroupAlertBehavior(NotificationCompat.GroupAlertChildren)
  .SetGroupSummary(true)
  .Build();

The main lines to look at are the last three. Again we set the group to the same as the other notifications but now we give this one a different alter behaviour and set it to become the group summary.

Finally let’s simulate messages arriving then see the grouping happen, so add the following the the method

Thread.Sleep(2000);
_notificationManager.Notify(2, notification1);
Thread.Sleep(2000);
_notificationManager.Notify(3, notification2);
Thread.Sleep(2000);
_notificationManager.Notify(4, summaryNotification);

Again a warning, this is not (as hopefully is obvious) good real world practise, for starters this method may be called on the main thread, depending upon your implementation and hence block that thread.

Code

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

Android notifications using MAUI (Part 6 of 10)

In this post we’re going to cover the tutorial Notifications Tutorial Part 6 – PROGRESS BAR NOTIFICATION – Android Studio Tutorial and we’re going to be adding a progress bar to our notification.

Overview

You may have a requirement to show a progress bar notification, for example a process to download a file or the likes takes place, your application goes into the background but we keep getting feedback via the notification’s progress bar.

Implementation

We’ll leave channel 1 for now and simply add our progress bar to SendOnChannel2. We’ll change the content title to “Download” and content text “Download in progress), we’ll add a progress bar to the notification and set it’s max and current value. The progress bar will be determinate i.e. it’s not one of those progress bars that bounces back and forth indeterminate.

We’re also going to simulate updates to the progress bar within this method. Let’s look at the SendOnChannel2 method

const int progressMax = 100;
var notification = new NotificationCompat.Builder(this, MainApplication.Channel2Id)
   .SetSmallIcon(Resource.Drawable.abc_btn_check_material)
   .SetContentTitle("Download")
   .SetContentText("Download in progress")
   .SetPriority(NotificationCompat.PriorityLow)
   .SetOngoing(true)
   .SetOnlyAlertOnce(true) // with high priority, stops the popup on every update
   .SetProgress(progressMax, 0, false);

   _notificationManager.Notify(2, notification.Build());

   // simulate progress, such as a download
   Task.Run(() =>
   {
      Thread.Sleep(2000);

      for (var progress = 0; progress <= progressMax; progress += 10)
      {
         notification.SetProgress(progressMax, progress, false);
         // same id (2) to ensure overwrite/updates existing
         _notificationManager.Notify(2, notification.Build());

         Thread.Sleep(1000);
      }

      notification.SetContentText("Download finished")
         .SetOngoing(false)
         .SetProgress(0, 0, false);

      // same id (2) to ensure overwrite/updates existing
      _notificationManager.Notify(2, notification.Build());
   });

This is pretty simple and hopefully fairly sel-explanatory. But to summarise, we add a progress bar, set it’s starting point then in a separate thread, pause for a bit, so the user would see the download simulation start, then update the progress bar and pause to just make it look like it’s busy downloading something. All this happens against the same notification id, hence updates the current progress. Eventually we finish the download simulation and update the progress bar to show this completed state.

Code

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

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.