Author Archives: purpleblob

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.

Swift unit tests, including and using .json files

Note: I’m running everything on Ubuntu 20.0.4 using Swift version 5.7.1. I’m assuming everything listed below works on Mac etc. as well.

I have a simple Eureka client package (named SwiftEureka) that I’ve been working on. Now I’ve tested it against a running instance of Eureka but I want to write some unit tests where I don’t need the server running. The idea simply being that I have a bunch of .json files which contain the JSON responses, taken from Eureka, for various REST calls.

So, to summarise, I basically want to have .json files within my Tests folder and use them within my unit tests.

Let’s assume we have our Tests/EurekaTests folder and within that we have a file named applications.json. We need to add this to the Package.swift file under the .testTarget section, like this

.testTarget(
   name: "EurekaTests",
   dependencies: ["SwiftEureka"],
      resources: [
         .process("applications.json")
      ]),

Now the file will be seen by Swift as part of the package’s bundle.

Next, we need to load the file into our tests. We do this by using the Bundle object, like this

guard let path = Bundle.module.url(forResource: "applications", withExtension: "json") else {
   XCTFail("Missing file: applications.json")
   return
}

let json = try Data(contentsOf: path)
let wrapper = try JSONDecoder().decode(ApplicationsWrapper.self, from: json)

Installing nanoFramework on the ATOM Lite ESP32 (M5Stack)

I have a wonderful little M5Stack ATOM Lite, ESP32 based dev kit to play with and as I’d had such success with the M5Core2 and nanoFramework, I thought I’d try the framework on the ATOM lite.

You, can check the device and the “Firmware Target” for the device from Recommended devices to start with .NET nanoFramework. So, for this device the target is ESP32_PICO.

If we connect your device to your computer’s USB port (hopefully the device will be recognised, if not see my previous post on setting up the M5Core2) execute the following command from the CLI, we’ll flash the device with the nanoFramework

nanoff --target ESP32_PICO --update --serialport COM10

Change the COM port to whatever your device is on. Also I’m again assuming you’ve installed nanoff, if not try running the following from the CLI “dotnet tool install -g nanoff”.

The ATOM lite comes with WiFi, bluetooth a NeoPixel RGB LED, button and even infrared.

Once you’ve installed nanoFramework, create a new nanoFramework project in Visual Studio 2022 (seem my previous posts on setting this up if you’ve not already got everything setup).

Let’s start with the LED, we’ll simply change the colour of the LED. First, you’ll need to add the package nanoFramework.AtomLite via NuGet. Next copy and paste this code into the Program.cs

while (true)
{
    AtomLite.NeoPixel.Image.SetPixel(0, 0, Color.Gray);
    AtomLite.NeoPixel.Update();

    Thread.Sleep(5000);

    AtomLite.NeoPixel.Image.SetPixel(0, 0, Color.Green);
    AtomLite.NeoPixel.Update();

    Thread.Sleep(5000);

    AtomLite.NeoPixel.Image.SetPixel(0, 0, Color.Red);
    AtomLite.NeoPixel.Update();
}

AdaptiveTrigger working in MAUI 7.x

This is just a quick update to my post Responsive, Reactive, Adaptive design in MAUI. The AdaptiveTrigger now works, so we can create adaptive UI’s like this

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="AdaptiveTriggerTest.MainPage">
    <VisualStateManager.VisualStateGroups>
        <VisualStateGroup x:Name="Responsive">
            <VisualState x:Name="Large">
                <VisualState.StateTriggers>
                    <AdaptiveTrigger MinWindowWidth="1200" />
                </VisualState.StateTriggers>
                <VisualState.Setters>
                    <Setter Property="BackgroundColor" Value="Blue"/>

                    <Setter TargetName="MainLabel" Property="Label.TextColor" Value="Green"/>
                    <Setter TargetName="MainLabel" Property="Label.FontSize" Value="128"/>
                </VisualState.Setters>
            </VisualState>
            <VisualState x:Name="Default">
                <VisualState.StateTriggers>
                    <AdaptiveTrigger MinWindowWidth="0" />
                </VisualState.StateTriggers>
                <VisualState.Setters>
                    <Setter Property="BackgroundColor" Value="Azure"/>

                    <Setter TargetName="MainLabel" Property="Label.TextColor" Value="Red"/>
                    <Setter TargetName="MainLabel" Property="Label.FontSize" Value="48"/>
                </VisualState.Setters>
            </VisualState>
        </VisualStateGroup>
    </VisualStateManager.VisualStateGroups>

    <VerticalStackLayout
            Spacing="25"
            Padding="30,0"
            VerticalOptions="Center">

        <Label
                x:Name="MainLabel"
                Text="Hello, World!"
                SemanticProperties.HeadingLevel="Level1"
                FontSize="32"
                HorizontalOptions="Center" />
    </VerticalStackLayout>
</ContentPage>

The AdaptiveTrigger MinWindowWidth=”1200″ basically designates what the UI style etc. is for Window’s with a width >= 1200. The second AdaptiveTrigger is for anything smaller.

Turning your M5Core2 into a nanoFramework based web server

Like most of my posts regarding nanoFramework and the M5Core2, I owe a debt to those who created this stuff and I’m really just going through some of the samples etc. Trying them out and documenting my findings. This post is no different, it’s based on the Welcome to the .NET nanoFramework WebServer repository

Add the NuGet package nanoFramework.WebServer to your nanoFramework project.

You’ll need to also include code to connect to your WiFi, so checkout my post on that subject – Wifi using nanoFramework on the M5Core2.

Assuming you’ve connected to your WiFi, we can set up a WebServer like this

using var server = new WebServer(80, HttpProtocol.Http, new[] { typeof(PowerController) });
server.Start();

Thread.Sleep(Timeout.Infinite);

The first line supplies an array of controllers, so you can have multiple controllers for your different endpoints. In this case we’ve just got the single controller PowerController. This is a simple class that includes RouteAtrribute and MethodAttribute adorned methods to acts as routes/endpoints.

Let’s look at the PowerController, which just returns some M5Core2.Power values when accessed via http://m5core2_ip_address/power.

public class PowerController
{
   [Route("power")]
   [Method("GET")]
   public void PowerRoute(WebServerEventArgs e)
   {
      var power = M5Core2.Power;
           
      var sb = new StringBuilder();
      sb.AppendLine("Power:");
      sb.AppendLine($"  Adc Frequency: {power.AdcFrequency}");
      sb.AppendLine($"  Adc Pin Current: {power.AdcPinCurrent}");
      sb.AppendLine($"  Adc Pin Current Setting: {power.AdcPinCurrentSetting}");
      sb.AppendLine($"  Adc Pin Enabled: {power.AdcPinEnabled}");
      sb.AppendLine($"  Batt. Temp. Monitor: {power.BatteryTemperatureMonitoring}");
      sb.AppendLine($"  Charging Current: {power.ChargingCurrent}");
      sb.AppendLine($"  Charging Stop Threshold: {power.ChargingStopThreshold}");
      sb.AppendLine($"  Charging Voltage: {power.ChargingVoltage}");
      sb.AppendLine($"  Dc Dc1 Voltage: {power.DcDc1Voltage.Millivolts} mV");
      sb.AppendLine($"  Dc Dc2 Voltage: {power.DcDc2Voltage.Millivolts} mV");
      sb.AppendLine($"  Dc Dc3 Voltage: {power.DcDc3Voltage.Millivolts} mV");
      sb.AppendLine($"  EXTEN Enable: {power.EXTENEnable}");
      sb.AppendLine($"  VOff Voltage: {power.VoffVoltage}");
      sb.AppendLine($"  Gpio0 Behavior: {power.Gpio0Behavior}");
      sb.AppendLine($"  Gpio0 Value: {power.Gpio0Value}");

      e.Context.Response.ContentType = "text/plain";
      WebServer.OutPutStream(e.Context.Response, sb.ToString());
}

As you can see from the last line of code, we send the response back with our payload, the string of power information.

We can also return HTTP codes using

WebServer.OutputHttpCode(e.Context.Response, HttpStatusCode.OK);

This is great, but what’s the IP address of our IoT device, so I can access the web server?

Well, ofcourse you could check your router or DHCP server, but better still, let’s output the IP address to the M5Core2 screen using

Console.WriteLine(IPGlobalProperties.GetIPAddress().ToString());

We can support multiple routes per method, such as

[ublic class PowerController
{
[Route("power")]
[Route("iotpower")]
[Method("GET")]
public void PowerRoute(WebServerEventArgs e)
{
// code removed
}

Note: Routes are usually case insensitive, unless you add the CaseSensitiveAttribute to your method.

Interacting with the M5Core2 Accelerometer and Gryoscope using nanoFramework

The M5Core includes an accelerometer which allows us to measure the rate of acceleration, as well as a gyroscope to sense angular movement.

We initialize the combined AccelerometerGyroscope and calibrate it by using the following code. The number, 100 in this case, is the number of iterations to calibrate the AccelerometerGyroscope

M5Core2.AccelerometerGyroscope.Calibrate(100);

Let’s look at the code to read the accelerometer and gyroscope (we’ll also read the internal temperature of the AccelerometerGyroscope)

Console.Clear();

M5Core2.AccelerometerGyroscope.Calibrate(100);

while (true)
{
   var accelerometer = M5Core2.AccelerometerGyroscope.GetAccelerometer();
   var gyroscope = M5Core2.AccelerometerGyroscope.GetGyroscope();
   var temperature = M5Core2.AccelerometerGyroscope.GetInternalTemperature();

   Console.CursorLeft = 0;
   Console.CursorTop = 1;

   Console.WriteLine("Accelerator:");
   Console.WriteLine($"  x={accelerometer.X}");
   Console.WriteLine($"  y={accelerometer.Y}");
   Console.WriteLine($"  z={accelerometer.Z}");
   Console.WriteLine("Gyroscope:");
   Console.WriteLine($"  x={gyroscope.X}");
   Console.WriteLine($"  y={gyroscope.Y}");
   Console.WriteLine($"  z={gyroscope.Z}");
   Console.WriteLine("Internal Temp:");
   Console.WriteLine($"  Celsius={temperature.DegreesCelsius}");

   Thread.Sleep(20);
}