I am having trouble with local notification on Android using Xamarin. The local notification shows every time the app is in the foreground or open. If I reboot the notification does not show. There is a RECEIVE_BOOT_COMPLETED
BroadcastReceiver
but I do not know if it is firing on reboot.
I cannot figure out what is wrong. I have tripled checked everything and feel like something small is missing.
Edit: I think I have it worked out for notifications on the app being closed thanks to this thread: Broadcast receiver not working when app is closed. Essentially, if debugging, the app needs to be reopened after closing it so that the receiver is called.
I used this doc to setup local notifications: https://docs.microsoft.com/en-us/xamarin/xamarin-forms/app-fundamentals/local-notifications
AndroidManifest.cs
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" android:versionCode="5" android:versionName="1.0.2.1" package="com.business.myapp" android:installLocation="preferExternal">
<uses-sdk android:minSdkVersion="21" android:targetSdkVersion="31" />
<application android:label="MyApp" android:theme="@style/MainTheme" android:supportsRtl="true" android:icon="@mipmap/icon"></application>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="com.android.alarm.permission.SET_ALARM" />
</manifest>
AlarmHandler.cs
using Android.Content;
using MyApp.Business.Services;
using MyApp.Global;
using MyApp.Utils;
using Serilog;
using System;
namespace MyApp.Droid
{
/// <summary>
/// Handle notification receiving.
///
/// https://github.com/xamarin/xamarin-forms-samples/blob/main/LocalNotifications/LocalNotifications/LocalNotifications.Android/AlarmHandler.cs
/// </summary>
[BroadcastReceiver(
Enabled = true,
Exported = true,
Label = "Local Notifications Broadcast Receiver")]
public class AlarmHandler : BroadcastReceiver
{
/// <summary>
/// Called when a notification is shown.
/// </summary>
/// <param name="context"></param>
/// <param name="intent"></param>
public override void OnReceive(Context context, Intent intent)
{
// if (intent?.Extras != null)
// {
if (!Settings.DailyNotificationEnabled) return;
// do not show notification if it is not time based on the settings time value.
// this handles cases where the user changes the notification time multiple times.
/* Disabled on 3/6/2022 to see if this helps notifications.
var timeSpan = DateTime.Now - NotificationDateTime.GetDailyNotificationDateTime();
var absTotalSeconds = Math.Abs(timeSpan.TotalSeconds);
if (absTotalSeconds > 59 && absTotalSeconds < 86399) return;
*/
string title = intent.GetStringExtra(AndroidNotificationManager.TitleKey);
string message = intent.GetStringExtra(AndroidNotificationManager.MessageKey);
title = Constants.NotificationOffllineTitle;
message = Constants.NotificationOffllineMessage;
try
{
var votdHttpRequest = new VotdHttpRequest();
var votdApiDto = votdHttpRequest.MakeApiCall(DateTime.Now, Settings.TwoLetterISOLanguageName);
title = $"{DateTime.Now.ToShortDateString()} {votdApiDto.@ref} ({votdApiDto.ver.ToUpper()})";
var scripture = Settings.DisplayKjvVersion ? votdApiDto.en_scrip_kjv : votdApiDto.en_scrip;
message = Remove.RecursiveHtmlDecode(scripture);
}
catch (Exception ex)
{
Log.Error(ex, "Failed recieving notification.");
}
AndroidNotificationManager manager = AndroidNotificationManager.Instance ?? new AndroidNotificationManager();
manager.Show(title, message);
// }
}
}
}
AndroidNotificationManager.cs
using System;
using Android.App;
using Android.Content;
using Android.Graphics;
using Android.OS;
using AndroidX.Core.App;
using MyApp.Notifications;
using Xamarin.Forms;
using AndroidApp = Android.App.Application;
[assembly: Dependency(typeof(MyApp.Droid.AndroidNotificationManager))]
namespace MyApp.Droid
{
/// <summary>
/// Daily notification manager.
/// </summary>
public class AndroidNotificationManager : INotificationManager
{
#region Private Fields
const string channelId = "default";
const string channelName = "Default";
const string channelDescription = "The default channel for notifications.";
bool channelInitialized = false;
int messageId = 0;
int pendingIntentId = 0;
NotificationManager manager;
#endregion
#region Public Properties
public const string TitleKey = "title";
public const string MessageKey = "message";
public event EventHandler NotificationReceived;
public static AndroidNotificationManager Instance { get; private set; }
public AndroidNotificationManager() => Initialize();
#endregion
#region Constructor
#endregion
#region Private Methods
/// <summary>
/// Create the notification channel.
/// </summary>
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;
}
/// <summary>
/// Get the time of notification in UTC ticks.
/// </summary>
/// <param name="notifyTime"></param>
/// <returns></returns>
long GetNotifyTime(DateTime notifyTime)
{
DateTime utcTime = TimeZoneInfo.ConvertTimeToUtc(notifyTime);
double epochDiff = (new DateTime(1970, 1, 1) - DateTime.MinValue).TotalSeconds;
long utcAlarmTime = utcTime.AddSeconds(-epochDiff).Ticks / 10000;
return utcAlarmTime; // milliseconds
}
#endregion
#region Public Methods
/// <summary>
/// Initialize the notification channel
/// </summary>
public void Initialize()
{
if (Instance == null)
{
CreateNotificationChannel();
Instance = this;
}
}
/// <summary>
/// Schedule a notification.
/// </summary>
/// <param name="title"></param>
/// <param name="message"></param>
/// <param name="notifyTime">Time of notification.</param>
public void SendNotification(string title, string message, int id, DateTime? notifyTime = null)
{
if (!channelInitialized)
{
CreateNotificationChannel();
}
if (notifyTime != null)
{
var intent = CreateIntent(id);
intent.PutExtra(TitleKey, title);
intent.PutExtra(MessageKey, message);
intent.SetIdentifier(id.ToString());
PendingIntent pendingIntent = PendingIntent.GetBroadcast(
AndroidApp.Context,
pendingIntentId ,
intent,
PendingIntentFlags.Immutable);
long triggerTime = GetNotifyTime(notifyTime.Value);
long millisecondsRepeating = AlarmManager.IntervalDay;
AlarmManager alarmManager = AndroidApp.Context.GetSystemService(Context.AlarmService) as AlarmManager;
alarmManager.SetRepeating(AlarmType.RtcWakeup, triggerTime, millisecondsRepeating, pendingIntent);
}
else
{
Show(title, message);
}
}
/// <summary>
/// Called when a notifiation is received.
/// </summary>
/// <param name="title"></param>
/// <param name="message"></param>
public void ReceiveNotification(string title, string message)
{
var args = new NotificationEventArgs()
{
Title = title,
Message = message,
};
NotificationReceived?.Invoke(null, args);
}
/// <summary>
/// Display the notification in the notification area.
/// </summary>
/// <param name="title"></param>
/// <param name="message"></param>
public void Show(string title, string message)
{
Intent intent = new Intent(AndroidApp.Context, typeof(MainActivity));
intent.PutExtra(TitleKey, title);
intent.PutExtra(MessageKey, message);
PendingIntent pendingIntent = PendingIntent.GetActivity(
AndroidApp.Context,
pendingIntentId ,
intent,
PendingIntentFlags.UpdateCurrent | PendingIntentFlags.Immutable);
var textStyle = new NotificationCompat.BigTextStyle();
textStyle.BigText(message);
textStyle.SetSummaryText(title);
NotificationCompat.Builder builder = new NotificationCompat.Builder(AndroidApp.Context, channelId)
.SetContentIntent(pendingIntent)
// .SetContentTitle(title) // not required with big text style
// .SetContentText(message) // not required with big text style
.SetLargeIcon(BitmapFactory.DecodeResource(AndroidApp.Context.Resources, Resource.Drawable.icon_votd))
.SetSmallIcon(Resource.Drawable.icon_votd)
.SetDefaults((int)NotificationDefaults.Sound | (int)NotificationDefaults.Vibrate)
.SetStyle(textStyle);
if (Build.VERSION.SdkInt >= BuildVersionCodes.Lollipop)
builder.SetVisibility((int)NotificationVisibility.Public);
Notification notification = builder.Build();
notification.Flags = NotificationFlags.AutoCancel;
manager.Notify(messageId , notification);
}
/// <summary>
/// Delete a notification.
/// </summary>
/// <param name="id"></param>
public void DeleteNotification(int id)
{
Intent intent = new Intent(AndroidApp.Context, typeof(AlarmHandler));
PendingIntent pendingIntent = PendingIntent.GetBroadcast(
AndroidApp.Context,
id, intent,
PendingIntentFlags.CancelCurrent | PendingIntentFlags.Immutable);
AlarmManager alarmManager = AndroidApp.Context.GetSystemService(Context.AlarmService) as AlarmManager;
alarmManager.Cancel(pendingIntent);
}
private Intent CreateIntent(int id)
{
return new Intent(Android.App.Application.Context, typeof(AlarmHandler)).SetAction("LocalNotifierIntent" id);
}
public void CancelAlarm(int id)
{
var intent = CreateIntent(id);
var pendingIntent = PendingIntent.GetBroadcast(
Android.App.Application.Context,
0,
intent,
PendingIntentFlags.UpdateCurrent | PendingIntentFlags.Immutable);
var alarmManager = GetAlarmManager();
alarmManager.Cancel(pendingIntent);
var notificationManager = NotificationManagerCompat.From(Android.App.Application.Context);
notificationManager.CancelAll();
notificationManager.Cancel(id);
}
private AlarmManager GetAlarmManager()
{
var alarmManager = Android.App.Application.Context.GetSystemService(Context.AlarmService) as AlarmManager;
return alarmManager;
}
#endregion
}
}
BootReciever.cs
using Android.App;
using Android.Content;
using MyApp.Notifications;
using MyApp.Utils;
using System;
using Xamarin.Forms;
namespace MyApp.Droid.Notifications
{
/// <summary>
/// Used to reschedule notifications on boot. Android notifications are deleted when the OS is rebooted.
/// </summary>
[BroadcastReceiver(
Enabled = true,
Label = "Reboot complete receiver",
Exported = true)]
[IntentFilter(new[] { Android.Content.Intent.ActionBootCompleted })]
public class BootReceiver : BroadcastReceiver
{
/// <summary>
/// Called on boot.
/// </summary>
/// <param name="context"></param>
/// <param name="intent"></param>
public override void OnReceive(Context context, Intent intent)
{
//if (intent.Action == "android.intent.action.BOOT_COMPLETED")
//{
// ScheduleDailyNotification();
//}
ScheduleDailyNotification();
}
/// <summary>
/// Schedule the daily notification.
/// </summary>
private void ScheduleDailyNotification()
{
if (Settings.DailyNotificationEnabled)
{
var notificationManager = DependencyService.Get<INotificationManager>();
var notificationDateTime = NotificationDateTime.GetDailyNotificationDateTime();
notificationManager.SendNotification(
Global.Constants.NotificationOffllineTitle,
Global.Constants.NotificationOffllineMessage,
Global.Constants.DailyNotificationId,
//DateTime.Now.AddSeconds(60));
notificationDateTime);
}
}
}
}
CodePudding user response:
I hate to delete a question from the internet.
The issue on boot received was using dependency injection in the boot receiver. Exception: You must call Xamarin.Forms.Forms.Init(); prior to using this property.
I just instantiated the class instead of using DI for this instance.
Here is what worked for me on the issue with the notification not showing when the app was closed: https://stackoverflow.com/a/60197247/814891
After registering my BroadcastReceiver (BR) statically in the manifest, applying the proper intent filters, using JobIntentService (and registering it in the manifest) to handle the work that was called from my BR, I was still getting inconsistent results.
Once all of what I listed above has been done you should be able to send an ADB command to activate your broadcast and process the work in your service even if the app is closed. This was working for me some of the time, but not all of the time.
This article describes limitation to BRs. "As of Android 3.1 the Android system excludes all receiver from receiving intents by default if the corresponding application has never been started by the user or if the user explicitly stopped the application via the Android menu" (AKA a user executes Force Stop)
When I start the app by debugging it, then swipe it closed on my device, my ADB command never activates my BR. However, after my debugging session is over, when I open up the app on my device and swipe it closed, I can activate my BR through ADB commands.
This occurs because when you debug an application, then manually swipe it closed on the device, Android considers this a Force Stop hence why my BR cannot be activated until I re-open the app on the device without debugging.
Scoured the internet for hours, and wasn't able to find my solution, so I thought I'd post it here just in case some poor unfortunate soul is encountering the same weird functionality I was.
Happy coding :)