Duplicate Notifications

Hi there! I’ve been trying to solve this issue for about 6 hours and I can’t seem to fix it.I wanna have a notification that repeats every 3 days, but I am facing a weird issue.I open my application, the notification displays properly, but if I close the application and remove it from recent apps and then open it again, now it sends 2 of the same notification at a time.

I am using repeatInterval to repeat this notification every 3 days, but I don’t know how I can prevent it from not starting again if the repeated notification was triggered already in a previous session, or detect that it has been triggered and not try sending another repeated notification again.

The notification that I’m having problems with is the PlayReminder.

Here’s the code :

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Unity.Notifications.Android;
using System;

public class NotificationManager : MonoBehaviour
{
    #region Notification Channels

    private AndroidNotificationChannel default_channel;
    private AndroidNotificationChannel reminder_channel;

    #endregion


    #region Daily Rewards Variables

    private NotificationStatus DailyReward_notificationStatus;
    private int DailyReward_ID;
    private AndroidNotification DailyReward_Reminder;

    #endregion

    #region Reminder to Play

    private NotificationStatus PlayReminder_notificationStatus;
    private int PlayReminder_ID;
    private AndroidNotification PlayReminder;

    #endregion

    // Start is called before the first frame update

    void Start()
    {
        #region Channels

        default_channel = new AndroidNotificationChannel() // Used for push notifications, events, discounts etc
        {
            Id = "default_channel_id",
            Name = "General Channel",
            Importance = Importance.Default,
            EnableVibration = true,
            EnableLights = true,
            Description = "Used for normal notifications",
        };
        default_channel.LockScreenVisibility = LockScreenVisibility.Public;
        AndroidNotificationCenter.RegisterNotificationChannel(default_channel);

        reminder_channel = new AndroidNotificationChannel() // Used for reminders like daily reward or checking back to play
        {
            Id = "reminder_channel_id",
            Name = "Reminder Channel",
            Importance = Importance.High,
            EnableLights = true,
            EnableVibration = true,
            Description = "Notifications to remind the player",
        };
        reminder_channel.LockScreenVisibility = LockScreenVisibility.Public;
        AndroidNotificationCenter.RegisterNotificationChannel(reminder_channel);


        AndroidNotificationCenter.CancelScheduledNotification(PlayReminder_ID); // Cancel it before rescheduling/sending again so we don't get duplicate notifications
        AndroidNotificationCenter.CancelNotification(PlayReminder_ID); // Cancel it before rescheduling/sending again so we don't get duplicate notifications
        AndroidNotificationCenter.CancelDisplayedNotification(PlayReminder_ID); // Cancel it before rescheduling/sending again so we don't get duplicate notifications

        #endregion

        #region Daily Reward
        DailyReward_Reminder = new AndroidNotification();
        DailyReward_Reminder.Title = "Daily Reward";
        DailyReward_Reminder.Text = "Your Daily Reward is ready to be claimed!";
        DailyReward_Reminder.FireTime = System.DateTime.Now.AddSeconds(25);
        DailyReward_Reminder.LargeIcon = "dailyreward_large";


        CheckDailyReward_NotificationStatus(); // Check notification status for daily rewards
        #endregion


        #region Play Reminder
        PlayReminder = new AndroidNotification();
        PlayReminder.Title = "Pssst, over here!";
        PlayReminder.Text = "You're missing out on a bunch of things, come check them out!";
        PlayReminder.FireTime = System.DateTime.Now.AddDays(3);
        PlayReminder.RepeatInterval = TimeSpan.FromDays(3);
        PlayReminder.Style = NotificationStyle.BigTextStyle;

        PlayReminder_notificationStatus = AndroidNotificationCenter.CheckScheduledNotificationStatus(PlayReminder_ID);

        if (PlayReminder_notificationStatus != NotificationStatus.Scheduled)
        {
            PlayReminder_ID = AndroidNotificationCenter.SendNotification(PlayReminder, "reminder_channel_id");
            Debug.Log("Notification existing, deleting sending another Play Reminder Notification..");
        }


        CheckPlayReminder_NotificationStatus();

        #endregion
    }

    #region Play Reminder

    void CheckPlayReminder_NotificationStatus()
    {
        PlayReminder_notificationStatus = AndroidNotificationCenter.CheckScheduledNotificationStatus(PlayReminder_ID);
        Debug.Log("Starting Play Reminder Notification Checks..");
        if (PlayReminder_notificationStatus == NotificationStatus.Delivered)
        {
            // Remove the previously shown notification from the status bar.
            Debug.Log("Play Reminder Notification already delivered, removing the notification if activated..");
            AndroidNotificationCenter.CancelNotification(PlayReminder_ID);
        }
    }


    #endregion



    #region Daily Rewards
    public void DailyRewardNotificationTrigger() // For manual triggers
    {

        DailyReward_ID = AndroidNotificationCenter.SendNotification(DailyReward_Reminder, "reminder_channel_id");

        Debug.Log("Notification Sent");
    }

    private void CheckDailyReward_NotificationStatus()
    {
        DailyReward_notificationStatus = AndroidNotificationCenter.CheckScheduledNotificationStatus(DailyReward_ID);

        Debug.Log("Starting Daily Reward Reminder Notification Checks..");
        if (DailyReward_notificationStatus == NotificationStatus.Delivered)
        {
            // Remove the previously shown notification from the status bar.
            Debug.Log("Daily Reward Notification already delivered, removing the notification..");
            AndroidNotificationCenter.CancelNotification(DailyReward_ID);
        }
    }
    #endregion
}

I used a 20 seconds fire time and 1 minute repeat interval for testing purposes on the PlayReminder notification.

Tried canceling the previous notification before sending another one ( like above ), tried rescheduling, tried everything.

Can someone get me out of this issue with a fix?

I cannot get it to work.

Thanks for the help! :slight_smile:

Can you store some data in PlayerPrefs to indicate that the notification for a particular 3 day window has already been sent? (Maybe just the timestamp of the most recent notification?) Then you could check that before sending the new notification to be sure it’s not too soon.

1 Like

You need to store a flag to say this notification has already been sent. I personally would not do it in PlayerPrefs as @PraetorBlue suggests as that can be accessed by users and is not secure.

Instead I would create a ScriptableObject to act as a data storage class, and add a reference to that in your notification class. You then add whatever you need to the SO , and check against the SO to determine what has happened. Can be used for all sorts of data not just this.

1 Like

Yeah but if I do it like this, how will I detect if something went wrong but the playerprefs was set that it is activated?

Hmm, this makes me wonder, should I also save my game data inside a SO or write that data to a json file? I currently don’t know how to use either haha but at least I could know which is the best way.

Also, a notification isn’t really sensitive data that the player could get something from. I use something to secure my playerprefs anyways.

An SO works nicely for saving game data yes :slight_smile: I would not do it as JSON unless you want people to be able to hack their saves using a text editor!

How are you going to store the persistent changes to the ScriptableObject though? Changes to SO’s don’t persist across play sessions in a built game. That only works inside the Unity editor.

And yes, PlayerPrefs can be edited by players, but that’s true for any local storage scheme. The only way to prevent that would be to store this data on a remote server somewhere and add network calls to your game to fetch the data. I think in this case it’s not a huge concern anyway since it’s just about sending a notification to a player.

Then what should I use to save data? I can’t use something like a remote server just how @PraetorBlue mentioned, that’s way too overkill and I’m not that experienced.

Also, your average android user doesn’t really know how to do that.

Sorry I should have been a bit clearer but am typing on phone, I meant store your stuff in an SO and then serialize and save on disk. Do reverse to get back data.

And yes I agree all data can be accessed, but playerprefs is probably the easiest and most documented so better to not use it as much as possible. Not saying storing it in there wil not work :slight_smile:

Essentially at some point, the data has to be serialized at some point, I just prefer working with SO as I can write all the serialization logic inside the actual SO itself and then I just take that from project to project and build on top of it :slight_smile:

JSON is super readable even to end-users/laymen so I prefer to serialize to something less readable, but ultimately it doesnt really matter what you choose for something like a notification flag!

1 Like

Start looking up youtube tutorials on create saved game systems. Options include JSON, BinaryFormatter etc…

I wouldn’t suggest going crazy trying to prevent players from cheating via editing a save file. If they’re determined enough they’ll be able to do it no matter what. But go ahead and decide yourself how much you care about that.

I’m planning to do some unlocks soon in my game, so I will kind of need to save data somewhere where it’s easy to expand and manage.

As for what Praetor said :

I’m not really sure about how to do this.How do I save a DateTime inside a string and then add 3 days to that for the check itself?

You can use https://docs.microsoft.com/en-us/dotnet/api/system.datetime.add?view=netcore-3.1 to add a timespan to a datetime

And you dont need to store it as a string, just save it as a DateTime object. No need to change to a string, you will do all that automatically when you serialize anyway.

1 Like

Is this a good way of doing it?

I repurposed a system from my daily rewards into something that I think does the work as I’m not the most familiarised with the DateTime stuff.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Unity.Notifications.Android;
using System;

public class NotificationManager : MonoBehaviour
{
    #region Notification Channels

    private AndroidNotificationChannel default_channel;
    private AndroidNotificationChannel reminder_channel;

    #endregion


    #region Daily Rewards Variables

    private NotificationStatus DailyReward_notificationStatus;
    private int DailyReward_ID;
    private AndroidNotification DailyReward_Reminder;

    #endregion

    #region Reminder to Play
    [SerializeField] private double PlayReminderNotificationCooldown = 3f; // Cooldown in days
    [SerializeField] private double elapsedTime; // Elapsed time since last play reminder
    [SerializeField] private DateTime currentDateTime; // Current date and time
    [SerializeField] private DateTime LastPlayReminderDateTime; // Last Notification date and time
    private NotificationStatus PlayReminder_notificationStatus;
    private int PlayReminder_ID;
    private AndroidNotification PlayReminder;

    #endregion

    // Start is called before the first frame update

    void Start()
    {
        #region Channels

        default_channel = new AndroidNotificationChannel() // Used for push notifications, events, discounts etc
        {
            Id = "default_channel_id",
            Name = "General Channel",
            Importance = Importance.Default,
            EnableVibration = true,
            EnableLights = true,
            Description = "Used for normal notifications",
        };
        default_channel.LockScreenVisibility = LockScreenVisibility.Public;
        AndroidNotificationCenter.RegisterNotificationChannel(default_channel);

        reminder_channel = new AndroidNotificationChannel() // Used for reminders like daily reward or checking back to play
        {
            Id = "reminder_channel_id",
            Name = "Reminder Channel",
            Importance = Importance.High,
            EnableLights = true,
            EnableVibration = true,
            Description = "Notifications to remind the player",
        };
        reminder_channel.LockScreenVisibility = LockScreenVisibility.Public;
        AndroidNotificationCenter.RegisterNotificationChannel(reminder_channel);


        AndroidNotificationCenter.CancelScheduledNotification(PlayReminder_ID); // Cancel it before rescheduling/sending again so we don't get duplicate notifications
        AndroidNotificationCenter.CancelNotification(PlayReminder_ID); // Cancel it before rescheduling/sending again so we don't get duplicate notifications
        AndroidNotificationCenter.CancelDisplayedNotification(PlayReminder_ID); // Cancel it before rescheduling/sending again so we don't get duplicate notifications

        #endregion

        #region Daily Reward
        DailyReward_Reminder = new AndroidNotification();
        DailyReward_Reminder.Title = "Daily Reward";
        DailyReward_Reminder.Text = "Your Daily Reward is ready to be claimed!";
        DailyReward_Reminder.FireTime = System.DateTime.Now.AddSeconds(25);
        DailyReward_Reminder.LargeIcon = "dailyreward_large";


        CheckDailyReward_NotificationStatus(); // Check notification status for daily rewards
        #endregion


        #region Play Reminder
        PlayReminder = new AndroidNotification();
        PlayReminder.Title = "Pssst, over here!";
        PlayReminder.Text = "You're missing out on a bunch of things, come check them out!";
        PlayReminder.FireTime = System.DateTime.Now.AddSeconds(20);
        PlayReminder.RepeatInterval = TimeSpan.FromMinutes(1);
        PlayReminder.Style = NotificationStyle.BigTextStyle;

        PlayReminder_notificationStatus = AndroidNotificationCenter.CheckScheduledNotificationStatus(PlayReminder_ID);

        currentDateTime = DateTime.Now;
        var LastPlayReminderNotification = DateTime.Parse(PlayerPrefs.GetString("LastPlayReminderNotificationSendTime"));

        elapsedTime = (currentDateTime - LastPlayReminderDateTime).TotalDays;
        if (elapsedTime >= PlayReminderNotificationCooldown || string.IsNullOrEmpty(PlayerPrefs.GetString("LastPlayReminderNotificationSendTime")))
        {
            PlayReminder_ID = AndroidNotificationCenter.SendNotification(PlayReminder, "reminder_channel_id");
            PlayerPrefs.SetString("LastPlayReminderNotificationSendTime", currentDateTime.ToString());
            Debug.Log("Notification cooldown expired, send a new notification..");
        }


        CheckPlayReminder_NotificationStatus();

        #endregion
    }

    #region Play Reminder

    void CheckPlayReminder_NotificationStatus()
    {
        PlayReminder_notificationStatus = AndroidNotificationCenter.CheckScheduledNotificationStatus(PlayReminder_ID);
        Debug.Log("Starting Play Reminder Notification Checks..");
        if (PlayReminder_notificationStatus == NotificationStatus.Delivered)
        {
            // Remove the previously shown notification from the status bar.
            Debug.Log("Play Reminder Notification already delivered, removing the notification if activated..");
            AndroidNotificationCenter.CancelNotification(PlayReminder_ID);
        }
    }


    #endregion



    #region Daily Rewards
    public void DailyRewardNotificationTrigger() // For manual triggers
    {

        DailyReward_ID = AndroidNotificationCenter.SendNotification(DailyReward_Reminder, "reminder_channel_id");

        Debug.Log("Notification Sent");
    }

    private void CheckDailyReward_NotificationStatus()
    {
        DailyReward_notificationStatus = AndroidNotificationCenter.CheckScheduledNotificationStatus(DailyReward_ID);

        Debug.Log("Starting Daily Reward Reminder Notification Checks..");
        if (DailyReward_notificationStatus == NotificationStatus.Delivered)
        {
            // Remove the previously shown notification from the status bar.
            Debug.Log("Daily Reward Notification already delivered, removing the notification..");
            AndroidNotificationCenter.CancelNotification(DailyReward_ID);
        }
    }
    #endregion
}

Edit : Also, how do I cancel my old notification after this is true? So the double thing doesn’t happen again. Do I just remove the repeat interval as this will make it repeat anyways?

The thing is that this way, without the repeatInterval, the notification won’t repeat itself without re-entering the game as I wish it to do.