增加sdk

This commit is contained in:
2026-07-14 10:20:45 +08:00
parent 20d09e4ebb
commit 57709d109d
1165 changed files with 59941 additions and 4485 deletions
@@ -0,0 +1,320 @@
using System;
using UnityEngine;
namespace Unity.Notifications.Android
{
/// <summary>
/// Allows applying a rich notification style to a notification.
/// </summary>
public enum NotificationStyle
{
/// <summary>
/// Use the default style.
/// </summary>
None = 0,
/// <summary>
/// Generate a large-format notification centered around an image.
/// </summary>
BigPictureStyle = 1,
/// <summary>
/// Generate a large-format notification that includes a lot of text.
/// </summary>
BigTextStyle = 2
}
/// <summary>
/// Allows applying an alert behaviour to grouped notifications.
/// </summary>
public enum GroupAlertBehaviours
{
/// <summary>
/// All notifications in a group with sound or vibration will make sound or vibrate, so this notification will not be muted when it is in a group.
/// </summary>
GroupAlertAll = 0,
/// <summary>
/// The summary notification in a group will be silenced (no sound or vibration) even if they would otherwise make sound or vibrate.
/// Use this to mute this notification if this notification is a group summary.
/// </summary>
GroupAlertSummary = 1,
/// <summary>
/// All children notification in a group will be silenced (no sound or vibration) even if they would otherwise make sound or vibrate.
/// Use this to mute this notification if this notification is a group child. This must be set on all children notifications you want to mute.
/// </summary>
GroupAlertChildren = 2,
}
/// <summary>
/// Data for setting up the big picture style notification.
/// Properties that are not available in devices API level are ignored. See Android documentation for availibility.
/// <see href="https://developer.android.com/reference/android/app/Notification.BigPictureStyle"/>
/// </summary>
public struct BigPictureStyle
{
/// <summary>
/// The override for large icon (requirements are the same).
/// </summary>
/// <see cref="AndroidNotification.LargeIcon"/>
public string LargeIcon { get; set; }
/// <summary>
/// The picture to be displayed.
/// Can be resource name (like icon), file path or an URI supported by Android.
/// </summary>
public string Picture { get; set; }
/// <summary>
/// The content title to be displayed in the notification.
/// </summary>
public string ContentTitle { get; set; }
/// <summary>
/// The content description to set.
/// </summary>
public string ContentDescription { get; set; }
/// <summary>
/// The summary text to be shown.
/// </summary>
public string SummaryText { get; set; }
/// <summary>
/// Whether to show big picture in place of large icon when collapsed.
/// </summary>
public bool ShowWhenCollapsed { get; set; }
}
/// <summary>
/// The AndroidNotification is used schedule a local notification, which includes the content of the notification.
/// </summary>
public struct AndroidNotification
{
/// <summary>
/// Notification title.
/// Set the first line of text in the notification.
/// </summary>
public string Title { get; set; }
/// <summary>
/// Notification body.
/// Set the second line of text in the notification.
/// </summary>
public string Text { get; set; }
/// <summary>
/// Notification small icon.
/// It will be used to represent the notification in the status bar and content view (unless overridden there by a large icon)
/// The icon has to be registered in Notification Settings or a PNG file has to be placed in the `res/drawable` folder of the Android library plugin
/// and it's name has to be specified without the extension.
/// Alternatively it can also be URI supported by the OS.
/// </summary>
public string SmallIcon { get; set; }
/// <summary>
/// The date and time when the notification should be delivered.
/// </summary>
public DateTime FireTime { get; set; }
/// <summary>
/// The notification will be be repeated on every specified time interval.
/// Do not set for one time notifications.
/// </summary>
public TimeSpan? RepeatInterval
{
get { return m_RepeatInterval; }
set { m_RepeatInterval = value.HasValue ? value.Value : (-1L).ToTimeSpan(); }
}
/// <summary>
/// Notification large icon.
/// Add a large icon to the notification content view. This image will be shown on the left of the notification view in place of the small icon (which will be placed in a small badge atop the large icon).
/// The icon has to be registered in Notification Settings or a PNG file has to be placed in the `res/drawable` folder of the Android library plugin
/// and it's name has to be specified without the extension.
/// Alternatively it can be a file path or system supported URI.
/// </summary>
public string LargeIcon { get; set; }
/// <summary>
/// Apply a custom style to the notification.
/// Currently only BigPicture and BigText styles are supported.
/// </summary>
public NotificationStyle Style { get; set; }
/// <summary>
/// Accent color to be applied by the standard style templates when presenting this notification.
/// The template design constructs a colorful header image by overlaying the icon image (stenciled in white) atop a field of this color. Alpha components are ignored.
/// </summary>
public Color? Color
{
get { return m_Color; }
set { m_Color = value.HasValue ? value.Value : new Color(0, 0, 0, 0); }
}
/// <summary>
/// Sets the number of items this notification represents.
/// Is displayed as a badge count on the notification icon if the launcher supports this behavior.
/// </summary>
public int Number { get; set; }
/// <summary>
/// This notification will automatically be dismissed when the user touches it.
/// By default this behavior is turned off.
/// </summary>
public bool ShouldAutoCancel { get; set; }
/// <summary>
/// Show the notification time field as a stopwatch instead of a timestamp.
/// </summary>
public bool UsesStopwatch { get; set; }
/// <summary>
///Set this property for the notification to be made part of a group of notifications sharing the same key.
/// Grouped notifications may display in a cluster or stack on devices which support such rendering.
/// Only available on Android 7.0 (API level 24) and above.
/// </summary>
public string Group { get; set; }
/// <summary>
/// Set this notification to be the group summary for a group of notifications. Requires the 'Group' property to also be set.
/// Grouped notifications may display in a cluster or stack on devices which support such rendering.
/// Only available on Android 7.0 (API level 24) and above.
/// </summary>
public bool GroupSummary { get; set; }
/// <summary>
/// Sets the group alert behavior for this notification. Set this property to mute this notification if alerts for this notification's group should be handled by a different notification.
/// This is only applicable for notifications that belong to a group. This must be set on all notifications you want to mute.
/// Only available on Android 8.0 (API level 26) and above.
/// </summary>
public GroupAlertBehaviours GroupAlertBehaviour { get; set; }
/// <summary>
/// The sort key will be used to order this notification among other notifications from the same package.
/// Notifications will be sorted lexicographically using this value.
/// </summary>
public string SortKey { get; set; }
/// <summary>
/// Use this to save arbitrary string data related to the notification.
/// </summary>
public string IntentData { get; set; }
/// <summary>
/// Enable it to show a timestamp on the notification when it's delivered, unless the "CustomTimestamp" property is set "FireTime" will be shown.
/// </summary>
public bool ShowTimestamp { get; set; }
/// <summary>
/// Set this to show custom date instead of the notification's "FireTime" as the notification's timestamp'.
/// </summary>
public DateTime CustomTimestamp
{
get { return m_CustomTimestamp; }
set
{
ShowCustomTimestamp = true;
m_CustomTimestamp = value;
}
}
/// <summary>
/// Set this notification to be shown when app is in the foreground (default: true).
/// </summary>
public bool ShowInForeground
{
get => !m_SilentInForeground;
set => m_SilentInForeground = !value;
}
/// <summary>
/// The necessary properties for big picture style notification.
/// For convenience, assigning this property will also set the Style property.
/// </summary>
public BigPictureStyle? BigPicture
{
get { return m_BigPictureStyle; }
set
{
m_BigPictureStyle = value;
if (m_BigPictureStyle.HasValue && Style == NotificationStyle.None)
Style = NotificationStyle.BigPictureStyle;
else if (!m_BigPictureStyle.HasValue && Style == NotificationStyle.BigPictureStyle)
Style = NotificationStyle.None;
}
}
internal bool ShowCustomTimestamp { get; set; }
private Color m_Color;
private TimeSpan m_RepeatInterval;
private DateTime m_CustomTimestamp;
private bool m_SilentInForeground;
private BigPictureStyle? m_BigPictureStyle;
/// <summary>
/// Create a notification struct with all optional fields set to default values.
/// </summary>
/// <param name="title">Notification title</param>
/// <param name="text">Text to show on notification</param>
/// <param name="fireTime">Date and time when to show, can be DateTime.Now to show right away</param>
public AndroidNotification(string title, string text, DateTime fireTime)
{
Title = title;
Text = text;
FireTime = fireTime;
SmallIcon = string.Empty;
ShouldAutoCancel = false;
LargeIcon = string.Empty;
Style = NotificationStyle.None;
Number = -1;
UsesStopwatch = false;
IntentData = string.Empty;
Group = string.Empty;
GroupSummary = false;
SortKey = string.Empty;
GroupAlertBehaviour = GroupAlertBehaviours.GroupAlertAll;
ShowTimestamp = false;
ShowCustomTimestamp = false;
m_BigPictureStyle = null;
m_RepeatInterval = (-1L).ToTimeSpan();
m_Color = new Color(0, 0, 0, 0);
m_CustomTimestamp = (-1L).ToDatetime();
m_SilentInForeground = false;
}
/// <summary>
/// Create a repeatable notification struct with all optional fields set to default values.
/// </summary>
/// <param name="title">Notification title</param>
/// <param name="text">Text to show on notification</param>
/// <param name="fireTime">Date and time when to show, can be DateTime.Now to show right away</param>
/// <param name="repeatInterval">Makes notification repeatable with this time interval</param>
/// <remarks>
/// There is a minimum period of 1 minute for repeating notifications.
/// </remarks>
public AndroidNotification(string title, string text, DateTime fireTime, TimeSpan repeatInterval)
: this(title, text, fireTime)
{
RepeatInterval = repeatInterval;
}
/// <summary>
/// Create a notification struct with a custom small icon and all optional fields set to default values.
/// </summary>
/// <param name="title">Notification title</param>
/// <param name="text">Text to show on notification</param>
/// <param name="fireTime">Date and time when to show, can be DateTime.Now to show right away</param>
/// <param name="repeatInterval">Makes notification repeatable with this time interval</param>
/// <param name="smallIcon">Name of the small icon to be shown on notification</param>
public AndroidNotification(string title, string text, DateTime fireTime, TimeSpan repeatInterval, string smallIcon)
: this(title, text, fireTime, repeatInterval)
{
SmallIcon = smallIcon;
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 5920d00d82f60ad438e2d483048328d6
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,28 @@
using System;
using UnityEngine;
namespace Unity.Notifications.Android
{
class NotificationCallback : AndroidJavaProxy
{
public NotificationCallback() : base("com.unity.androidnotifications.NotificationCallback")
{
}
public override AndroidJavaObject Invoke(string methodName, AndroidJavaObject[] args)
{
if (methodName.Equals("onSentNotification", StringComparison.InvariantCulture) && args != null && args.Length == 1)
{
onSentNotification(args[0]);
return null;
}
return base.Invoke(methodName, args);
}
public void onSentNotification(AndroidJavaObject notification)
{
AndroidReceivedNotificationMainThreadDispatcher.GetInstance().EnqueueReceivedNotification(notification);
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: df44dfe6f4b394ca988636822db28499
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 330d6c453457d452dba1912232a97e01
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,183 @@
using System;
namespace Unity.Notifications.Android
{
/// <summary>
/// The level of interruption of this notification channel.
/// The importance of a notification is used to determine how much the notification should interrupt the user (visually and audibly).
/// The higher the importance of a notification, the more interruptive the notification will be.
/// </summary>
/// <remarks>
/// The exact behaviour of each importance level might vary depending on the device and OS version on devices running Android 7.1 or older.
/// </remarks>
public enum Importance
{
/// <summary>
/// A notification with no importance: does not show in the shade.
/// </summary>
None = 0,
/// <summary>
/// Low importance, notification is shown everywhere, but is not intrusive.
/// </summary>
Low = 2,
/// <summary>
/// Default importance, notification is shown everywhere, makes noise, but does not intrude visually.
/// </summary>
Default = 3,
/// <summary>
/// High importance, notification is shown everywhere, makes noise and is shown on the screen.
/// </summary>
High = 4,
}
/// <summary>
/// Determines whether notifications appear on the lock screen.
/// </summary>
public enum LockScreenVisibility
{
/// <summary>
/// Do not reveal any part of this notification on a secure lock screen.
/// </summary>
Secret = -1,
/// <summary>
/// Show this notification on all lock screens, but conceal sensitive or private information on secure lock screens.
/// </summary>
Private = 0,
/// <summary>
/// Show this notification in its entirety on the lock screen.
/// </summary>
Public = 1,
}
/// <summary>
/// The wrapper of the Android notification channel. Use this to group notifications by groups.
/// </summary>
public struct AndroidNotificationChannel
{
/// <summary>
/// Notification channel identifier.
/// Must be specified when scheduling notifications.
/// </summary>
public string Id { get; set; }
/// <summary>
/// Notification channel name which is visible to users.
/// </summary>
public string Name { get; set; }
/// <summary>
/// User visible description of the notification channel.
/// </summary>
public string Description { get; set; }
/// <summary>
/// The ID of the registered channel group this channel belongs to.
/// </summary>
public string Group { get; set; }
/// <summary>
/// Importance level which is applied to all notifications sent to the channel.
/// This can be changed by users in the settings app. Android uses importance to determine how much the notification should interrupt the user (visually and audibly).
/// The higher the importance of a notification, the more interruptive the notification will be.
/// The possible importance levels are the following:
/// High: Makes a sound and appears as a heads-up notification.
/// Default: Makes a sound.
/// Low: No sound.
/// None: No sound and does not appear in the status bar.
/// </summary>
public Importance Importance { get; set; }
/// <summary>
/// Whether or not notifications posted to this channel can bypass the Do Not Disturb.
/// This can be changed by users in the settings app.
/// </summary>
public bool CanBypassDnd { get; set; }
/// <summary>
/// Whether notifications posted to this channel can appear as badges in a Launcher application.
/// </summary>
public bool CanShowBadge { get; set; }
/// <summary>
/// Sets whether notifications posted to this channel should display notification lights, on devices that support that feature.
/// This can be changed by users in the settings app.
/// </summary>/
public bool EnableLights { get; set; }
/// <summary>
/// Sets whether notification posted to this channel should vibrate.
/// This can be changed by users in the settings app.
/// </summary>
public bool EnableVibration { get; set; }
/// <summary>
/// Sets the vibration pattern for notifications posted to this channel.
/// </summary>
public long[] VibrationPattern { get; set; }
/// <summary>
/// Sets whether or not notifications posted to this channel are shown on the lockscreen in full or redacted form.
/// This can be changed by users in the settings app.
/// </summary>
public LockScreenVisibility LockScreenVisibility { get; set; }
/// <summary>
/// Indicates whether a channel is enabled in the Settings app. User can block notifications for the entire app or for individual channels. The Importance of a blocked channel is set to None.
/// </summary>
public bool Enabled
{
get { return Importance != Importance.None; }
}
/// <summary>
/// Create a notification channel struct with all optional fields set to default values.
/// </summary>
/// <param name="id">ID for the channel</param>
/// <param name="name">Channel name</param>
/// <param name="description">Channel description</param>
/// <param name="importance">Importance of the channel</param>
public AndroidNotificationChannel(string id, string name, string description, Importance importance)
{
Id = id;
Name = name;
Description = description;
Group = null;
this.Importance = importance;
CanBypassDnd = false;
CanShowBadge = true;
EnableLights = false;
EnableVibration = true;
VibrationPattern = null;
this.LockScreenVisibility = LockScreenVisibility.Public;
}
}
/// <summary>
/// Notification channel group description.
/// It is optional to put channels into groups, but looks nicer in Settings UI.
/// </summary>
public struct AndroidNotificationChannelGroup
{
/// <summary>
/// A unique ID for this group. Will rename the group if already exists.
/// </summary>
public string Id { get; set; }
/// <summary>
/// A user visible name for this group.
/// </summary>
public string Name { get; set; }
/// <summary>
/// A description for this group.
/// </summary>
public string Description { get; set; }
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 6f3829a6c0515ee45b4e641fbd0999f6
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,83 @@
using System;
using UnityEngine;
namespace Unity.Notifications.Android
{
internal static class AndroidNotificationExtensions
{
public static Importance ToImportance(this int importance)
{
if (Enum.IsDefined(typeof(Importance), importance))
return (Importance)importance;
return Importance.Default;
}
public static LockScreenVisibility ToLockScreenVisibility(this int lockscreenVisibility)
{
if (Enum.IsDefined(typeof(LockScreenVisibility), lockscreenVisibility))
return (LockScreenVisibility)lockscreenVisibility;
return LockScreenVisibility.Public;
}
public static NotificationStyle ToNotificationStyle(this int notificationStyle)
{
if (Enum.IsDefined(typeof(NotificationStyle), notificationStyle))
return (NotificationStyle)notificationStyle;
return NotificationStyle.None;
}
public static GroupAlertBehaviours ToGroupAlertBehaviours(this int groupAlertBehaviour)
{
if (Enum.IsDefined(typeof(GroupAlertBehaviours), groupAlertBehaviour))
return (GroupAlertBehaviours)groupAlertBehaviour;
return GroupAlertBehaviours.GroupAlertAll;
}
public static Color ToColor(this int color)
{
int a = (color >> 24) & 0xff;
int r = (color >> 16) & 0xff;
int g = (color >> 8) & 0xff;
int b = (color) & 0xff;
return new Color32((byte)r, (byte)g, (byte)b, (byte)a);
}
public static int ToInt(this Color? color)
{
if (!color.HasValue)
return 0;
var color32 = (Color32)color.Value;
return (color32.a & 0xff) << 24 | (color32.r & 0xff) << 16 | (color32.g & 0xff) << 8 | (color32.b & 0xff);
}
public static long ToLong(this DateTime dateTime)
{
DateTime origin = new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc);
TimeSpan diff = dateTime.ToUniversalTime() - origin;
return (long)Math.Floor(diff.TotalMilliseconds);
}
public static DateTime ToDatetime(this long dateTime)
{
DateTime origin = new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc);
return origin.AddMilliseconds(dateTime).ToLocalTime();
}
public static long ToLong(this TimeSpan? timeSpan)
{
return timeSpan.HasValue ? (long)timeSpan.Value.TotalMilliseconds : -1L;
}
public static TimeSpan ToTimeSpan(this long timeSpan)
{
return TimeSpan.FromMilliseconds(timeSpan);
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: b97d256371bb62248bb9f4404a72030a
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,43 @@
using UnityEngine;
namespace Unity.Notifications.Android
{
/// <summary>
/// Wrapper for the AndroidNotification. Contains the notification's id and channel.
/// </summary>
public class AndroidNotificationIntentData
{
/// <summary>
/// The id of the notification.
/// </summary>
public int Id { get; }
/// <summary>
/// The channel id that the notification was sent to.
/// </summary>
public string Channel { get; }
/// <summary>
/// Returns the AndroidNotification.
/// </summary>
public AndroidNotification Notification { get; }
/// <summary>
/// Returns the proxy to the Android Java instance of Notification class.
/// </summary>
public AndroidJavaObject NativeNotification { get; internal set; }
/// <summary>
/// Create an AndroidNotificationIntentData with AndroidNotification, id, and channel id.
/// </summary>
/// <param name="id">Notification id</param>
/// <param name="channelId">ID of the notification channel</param>
/// <param name="notification">Data of the received notification</param>
public AndroidNotificationIntentData(int id, string channelId, AndroidNotification notification)
{
Id = id;
Channel = channelId;
Notification = notification;
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 25b01cf50533ff64387fe382f4075a2c
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,76 @@
using System;
using System.Collections.Generic;
using UnityEngine;
namespace Unity.Notifications.Android
{
/// <summary>
/// Class that queues the received notifications and triggers the notification callbacks.
/// </summary>
public class AndroidReceivedNotificationMainThreadDispatcher : MonoBehaviour
{
private static AndroidReceivedNotificationMainThreadDispatcher instance = null;
private List<AndroidJavaObject> m_ReceivedNotificationQueue = new List<AndroidJavaObject>();
private List<AndroidJavaObject> m_ReceivedNotificationList = new List<AndroidJavaObject>();
internal void EnqueueReceivedNotification(AndroidJavaObject notification)
{
lock (this)
{
m_ReceivedNotificationQueue.Add(notification);
}
}
internal static AndroidReceivedNotificationMainThreadDispatcher GetInstance()
{
return instance;
}
/// <summary>
/// Update is called once per frame.
/// </summary>
public void Update()
{
// Note: Don't call callbacks while locking receivedNotificationQueue, otherwise there's a risk
// that callback might introduce an operations which would create a deadlock
lock (this)
{
if (m_ReceivedNotificationQueue.Count == 0)
return;
var temp = m_ReceivedNotificationQueue;
m_ReceivedNotificationQueue = m_ReceivedNotificationList;
m_ReceivedNotificationList = temp;
}
foreach (var notification in m_ReceivedNotificationList)
{
try
{
AndroidNotificationCenter.ReceivedNotificationCallback(notification);
}
catch (Exception e)
{
Debug.LogException(e);
}
}
m_ReceivedNotificationList.Clear();
}
void Awake()
{
if (instance == null)
{
instance = this;
DontDestroyOnLoad(this.gameObject);
}
}
void OnDestroy()
{
instance = null;
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 253fc88305aec43f3886f5484cfbb307
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,3 @@
using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("Unity.Android.Notifications.Tests")]
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 76998f4bffc9142d18bb6fa7e954e01c
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,92 @@
using UnityEngine.Android;
namespace Unity.Notifications.Android
{
/// <summary>
/// Represents a status of the Android runtime permission.
/// </summary>
public enum PermissionStatus
{
/// <summary>
/// No permission as user was not prompted for it.
/// </summary>
NotRequested = 0,
/// <summary>
/// User gave permission.
/// </summary>
Allowed = 1,
/// <summary>
/// User denied permission.
/// </summary>
Denied = 2,
/// <summary>
/// No longer used. User denied permission and expressed intent to not be prompted again.
/// </summary>
DeniedDontAskAgain = 3,
/// <summary>
/// A request for permission was made and user hasn't responded yet.
/// </summary>
RequestPending = 4,
/// <summary>
/// Notifications are blocked for this app. Before API level 33 this means they were disabled in Settings.
/// <see cref="https://developer.android.com/reference/android/app/NotificationManager#areNotificationsEnabled()"/>
/// </summary>
NotificationsBlockedForApp = 5,
}
/// <summary>
/// A class to request permission to post notifications.
/// Before Android 13 (API 33) it is not required and Status will become Allowed immediately.
/// May succeed or fail immediately. Users response is saved to PlayerPrefs.
/// Respects users wish to not be asked again.
/// </summary>
/// <seealso cref="AndroidNotificationCenter.UserPermissionToPost"/>
/// <seealso cref="AndroidNotificationCenter.SETTING_POST_NOTIFICATIONS_PERMISSION"/>
public class PermissionRequest
{
/// <summary>
/// The status of this request.
/// Value other than RequestPending means request has completed.
/// </summary>
public PermissionStatus Status { get; set; }
/// <summary>
/// Create a new request.
/// Will show user a dialog asking for permission if that is required to post notifications and user hasn't permanently denied it already.
/// </summary>
public PermissionRequest()
{
Status = AndroidNotificationCenter.UserPermissionToPost;
switch (Status)
{
case PermissionStatus.NotRequested:
case PermissionStatus.Denied:
case PermissionStatus.DeniedDontAskAgain: // this one is no longer used, but might be found in settings
Status = RequestPermission();
break;
}
}
PermissionStatus RequestPermission()
{
if (!AndroidNotificationCenter.CanRequestPermissionToPost)
return PermissionStatus.Denied;
var callbacks = new PermissionCallbacks();
callbacks.PermissionGranted += (unused) => PermissionResponse(PermissionStatus.Allowed);
callbacks.PermissionDenied += (unused) => PermissionResponse(PermissionStatus.Denied);
Permission.RequestUserPermission(AndroidNotificationCenter.PERMISSION_POST_NOTIFICATIONS, callbacks);
return PermissionStatus.RequestPending;
}
void PermissionResponse(PermissionStatus status)
{
Status = status;
AndroidNotificationCenter.SetPostPermissionSetting(status);
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 110fbd120961b4105a52b39fc24273be
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: b4d5e3b0fd3074704a1c4fcee255f85c
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,80 @@
fileFormatVersion: 2
guid: 5a06b648d726ee0479a00bac712c49f1
PluginImporter:
externalObjects: {}
serializedVersion: 2
iconMap: {}
executionOrder: {}
defineConstraints: []
isPreloaded: 0
isOverridable: 1
isExplicitlyReferenced: 0
validateReferences: 1
platformData:
- first:
: Any
second:
enabled: 0
settings:
Exclude Android: 0
Exclude Editor: 1
Exclude Linux64: 1
Exclude OSXUniversal: 1
Exclude Win: 1
Exclude Win64: 1
Exclude iOS: 1
- first:
Android: Android
second:
enabled: 1
settings:
CPU: ARMv7
- first:
Any:
second:
enabled: 0
settings: {}
- first:
Editor: Editor
second:
enabled: 0
settings:
CPU: AnyCPU
DefaultValueInitialized: true
OS: AnyOS
- first:
Standalone: Linux64
second:
enabled: 0
settings:
CPU: None
- first:
Standalone: OSXUniversal
second:
enabled: 0
settings:
CPU: None
- first:
Standalone: Win
second:
enabled: 0
settings:
CPU: None
- first:
Standalone: Win64
second:
enabled: 0
settings:
CPU: None
- first:
iPhone: iOS
second:
enabled: 0
settings:
AddToEmbeddedBinaries: false
CPU: AnyCPU
CompileFlags:
FrameworkDependencies:
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,44 @@
plugins {
id 'com.android.library'
}
def getCompileSdk(unityLib) {
def unityCompileSdk = unityLib.compileSdkVersion
def version = unityCompileSdk.find('\\d+').toInteger()
if (version < 33) {
return 33
}
return version
}
android {
// Checking if namespace exists is needed for 2021.3 (AGP 4.0.1)
// When 2021.3 is dropped, remove this check and package="com.unity.androidnotifications" from AndroidManifest
if (project.android.hasProperty("namespace")) {
namespace "com.unity.androidnotifications"
}
def unityLib = project(':unityLibrary').extensions.getByName('android')
compileSdkVersion getCompileSdk(unityLib)
buildToolsVersion unityLib.buildToolsVersion
defaultConfig {
minSdkVersion 22
consumerProguardFiles "proguard-rules.pro"
}
buildTypes {
release {
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
}
dependencies {
api files('../libs/unity-classes.jar')
}
@@ -0,0 +1,3 @@
# Accessed from C# code
-keep class com.unity.androidnotifications.UnityNotificationManager { public *; }
-keep interface com.unity.androidnotifications.NotificationCallback { *; }
@@ -0,0 +1,12 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.unity.androidnotifications">
<application>
<receiver android:name="com.unity.androidnotifications.UnityNotificationManager" android:exported="false" />
<receiver android:name="com.unity.androidnotifications.UnityNotificationRestartReceiver" android:enabled="false" android:exported="false">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
</intent-filter>
</receiver>
<meta-data android:name="com.unity.androidnotifications.exact_scheduling" android:value="0" />
</application>
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
</manifest>
@@ -0,0 +1,203 @@
package com.unity.androidnotifications;
import static com.unity.androidnotifications.UnityNotificationManager.KEY_FIRE_TIME;
import static com.unity.androidnotifications.UnityNotificationManager.KEY_ID;
import static com.unity.androidnotifications.UnityNotificationManager.TAG_UNITY;
import android.app.Notification;
import android.os.Bundle;
import android.util.Log;
import java.util.Calendar;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.LinkedTransferQueue;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
public class UnityNotificationBackgroundThread extends Thread {
private static abstract class Task {
// returns true if notificationIds was modified (needs to be saved)
public abstract boolean run(UnityNotificationManager manager, ConcurrentHashMap<Integer, Notification.Builder> notifications);
}
private static class ScheduleNotificationTask extends Task {
private int notificationId;
private Notification.Builder notificationBuilder;
private boolean isCustomized;
private boolean isNew;
public ScheduleNotificationTask(int id, Notification.Builder builder, boolean customized, boolean addedNew) {
notificationId = id;
notificationBuilder = builder;
isCustomized = customized;
isNew = addedNew;
}
@Override
public boolean run(UnityNotificationManager manager, ConcurrentHashMap<Integer, Notification.Builder> notifications) {
String id = String.valueOf(notificationId);
Integer ID = Integer.valueOf(notificationId);
boolean didSchedule = false;
try {
UnityNotificationManager.mUnityNotificationManager.performNotificationScheduling(notificationId, notificationBuilder, isCustomized);
didSchedule = true;
} finally {
// if failed to schedule or replace, remove
if (!didSchedule) {
notifications.remove(notificationId);
manager.cancelPendingNotificationIntent(notificationId);
manager.deleteExpiredNotificationIntent(id);
}
}
return isNew;
}
}
private static class CancelNotificationTask extends Task {
private int notificationId;
public CancelNotificationTask(int id) {
notificationId = id;
}
@Override
public boolean run(UnityNotificationManager manager, ConcurrentHashMap<Integer, Notification.Builder> notifications) {
manager.cancelPendingNotificationIntent(notificationId);
if (notifications.remove(notificationId) != null) {
manager.deleteExpiredNotificationIntent(String.valueOf(notificationId));
return true;
}
return false;
}
}
private static class CancelAllNotificationsTask extends Task {
@Override
public boolean run(UnityNotificationManager manager, ConcurrentHashMap<Integer, Notification.Builder> notifications) {
if (notifications.isEmpty())
return false;
Enumeration<Integer> ids = notifications.keys();
while (ids.hasMoreElements()) {
Integer notificationId = ids.nextElement();
manager.cancelPendingNotificationIntent(notificationId);
manager.deleteExpiredNotificationIntent(String.valueOf(notificationId));
}
notifications.clear();
return true;
}
}
private static class HousekeepingTask extends Task {
UnityNotificationBackgroundThread thread;
public HousekeepingTask(UnityNotificationBackgroundThread th) {
thread = th;
}
@Override
public boolean run(UnityNotificationManager manager, ConcurrentHashMap<Integer, Notification.Builder> notifications) {
HashSet<String> notificationIds = new HashSet<>();
Enumeration<Integer> ids = notifications.keys();
while (ids.hasMoreElements()) {
notificationIds.add(String.valueOf(ids.nextElement()));
}
thread.performHousekeeping(notificationIds);
return false;
}
}
private static final int TASKS_FOR_HOUSEKEEPING = 50;
private LinkedTransferQueue<Task> mTasks = new LinkedTransferQueue();
private ConcurrentHashMap<Integer, Notification.Builder> mScheduledNotifications;
private UnityNotificationManager mManager;
private int mTasksSinceHousekeeping = TASKS_FOR_HOUSEKEEPING; // we want hoursekeeping at the start
public UnityNotificationBackgroundThread(UnityNotificationManager manager, ConcurrentHashMap<Integer, Notification.Builder> scheduledNotifications) {
mManager = manager;
mScheduledNotifications = scheduledNotifications;
// rescheduling after reboot may have loaded, otherwise load here
if (mScheduledNotifications.size() == 0)
loadNotifications();
}
public void enqueueNotification(int id, Notification.Builder notificationBuilder, boolean customized, boolean addedNew) {
mTasks.add(new UnityNotificationBackgroundThread.ScheduleNotificationTask(id, notificationBuilder, customized, addedNew));
}
public void enqueueCancelNotification(int id) {
mTasks.add(new CancelNotificationTask(id));
}
public void enqueueCancelAllNotifications() {
mTasks.add(new CancelAllNotificationsTask());
}
private void enqueueHousekeeping() {
mTasks.add(new HousekeepingTask(this));
}
@Override
public void run() {
boolean haveChanges = false;
while (true) {
try {
Task task = mTasks.take();
haveChanges |= executeTask(mManager, task, mScheduledNotifications);
if (!(task instanceof HousekeepingTask))
++mTasksSinceHousekeeping;
if (mTasks.size() == 0 && haveChanges) {
haveChanges = false;
enqueueHousekeeping();
}
} catch (InterruptedException e) {
if (mTasks.isEmpty())
break;
}
}
}
private boolean executeTask(UnityNotificationManager manager, Task task, ConcurrentHashMap<Integer, Notification.Builder> notifications) {
try {
return task.run(manager, notifications);
} catch (Exception e) {
Log.e(TAG_UNITY, "Exception executing notification task", e);
return false;
}
}
private void performHousekeeping(Set<String> notificationIds) {
// don't do housekeeping if last task we did was housekeeping (other=1)
boolean performHousekeeping = mTasksSinceHousekeeping >= TASKS_FOR_HOUSEKEEPING;
mTasksSinceHousekeeping = 0;
if (performHousekeeping)
mManager.performNotificationHousekeeping(notificationIds);
mManager.saveScheduledNotificationIDs(notificationIds);
}
private void loadNotifications() {
List<Notification.Builder> notifications = mManager.loadSavedNotifications();
if (notifications == null || notifications.size() == 0)
return;
final long currentTime = Calendar.getInstance().getTime().getTime();
boolean needHousekeeping = false;
for (Notification.Builder builder : notifications) {
Bundle extras = builder.getExtras();
int id = extras.getInt(KEY_ID, -1);
long fireTime = extras.getLong(KEY_FIRE_TIME, -1);
boolean inFuture = fireTime - currentTime > 0;
if (inFuture)
mScheduledNotifications.put(id, builder);
else
needHousekeeping = true;
}
if (needHousekeeping)
enqueueHousekeeping();
}
}
@@ -0,0 +1,67 @@
package com.unity.androidnotifications;
import android.app.Notification;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.os.AsyncTask;
import android.os.Bundle;
import android.util.Log;
import java.util.Calendar;
import java.util.Date;
import java.util.List;
import static com.unity.androidnotifications.UnityNotificationManager.KEY_FIRE_TIME;
import static com.unity.androidnotifications.UnityNotificationManager.KEY_ID;
import static com.unity.androidnotifications.UnityNotificationManager.KEY_REPEAT_INTERVAL;
import static com.unity.androidnotifications.UnityNotificationManager.TAG_UNITY;
public class UnityNotificationRestartReceiver extends BroadcastReceiver {
private static final long EXPIRATION_TRESHOLD = 600000; // 10 minutes
@Override
public void onReceive(Context context, Intent received_intent) {
Log.d(TAG_UNITY, "Rescheduling notifications after restart");
if (Intent.ACTION_BOOT_COMPLETED.equals(received_intent.getAction())) {
AsyncTask.execute(() -> { rescheduleSavedNotifications(context); });
}
}
private static void rescheduleSavedNotifications(Context context) {
UnityNotificationManager manager = UnityNotificationManager.getNotificationManagerImpl(context);
List<Notification.Builder> saved_notifications = manager.loadSavedNotifications();
Date currentDate = Calendar.getInstance().getTime();
for (Notification.Builder notificationBuilder : saved_notifications) {
rescheduleNotification(manager, currentDate, notificationBuilder);
}
}
private static boolean rescheduleNotification(UnityNotificationManager manager, Date currentDate, Notification.Builder notificationBuilder) {
try {
Bundle extras = notificationBuilder.getExtras();
long repeatInterval = extras.getLong(KEY_REPEAT_INTERVAL, 0L);
long fireTime = extras.getLong(KEY_FIRE_TIME, 0L);
Date fireTimeDate = new Date(fireTime);
boolean isRepeatable = repeatInterval > 0;
if (fireTimeDate.after(currentDate) || isRepeatable) {
manager.scheduleAlarmWithNotification(notificationBuilder);
return true;
} else if (currentDate.getTime() - fireTime < EXPIRATION_TRESHOLD) {
// notification is in the past, but not by much, send now
int id = extras.getInt(KEY_ID);
manager.notify(id, notificationBuilder);
return true;
} else {
Log.d(TAG_UNITY, "Notification expired, not rescheduling, ID: " + extras.getInt(KEY_ID, -1));
return false;
}
} catch (Exception e) {
Log.e(TAG_UNITY, "Failed to reschedule notification", e);
return false;
}
}
}
@@ -0,0 +1,652 @@
package com.unity.androidnotifications;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import android.app.Notification;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.ActivityInfo;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.content.res.Resources;
import android.os.Build;
import android.os.Bundle;
import android.os.Parcel;
import android.os.Parcelable;
import android.util.Base64;
import android.util.Log;
import static com.unity.androidnotifications.UnityNotificationManager.KEY_CHANNEL_ID;
import static com.unity.androidnotifications.UnityNotificationManager.KEY_FIRE_TIME;
import static com.unity.androidnotifications.UnityNotificationManager.KEY_ID;
import static com.unity.androidnotifications.UnityNotificationManager.KEY_INTENT_DATA;
import static com.unity.androidnotifications.UnityNotificationManager.KEY_LARGE_ICON;
import static com.unity.androidnotifications.UnityNotificationManager.KEY_NOTIFICATION;
import static com.unity.androidnotifications.UnityNotificationManager.KEY_REPEAT_INTERVAL;
import static com.unity.androidnotifications.UnityNotificationManager.KEY_SMALL_ICON;
import static com.unity.androidnotifications.UnityNotificationManager.KEY_SHOW_IN_FOREGROUND;
import static com.unity.androidnotifications.UnityNotificationManager.KEY_BIG_LARGE_ICON;
import static com.unity.androidnotifications.UnityNotificationManager.KEY_BIG_PICTURE;
import static com.unity.androidnotifications.UnityNotificationManager.KEY_BIG_CONTENT_TITLE;
import static com.unity.androidnotifications.UnityNotificationManager.KEY_BIG_SUMMARY_TEXT;
import static com.unity.androidnotifications.UnityNotificationManager.KEY_BIG_CONTENT_DESCRIPTION;
import static com.unity.androidnotifications.UnityNotificationManager.KEY_BIG_SHOW_WHEN_COLLAPSED;
import static com.unity.androidnotifications.UnityNotificationManager.TAG_UNITY;
class UnityNotificationUtilities {
/*
We serialize notifications and save them to shared prefs, so that if app is killed, we can recreate them.
The serialized BLOB starts with a four byte magic number descibing serialization type, followed by an integer version.
IMPORTANT: IF YOU DO A CHANGE THAT AFFECTS THE LAYOUT, BUMP THE VERSION, AND ENSURE OLD VERSION STILL DESERIALIZES. ADD TEST.
In real life app can get updated having old serialized notifications present, so we should be able to deserialize them.
*/
// magic stands for "Unity Mobile Notifications Notification"
static final byte[] UNITY_MAGIC_NUMBER = new byte[] { 'U', 'M', 'N', 'N'};
private static final byte[] UNITY_MAGIC_NUMBER_PARCELLED = new byte[] { 'U', 'M', 'N', 'P'};
private static final int NOTIFICATION_SERIALIZATION_VERSION = 3;
private static final int INTENT_SERIALIZATION_VERSION = 0;
static final String SAVED_NOTIFICATION_PRIMARY_KEY = "data";
static final String SAVED_NOTIFICATION_FALLBACK_KEY = "fallback.data";
protected static int findResourceIdInContextByName(Context context, String name) {
if (name == null)
return 0;
try {
Resources res = context.getResources();
if (res != null) {
int id = res.getIdentifier(name, "mipmap", context.getPackageName());
if (id == 0)
return res.getIdentifier(name, "drawable", context.getPackageName());
else
return id;
}
return 0;
} catch (Resources.NotFoundException e) {
return 0;
}
}
/* Originally we used to serialize a bundle with predefined list of values.
After we exposed entire Notification.Builder to users, this is not sufficient anymore.
Unfortunately, while Notification itself is Parcelable and can be marshalled to bytes,
it's contents are not guaranteed to be (Binder objects).
Hence what we try to do here is:
- serialize as is if notification is possibly customized by user
- otherwise serialize our stuff, since there is nothing more
*/
protected static void serializeNotification(SharedPreferences prefs, Notification notification, boolean serializeParcel) {
try {
String serialized;
ByteArrayOutputStream data = new ByteArrayOutputStream();
DataOutputStream out = new DataOutputStream(data);
if (serializeParcel) {
Intent intent = new Intent();
intent.putExtra(KEY_NOTIFICATION, notification);
if (serializeNotificationParcel(intent, out)) {
out.close();
byte[] bytes = data.toByteArray();
serialized = Base64.encodeToString(bytes, 0, bytes.length, 0);
} else {
return; // failed
}
}
else {
if (serializeNotificationCustom(notification, out)) {
out.flush();
byte[] bytes = data.toByteArray();
serialized = Base64.encodeToString(bytes, 0, bytes.length, 0);
} else {
return; // failed
}
}
SharedPreferences.Editor editor = prefs.edit().clear();
editor.putString(SAVED_NOTIFICATION_PRIMARY_KEY, serialized);
editor.apply();
} catch (Exception e) {
Log.e(TAG_UNITY, "Failed to serialize notification", e);
}
}
static boolean serializeNotificationParcel(Intent intent, DataOutputStream out) {
try {
byte[] bytes = serializeParcelable(intent);
if (bytes == null || bytes.length == 0)
return false;
out.write(UNITY_MAGIC_NUMBER_PARCELLED);
out.writeInt(INTENT_SERIALIZATION_VERSION);
out.writeInt(bytes.length);
out.write(bytes);
return true;
} catch (Exception e) {
Log.e(TAG_UNITY, "Failed to serialize notification as Parcel", e);
} catch (OutOfMemoryError e) {
Log.e(TAG_UNITY, "Failed to serialize notification as Parcel", e);
}
return false;
}
private static boolean serializeNotificationCustom(Notification notification, DataOutputStream out) {
try {
out.write(UNITY_MAGIC_NUMBER);
out.writeInt(NOTIFICATION_SERIALIZATION_VERSION);
// serialize extras
boolean showWhen = notification.extras.getBoolean(Notification.EXTRA_SHOW_WHEN, false);
out.writeInt(notification.extras.getInt(KEY_ID));
serializeString(out, notification.extras.getString(Notification.EXTRA_TITLE));
serializeString(out, notification.extras.getString(Notification.EXTRA_TEXT));
serializeString(out, notification.extras.getString(KEY_SMALL_ICON));
serializeString(out, notification.extras.getString(KEY_LARGE_ICON));
out.writeLong(notification.extras.getLong(KEY_FIRE_TIME, -1));
out.writeLong(notification.extras.getLong(KEY_REPEAT_INTERVAL, -1));
serializeString(out, notification.extras.getString(Notification.EXTRA_BIG_TEXT));
out.writeBoolean(notification.extras.getBoolean(Notification.EXTRA_SHOW_CHRONOMETER, false));
out.writeBoolean(showWhen);
serializeString(out, notification.extras.getString(KEY_INTENT_DATA));
out.writeBoolean(notification.extras.getBoolean(KEY_SHOW_IN_FOREGROUND, true));
String bigPicture = notification.extras.getString(KEY_BIG_PICTURE);
serializeString(out, bigPicture);
if (bigPicture != null && bigPicture.length() > 0) {
// the following only need to be put in if big picture is there
serializeString(out, notification.extras.getString(KEY_BIG_LARGE_ICON));
serializeString(out, notification.extras.getString(KEY_BIG_CONTENT_TITLE));
serializeString(out, notification.extras.getString(KEY_BIG_CONTENT_DESCRIPTION));
serializeString(out, notification.extras.getString(KEY_BIG_SUMMARY_TEXT));
out.writeBoolean(notification.extras.getBoolean(KEY_BIG_SHOW_WHEN_COLLAPSED, false));
}
serializeString(out, Build.VERSION.SDK_INT < Build.VERSION_CODES.O ? null : notification.getChannelId());
Integer color = UnityNotificationManager.getNotificationColor(notification);
out.writeBoolean(color != null);
if (color != null)
out.writeInt(color);
out.writeInt(notification.number);
out.writeBoolean(0 != (notification.flags & Notification.FLAG_AUTO_CANCEL));
serializeString(out, notification.getGroup());
out.writeBoolean(0 != (notification.flags & Notification.FLAG_GROUP_SUMMARY));
out.writeInt(UnityNotificationManager.getNotificationGroupAlertBehavior(notification));
serializeString(out, notification.getSortKey());
if (showWhen)
out.writeLong(notification.when);
return true;
} catch (Exception e) {
Log.e(TAG_UNITY, "Failed to serialize notification", e);
return false;
}
}
static void serializeString(DataOutputStream out, String s) throws IOException {
if (s == null || s.length() == 0)
out.writeInt(0);
else {
byte[] bytes = s.getBytes(StandardCharsets.UTF_8);
out.writeInt(bytes.length);
out.write(bytes);
}
}
static byte[] serializeParcelable(Parcelable obj) {
try {
Parcel p = Parcel.obtain();
Bundle b = new Bundle();
b.putParcelable("obj", obj);
p.writeParcelable(b, 0);
byte[] result = p.marshall();
p.recycle();
return result;
} catch (Exception e) {
Log.e(TAG_UNITY, "Failed to serialize Parcelable", e);
} catch (OutOfMemoryError e) {
Log.e(TAG_UNITY, "Failed to serialize Parcelable", e);
}
return null;
}
protected static Object deserializeNotification(Context context, SharedPreferences prefs) {
String serializedIntentData = prefs.getString(SAVED_NOTIFICATION_PRIMARY_KEY, "");
if (null == serializedIntentData || serializedIntentData.length() <= 0)
return null;
byte[] bytes = Base64.decode(serializedIntentData, 0);
Object notification = deserializeNotification(context, bytes);
if (notification != null)
return notification;
serializedIntentData = prefs.getString(SAVED_NOTIFICATION_FALLBACK_KEY, "");
if (null == serializedIntentData || serializedIntentData.length() <= 0)
return null;
bytes = Base64.decode(serializedIntentData, 0);
return deserializeNotification(context, bytes);
}
/* See serialization method above for explaination of fallbacks.
This one matches it with one additional fallback: support for "old" bundle serialization.
*/
private static Object deserializeNotification(Context context, byte[] bytes) {
ByteArrayInputStream data = new ByteArrayInputStream(bytes);
DataInputStream in = new DataInputStream(data);
Notification notification = deserializeNotificationParcelable(in);
if (notification != null)
return notification;
data.reset();
Notification.Builder builder = deserializeNotificationCustom(context, in);
if (builder == null) {
builder = deserializedFromOldIntent(context, bytes);
}
return builder;
}
private static boolean readAndCheckMagicNumber(DataInputStream in, byte[] magic) {
try {
boolean magicNumberMatch = true;
for (int i = 0; i < magic.length; ++i) {
byte b = in.readByte();
if (b != magic[i]) {
magicNumberMatch = false;
break;
}
}
return magicNumberMatch;
} catch (Exception e) {
return false;
}
}
private static Notification deserializeNotificationParcelable(DataInputStream in) {
try {
if (!readAndCheckMagicNumber(in, UNITY_MAGIC_NUMBER_PARCELLED))
return null;
int version = in.readInt();
if (version < 0 || version > INTENT_SERIALIZATION_VERSION)
return null;
Intent intent = deserializeParcelable(in);
Notification notification = intent.getParcelableExtra(KEY_NOTIFICATION);
return notification;
} catch (Exception e) {
Log.e(TAG_UNITY, "Failed to deserialize notification intent", e);
} catch (OutOfMemoryError e) {
Log.e(TAG_UNITY, "Failed to deserialize notification intent", e);
}
return null;
}
private static Notification.Builder deserializeNotificationCustom(Context context, DataInputStream in) {
try {
if (!readAndCheckMagicNumber(in, UNITY_MAGIC_NUMBER))
return null;
int version = in.readInt();
if (version < 0 || version > NOTIFICATION_SERIALIZATION_VERSION)
return null;
// deserialize extras
int id = 0;
String title, text, smallIcon, largeIcon, bigText, intentData;
long fireTime, repeatInterval;
boolean usesStopWatch, showWhen, showInForeground = true;
Bundle extras = null;
String bigPicture = null, bigLargeIcon = null, bigContentTitle = null, bigSummary = null, bigContentDesc = null;
boolean bigShowWhenCollapsed = false;
if (version < 2) {
// no longer serialized since v2
extras = deserializeParcelable(in);
}
// before v2 it was extras or variables, since 2 always variables
if (extras == null) {
// extras serialized manually
id = in.readInt();
title = deserializeString(in);
text = deserializeString(in);
smallIcon = deserializeString(in);
largeIcon = deserializeString(in);
fireTime = in.readLong();
repeatInterval = in.readLong();
bigText = deserializeString(in);
usesStopWatch = in.readBoolean();
showWhen = in.readBoolean();
intentData = deserializeString(in);
if (version > 0)
showInForeground = in.readBoolean();
if (version >= 3) {
bigPicture = deserializeString(in);
if (bigPicture != null && bigPicture.length() > 0) {
// the following only need to be put in if big picture is there
bigLargeIcon = deserializeString(in);
bigContentTitle = deserializeString(in);
bigContentDesc = deserializeString(in);
bigSummary = deserializeString(in);
bigShowWhenCollapsed = in.readBoolean();
}
}
} else {
title = extras.getString(Notification.EXTRA_TITLE);
text = extras.getString(Notification.EXTRA_TEXT);
smallIcon = extras.getString(KEY_SMALL_ICON);
largeIcon = extras.getString(KEY_LARGE_ICON);
fireTime = extras.getLong(KEY_FIRE_TIME, -1);
repeatInterval = extras.getLong(KEY_REPEAT_INTERVAL, -1);
bigText = extras.getString(Notification.EXTRA_BIG_TEXT);
usesStopWatch = extras.getBoolean(Notification.EXTRA_SHOW_CHRONOMETER, false);
showWhen = extras.getBoolean(Notification.EXTRA_SHOW_WHEN, false);
intentData = extras.getString(KEY_INTENT_DATA);
}
String channelId = deserializeString(in);
boolean haveColor = in.readBoolean();
int color = 0;
if (haveColor)
color = in.readInt();
int number = in.readInt();
boolean shouldAutoCancel = in.readBoolean();
String group = deserializeString(in);
boolean groupSummary = in.readBoolean();
int groupAlertBehavior = in.readInt();
String sortKey = deserializeString(in);
long when = showWhen ? in.readLong() : 0;
UnityNotificationManager manager = UnityNotificationManager.getNotificationManagerImpl(context);
Notification.Builder builder = manager.createNotificationBuilder(channelId);
if (extras != null)
builder.setExtras(extras);
else {
builder.getExtras().putInt(KEY_ID, id);
UnityNotificationManager.setNotificationIcon(builder, KEY_SMALL_ICON, smallIcon);
UnityNotificationManager.setNotificationIcon(builder, KEY_LARGE_ICON, largeIcon);
if (fireTime != -1)
builder.getExtras().putLong(KEY_FIRE_TIME, fireTime);
if (repeatInterval != -1)
builder.getExtras().putLong(KEY_REPEAT_INTERVAL, repeatInterval);
if (intentData != null)
builder.getExtras().putString(KEY_INTENT_DATA, intentData);
builder.getExtras().putBoolean(KEY_SHOW_IN_FOREGROUND, showInForeground);
}
if (title != null)
builder.setContentTitle(title);
if (text != null)
builder.setContentText(text);
if (bigText != null)
builder.setStyle(new Notification.BigTextStyle().bigText(bigText));
else if (bigPicture != null)
manager.setupBigPictureStyle(builder, bigLargeIcon, bigPicture, bigContentTitle, bigContentDesc, bigSummary, bigShowWhenCollapsed);
if (haveColor)
UnityNotificationManager.setNotificationColor(builder, color);
if (number >= 0)
builder.setNumber(number);
builder.setAutoCancel(shouldAutoCancel);
UnityNotificationManager.setNotificationUsesChronometer(builder, usesStopWatch);
if (group != null && group.length() > 0)
builder.setGroup(group);
builder.setGroupSummary(groupSummary);
UnityNotificationManager.setNotificationGroupAlertBehavior(builder, groupAlertBehavior);
if (sortKey != null && sortKey.length() > 0)
builder.setSortKey(sortKey);
if (showWhen) {
builder.setShowWhen(true);
builder.setWhen(when);
}
return builder;
} catch (Exception e) {
Log.e(TAG_UNITY, "Failed to deserialize notification", e);
} catch (OutOfMemoryError e) {
Log.e(TAG_UNITY, "Failed to deserialize notification", e);
}
return null;
}
private static Notification.Builder deserializedFromOldIntent(Context context, byte[] bytes) {
try {
Parcel p = Parcel.obtain();
p.unmarshall(bytes, 0, bytes.length);
p.setDataPosition(0);
Bundle bundle = new Bundle();
bundle.readFromParcel(p);
int id = bundle.getInt(KEY_ID, -1);
String channelId = bundle.getString("channelID");
String textTitle = bundle.getString("textTitle");
String textContent = bundle.getString("textContent");
String smallIcon = bundle.getString("smallIconStr");
boolean autoCancel = bundle.getBoolean("autoCancel", false);
boolean usesChronometer = bundle.getBoolean("usesChronometer", false);
long fireTime = bundle.getLong(KEY_FIRE_TIME, -1);
long repeatInterval = bundle.getLong(KEY_REPEAT_INTERVAL, -1);
String largeIcon = bundle.getString("largeIconStr");
int style = bundle.getInt("style", -1);
int color = bundle.getInt("color", 0);
int number = bundle.getInt("number", 0);
String intentData = bundle.getString(KEY_INTENT_DATA);
String group = bundle.getString("group");
boolean groupSummary = bundle.getBoolean("groupSummary", false);
String sortKey = bundle.getString("sortKey");
int groupAlertBehaviour = bundle.getInt("groupAlertBehaviour", -1);
boolean showTimestamp = bundle.getBoolean("showTimestamp", false);
Notification.Builder builder = UnityNotificationManager.getNotificationManagerImpl(context).createNotificationBuilder(channelId);
builder.getExtras().putInt(KEY_ID, id);
builder.setContentTitle(textTitle);
builder.setContentText(textContent);
UnityNotificationManager.setNotificationIcon(builder, KEY_SMALL_ICON, smallIcon);
builder.setAutoCancel(autoCancel);
builder.setUsesChronometer(usesChronometer);
builder.getExtras().putLong(KEY_FIRE_TIME, fireTime);
builder.getExtras().putLong(KEY_REPEAT_INTERVAL, repeatInterval);
UnityNotificationManager.setNotificationIcon(builder, KEY_LARGE_ICON, largeIcon);
if (style == 2)
builder.setStyle(new Notification.BigTextStyle().bigText(textContent));
if (color != 0)
UnityNotificationManager.setNotificationColor(builder, color);
if (number >= 0)
builder.setNumber(number);
if (intentData != null)
builder.getExtras().putString(KEY_INTENT_DATA, intentData);
if (null != group && group.length() > 0)
builder.setGroup(group);
builder.setGroupSummary(groupSummary);
if (null != sortKey && sortKey.length() > 0)
builder.setSortKey(sortKey);
UnityNotificationManager.setNotificationGroupAlertBehavior(builder, groupAlertBehaviour);
builder.setShowWhen(showTimestamp);
return builder;
} catch (Exception e) {
Log.e(TAG_UNITY, "Failed to deserialize old style notification", e);
} catch (OutOfMemoryError e) {
Log.e(TAG_UNITY, "Failed to deserialize old style notification", e);
}
return null;
}
private static String deserializeString(DataInputStream in) throws IOException {
int length = in.readInt();
if (length <= 0)
return null;
byte[] bytes = new byte[length];
int didRead = in.read(bytes);
if (didRead != bytes.length)
throw new IOException("Insufficient amount of bytes read");
return new String(bytes, StandardCharsets.UTF_8);
}
private static <T extends Parcelable> T deserializeParcelable(DataInputStream in) throws IOException {
int length = in.readInt();
if (length <= 0)
return null;
byte[] bytes = new byte[length];
int didRead = in.read(bytes);
if (didRead != bytes.length)
throw new IOException("Insufficient amount of bytes read");
try {
Parcel p = Parcel.obtain();
p.unmarshall(bytes, 0, bytes.length);
p.setDataPosition(0);
Bundle b = p.readParcelable(UnityNotificationUtilities.class.getClassLoader());
p.recycle();
if (b != null) {
return b.getParcelable("obj");
}
} catch (Exception e) {
Log.e(TAG_UNITY, "Failed to deserialize parcelable", e);
} catch (OutOfMemoryError e) {
Log.e(TAG_UNITY, "Failed to deserialize parcelable", e);
}
return null;
}
// Returns Activity class to be opened when notification is tapped
// Search is done in this order:
// * class specified in meta-data key custom_notification_android_activity
// * the only enabled activity with name ending in either .UnityPlayerActivity or .UnityPlayerGameActivity
// * the only enabled activity in the package
protected static Class<?> getOpenAppActivity(Context context) {
try {
PackageManager pm = context.getPackageManager();
ApplicationInfo ai = pm.getApplicationInfo(context.getPackageName(), PackageManager.GET_META_DATA);
Bundle bundle = ai.metaData;
if (bundle.containsKey("custom_notification_android_activity")) {
try {
return Class.forName(bundle.getString("custom_notification_android_activity"));
} catch (ClassNotFoundException e) {
Log.e(TAG_UNITY, "Specified activity class for notifications not found: " + e.getMessage());
}
}
Log.w(TAG_UNITY, "No custom_notification_android_activity found, attempting to find app activity class");
ActivityInfo[] aInfo = pm.getPackageInfo(context.getPackageName(), PackageManager.GET_ACTIVITIES).activities;
if (aInfo == null) {
Log.e(TAG_UNITY, "Could not get package activities");
return null;
}
String activityClassName = null;
boolean activityIsUnity = false, activityConflict = false;
for (ActivityInfo info : aInfo) {
// activity alias not supported
if (!info.enabled || info.targetActivity != null)
continue;
boolean candidateIsUnity = isUnityActivity(info.name);
if (activityClassName == null) {
activityClassName = info.name;
activityIsUnity = candidateIsUnity;
continue;
}
// two Unity activities is a hard conflict
// two non-Unity activities is a conflict unless we find a Unity activity later on
if (activityIsUnity == candidateIsUnity) {
activityConflict = true;
if (activityIsUnity && candidateIsUnity)
break;
continue;
}
if (candidateIsUnity) {
activityClassName = info.name;
activityIsUnity = candidateIsUnity;
activityConflict = false;
}
}
if (activityConflict) {
Log.e(TAG_UNITY, "Multiple choices for activity for notifications, set activity explicitly in Notification Settings");
return null;
}
if (activityClassName == null) {
Log.e(TAG_UNITY, "Activity class for notifications not found");
return null;
}
return Class.forName(activityClassName);
} catch (PackageManager.NameNotFoundException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
Log.e(TAG_UNITY, "Failed to find activity class: " + e.getMessage());
}
return null;
}
private static boolean isUnityActivity(String name) {
return name.endsWith(".UnityPlayerActivity") || name.endsWith(".UnityPlayerGameActivity");
}
protected static Notification.Builder recoverBuilder(Context context, Notification notification) {
try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
Notification.Builder builder = Notification.Builder.recoverBuilder(context, notification);
// extras not recovered, transfer manually
builder.setExtras(notification.extras);
return builder;
}
} catch (Exception e) {
Log.e(TAG_UNITY, "Failed to recover builder for notification!", e);
} catch (OutOfMemoryError e) {
Log.e(TAG_UNITY, "Failed to recover builder for notification!", e);
}
return recoverBuilderCustom(context, notification);
}
private static Notification.Builder recoverBuilderCustom(Context context, Notification notification) {
String channelID = notification.extras.getString(KEY_CHANNEL_ID);
Notification.Builder builder = UnityNotificationManager.getNotificationManagerImpl(context).createNotificationBuilder(channelID);
UnityNotificationManager.setNotificationIcon(builder, KEY_SMALL_ICON, notification.extras.getString(KEY_SMALL_ICON));
String largeIcon = notification.extras.getString(KEY_LARGE_ICON);
if (largeIcon != null && !largeIcon.isEmpty())
UnityNotificationManager.setNotificationIcon(builder, KEY_LARGE_ICON, largeIcon);
builder.setContentTitle(notification.extras.getString(Notification.EXTRA_TITLE));
builder.setContentText(notification.extras.getString(Notification.EXTRA_TEXT));
builder.setAutoCancel(0 != (notification.flags & Notification.FLAG_AUTO_CANCEL));
if (notification.number >= 0)
builder.setNumber(notification.number);
String bigText = notification.extras.getString(Notification.EXTRA_BIG_TEXT);
if (bigText != null)
builder.setStyle(new Notification.BigTextStyle().bigText(bigText));
builder.setWhen(notification.when);
String group = notification.getGroup();
if (group != null && !group.isEmpty())
builder.setGroup(group);
builder.setGroupSummary(0 != (notification.flags & Notification.FLAG_GROUP_SUMMARY));
String sortKey = notification.getSortKey();
if (sortKey != null && !sortKey.isEmpty())
builder.setSortKey(sortKey);
builder.setShowWhen(notification.extras.getBoolean(Notification.EXTRA_SHOW_WHEN, false));
Integer color = UnityNotificationManager.getNotificationColor(notification);
if (color != null)
UnityNotificationManager.setNotificationColor(builder, color);
UnityNotificationManager.setNotificationUsesChronometer(builder, notification.extras.getBoolean(Notification.EXTRA_SHOW_CHRONOMETER, false));
UnityNotificationManager.setNotificationGroupAlertBehavior(builder, UnityNotificationManager.getNotificationGroupAlertBehavior(notification));
builder.getExtras().putInt(KEY_ID, notification.extras.getInt(KEY_ID, 0));
builder.getExtras().putLong(KEY_REPEAT_INTERVAL, notification.extras.getLong(KEY_REPEAT_INTERVAL, 0));
builder.getExtras().putLong(KEY_FIRE_TIME, notification.extras.getLong(KEY_FIRE_TIME, 0));
String intentData = notification.extras.getString(KEY_INTENT_DATA);
if (intentData != null && !intentData.isEmpty())
builder.getExtras().putString(KEY_INTENT_DATA, intentData);
return builder;
}
}
@@ -0,0 +1,16 @@
{
"name": "Unity.Notifications.Android",
"references": [],
"optionalUnityReferences": [],
"includePlatforms": [
"Android",
"Editor"
],
"excludePlatforms": [],
"allowUnsafeCode": false,
"overrideReferences": false,
"precompiledReferences": [],
"autoReferenced": true,
"defineConstraints": [],
"versionDefines": []
}
@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: ac145e6b8c6034cdbadc8c6e26aedbcf
AssemblyDefinitionImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant: