Wade Baird
Wade Baird

Reputation: 333

Trouble getting local notifications to fire in Android when the application is closed (using AlarmManager)

I am trying to augment an application I am writing with daily reminders for the user to open the app. The user sets the number and times of these reminders. I have already implemented it very easily in iOS following these two guidelines:

Older one: https://www.c-sharpcorner.com/article/how-to-send-local-notification-with-repeat-interval-in-xamarin-forms/ Newer one: https://learn.microsoft.com/en-us/xamarin/xamarin-forms/app-fundamentals/local-notifications

For Android, I have it all working well when the application is open or in the background. However, I am having problems when the application is closed. I can load the app, create the reminders, which calls the "SendNotification" in the code below. If I close the app I never see the notifications open up in the notification area. I am aware of having to use a BootAction BroadcastReceiver for the device reboot scenario as I understand that the Alarms are all canceled in the case. With my log statements, for that scenario, I know that the receiver is called and the alarms are scheduled. But I also never see any notification appear for those.

For both of these scenarios where the app isn't open (closed and device rebooted) I have done a lot of searching and reading but can't find a definitive answer on how to get it working. I see recommendations about writing a service, but not sure how to do that as I have my AlarmHandler Broadcast receiver set to receive the incoming intents for the alarms and that never happens. For the device reboot, my AlarmHandler is running and isn't disposed of, so it should be able to receive them.

Here is the code I have:

My NotificationManager which does the device specific calls:

[assembly: UsesPermission(Android.Manifest.Permission.SetAlarm)]
[assembly: UsesPermission(Android.Manifest.Permission.ScheduleExactAlarm)]
[assembly: UsesPermission(Android.Manifest.Permission.WakeLock)]
[assembly: Dependency(typeof(Sunbreak.Droid.Services.AndroidDeviceNotificationManager))]
namespace Sunbreak.Droid.Services;

//Old https://www.c-sharpcorner.com/article/how-to-send-local-notification-with-repeat-interval-in-xamarin-forms/
//New https://learn.microsoft.com/en-us/xamarin/xamarin-forms/app-fundamentals/local-notifications
//New https://developer.android.com/training/scheduling/alarms
public class AndroidDeviceNotificationManager : IDeviceNotificationManager
{
    const string channelId = "daily-reminders";
    const string channelName = "Reminders";
    const string channelDescription = "The channel for daily reminder notifications.";

    public const string MessageIdKey = "messageId";
    public const string TitleKey = "title";
    public const string MessageKey = "message";

    private bool channelInitialized = false;

    private NotificationManager manager;

    public event EventHandler NotificationReceived;

    private bool HasNotificationsPermission { get; set; }

    public static AndroidDeviceNotificationManager Instance { get; private set; }

    public AndroidDeviceNotificationManager() => Initialize();

    public void Initialize()
    {
        if (Instance == null)
        {
            CreateNotificationChannel();
            Instance = this;
        }

        HasNotificationsPermission = CheckPermissions().Result;
    }

    private AlarmManager GetAlarmManager()
    {
        return AndroidApp.Context.GetSystemService(Context.AlarmService) as AlarmManager;
    }

    private PendingIntent CreateBroadcastPendingIntent(int messageId, string title = null, string message = null)
    {
        Intent intent = new(AndroidApp.Context, typeof(AlarmHandler));
        intent.PutExtra(MessageIdKey, messageId);

        if (title != null)
        {
            intent.PutExtra(TitleKey, title);
        }

        if (message != null)
        {
            intent.PutExtra(MessageKey, message);
        }

        return PendingIntent.GetBroadcast(AndroidApp.Context, messageId, intent, PendingIntentFlags.Immutable);
    }

    public void SendNotification(int messageId, string title, string message, TimeSpan notifyTime)
    {
        if (!HasNotificationsPermission)
        {
            return;
        }

        if (!channelInitialized)
        {
            CreateNotificationChannel();
        }
    
        Log.Info(BootBroadcastReceiver.LOG_TAG, $"AndroidDeviceNotificationService SendNotification MessageId:{messageId}, NotifyTime:{notifyTime}.");

        PendingIntent pendingIntent = CreateBroadcastPendingIntent(messageId, title, message);

        AlarmManager alarmManager = GetAlarmManager();
        alarmManager.SetRepeating(AlarmType.RtcWakeup, GetNotifyTime(notifyTime), AlarmManager.IntervalDay, pendingIntent);
    }

    public void ReceiveNotification(string title, string message)
    {
        var args = new NotificationEventArgs()
        {
            Title = title,
            Message = message,
        };
        NotificationReceived?.Invoke(null, args);
    }

    public void Show(int messageId, string title, string message)
    {
        var intent = new Intent(AndroidApp.Context, typeof(MainActivity));
        intent.PutExtra(MessageIdKey, messageId);
        intent.PutExtra(TitleKey, title);
        intent.PutExtra(MessageKey, message);

        PendingIntent pendingIntent = PendingIntent.GetActivity(AndroidApp.Context, messageId, intent, PendingIntentFlags.Immutable);

        NotificationCompat.Builder builder = new NotificationCompat.Builder(AndroidApp.Context, channelId)
            .SetContentIntent(pendingIntent)
            .SetContentTitle(title)
            .SetContentText(message)
            .SetSmallIcon(Resource.Drawable.launchimage)
            .SetDefaults((int)NotificationDefaults.Sound | (int)NotificationDefaults.Vibrate);

        Notification notification = builder.Build();
        manager.Notify(messageId, notification);
    }

    private void CreateNotificationChannel()
    {
        manager = (NotificationManager)AndroidApp.Context.GetSystemService(AndroidApp.NotificationService);

        if (Build.VERSION.SdkInt >= BuildVersionCodes.O)
        {
            var channelNameJava = new Java.Lang.String(channelName);
            var channel = new NotificationChannel(channelId, channelNameJava, NotificationImportance.Default)
            {
                Description = channelDescription
            };
            manager.CreateNotificationChannel(channel);
        }

        channelInitialized = true;
    }

    private long GetNotifyTime(TimeSpan notifyTime)
    {
        DateTime localTime = DateTime.Today.AddSeconds(notifyTime.TotalSeconds);
        DateTime utcTime = TimeZoneInfo.ConvertTimeToUtc(localTime);
        double epochDiff = (new DateTime(1970, 1, 1) - DateTime.MinValue).TotalSeconds;
        long utcAlarmTime = utcTime.AddSeconds(-epochDiff).Ticks / 10000;
        return utcAlarmTime; // milliseconds
    }

    public void CancelNotification(int messageId)
    {
        PendingIntent pendingIntent = CreateBroadcastPendingIntent(messageId);
        if (pendingIntent != null)
        {
            var alarmManager = GetAlarmManager();
            alarmManager.Cancel(pendingIntent);
            var notificationManager = NotificationManagerCompat.From(AndroidApp.Context);
            notificationManager.Cancel(messageId);
        }
    }

    public void CancelAllNotifications()
    {
        throw new NotImplementedException();
    }

    public List<string> UngrantedPermissionsList()
    {
        var listPermissions = new List<string>();
        // Build array of permissions needed for app usage
        if (AndroidApp.Context.CheckSelfPermission(Manifest.Permission.WakeLock) != Permission.Granted)
        {
            listPermissions.Add(Manifest.Permission.WakeLock);
        }
        if (AndroidApp.Context.CheckSelfPermission(Manifest.Permission.SetAlarm) != Permission.Granted)
        {
            listPermissions.Add(Manifest.Permission.SetAlarm);
        }
        if (AndroidApp.Context.CheckSelfPermission(Manifest.Permission.ReceiveBootCompleted) != Permission.Granted)
        {
            listPermissions.Add(Manifest.Permission.ReceiveBootCompleted);
        }
        if (Android.OS.Build.VERSION.SdkInt >= Android.OS.BuildVersionCodes.S)
        {
            if (AndroidApp.Context.CheckSelfPermission(Manifest.Permission.ScheduleExactAlarm) != Permission.Granted)
            {
                listPermissions.Add(Manifest.Permission.ScheduleExactAlarm);
            }
        }

        return listPermissions;
    }

    public Task<bool> CheckPermissions()
    {
        var permissionsList = UngrantedPermissionsList();
        HasNotificationsPermission = permissionsList.Count == 0;
        return Task.FromResult(HasNotificationsPermission);
    }

    public Task<IList<Models.NotificationInfo>> GetPendingNotifications()
    {
        var notificationInfos = new List<Models.NotificationInfo>();

        return Task.FromResult<IList<Models.NotificationInfo>>(notificationInfos);
    }
}

Here is my class which handles the Broadcast Receiving for both when the app is open and the on Boot scenarios (I tried having this in two seperate receivers and it works / doesn't work the same either way)

[BroadcastReceiver(Enabled = true, Exported = true, Label = "Local Notifications Broadcast Receiver")]
[IntentFilter(new[] { Intent.ActionBootCompleted })]
public class AlarmHandler : BroadcastReceiver
{
    private readonly AndroidDeviceNotificationManager NotificationService;
    private readonly Analytics Analytics = new();
    public const string LOG_TAG = "MYAPP_NOTIFICATIONS_LOG";

    public AlarmHandler()
    {
        Log.Info(LOG_TAG, $"{nameof(AlarmHandler)} Created.");

        Microsoft.AppCenter.AppCenter.Start($"{App.AndroidAppCenterId};", new Type[] { typeof(Microsoft.AppCenter.Analytics.Analytics), typeof(Microsoft.AppCenter.Crashes.Crashes) });
        NotificationService = AndroidDeviceNotificationManager.Instance ?? new AndroidDeviceNotificationManager();
    }

    public override void OnReceive(Context context, Intent intent)
    {
        try
        {
            Log.Info(LOG_TAG, $"{nameof(AlarmHandler)}.{nameof(OnReceive)}.");
            if (intent.Action == Intent.ActionBootCompleted)
            {
                Log.Info(LOG_TAG, $"{nameof(AlarmHandler)}.{nameof(OnReceive)} ActionBookComplete.");
                var startIntent = new Intent(context, typeof(AlarmSchedulingService));
                context.StartService(startIntent);
            }
            else if (intent?.Extras != null)
            {
                int messageId = intent.GetIntExtra(AndroidDeviceNotificationManager.MessageIdKey, -1);
                string title = intent.GetStringExtra(AndroidDeviceNotificationManager.TitleKey);
                string message = intent.GetStringExtra(AndroidDeviceNotificationManager.MessageKey);

                if (messageId != -1)
                {
                    NotificationService.Show(messageId, title, message);
                }
                else
                {
                    Analytics.TrackError(new ApplicationException($"Failed to show notification: Title '{title}', Message '{message}'."));
                }
            }
        }
        catch(Exception ex)
        {
            Analytics.TrackError(ex);
        }
    }

    protected override void Dispose(bool disposing)
    {
        Log.Info(LOG_TAG, $"{nameof(AlarmHandler)}.{nameof(Dispose)}.");
        base.Dispose(disposing);
    }
}

Here's my MainActivity:

[Activity(Theme = "@style/MainTheme.Splash", MainLauncher = true, Label = "App Name", Icon = "@mipmap/icon",
    ConfigurationChanges = ConfigChanges.ScreenSize | ConfigChanges.Orientation | ConfigChanges.UiMode | ConfigChanges.ScreenLayout |
    ConfigChanges.SmallestScreenSize, LaunchMode = LaunchMode.SingleTop)]
public class MainActivity : global::Xamarin.Forms.Platform.Android.FormsAppCompatActivity
{
    private const int PERMISSION_REQUEST_CODE_NOTIFICATION_USAGE = 4500;  // Arbitrary number to identify our permissions

    protected override void OnCreate(Bundle savedInstanceState)
    {
        base.OnCreate(savedInstanceState);

        Acr.UserDialogs.UserDialogs.Init(this);
        Xamarin.Essentials.Platform.Init(this, savedInstanceState);
        global::Xamarin.Forms.Forms.Init(this, savedInstanceState);

        LoadApplication(new App());

        CreateNotificationFromIntent(Intent);

        var notificationService = DependencyService.Get<IDeviceNotificationManager>();
        var listPermissions = ((AndroidDeviceNotificationManager)notificationService).UngrantedPermissionsList();

        if (listPermissions.Count > 0)
        {
            // Make the request with the permissions needed...and then check OnRequestPermissionsResult() for the results
            ActivityCompat.RequestPermissions(this, listPermissions.ToArray(), PERMISSION_REQUEST_CODE_NOTIFICATION_USAGE);
        }
    }

    protected override void OnNewIntent(Intent intent)
    {
        CreateNotificationFromIntent(intent);
    }

    private void CreateNotificationFromIntent(Intent intent)
    {
        if (intent?.Extras != null)
        {
            string title = intent.GetStringExtra(AndroidDeviceNotificationManager.TitleKey);
            string message = intent.GetStringExtra(AndroidDeviceNotificationManager.MessageKey);
            DependencyService.Get<IDeviceNotificationManager>().ReceiveNotification(title, message);
        }
    }

    public override void OnRequestPermissionsResult(int requestCode, string[] permissions, [GeneratedEnum] Android.Content.PM.Permission[] grantResults)
    {
        Xamarin.Essentials.Platform.OnRequestPermissionsResult(requestCode, permissions, grantResults);

        base.OnRequestPermissionsResult(requestCode, permissions, grantResults);
    }
}

I am guessing there is an issue with this line in the NotificationManager:

var intent = new Intent(AndroidApp.Context, typeof(MainActivity));

But this is in the Show method and the code isn't even getting there in the App closed scenarios. I don't even know what to put in here for the Intent's class name in those scenarios.

Thank you for any help you may offer.

Upvotes: 0

Views: 582

Answers (1)

Liyun Zhang - MSFT
Liyun Zhang - MSFT

Reputation: 14299

If your device's android version is higher than 8.0, you need to start a foreground service which will show a notification. The system will prevent auto start a service or a activity from background.

ForegroundServiceDemo.cs:

[Service]
public class ForegroundServiceDemo : Service
{
    public const string NOTIFICATION_CHANNEL_ID = "1000";
    private readonly int NOTIFICATION_ID = 1000;
    private readonly string NOTIFICATION_CHANNEL_NAME = "hello";
    private void startForegroundService()
    {
            var notifcationManager = GetSystemService(Context.NotificationService) as NotificationManager;
            var intent = new Intent(this, typeof(MainActivity));
            PendingIntent pendingIntent = PendingIntent.GetActivity(this, 1, intent, PendingIntentFlags.OneShot);

            if (Build.VERSION.SdkInt >= BuildVersionCodes.O)
            {
                createNotificationChannel(notifcationManager);
            }
            var notification = new NotificationCompat.Builder(this, NOTIFICATION_CHANNEL_ID);
            notification.SetAutoCancel(false);
            notification.SetSmallIcon(Resource.Drawable.icon);
            notification.SetContentTitle("This is notification title");
            notification.SetContentText("This is notification message");
            notification.SetContentIntent(pendingIntent);
            StartForeground(NOTIFICATION_ID, notification.Build());
    }

    private void createNotificationChannel(NotificationManager notificationMnaManager)
    {
            var channel = new NotificationChannel(NOTIFICATION_CHANNEL_ID, NOTIFICATION_CHANNEL_NAME,
                NotificationImportance.Low);
            notificationMnaManager.CreateNotificationChannel(channel);
    }

    public override IBinder OnBind(Intent intent)
    {
        return null;
    }

    public override StartCommandResult OnStartCommand(Intent intent, StartCommandFlags flags, int startId)
    {
        startForegroundService();
        return StartCommandResult.RedeliverIntent;
    }
    public override void OnDestroy()
    {
        base.OnDestroy();
    }
}

Start the foreground service in the broadcast receiver:

 Intent new_intent = new Intent(context, typeof(ForegroundServiceDemo));
 new_intent.AddFlags(ActivityFlags.NewTask);
 context.StartForegroundService(new_intent);

In addition, you need to add the permission of the foreground service:

<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.REQUEST_COMPANION_START_FOREGROUND_SERVICES_FROM_BACKGROUND" />
<uses-permission android:name="android.permission.START_FOREGROUND_SERVICES_FROM_BACKGROUND" />

Upvotes: 1

Related Questions