Getting started with MassTransit

What is MassTransit?

MassTransit is an abstraction over various messaging transports, such as RabbitMQ, Azure Service Bus, Amazon SQS and others. We can use MassTransit in place of these transports and, ofcourse, change the underlying transport without affecting our code. It also includes some microservice based patterns, as Sagas, Routing Slip etc.

MassTransit offers an in-memory transport for testing code against.

Creating a simple test application

Note: I’m basically going to reproduce the code from In Memory, so I’d suggest going there to check the latest steps.

First off let’s add the MassTransit project templates to our machine

dotnet new --install MassTransit.Templates

Now this will add a bunch of useful templates that we can use via the dotnet CLI or via an IDE such as Visual Studio. In this case we’ll follow the In Memory tutorial by running the following command to create our project

dotnet new mtworker -n HelloWorld

or from Visual Studio or Rider you can create a MassTransit project from the MassTransit Worker project template.

Note: I’m differing from the MassTransit tutorial by naming my project HelloWorld, for no other reason that I pretty much always start these sorts of things with a HelloWorld project. So just in case you’re following along both the Mass Transit tutorial and this post, this is the only real difference in code.

Default project

At the time of writing, the default project targets .NET 6.0 so I’ve changed that to .NET 7.0 and language support to C# 11 in Rider, so my .csproj has the following

<PropertyGroup>
  <TargetFramework>net7.0</TargetFramework>
  <LangVersion>11</LangVersion>
</PropertyGroup>

All the code is currenting within the Program.cs, the code that matters at the moment is

services.AddMassTransit(x =>
{
  x.SetKebabCaseEndpointNameFormatter();

  // By default, sagas are in-memory, but should be changed to a durable
  // saga repository.
  x.SetInMemorySagaRepositoryProvider();

  var entryAssembly = Assembly.GetEntryAssembly();

  x.AddConsumers(entryAssembly);
  x.AddSagaStateMachines(entryAssembly);
  x.AddSagas(entryAssembly);
  x.AddActivities(entryAssembly);

  x.UsingInMemory((context, cfg) => { cfg.ConfigureEndpoints(context); });
});

As you can see we add MassTransit and at the end of the code we set MassTransit to use the in memory transport. Whilst this will build and run, it’s not really doing anything for us as we need to add at least one contract.

Adding a contract

From the MassTransit tutorial we simply run the following command in the CLI from the .csproj folder

dotnet new mtconsumer

This will add the Consumers folder along with the Contracts folder along with the consumer and contract code. Within the Consumer folder we have two files, the HelloWorldConsumer.cs file looks like this

Note: I’m not include the using and namespace lines in these files so that we can concentrate on the implementation code.

public class HelloWorldConsumer :
  IConsumer<HelloWorld>
{
  public Task Consume(ConsumeContext<HelloWorld> context)
  {
    return Task.CompletedTask;
  }
}

The other file in the HelloWorldConsumerDefinition.cs looks like this

public class HelloWorldConsumerDefinition :
  ConsumerDefinition<HelloWorldConsumer>
{
  protected override void ConfigureConsumer(IReceiveEndpointConfigurator endpointConfigurator, IConsumerConfigurator<HelloWorldConsumer> consumerConfigurator)
  {
    endpointConfigurator.UseMessageRetry(r => r.Intervals(500, 1000));
  }
}

The Contracts folder includes a single file, HelloWorld.cs which looks like this

public record HelloWorld
{
  public string Value { get; init; }
}

At this point we now have a consumer and a message/contract.

When a message arrives on the transport, our consumer, Consume message is called. So let’s add some logging to the consumer, exactly like the MassTransit example, so add the following to the HelloWorldConsumer class

private readonly ILogger<HelloWorldConsumer> _logger;

public HelloWorldConsumer(ILogger<HelloWorldConsumer> logger)
{
  _logger = logger;
}

Now in the Consume method we’ll just add a line of code to log information from the message so the method looks like this

public Task Consume(ConsumeContext<HelloWorld> context)
{
  _logger.LogInformation("Hello {Name}", context.Message.Value);
  return Task.CompletedTask;
}

The HelloWorldConsumerDefinition class can be used for setting up out configuration, we’re not going to do anything with this at the moment. Instead we need some code to publish methods. We are going to add a background service.

Background service

Add a new file/class to the root folder, named Worker. Here’s the one in the MassTransit tutorial

public class Worker : BackgroundService
{
    private readonly IBus _bus;

    public Worker(IBus bus)
    {
        _bus = bus;
    }
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            await _bus.Publish(new Contracts.HelloWorld(value: $"The time is {DateTimeOffset.Now}"), stoppingToken);
            await Task.Delay(1000, stoppingToken);
        }
    }
}

Next, we need to register our work, by adding the following code to the Program.cs

services.AddHostedService<Worker>();

Does it run?

If all’s gone to plan, we can either

dotnet run

or run via Visual Studio or Rider. This will run our service and we’ll see output to the console from our logging code.

What next?

This is fun, but let’s face it we really want to see this working with a “real” transport, so let’s make changes to work with RabbitMQ. To host an instance of RabbitMQ within Docker we can use the MassTransit image

docker run -p 15672:15672 -p 5672:5672 masstransit/rabbitmq

To check whether RabbitMQ is up and running, use your browser to connect to the machine hosting it, on port 15672 (i.e. http://192.168.1.123:15672/). To login, the default username and password are both guest.

Now we need to add the MassTransit RabbitMQ transport to our project, so add MassTransit.RabbitMQ via the nuget package manager or from the CLI use

dotnet add package MassTransit.RabbitMQ

Finally, replace everything x.UsingInMemory((context, cfg) => { cfg.ConfigureEndpoints(context); }); within Program.cs with

x.UsingRabbitMq((context,cfg) =>
{
  cfg.Host("192.168.1.123", "/", h => {
    h.Username("guest");
    h.Password("guest");
 });
 cfg.ConfigureEndpoints(context);
});

Note: If you’ve changed the user or password, obviously replace those in the code above. Also replace 192.168.1.123 with localhost or your remote server name/ip.

Run your MassTransit service.

Next, access your RabbitMQ dashboard and you should see messages coming into RabbitMQ (note: if you’ve changed the port, then the port number goes into the Host code like this, we’re using port 1234 to demonstrate a different port number)

x.UsingRabbitMq((context,cfg) =>
{
  cfg.Host("192.168.1.123", 1234, "/", h => {
    h.Username("guest");
    h.Password("guest");
 });
 cfg.ConfigureEndpoints(context);
});

Note: remember to allow access to the remote server’s port 5672, if it’s not already accessible.

Is the checkbox checked on my AppiumWebElement ?

From Appium/WinAppDriver you’ll probably want to check if a checkbox or radio button element is checked at some point. For such elements we use the AppiumWebElement Selected property to get the current state. To set it within UI automation we would click on it, so for example we might have code like this

public bool IsChecked
{
   get => Selected;
   set
   {
      if(IsChecked != value)
      {
         Click();
      }
   }
}

Note: In my case I wrap the AppiumWebElement in a CheckBoxElement, but this could be in a subclass or extension method, you get the idea.

Plugin.Maui.Audio plays sound even if Android device is set to vibrate

I have a MAUI application which alerts the user with a sound and vibration when a task is completed. I noticed that turning the phone to vibrate didn’t actually stop the sound being played via the Plugin.Maui.Audio (as I sort of expected it to do).

It looks like what we need to do is write some Android specific code to check the Android AudioManager’s RingerMode.

If we assume we have a device independent enum such as the following (this simply mirrors the Android RingerMode)

public enum AudioMode
{
    Silent = 0,
    Vibrate = 1,
    Normal = 2
}

Now, from our Android code we would expose the RingerMode like this

if (Context.GetSystemService(AudioService) is not AudioManager audioService)
  return AudioMode.Normal;

switch (audioService.RingerMode)
{
  case RingerMode.Normal:
    return AudioMode.Normal;
  case RingerMode.Silent:
    return AudioMode.Silent;
  case RingerMode.Vibrate:
    return AudioMode.Vibrate;
}

return AudioMode.Normal;

Now we would simply wrap our Plugin.Maui.Audio code in an if statement, checking the AudioMode of the device. i.e. if Vibrate or Silent, don’t play the audio. For example

if (_deviceAudioService.AudioMode == AudioMode.Normal)
{
  var audioPlayer =
    _audioManager.CreatePlayer(await FileSystem.OpenAppPackageFileAsync("chimes.wav"));
  audioPlayer.Volume = Math.Clamp(_configuration.NotificationSoundVolume / 100.0, 0, 1);
  audioPlayer.Play();
}

Changing the origin of your local git repo.

Git, being a distributed source control system, allows us to switch the remote/origin for the push or fetch of our local repository. Or to put it another way…

We’ve just moved from one remote git repository to another, how do we update our local code base to “retarget” to the new location?

When you’re using GitHub, GitLab, bitbucket etc. you might come to a point where you migrate from one server to another. Ofcourse you can simply clone your repo. again from the new server OR you can just target you fetch/push origin to the new location like this…

To check your current remote/origin, just run

git remote -v

Now if you wish to change the remote/origin, simply use

git remote set-url remote_name remote_url

Where remote_name might be origin and the remote_url is the new .git URL of your server.

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.