488 lines
19 KiB
C#
Raw Permalink Normal View History

2025-05-18 01:04:31 +08:00
using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using UnityEngine;
namespace Unity.Notifications.iOS
{
/// <summary>
/// Constants indicating how to present a notification in a foreground app
/// </summary>
[Flags]
public enum PresentationOption
{
/// <summary>
/// No options are set.
/// </summary>
None = 0,
/// <summary>
/// Apply the notification's badge value to the apps icon.
/// </summary>
Badge = 1 << 0,
/// <summary>
/// Play the sound associated with the notification.
/// </summary>
Sound = 1 << 1,
/// <summary>
/// Display the alert using the content provided by the notification.
/// </summary>
Alert = 1 << 2,
}
/// <summary>
/// The type of sound to use for the notification.
/// See Apple documentation for details.
/// </summary>
/// <see href="https://developer.apple.com/documentation/usernotifications/unnotificationsound?language=objc"/>
public enum NotificationSoundType
{
/// <summary>
/// Play the default sound.
/// </summary>
Default = 0,
/// <summary>
/// Critical sound (bypass Do Not Disturb)
/// </summary>
Critical = 1,
/// <summary>
/// Ringtone sound.
/// </summary>
Ringtone = 2,
/// <summary>
/// No sound.
/// </summary>
None = 4,
}
[StructLayout(LayoutKind.Sequential)]
internal struct TimeTriggerData
{
public Int32 interval;
public Byte repeats;
}
[StructLayout(LayoutKind.Sequential)]
internal struct CalendarTriggerData
{
public Int32 year;
public Int32 month;
public Int32 day;
public Int32 hour;
public Int32 minute;
public Int32 second;
public Byte repeats;
}
[StructLayout(LayoutKind.Sequential)]
internal struct LocationTriggerData
{
public double latitude;
public double longitude;
public float radius;
public Byte notifyOnEntry;
public Byte notifyOnExit;
public Byte repeats;
}
[StructLayout(LayoutKind.Explicit)]
internal struct TriggerData
{
[FieldOffset(0)]
public TimeTriggerData timeInterval;
[FieldOffset(0)]
public CalendarTriggerData calendar;
[FieldOffset(0)]
public LocationTriggerData location;
}
[StructLayout(LayoutKind.Sequential)]
internal struct iOSNotificationData
{
public string identifier;
public string title;
public string body;
public Int32 badge;
public string subtitle;
public string categoryIdentifier;
public string threadIdentifier;
public Int32 soundType;
public float soundVolume;
public string soundName;
public IntPtr userInfo;
public IntPtr attachments;
// Trigger
public Int32 triggerType;
public TriggerData trigger;
}
/// <summary>
/// The iOSNotification class is used schedule local notifications. It includes the content of the notification and the trigger conditions for delivery.
/// An instance of this class is also returned when receiving remote notifications..
/// </summary>
/// <remarks>
/// Create an instance of this class when you want to schedule the delivery of a local notification. It contains the entire notification payload to be delivered
/// (which corresponds to UNNotificationContent) and also the NotificationTrigger object with the conditions that trigger the delivery of the notification.
/// To schedule the delivery of your notification, pass an instance of this class to the <see cref="iOSNotificationCenter.ScheduleNotification"/> method.
/// </remarks>
public class iOSNotification
{
/// <summary>
/// The unique identifier for this notification request.
/// </summary>
/// <remarks>
/// If not explicitly specified the identifier will be automatically generated when creating the notification.
/// </remarks>
public string Identifier
{
get { return data.identifier; }
set { data.identifier = value; }
}
/// <summary>
/// The identifier of the app-defined category object.
/// </summary>
public string CategoryIdentifier
{
get { return data.categoryIdentifier; }
set { data.categoryIdentifier = value; }
}
/// <summary>
/// An identifier that used to group related notifications together.
/// </summary>
/// <remarks>
/// Automatic notification grouping according to the thread identifier is only supported on iOS 12 and above.
/// </remarks>
public string ThreadIdentifier
{
get { return data.threadIdentifier; }
set { data.threadIdentifier = value; }
}
/// <summary>
/// A short description of the reason for the notification.
/// </summary>
public string Title
{
get { return data.title; }
set { data.title = value; }
}
/// <summary>
/// A secondary description of the reason for the notification.
/// </summary>
public string Subtitle
{
get { return data.subtitle; }
set { data.subtitle = value; }
}
/// <summary>
/// The message displayed in the notification alert.
/// </summary>
public string Body
{
get { return data.body; }
set { data.body = value; }
}
/// <summary>
/// Indicates whether the notification alert should be shown when the app is open.
/// </summary>
/// <remarks>
/// Subscribe to the <see cref="iOSNotificationCenter.OnNotificationReceived"/> event to receive a callback when the notification is triggered.
/// </remarks>
public bool ShowInForeground
{
get
{
string value;
if (userInfo.TryGetValue("showInForeground", out value))
return value == "YES";
return false;
}
set { userInfo["showInForeground"] = value ? "YES" : "NO"; }
}
/// <summary>
/// Presentation options for displaying the local of notification when the app is running. Only works if <see cref="iOSNotification.ShowInForeground"/> is enabled and user has allowed enabled the requested options for your app.
/// </summary>
public PresentationOption ForegroundPresentationOption
{
get
{
try
{
string value;
if (userInfo.TryGetValue("showInForegroundPresentationOptions", out value))
return (PresentationOption)Int32.Parse(value);
return default;
}
catch (Exception)
{
return default;
}
}
set { userInfo["showInForegroundPresentationOptions"] = ((int)value).ToString(); }
}
/// <summary>
/// The number to display as a badge on the apps icon.
/// </summary>
public int Badge
{
get { return data.badge; }
set { data.badge = value; }
}
/// <summary>
/// The type of sound to be played.
/// </summary>
public NotificationSoundType SoundType
{
get { return (NotificationSoundType)data.soundType; }
set { data.soundType = (int)value; }
}
/// <summary>
/// The name of the sound to be played. Use null for system default sound.
/// See Apple documentation for named sounds and sound file placement.
/// </summary>
public string SoundName
{
get { return data.soundName; }
set { data.soundName = value; }
}
/// <summary>
/// The volume for the sound. Use null to use the default volume.
/// See Apple documentation for supported values.
/// </summary>
/// <see href="https://developer.apple.com/documentation/usernotifications/unnotificationsound/2963118-defaultcriticalsoundwithaudiovol?language=objc"/>
public float? SoundVolume { get; set; }
/// <summary>
/// Arbitrary string data which can be retrieved when the notification is used to open the app or is received while the app is running.
/// </summary>
public string Data
{
get
{
string value;
userInfo.TryGetValue("data", out value);
return value;
}
set { userInfo["data"] = value; }
}
/// <summary>
/// Key-value collection sent or received with the notification.
/// Note, that some of the other notification properties are transfered using this collection, it is not recommended to modify existing items.
/// </summary>
public Dictionary<string, string> UserInfo
{
get { return userInfo; }
}
/// <summary>
/// A list of notification attachments.
/// Notification attachments can be images, audio or video files. Refer to Apple documentation on supported formats.
/// </summary>
/// <see cref="https://developer.apple.com/documentation/usernotifications/unmutablenotificationcontent/1649857-attachments?language=objc"/>
public List<iOSNotificationAttachment> Attachments { get; set; }
/// <summary>
/// The conditions that trigger the delivery of the notification.
/// For notification that were already delivered and whose instance was returned by <see cref="iOSNotificationCenter.OnRemoteNotificationReceived"/> or <see cref="iOSNotificationCenter.OnRemoteNotificationReceived"/>
/// use this property to determine what caused the delivery to occur. You can do this by comparing <see cref="iOSNotification.Trigger"/> to any of the notification trigger types that implement it, such as
/// <see cref="iOSNotificationLocationTrigger"/>, <see cref="iOSNotificationPushTrigger"/>, <see cref="iOSNotificationTimeIntervalTrigger"/>, <see cref="iOSNotificationCalendarTrigger"/>.
/// </summary>
/// <example>
/// <code>
/// notification.Trigger is iOSNotificationPushTrigger
/// </code>
/// </example>
public iOSNotificationTrigger Trigger
{
set
{
switch (value.Type)
{
case iOSNotificationTriggerType.TimeInterval:
{
var trigger = (iOSNotificationTimeIntervalTrigger)value;
data.trigger.timeInterval.interval = trigger.timeInterval;
if (trigger.Repeats && trigger.timeInterval < 60)
throw new ArgumentException("Time interval must be 60 seconds or greater for repeating notifications.");
data.trigger.timeInterval.repeats = (byte)(trigger.Repeats ? 1 : 0);
break;
}
case iOSNotificationTriggerType.Calendar:
{
var trigger = ((iOSNotificationCalendarTrigger)value);
if (userInfo == null)
userInfo = new Dictionary<string, string>();
userInfo["OriginalUtc"] = trigger.UtcTime ? "1" : "0";
if (!trigger.UtcTime)
trigger = trigger.ToUtc();
data.trigger.calendar.year = trigger.Year != null ? trigger.Year.Value : -1;
data.trigger.calendar.month = trigger.Month != null ? trigger.Month.Value : -1;
data.trigger.calendar.day = trigger.Day != null ? trigger.Day.Value : -1;
data.trigger.calendar.hour = trigger.Hour != null ? trigger.Hour.Value : -1;
data.trigger.calendar.minute = trigger.Minute != null ? trigger.Minute.Value : -1;
data.trigger.calendar.second = trigger.Second != null ? trigger.Second.Value : -1;
data.trigger.calendar.repeats = (byte)(trigger.Repeats ? 1 : 0);
break;
}
case iOSNotificationTriggerType.Location:
{
var trigger = (iOSNotificationLocationTrigger)value;
data.trigger.location.latitude = trigger.Latitude;
data.trigger.location.longitude = trigger.Longitude;
data.trigger.location.notifyOnEntry = (byte)(trigger.NotifyOnEntry ? 1 : 0);
data.trigger.location.notifyOnExit = (byte)(trigger.NotifyOnExit ? 1 : 0);
data.trigger.location.radius = trigger.Radius;
data.trigger.location.repeats = (byte)(trigger.Repeats ? 1 : 0);
break;
}
case iOSNotificationTriggerType.Push:
break;
default:
throw new Exception($"Unknown trigger type {value.Type}");
}
data.triggerType = (int)value.Type;
}
get
{
switch ((iOSNotificationTriggerType)data.triggerType)
{
case iOSNotificationTriggerType.TimeInterval:
return new iOSNotificationTimeIntervalTrigger()
{
timeInterval = data.trigger.timeInterval.interval,
Repeats = data.trigger.timeInterval.repeats != 0,
};
case iOSNotificationTriggerType.Calendar:
{
var trigger = new iOSNotificationCalendarTrigger()
{
Year = (data.trigger.calendar.year > 0) ? (int?)data.trigger.calendar.year : null,
Month = (data.trigger.calendar.month > 0) ? (int?)data.trigger.calendar.month : null,
Day = (data.trigger.calendar.day > 0) ? (int?)data.trigger.calendar.day : null,
Hour = (data.trigger.calendar.hour >= 0) ? (int?)data.trigger.calendar.hour : null,
Minute = (data.trigger.calendar.minute >= 0) ? (int?)data.trigger.calendar.minute : null,
Second = (data.trigger.calendar.second >= 0) ? (int?)data.trigger.calendar.second : null,
UtcTime = true,
Repeats = data.trigger.calendar.repeats != 0
};
if (userInfo != null)
{
string utc;
if (userInfo.TryGetValue("OriginalUtc", out utc))
{
if (utc == "0")
trigger = trigger.ToLocal();
}
else
trigger.UtcTime = false;
}
else
trigger.UtcTime = false;
return trigger;
}
case iOSNotificationTriggerType.Location:
return new iOSNotificationLocationTrigger()
{
Latitude = data.trigger.location.latitude,
Longitude = data.trigger.location.longitude,
Radius = data.trigger.location.radius,
NotifyOnEntry = data.trigger.location.notifyOnEntry != 0,
NotifyOnExit = data.trigger.location.notifyOnExit != 0,
Repeats = data.trigger.location.repeats != 0,
};
case iOSNotificationTriggerType.Push:
return new iOSNotificationPushTrigger();
default:
throw new Exception($"Unknown trigger type {data.triggerType}");
}
}
}
private static string GenerateUniqueID()
{
return Math.Abs(DateTime.Now.ToString("yyMMddHHmmssffffff").GetHashCode()).ToString();
}
/// <summary>
/// Create a new instance of <see cref="iOSNotification"/> and automatically generate an unique string for <see cref="iOSNotification.Identifier"/> with all optional fields set to default values.
/// </summary>
public iOSNotification() : this(GenerateUniqueID())
{
}
/// <summary>
/// Specify a <see cref="iOSNotification.Identifier"/> and create a notification object with all optional fields set to default values.
/// </summary>
/// <param name="identifier"> Unique identifier for the local notification tha can later be used to track or change it's status.</param>
public iOSNotification(string identifier)
{
data = new iOSNotificationData();
data.identifier = identifier;
data.title = "";
data.body = "";
data.badge = -1;
data.subtitle = "";
data.categoryIdentifier = "";
data.threadIdentifier = "";
data.triggerType = -1;
data.userInfo = IntPtr.Zero;
userInfo = new Dictionary<string, string>();
Data = "";
ShowInForeground = false;
ForegroundPresentationOption = PresentationOption.Alert | PresentationOption.Sound;
}
internal iOSNotification(iOSNotificationWithUserInfo data)
{
this.data = data.data;
userInfo = data.userInfo;
Attachments = data.attachments;
}
iOSNotificationData data;
Dictionary<string, string> userInfo;
internal iOSNotificationWithUserInfo GetDataForSending()
{
if (data.identifier == null)
data.identifier = GenerateUniqueID();
if (SoundVolume.HasValue)
data.soundVolume = SoundVolume.Value;
else
data.soundVolume = -1.0f;
iOSNotificationWithUserInfo ret;
ret.data = data;
ret.userInfo = userInfo;
ret.attachments = Attachments;
return ret;
}
}
}