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.