#if UNITY_ANDROID using System; using System.Collections.Generic; using System.IO; using System.Xml; using UnityEditor; using UnityEditor.Android; namespace Unity.Notifications { public class AndroidNotificationPostProcessor : IPostGenerateGradleAndroidProject { const string kAndroidNamespaceURI = "http://schemas.android.com/apk/res/android"; public int callbackOrder { get { return 0; } } public void OnPostGenerateGradleAndroidProject(string projectPath) { MinSdkCheck(); CopyNotificationResources(projectPath); InjectAndroidManifest(projectPath); } private void MinSdkCheck() { #if !UNITY_2021_2_OR_NEWER // API level 21 not supported since 2021.2, need to check for prior releases const AndroidSdkVersions kMinAndroidSdk = AndroidSdkVersions.AndroidApiLevel21; if (PlayerSettings.Android.minSdkVersion < AndroidSdkVersions.AndroidApiLevel21) throw new NotSupportedException(string.Format("Minimum Android API level supported by notifications package is {0}, your Player Settings have it set to {1}", (int)kMinAndroidSdk, PlayerSettings.Android.minSdkVersion)); #endif } private void CopyNotificationResources(string projectPath) { // The projectPath points to the the parent folder instead of the actual project path. if (!Directory.Exists(Path.Combine(projectPath, "src"))) { projectPath = Path.Combine(projectPath, PlayerSettings.productName); } // Get the icons set in the UnityNotificationEditorManager and write them to the res folder, then we can use the icons as res. var icons = NotificationSettingsManager.Initialize().GenerateDrawableResourcesForExport(); foreach (var icon in icons) { var fileInfo = new FileInfo(string.Format("{0}/src/main/res/{1}", projectPath, icon.Key)); if (fileInfo.Directory != null) { fileInfo.Directory.Create(); File.WriteAllBytes(fileInfo.FullName, icon.Value); } } } private void InjectAndroidManifest(string projectPath) { var manifestPath = string.Format("{0}/src/main/AndroidManifest.xml", projectPath); if (!File.Exists(manifestPath)) throw new FileNotFoundException(string.Format("'{0}' doesn't exist.", manifestPath)); XmlDocument manifestDoc = new XmlDocument(); manifestDoc.Load(manifestPath); InjectReceivers(manifestPath, manifestDoc); var settings = NotificationSettingsManager.Initialize().AndroidNotificationSettingsFlat; var useCustomActivity = GetSetting(settings, NotificationSettings.AndroidSettings.USE_CUSTOM_ACTIVITY); if (useCustomActivity) { var customActivity = GetSetting(settings, NotificationSettings.AndroidSettings.CUSTOM_ACTIVITY_CLASS); AppendAndroidMetadataField(manifestPath, manifestDoc, "custom_notification_android_activity", customActivity); } var enableRescheduleOnRestart = GetSetting(settings, NotificationSettings.AndroidSettings.RESCHEDULE_ON_RESTART); if (enableRescheduleOnRestart) { AppendAndroidMetadataField(manifestPath, manifestDoc, "reschedule_notifications_on_restart", "true"); AppendAndroidPermissionField(manifestPath, manifestDoc, "android.permission.RECEIVE_BOOT_COMPLETED"); } AppendAndroidPermissionField(manifestPath, manifestDoc, "android.permission.POST_NOTIFICATIONS"); var exactScheduling = GetSetting(settings, NotificationSettings.AndroidSettings.EXACT_ALARM); bool enableExact = (exactScheduling & AndroidExactSchedulingOption.ExactWhenAvailable) != 0; AppendAndroidMetadataField(manifestPath, manifestDoc, "com.unity.androidnotifications.exact_scheduling", enableExact ? "1" : "0"); if (enableExact) { bool scheduleExact = (exactScheduling & AndroidExactSchedulingOption.AddScheduleExactPermission) != 0; bool useExact = (exactScheduling & AndroidExactSchedulingOption.AddUseExactAlarmPermission) != 0; // as documented here: https://developer.android.com/reference/android/Manifest.permission#USE_EXACT_ALARM // only one of these two attributes should be used or max sdk set so on any device it's one or the other if (scheduleExact) AppendAndroidPermissionField(manifestPath, manifestDoc, "android.permission.SCHEDULE_EXACT_ALARM", useExact ? "32" : null); if (useExact) AppendAndroidPermissionField(manifestPath, manifestDoc, "android.permission.USE_EXACT_ALARM"); } manifestDoc.Save(manifestPath); } private static T GetSetting(List settings, string key) { return (T)settings.Find(i => i.Key == key).Value; } internal static void InjectReceivers(string manifestPath, XmlDocument manifestXmlDoc) { const string kNotificationManagerName = "com.unity.androidnotifications.UnityNotificationManager"; const string kNotificationRestartOnBootName = "com.unity.androidnotifications.UnityNotificationRestartOnBootReceiver"; var applicationXmlNode = manifestXmlDoc.SelectSingleNode("manifest/application"); if (applicationXmlNode == null) throw new ArgumentException(string.Format("Missing 'application' node in '{0}'.", manifestPath)); XmlElement notificationManagerReceiver = null; XmlElement notificationRestartOnBootReceiver = null; var receiverNodes = manifestXmlDoc.SelectNodes("manifest/application/receiver"); if (receiverNodes != null) { // Check existing receivers. foreach (XmlNode node in receiverNodes) { var element = node as XmlElement; if (element == null) continue; var elementName = element.GetAttribute("name", kAndroidNamespaceURI); if (elementName == kNotificationManagerName) notificationManagerReceiver = element; else if (elementName == kNotificationRestartOnBootName) notificationRestartOnBootReceiver = element; if (notificationManagerReceiver != null && notificationRestartOnBootReceiver != null) break; } } // Create notification manager receiver if necessary. if (notificationManagerReceiver == null) { notificationManagerReceiver = manifestXmlDoc.CreateElement("receiver"); notificationManagerReceiver.SetAttribute("name", kAndroidNamespaceURI, kNotificationManagerName); applicationXmlNode.AppendChild(notificationManagerReceiver); } notificationManagerReceiver.SetAttribute("exported", kAndroidNamespaceURI, "false"); // Create notification restart-on-boot receiver if necessary. if (notificationRestartOnBootReceiver == null) { notificationRestartOnBootReceiver = manifestXmlDoc.CreateElement("receiver"); notificationRestartOnBootReceiver.SetAttribute("name", kAndroidNamespaceURI, kNotificationRestartOnBootName); var intentFilterNode = manifestXmlDoc.CreateElement("intent-filter"); var actionNode = manifestXmlDoc.CreateElement("action"); actionNode.SetAttribute("name", kAndroidNamespaceURI, "android.intent.action.BOOT_COMPLETED"); intentFilterNode.AppendChild(actionNode); notificationRestartOnBootReceiver.AppendChild(intentFilterNode); applicationXmlNode.AppendChild(notificationRestartOnBootReceiver); } notificationRestartOnBootReceiver.SetAttribute("enabled", kAndroidNamespaceURI, "false"); notificationRestartOnBootReceiver.SetAttribute("exported", kAndroidNamespaceURI, "false"); } internal static void AppendAndroidPermissionField(string manifestPath, XmlDocument xmlDoc, string name, string maxSdk = null) { var manifestNode = xmlDoc.SelectSingleNode("manifest"); if (manifestNode == null) throw new ArgumentException(string.Format("Missing 'manifest' node in '{0}'.", manifestPath)); XmlElement metaDataNode = null; foreach (XmlNode node in manifestNode.ChildNodes) { if (!(node is XmlElement) || node.Name != "uses-permission") continue; var element = (XmlElement)node; var elementName = element.GetAttribute("name", kAndroidNamespaceURI); if (elementName == name) { if (maxSdk == null) return; var maxSdkAttr = element.GetAttribute("maxSdkVersion", kAndroidNamespaceURI); if (!string.IsNullOrEmpty(maxSdkAttr)) return; metaDataNode = element; } } if (metaDataNode == null) { metaDataNode = xmlDoc.CreateElement("uses-permission"); metaDataNode.SetAttribute("name", kAndroidNamespaceURI, name); } if (maxSdk != null) metaDataNode.SetAttribute("maxSdkVersion", kAndroidNamespaceURI, maxSdk); manifestNode.AppendChild(metaDataNode); } internal static void AppendAndroidMetadataField(string manifestPath, XmlDocument xmlDoc, string name, string value) { var applicationNode = xmlDoc.SelectSingleNode("manifest/application"); if (applicationNode == null) throw new ArgumentException(string.Format("Missing 'application' node in '{0}'.", manifestPath)); var nodes = xmlDoc.SelectNodes("manifest/application/meta-data"); if (nodes != null) { // Check if there is a 'meta-data' with the same name. foreach (XmlNode node in nodes) { var element = node as XmlElement; if (element == null) continue; var elementName = element.GetAttribute("name", kAndroidNamespaceURI); if (elementName == name) { element.SetAttribute("value", kAndroidNamespaceURI, value); return; } } } XmlElement metaDataNode = xmlDoc.CreateElement("meta-data"); metaDataNode.SetAttribute("name", kAndroidNamespaceURI, name); metaDataNode.SetAttribute("value", kAndroidNamespaceURI, value); applicationNode.AppendChild(metaDataNode); } } } #endif