MattVon
MattVon

Reputation: 521

How can I receive a Push Notification when my Xamarin Android app is stopped?

I've seen many different types of solutions which may have worked in the past, but nothing solid that has worked for myself. And it's a minefield of what people say works, what doesn't work, what has changed, etc. But I'm trying to find not only a solution but hopefully an understanding - because right now I am seriously confused.


What I can do right now - My Xamarin Forms app (Android) can receive push notification if the app is in the Foreground/Background, I can also intercept these notifications when the user taps them so I can tell my app what to do.

What I am trying to do - Essentially the above but in the state of where the app has been completely stopped.


I have my Firebase Messaging setup which is wired to Azure Notification Hub - unfortunately, I won't be moving away from Azure (just in case anyone suggests to drop it). Most of what I have currently is information I've managed to stitch together from various Microsoft Documentation (here I don't use AppCenter - just used this to cross-reference any useful code, here, here, and here ), other StackOverflow questions (such as here, here, and here - far too many more to link) and the Xamarin forum - so again, apologies if there is any obsolete code being used (please let me know - I have tried my best to use up-to-date methods, etc).

The type of push notifications that I am sending are Data Messages which I read up on here, I'm using custom data in my notifications therefore it is my understanding this is the correct type of push I want to send, as shown below.

{
    "data": {
        "title": "Title Test",
        "body": "Push notification body test",
        "area": "SelectedPage"
    }
}

Below is the current code I have setup in my project to handle push notifications thus far.

Manifest

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" android:versionCode="1" android:versionName="1.0" package="com.companyname.pushtesting" android:installLocation="auto">
    <uses-sdk android:minSdkVersion="21" android:targetSdkVersion="28" />
    <application android:label="Push Testing">

        <receiver android:name="com.google.firebase.iid.FirebaseInstanceIdInternalReceiver" android:exported="false" />
        <receiver android:name="com.google.firebase.iid.FirebaseInstanceIdReceiver" android:exported="true" android:permission="com.google.android.c2dm.permission.SEND">
            <intent-filter>
                <action android:name="com.google.android.c2dm.intent.RECEIVE" />
                <action android:name="com.google.android.c2dm.intent.REGISTRATION" />
                <category android:name="${applicationId}" />
            </intent-filter>
        </receiver>

    </application>
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
</manifest>

MainActivity.cs

I have LaunchMode = LaunchMode.SingleTop, would my understanding be correct, for this is to ensure the current Activity is still used instead of creating a new one - for instance, when the user would tap a notification - implementing it plus additional code (below) seems to suggest this is true.

protected override void OnNewIntent(Intent intent) {
    base.OnNewIntent(intent);

    String area = String.Empty;
    String extraInfo = String.Empty;

    if (intent.Extras != null) {
        foreach (String key in intent.Extras.KeySet()) {
            String value = intent.Extras.GetString(key);
            if (key == "Area" && !String.IsNullOrEmpty(value)) {
                area = value;
            } else if (key == "ExtraInfo" && !String.IsNullOrEmpty(value)) {
                extraInfo = value;
            }
        }
    }
    NavigationExtension.HandlePushNotificationNavigation(area, extraInfo);
}

Using OnNewIntent to intercept the push notification when the user interacts with it.

MyFirebaseMessaging.cs

using System;
using System.Threading.Tasks;
using Android.App;
using Android.Content;
using Android.Support.V4.App;
using Android.Util;
using Firebase.Messaging;
using PushTesting.Models;
using WindowsAzure.Messaging;

namespace PushTesting.Droid.Services {

    [Service]
    [IntentFilter(new[] { "com.google.firebase.MESSAGING_EVENT" })]
    public class OcsFirebaseMessaging : FirebaseMessagingService {

        private const String NotificationChannelId = "1152";
        private const String NotificationChannelName = "Push Notifications";
        private const String NotificationChannelDescription = "Receive notifications";
        private NotificationManager notificationManager;

        public override void OnNewToken(String token) => SendTokenToAzure(token);

        /// <summary>
        /// Sends the token to Azure for registration against the device
        /// </summary>
        private void SendTokenToAzure(String token) {
            try {
                NotificationHub hub = new NotificationHub(Constants.AzureConstants.NotificationHub, Constants.AzureConstants.ListenConnectionString, Android.App.Application.Context);

                Task.Run(() => hub.Register(token, new String[] { }));
            } catch (Exception ex) {
                Log.Error("ERROR", $"Error registering device: {ex.Message}");
            }
        }

        /// <summary>
        /// When the app receives a notification, this method is called
        /// </summary>
        public override void OnMessageReceived(RemoteMessage remoteMessage) {
            Boolean hasTitle = remoteMessage.Data.TryGetValue("title", out String title);
            Boolean hasBody = remoteMessage.Data.TryGetValue("body", out String body);
            Boolean hasArea = remoteMessage.Data.TryGetValue("area", out String area);
            Boolean hasExtraInfo = remoteMessage.Data.TryGetValue("extraInfo", out String extraInfo);

            PushNotificationModel push = new PushNotificationModel {
                Title = hasTitle ? title : String.Empty,
                Body = hasBody ? body : String.Empty,
                Area = hasArea ? area : String.Empty,
                ExtraInfo = hasExtraInfo ? extraInfo : String.Empty
            };

            SendNotification(push);
        }

        /// <summary>
        /// Handles the notification to ensure the Notification manager is updated to alert the user
        /// </summary>
        private void SendNotification(PushNotificationModel push) {
            // Create relevant non-repeatable Id to allow multiple notifications to be displayed in the Notification Manager
            Int32 notificationId = Int32.Parse(DateTime.Now.ToString("MMddHHmmsss"));

            Intent intent = new Intent(this, typeof(MainActivity));
            intent.AddFlags(ActivityFlags.ClearTop | ActivityFlags.SingleTop);
            intent.PutExtra("Area", push.Area);
            intent.PutExtra("ExtraInfo", push.ExtraInfo);

            PendingIntent pendingIntent = PendingIntent.GetActivity(this, notificationId, intent, PendingIntentFlags.UpdateCurrent);
            notificationManager = (NotificationManager)GetSystemService(Context.NotificationService);

            // Creates Notification Channel for Android devices running Oreo (8.0.0) or later
            if (Android.OS.Build.VERSION.SdkInt >= Android.OS.BuildVersionCodes.O) {
                NotificationChannel notificationChannel = new NotificationChannel(NotificationChannelId, NotificationChannelName, NotificationImportance.High) {
                    Description = NotificationChannelDescription
                };

                notificationManager.CreateNotificationChannel(notificationChannel);
            }

            // Builds notification for Notification Manager
            Notification notification = new NotificationCompat.Builder(this, NotificationChannelId)
            .SetSmallIcon(Resource.Drawable.ic_launcher)
            .SetContentTitle(push.Title)
            .SetContentText(push.Body)
            .SetContentIntent(pendingIntent)
            .SetAutoCancel(true)
            .SetShowWhen(false)
            .Build();

            notificationManager.Notify(notificationId, notification);
        }
    }
}

Then finally my Firebase class where I use OnNewToken() to register to Azure with, OnMessageReceived() is overridden so I can handle a push notification being received and then SendNotification() which is being used to build and show the notification.

Any and all help will be highly appreciated!

Added Sample Project to GitHub

Upvotes: 3

Views: 4497

Answers (1)

Frank
Frank

Reputation: 1080

There were a number of issues for me (more details here):

  • Unfamiliarity with the Android app architecture and how that relates to the FirebaseMessagingService lifecycle: There is the app, services and receivers. When the 'app' (from a user perspective) is closed, by swiping from recents, the services/receivers are available to react to 'intents' (the incoming notification message). When the app is 'force stopped', no ui, services or receivers run.
  • Visual Studio force stops apps after the debug session ends. To diagnose a 'stopped' app you need to run the app on device and look at Device Logs.
  • The FirebaseMessagingService lifecycle is such that when the app is in the stopped state the ctor no longer has access to properties or methods of the shared app (I had to work around this by removing abstractions and making code platform specific - in particular a DependencyService that could not be used as it was not available).
  • MS docs are out of date. Firebase manifest entries no longer require the Receiver elements for example.
  • You cannot attach debugger to a 'stopped' app in Visual Studio.
  • Error messages on device are cryptic.

In the end the solution was to View->Other Windows -> Device Logs and run the app a second time to avoid the Force Stopped state, to discover the ctor lifecycle issue, which I had to work around by moving out code that touched the shared app library from FirebaseMessagingService.

Upvotes: 1

Related Questions