using System; using System.Collections; using System.Collections.Generic; using System.IO; using System.Linq; #if UNITY_ANDROID using Unity.Notifications.Android; using NotificationSamples.Android; #elif UNITY_IOS using NotificationSamples.iOS; #endif using UnityEngine; namespace NotificationSamples { /// /// Global notifications manager that serves as a wrapper for multiple platforms' notification systems. /// public class GameNotificationsManager : MonoBehaviour { // Default filename for notifications serializer private const string DefaultFilename = "notifications.bin"; // Minimum amount of time that a notification should be into the future before it's queued when we background. private static readonly TimeSpan MinimumNotificationTime = new TimeSpan(0, 0, 2); [Flags] public enum OperatingMode { /// /// Do not perform any queueing at all. All notifications are scheduled with the operating system /// immediately. /// NoQueue = 0x00, /// /// /// Queue messages that are scheduled with this manager. /// No messages will be sent to the operating system until the application is backgrounded. /// /// /// If badge numbers are not set, will automatically increment them. This will only happen if NO badge numbers /// for pending notifications are ever set. /// /// Queue = 0x01, /// /// When the application is foregrounded, clear all pending notifications. /// ClearOnForegrounding = 0x02, /// /// After clearing events, will put future ones back into the queue if they are marked with . /// /// /// Only valid if is also set. /// RescheduleAfterClearing = 0x04, /// /// Combines the behaviour of and . /// QueueAndClear = Queue | ClearOnForegrounding, /// /// /// Combines the behaviour of , and /// . /// /// /// Ensures that messages will never be displayed while the application is in the foreground. /// /// QueueClearAndReschedule = Queue | ClearOnForegrounding | RescheduleAfterClearing, } [SerializeField, Tooltip("The operating mode for the notifications manager.")] private OperatingMode mode = OperatingMode.QueueClearAndReschedule; [SerializeField, Tooltip( "Check to make the notifications manager automatically set badge numbers so that they increment.\n" + "Schedule notifications with no numbers manually set to make use of this feature.")] private bool autoBadging = true; /// /// Event fired when a scheduled local notification is delivered while the app is in the foreground. /// public event Action LocalNotificationDelivered; /// /// Event fired when a queued local notification is cancelled because the application is in the foreground /// when it was meant to be displayed. /// /// public event Action LocalNotificationExpired; /// /// Gets the implementation of the notifications for the current platform; /// public IGameNotificationsPlatform Platform { get; private set; } /// /// Gets a collection of notifications that are scheduled or queued. /// public List PendingNotifications { get; private set; } /// /// Gets or sets the serializer to use to save pending notifications to disk if we're in /// mode. /// public IPendingNotificationsSerializer Serializer { get; set; } /// /// Gets the operating mode for this manager. /// /// public OperatingMode Mode => mode; /// /// Gets whether this manager automatically increments badge numbers. /// public bool AutoBadging => autoBadging; /// /// Gets whether this manager has been initialized. /// public bool Initialized { get; private set; } // Flag set when we're in the foreground private bool inForeground = true; /// /// Clean up platform object if necessary /// protected virtual void OnDestroy() { if (Platform == null) { return; } Platform.NotificationReceived -= OnNotificationReceived; if (Platform is IDisposable disposable) { disposable.Dispose(); } inForeground = false; } /// /// Check pending list for expired notifications, when in queue mode. /// protected virtual void Update() { if (PendingNotifications == null || !PendingNotifications.Any() || (mode & OperatingMode.Queue) != OperatingMode.Queue) { return; } // Check each pending notification for expiry, then remove it for (int i = PendingNotifications.Count - 1; i >= 0; --i) { PendingNotification queuedNotification = PendingNotifications[i]; DateTime? time = queuedNotification.Notification.DeliveryTime; if (time != null && time < DateTime.Now) { PendingNotifications.RemoveAt(i); LocalNotificationExpired?.Invoke(queuedNotification); } } } /// /// Respond to application foreground/background events. /// protected void OnApplicationFocus(bool hasFocus) { if (Platform == null || !Initialized) { return; } inForeground = hasFocus; if (hasFocus) { OnForegrounding(); return; } Platform.OnBackground(); // Backgrounding // Queue future dated notifications if ((mode & OperatingMode.Queue) == OperatingMode.Queue) { // Filter out past events for (var i = PendingNotifications.Count - 1; i >= 0; i--) { PendingNotification pendingNotification = PendingNotifications[i]; // Ignore already scheduled ones if (pendingNotification.Notification.Scheduled) { continue; } // If a non-scheduled notification is in the past (or not within our threshold) // just remove it immediately if (pendingNotification.Notification.DeliveryTime != null && pendingNotification.Notification.DeliveryTime - DateTime.Now < MinimumNotificationTime) { PendingNotifications.RemoveAt(i); } } // Sort notifications by delivery time, if no notifications have a badge number set bool noBadgeNumbersSet = PendingNotifications.All(notification => notification.Notification.BadgeNumber == null); if (noBadgeNumbersSet && AutoBadging) { PendingNotifications.Sort((a, b) => { if (!a.Notification.DeliveryTime.HasValue) { return 1; } if (!b.Notification.DeliveryTime.HasValue) { return -1; } return a.Notification.DeliveryTime.Value.CompareTo(b.Notification.DeliveryTime.Value); }); // Set badge numbers incrementally var badgeNum = 1; foreach (PendingNotification pendingNotification in PendingNotifications) { if (pendingNotification.Notification.DeliveryTime.HasValue && !pendingNotification.Notification.Scheduled) { pendingNotification.Notification.BadgeNumber = badgeNum++; } } } for (int i = PendingNotifications.Count - 1; i >= 0; i--) { PendingNotification pendingNotification = PendingNotifications[i]; // Ignore already scheduled ones if (pendingNotification.Notification.Scheduled) { continue; } // Schedule it now Platform.ScheduleNotification(pendingNotification.Notification); } // Clear badge numbers again (for saving) if (noBadgeNumbersSet && AutoBadging) { foreach (PendingNotification pendingNotification in PendingNotifications) { if (pendingNotification.Notification.DeliveryTime.HasValue) { pendingNotification.Notification.BadgeNumber = null; } } } } // Calculate notifications to save var notificationsToSave = new List(PendingNotifications.Count); foreach (PendingNotification pendingNotification in PendingNotifications) { // If we're in clear mode, add nothing unless we're in rescheduling mode // Otherwise add everything if ((mode & OperatingMode.ClearOnForegrounding) == OperatingMode.ClearOnForegrounding) { if ((mode & OperatingMode.RescheduleAfterClearing) != OperatingMode.RescheduleAfterClearing) { continue; } // In reschedule mode, add ones that have been scheduled, are marked for // rescheduling, and that have a time if (pendingNotification.Reschedule && pendingNotification.Notification.Scheduled && pendingNotification.Notification.DeliveryTime.HasValue) { notificationsToSave.Add(pendingNotification); } } else { // In non-clear mode, just add all scheduled notifications if (pendingNotification.Notification.Scheduled) { notificationsToSave.Add(pendingNotification); } } } // Save to disk Serializer.Serialize(notificationsToSave); } /// /// Initialize the notifications manager. /// /// An optional collection of channels to register, for Android /// has already been called. public IEnumerator Initialize(params GameNotificationChannel[] channels) { if (Initialized) { throw new InvalidOperationException("NotificationsManager already initialized."); } Initialized = true; #if UNITY_ANDROID Platform = new AndroidNotificationsPlatform(); // Register the notification channels var doneDefault = false; foreach (GameNotificationChannel notificationChannel in channels) { if (!doneDefault) { doneDefault = true; ((AndroidNotificationsPlatform)Platform).DefaultChannelId = notificationChannel.Id; } long[] vibrationPattern = null; if (notificationChannel.VibrationPattern != null) vibrationPattern = notificationChannel.VibrationPattern.Select(v => (long)v).ToArray(); // Wrap channel in Android object var androidChannel = new AndroidNotificationChannel(notificationChannel.Id, notificationChannel.Name, notificationChannel.Description, (Importance)notificationChannel.Style) { CanBypassDnd = notificationChannel.HighPriority, CanShowBadge = notificationChannel.ShowsBadge, EnableLights = notificationChannel.ShowLights, EnableVibration = notificationChannel.Vibrates, LockScreenVisibility = (LockScreenVisibility)notificationChannel.Privacy, VibrationPattern = vibrationPattern }; AndroidNotificationCenter.RegisterNotificationChannel(androidChannel); } #elif UNITY_IOS Platform = new iOSNotificationsPlatform(); #endif if (Platform == null) { yield break; } PendingNotifications = new List(); Platform.NotificationReceived += OnNotificationReceived; // Check serializer if (Serializer == null) { Serializer = new DefaultSerializer(Path.Combine(Application.persistentDataPath, DefaultFilename)); } yield return Platform.RequestNotificationPermission(); OnForegrounding(); } /// /// Creates a new notification object for the current platform. /// /// The new notification, ready to be scheduled, or null if there's no valid platform. /// has not been called. public IGameNotification CreateNotification() { if (!Initialized) { throw new InvalidOperationException("Must call Initialize() first."); } return Platform?.CreateNotification(); } /// /// Schedules a notification to be delivered. /// /// The notification to deliver. public PendingNotification ScheduleNotification(IGameNotification notification) { if (!Initialized) { throw new InvalidOperationException("Must call Initialize() first."); } if (notification == null || Platform == null) { return null; } // If we queue, don't schedule immediately. // Also immediately schedule non-time based deliveries (for iOS) if ((mode & OperatingMode.Queue) != OperatingMode.Queue || notification.DeliveryTime == null) { Platform.ScheduleNotification(notification); } else if (!notification.Id.HasValue) { // Generate an ID for items that don't have one (just so they can be identified later) int id = Math.Abs(DateTime.Now.ToString("yyMMddHHmmssffffff").GetHashCode()); notification.Id = id; } // Register pending notification var result = new PendingNotification(notification); PendingNotifications.Add(result); return result; } /// /// Cancels a scheduled notification. /// /// The ID of the notification to cancel. /// has not been called. public void CancelNotification(int notificationId) { if (!Initialized) { throw new InvalidOperationException("Must call Initialize() first."); } if (Platform == null) { return; } Platform.CancelNotification(notificationId); // Remove the cancelled notification from scheduled list int index = PendingNotifications.FindIndex(scheduledNotification => scheduledNotification.Notification.Id == notificationId); if (index >= 0) { PendingNotifications.RemoveAt(index); } } /// /// Cancels all scheduled notifications. /// /// has not been called. public void CancelAllNotifications() { if (!Initialized) { throw new InvalidOperationException("Must call Initialize() first."); } if (Platform == null) { return; } Platform.CancelAllScheduledNotifications(); PendingNotifications.Clear(); } /// /// Dismisses a displayed notification. /// /// The ID of the notification to dismiss. /// has not been called. public void DismissNotification(int notificationId) { if (!Initialized) { throw new InvalidOperationException("Must call Initialize() first."); } Platform?.DismissNotification(notificationId); } /// /// Dismisses all displayed notifications. /// /// has not been called. public void DismissAllNotifications() { if (!Initialized) { throw new InvalidOperationException("Must call Initialize() first."); } Platform?.DismissAllDisplayedNotifications(); } /// /// /// /// /// public IGameNotification GetLastNotification() { if (!Initialized) { throw new InvalidOperationException("Must call Initialize() first."); } return Platform?.GetLastNotification(); } /// /// Event fired by when a notification is received. /// private void OnNotificationReceived(IGameNotification deliveredNotification) { // Ignore for background messages (this happens on Android sometimes) if (!inForeground) { return; } // Find in pending list int deliveredIndex = PendingNotifications.FindIndex(scheduledNotification => scheduledNotification.Notification.Id == deliveredNotification.Id); if (deliveredIndex >= 0) { LocalNotificationDelivered?.Invoke(PendingNotifications[deliveredIndex]); PendingNotifications.RemoveAt(deliveredIndex); } } // Clear foreground notifications and reschedule stuff from a file private void OnForegrounding() { PendingNotifications.Clear(); Platform.OnForeground(); // Deserialize saved items IList loaded = Serializer?.Deserialize(Platform); // Foregrounding if ((mode & OperatingMode.ClearOnForegrounding) == OperatingMode.ClearOnForegrounding) { // Clear on foregrounding Platform.CancelAllScheduledNotifications(); // Only reschedule in reschedule mode, and if we loaded any items if (loaded == null || (mode & OperatingMode.RescheduleAfterClearing) != OperatingMode.RescheduleAfterClearing) { return; } // Reschedule notifications from deserialization foreach (IGameNotification savedNotification in loaded) { if (savedNotification.DeliveryTime > DateTime.Now) { PendingNotification pendingNotification = ScheduleNotification(savedNotification); pendingNotification.Reschedule = true; } } } else { // Just create PendingNotification wrappers for all deserialized items. // We're not rescheduling them because they were not cleared if (loaded == null) { return; } foreach (IGameNotification savedNotification in loaded) { if (savedNotification.DeliveryTime > DateTime.Now) { PendingNotifications.Add(new PendingNotification(savedNotification)); } } } } } }