shinyorg / shiny

.NET Framework for Backgrounding & Device Hardware Services (iOS, Android, & Catalyst)

Home Page:https://shinylib.net

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Cannot send local notification with a ScheduleDate set using 3.2.4

munkii opened this issue · comments

Component/Nuget

Notifications (Shiny.Notifications)

What operating system(s) are effected?

  • iOS (13+ supported)
  • Mac Catalyst
  • Android (8+ supported)

Version(s) of Operation Systems

Android 12, 13 and 14 and 9 (Physical devices)

Hosting Model

  • MAUI
  • Native/Classic Xamarin
  • Manual

Steps To Reproduce

Using Shiny 3.2.4 set the ScheduleDate on the local notifcation and try and send it. Notifcation does not appear

  • Previous version 2.7.3 worked fine with Android
  • 3.2.3 works fine with iOS

Expected Behavior

I'd expect to see a scheduled notification or an error.

Permission issues with Android 14 wouldn't be that surprising and I have logged an issue with Xamarin Essentials FWIW

Actual Behavior

No error and no Notification. If I do not set the ScheduleData the notifcation happens immediately without issue

Exception or Log output

No response

Code Sample

var status = await this.scheduleExactNotificationPermission.CheckStatusAsync();
if (status != PermissionStatus.Granted)
{
    status = await this.scheduleExactNotificationPermission.RequestAsync();
}

AccessState state = await this.notificationManager.RequestAccess(AccessRequestFlags.TimeSensitivity);

if (state == AccessState.Available || status == PermissionStatus.Granted)
{
    var pendingNotificationsOnThisDevice = await this.notificationManager.GetPendingNotifications();

    if (pendingNotificationsOnThisDevice.Any(n => n.Title == title) == false)
    {
        var notification = new Notification()
        {
            Title = title,
            Message = reminderText,
            Channel = "DefaultH",
            ScheduleDate = DateTime.Now.AddMinutes(30),
        };

#if DEBUG
        notification.ScheduleDate = DateTime.Now.AddMinutes(2);
        notification.Message += " DEBUG";
#endif
        System.Diagnostics.Debug.WriteLine("RemindToCallAsync Create Scheduled Notification");

        await this.notificationManager.Send(notification);
    }
    else
    {
        // There is a pending notification to call the Nurse Team. Do not hassle user with another one.
        this.logger.LogInteraction("Pending notification. Do not add another.");
    }
}

AndroidManifest

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:usesCleartextTraffic="false" android:versionCode="1" android:versionName="3.7.0" package="com.Us.OurProjectApp" android:installLocation="auto">
	<uses-sdk android:minSdkVersion="24" android:targetSdkVersion="33" />
	<uses-permission android:name="android.permission.INTERNET" />
	<uses-permission android:name="android.permission.WAKE_LOCK" />
	<uses-permission android:name="android.permission.USE_EXACT_ALARM" />
	<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />
	<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
	<uses-permission android:name="android.permission.VIBRATE" />
	<uses-permission android:name="android.permission.BLUETOOTH" tools:node="replace" android:maxSdkVersion="30" />
	<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" tools:node="replace" android:maxSdkVersion="30" />
	<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
	<uses-permission-sdk-23 android:name="android.permission.ACCESS_COARSE_LOCATION" tools:node="remove" android:maxSdkVersion="30" />
	<uses-permission-sdk-23 android:name="android.permission.ACCESS_FINE_LOCATION" tools:node="remove" android:maxSdkVersion="30" />
	<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" tools:node="replace" tools:targetApi="31" />
	<uses-permission android:name="android.permission.BLUETOOTH_SCAN" tools:node="replace" android:usesPermissionFlags="neverForLocation" tools:targetApi="31" />
	<uses-feature android:name="android.hardware.bluetooth_le" android:required="true" />
	<application android:label="US RM" android:icon="@mipmap/app_icon" Name="US RM" tools:replace="android:label">
		<meta-data android:name="com.google.firebase.messaging.default_notification_channel_id" android:value="DefaultH" />
		<activity android:name="microsoft.identity.client.BrowserTabActivity" android:exported="true">
			<intent-filter>
				<action android:name="android.intent.action.VIEW" />
				<category android:name="android.intent.category.DEFAULT" />
				<category android:name="android.intent.category.BROWSABLE" />
				<data android:scheme="msalGUIDHERE" android:host="auth" />
			</intent-filter>
		</activity>
	</application>
	<queries>
		<intent>
			<action android:name="android.support.customtabs.action.CustomTabsService" />
		</intent>
		<intent>
			<action android:name="android.intent.action.SENDTO" />
			<data android:scheme="mailto" />
		</intent>
		<intent>
			<action android:name="android.intent.action.DIAL" />
			<data android:scheme="tel" />
		</intent>
	</queries>
</manifest>

AssemblyInfo Permission attributes

[assembly: UsesPermission(Android.Manifest.Permission.Internet)]
[assembly: UsesPermission(Android.Manifest.Permission.AccessNetworkState)]
[assembly: UsesPermission(Android.Manifest.Permission.WriteExternalStorage)]
[assembly: UsesPermission(Android.Manifest.Permission.PostNotifications)]
[assembly: UsesPermission(Android.Manifest.Permission.ScheduleExactAlarm)]
[assembly: UsesPermission(Android.Manifest.Permission.UseExactAlarm)]

Code of Conduct

  • I have supplied a reproducible sample that is NOT FROM THE SHINY SAMPLES!
  • I am a Sponsor OR I am using the LATEST stable/beta version from nuget (v3.0 stable - ALPHAS are not taking issues - Sponsors can still send v2 issues)
  • I am Sponsor OR My GitHub account is 30+ days old
  • I understand that if I am checking these boxes and I am not actually following what they are saying, I will be removed from this repository!

Can you share your android manifest

Previous versions of local notifications used shiny jobs which caused delays. V3 uses the alarm which needs a permission request. Try the extension off INotificationManager called RequestRequiredAccess(Notification notification)

I tried calling RequestRequiredAccess with a notification that had ScheduleDate set and I got NullReferenceException. I copied the extension method so I could run it locally.

var request = AccessRequestFlags.Notification;
if (notification.RepeatInterval != null)
    request |= AccessRequestFlags.TimeSensitivity;

if (notification.ScheduleDate != null)
{
    var channelId = notification.Channel ?? Channel.Default.Identifier;
    var channel = notificationManager.GetChannel(channelId)!;

    if (channel!.Importance == ChannelImportance.High)
        request |= AccessRequestFlags.TimeSensitivity;
}

if (notification.Geofence != null)
    request |= AccessRequestFlags.LocationAware;

return await this.notificationManager
    .RequestAccess(request)
    .ConfigureAwait(false);

The call to RequestAccess sees the following callback to MainActivity.OnRequestPermissionsResult

  1. requestCode = 4
  2. permissions = ["android.permission.POST_NOTIFICATIONS" "android.permission.SCHEDULE_EXACT_ALARM"]
  3. grantResults = [0 -1]

In one of my debug scenarios the channel had not been created. That throws a NullReferenceException when trying to read channel!.Importance. I do however agree I should not be calling this without checking the channel has been created. I am just pointing out that the method should probably check too.

Once the code runs on a device with the Channel I get an AccessState of Restricted.If I remove the Channel then the AccessRequestFlags.TimeSensitivity flag is not set and I get AccessState of Available.

This one is going to require some fiddling. Android 14 (13 a bit) changes the way the alarm permission works.

Hi, @munkii, @aritchie
I spend some time on investigation on Shiny 3.3.3 / dev branch of source code and MAUI but behavior should be really similar also for Xamarin and my conclusions are that there is couple things:

  1. in manifest probably should be max api 33 for SCHEDULE_EXACT_ALARM (now with Shiny.Templates is set to 32)
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" android:maxSdkVersion="33" />
<uses-permission android:name="android.permission.USE_EXACT_ALARM" />
  1. for api 34 we should check permissions on different way like android documentation suggest + request user to open special access settings in case when permission was not granted yet (on api 34 default is not granted, unfortunately I don't have good idea how to implement it in library)
 if (!this.Alarms.CanScheduleExactAlarms())
{
    var intent = new Android.Content.Intent(Android.Provider.Settings.ActionRequestScheduleExactAlarm);
    Application.Current.Context.StartActivity(intent);
}
  1. implementation of Send(Notification notification) in Platforms/Android/NotificationManager.cs code.
    After scheduling notification I checked pending notifications and was always 0 on android:
 var list = await notificationManager.GetPendingNotifications();
 var pendingNotificationsCount = list.Count(); // result is 0, but I scheduled notifications in future :(

so I checked implementation of GetPendingNotifications() which is:

public Task<IList<Notification>> GetPendingNotifications()
       => Task.FromResult((IList<Notification>)this.repository.GetList<AndroidNotification>().OfType<Notification>().ToList());

then if pending notifications are coming from repository I checked place where potentially AndroidNotification should be stored in repository which is Send(Notification notification):

public async Task Send(Notification notification)
{
    notification.AssertValid();
    var android = notification.TryToNative<AndroidNotification>();

    (...)

    if (notification.ScheduleDate == null && notification.Geofence == null)
    {
        var native = builder.Build();
        this.manager.NativeManager.Notify(notification.Id, native);
    }
    else
    {
        // ensure a channel is set
        notification.Channel = channel!.Identifier;
        this.repository.Set(notification);

        if (notification.ScheduleDate != null)
            this.manager.SetAlarm(notification);
    }
}

then we can see that into repository we store generic Notification type instead of AndroidNotification so I stored it as well. I added after this.repository.Set(notification); :

//re-asign android variable because we did manipulation from the moment when it was defined
android = notification.TryToNative<AndroidNotification>();
this.repository.Set(android);

and magic just happened - Scheduled Notifications started to works!

I'm not sure how it should be, maybe this.repository.Set(notification) should be removed or maybe not or maybe something else should be changed in other files (I dont know how the implementation of notifications looks like in other files and if generic type need to be stored or no or maybe AndroidNotificationProcessor is looking for wrong stuff in repository).

However I hope it can help you to understand better problem and solve it in plugin :)

@anchorit3 while I appreciate the effort. Most of the work is done in the 4.0 branch. The issue isn't really with the "making it happen", but with how much of a mess the permissions have gotten now due to this additional permission. I'm working on that part, but the plugin will do all of the permissions properly in the future.