While building out the Maui client for Chatter I ran into an issue with Android. Everything worked great in Windows, but Android just wouldn’t connect, throwing a “Connection failure” exception. There were two specific changes I had to make to support connecting to my localhost API from Maui Android to get everything to work.

As always, all code is available up at the Chatter project up on GitHub.

First, I had to accept that I needed to abandon the plug-in architecture for the Maui client. It would work fine if I only wanted to use the Windows version but that defeated the purpose of using Maui. I already had a WPF app that did everything I needed for the client. In Android, everything is bundled into an APK and that represents what the application needs to run. So going forward, for Maui, I accepted I would have to abandon the plug-in aspect and simply manually instantiate the chat handlers.

When I did this and started the Maui app in the Android emulator I started to run into the problem that the handlers were unable to connect to the API end-point. In looking into the code of the BackgroundChatHandler I can see that it is hard-coded to connect to localhost.

string baseUrl = "https://localhost:7076";

But it’s important to remember that the Maui Android client runs in its own operating system, Android. Localhost is just that, local to the emulator. After doing a lot of googling and digging around in the documentation, I found the solution. In the Microsoft docs it states that I have to use 10.0.2.2 as the end-point address because Android will use this as an alias to 127.0.0.1 on my local machine.

        //this is done per: https://learn.microsoft.com/en-us/dotnet/maui/data-cloud/local-web-services?view=net-maui-8.0
        if (DeviceInfo.Current.Platform == DevicePlatform.Android)
        {
            ChatPlugins.Add(new BackgroundChatHandler(messenger, "https://10.0.2.2:7076"));
            ChatPlugins.Add(new SignalRChatHandler(messenger, "https://10.0.2.2:7076/chatterChatHub"));
        }
        else
        {
            ChatPlugins.Add(new BackgroundChatHandler(messenger));
            ChatPlugins.Add(new SignalRChatHandler(messenger));
        }
        
        ChatPlugins.Add(new BasicChatHandler(messenger));
        ChatHandler = ChatPlugins[0];

In the code above I changed the handlers to take an end-point in the constructor and instantiate them to https://10.0.2.2 so that Android knows to connect to my localhost. But that is only half the problem. This was the most common solution in a large number of StackOverflow posts about how to solve this issue. Even after implementing this I still ran into the “Connection failure” exception.

Well, it turns out that Android and iOS will reject self-signed certs. So the issue is that it won’t connect to my localhost over SSL. After doing even more digging and searching I found the solution. Turns out I have to manually force the HttpClient to accept the localhost cert. This is documented in Microsoft’s documentation on Deployment & debugging for Xamarin.

For my background job chat handler this has a straight-forward solution as discussed in the documentation.

    public BackgroundChatHandler(IMessenger messenger) : this(messenger, null) { }
    public BackgroundChatHandler(IMessenger messenger, string? endPoint) : base(messenger)
    {
        baseUrl = endPoint ?? "https://localhost:7076";
#if DEBUG
        client = new HttpClient(GetInsecureHandler());
#else
        client = new HttpClient();
#endif

    }
    
    /// <summary>
    /// Done per: https://learn.microsoft.com/en-us/previous-versions/xamarin/cross-platform/deploy-test/connect-to-local-web-services#bypass-the-certificate-security-check
    /// </summary>
    /// <returns></returns>
    public static HttpClientHandler GetInsecureHandler()
    {
        //when using this in android it doesn't like self-issued certs
        HttpClientHandler handler = new HttpClientHandler();
        handler.ServerCertificateCustomValidationCallback = (message, cert, chain, errors) =>
        {
            if (cert.Issuer.Equals("CN=localhost"))
                return true;
            return errors == System.Net.Security.SslPolicyErrors.None;
        };
        return handler;
    }

This implementation is straight from the documentation. If I’m running in debug it will use the GetInsecureHandler to allow for support of self-certs. Well, this works pretty great. But how do I handle SignalR? I rely on the SignalR libraries to manage the connection. Since I don’t have access to it’s back-end for working with the HttpClient, what do I do? I did some more googling around looking for a way to set up my own HttpClientHandler. It turns out it can be done.

    public SignalRChatHandler(IMessenger messenger) : this(messenger, null) { }
    public SignalRChatHandler(IMessenger messenger, string? endPoint) : base(messenger)
    {
        var url = endPoint ?? "https://localhost:7076/chatterChatHub";

        connection = new HubConnectionBuilder()
#if DEBUG
                        .WithUrl(new Uri(url), options =>
                        {
                            //when using this in android it doesn't like self-issued certs
                            //Done per: https://learn.microsoft.com/en-us/previous-versions/xamarin/cross-platform/deploy-test/connect-to-local-web-services#bypass-the-certificate-security-check
                            HttpClientHandler handler = new HttpClientHandler();
                            handler.ServerCertificateCustomValidationCallback = (message, cert, chain, errors) =>
                            {
                                if (cert.Issuer.Equals("CN=localhost"))
                                    return true;
                                return errors == System.Net.Security.SslPolicyErrors.None;
                            };
                            options.HttpMessageHandlerFactory = _ => handler;
                        })
#else
                        .WithUrl(url)
#endif
                        .WithAutomaticReconnect()
                        .Build();

        connection.Reconnecting += (sender) =>
        {
            Messenger.Send(new WriteToChatMessage("system", "Trying to reconnect to server", "Red", "White", "Normal"));
            return Task.CompletedTask;
        };
        connection.Reconnected += (sender) =>
        {
            Messenger.Send(new WriteToChatMessage("system", "Reconnected to server", "Red", "White", "Normal"));
            return Task.CompletedTask;
        };
        connection.Closed += (sender) =>
        {
            Messenger.Send(new WriteToChatMessage("system", "Connection to server closed", "Red", "White", "Normal"));
            return Task.CompletedTask;
        };

        connection.On<WriteToChatMessage>("ReceiveMessage", (message) =>
        {
            Messenger.Send(message);
        });
    }

Looking at the ctor in SignalRChatHandler I take a similar approach to the BackgroundJobHandler, I just have to specify custom options when defining the Url for the hub. I instantiate a new HttpClientHandler in exactly the same way but I use a lambda to define a factory for getting the handler so the SignalR hub knows what it needs to use.

So there were two steps to get this all to work:

  1. When running in Android connect to https://10.0.2.2
  2. Supply an HttpClientHandler for the HttpClient that allows for self-signed certs to be run

Next week I’ll cover the Maui set up and implementing MVVM.

Thanks,
Brian

Leave a Reply

Your email address will not be published. Required fields are marked *

FormatException

928 East Plymouth Drive Asbury Park, NJ 07712