diff --git a/Runtime/Guru.Runtime.asmdef b/Runtime/Guru.Runtime.asmdef index 448fd11..8d63c03 100644 --- a/Runtime/Guru.Runtime.asmdef +++ b/Runtime/Guru.Runtime.asmdef @@ -17,7 +17,9 @@ "Google.Play.Review", "Google.Play.Common", "Guru.LitJson", - "Unity.Advertisement.IosSupport" + "Unity.Advertisement.IosSupport", + "Unity.Notifications.Android", + "Unity.Notifications.iOS" ], "includePlatforms": [], "excludePlatforms": [], diff --git a/Runtime/GuruNoification.meta b/Runtime/GuruNoification.meta new file mode 100644 index 0000000..6175479 --- /dev/null +++ b/Runtime/GuruNoification.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 1fef034d48ac449fb99531b40139954e +timeCreated: 1718844748 \ No newline at end of file diff --git a/Runtime/GuruNoification/Editor.meta b/Runtime/GuruNoification/Editor.meta new file mode 100644 index 0000000..fcf0d90 --- /dev/null +++ b/Runtime/GuruNoification/Editor.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 8c0dd2d4b63445c2828e05c10274d672 +timeCreated: 1718845536 \ No newline at end of file diff --git a/Runtime/GuruNoification/Editor/GuruNotification.Editor.asmdef b/Runtime/GuruNoification/Editor/GuruNotification.Editor.asmdef new file mode 100644 index 0000000..7b21d1c --- /dev/null +++ b/Runtime/GuruNoification/Editor/GuruNotification.Editor.asmdef @@ -0,0 +1,6 @@ +{ + "name": "GuruNotification.Editor", + "includePlatforms": [ + "Editor" + ] +} \ No newline at end of file diff --git a/Runtime/GuruNoification/Editor/GuruNotification.Editor.asmdef.meta b/Runtime/GuruNoification/Editor/GuruNotification.Editor.asmdef.meta new file mode 100644 index 0000000..6e9e119 --- /dev/null +++ b/Runtime/GuruNoification/Editor/GuruNotification.Editor.asmdef.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: e75025463d0c436482bf4c3dab674315 +timeCreated: 1718845553 \ No newline at end of file diff --git a/Runtime/GuruNoification/Manager.meta b/Runtime/GuruNoification/Manager.meta new file mode 100644 index 0000000..c1125a0 --- /dev/null +++ b/Runtime/GuruNoification/Manager.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: d6dd827f370441718ca2e49f3f603e4e +timeCreated: 1718845525 \ No newline at end of file diff --git a/Runtime/GuruNoification/Manager/INotificationAgent.cs b/Runtime/GuruNoification/Manager/INotificationAgent.cs new file mode 100644 index 0000000..3f5680c --- /dev/null +++ b/Runtime/GuruNoification/Manager/INotificationAgent.cs @@ -0,0 +1,15 @@ +namespace Guru.Notification +{ + using System; + public interface INotificationAgent + { + void Init(); + + string GetStatus(); + + bool IsAllowed(); + + void RequestPermission(Action callback = null); + + } +} \ No newline at end of file diff --git a/Runtime/GuruNoification/Manager/INotificationAgent.cs.meta b/Runtime/GuruNoification/Manager/INotificationAgent.cs.meta new file mode 100644 index 0000000..ed165bd --- /dev/null +++ b/Runtime/GuruNoification/Manager/INotificationAgent.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 43e051a65e9b469b8b9e8a9f6b4be944 +timeCreated: 1718844775 \ No newline at end of file diff --git a/Runtime/GuruNoification/Manager/NotificationAgentAndroid.cs b/Runtime/GuruNoification/Manager/NotificationAgentAndroid.cs new file mode 100644 index 0000000..1e1a087 --- /dev/null +++ b/Runtime/GuruNoification/Manager/NotificationAgentAndroid.cs @@ -0,0 +1,247 @@ +namespace Guru.Notification +{ + using System; + using UnityEngine; +#if UNITY_ANDROID + using UnityEngine.Android; + using Unity.Notifications.Android; +#endif + + public class NotificationAgentAndroid : INotificationAgent + { + public const string FCM_DEFAULT_CHANNEL_ID = "fcm_default_channel"; + private const string STATUS_GRANTED = "granted"; + private const string STATUS_DENIDED = "denied"; + private const int REQUEST_PERMISSION_SDK_VERSION = 33; + private const string PERMISSION_POST_NOTIFICATION = "android.permission.POST_NOTIFICATIONS"; + + private bool _initOnce = false; + private string _notiStatus; + private bool _isPluginReady = false; + + /// + /// 初始化 + /// + public void Init() + { + if (!_initOnce) return; + + SetGrantStatus(false); +#if UNITY_ANDROID + InitPlugins(); +#endif + } + + /// + /// 获取状态 + /// + /// + public string GetStatus() + { + if (!_initOnce) Init(); +#if UNITY_ANDROID + UpdateNotiStatus(); +#endif + return _notiStatus; + } + + /// + /// 设置授权状态 + /// + /// + private void SetGrantStatus(bool flag = false) + { + _notiStatus = flag ? STATUS_GRANTED : STATUS_DENIDED; + } + + + public bool IsAllowed() + { + return _notiStatus == STATUS_GRANTED; + } + + public void RequestPermission(Action callback = null) + { +#if UNITY_ANDROID + RequestAndroidPermission(callback); +#endif + } + + // -------------------- Android 获取状态逻辑 -------------------- + +#if UNITY_ANDROID + + private PermissionStatus _permissionStatus; + + private void TryExecute(Action handler) + { + try + { + handler?.Invoke(); + } + catch (Exception ex) + { + Debug.LogError(ex); + } + } + + + /// + /// 初始化插件 + /// + private void InitPlugins() + { + _isPluginReady = AndroidNotificationCenter.Initialize(); + + if (!_isPluginReady) + { + Debug.LogError($"[Noti][AND] --- AndroidNotificationCenter init failed!"); + // TODO: 处理初始化失败的情况 + return; + } + + UpdateNotiStatus(); + } + + /// + /// 更新 Notification 状态码 + /// + private void UpdateNotiStatus() + { + if (!_isPluginReady) return; + + TryExecute(() => + { + _permissionStatus = AndroidNotificationCenter.UserPermissionToPost; + switch (_permissionStatus) + { + case PermissionStatus.Allowed: + SetGrantStatus(true); + break; + default: + _notiStatus = STATUS_DENIDED; + break; + } + }); + } + + + private Action _onPermissionCallback; + private PermissionCallbacks _permissionCallbacks; + private void RequestAndroidPermission(Action callback = null) + { + if (!_isPluginReady) return; + + _onPermissionCallback = callback; + UpdateNotiStatus(); + + TryExecute(() => + { + bool hasPerm = Permission.HasUserAuthorizedPermission(PERMISSION_POST_NOTIFICATION); + var sdkInt = GetAndroidSDKVersion(); + if (sdkInt < REQUEST_PERMISSION_SDK_VERSION) + { + // 低版本处理方式 + if (_notiStatus == STATUS_GRANTED) + { + SetGrantStatus(true); + // 已经允许了 + callback?.Invoke(_notiStatus); + + } + else + { + if (_permissionStatus == PermissionStatus.NotRequested || + AndroidNotificationCenter.ShouldShowPermissionToPostRationale) + { + AndroidNotificationCenter.OpenNotificationSettings(FCM_DEFAULT_CHANNEL_ID); // 打开ChannelID + } + } + + } + else if (hasPerm) + { + SetGrantStatus(true); + // 已经允许了 + callback?.Invoke(_notiStatus); + } + else + { + // 未允许 + // 则请求弹窗 + Permission.RequestUserPermission(PERMISSION_POST_NOTIFICATION, SetupPermissionCallbacks()); + } + }); + } + + private PermissionCallbacks SetupPermissionCallbacks() + { + if(_permissionCallbacks != null) DisposePermissionCallbacks(); + _permissionCallbacks = new PermissionCallbacks(); + _permissionCallbacks.PermissionDenied += OnPermissionGranted; + _permissionCallbacks.PermissionDeniedAndDontAskAgain += OnPermissionGranted; + _permissionCallbacks.PermissionGranted += OnPermissionGranted; + return _permissionCallbacks; + } + + private void DisposePermissionCallbacks() + { + if (_permissionCallbacks != null) + { + _permissionCallbacks.PermissionGranted -= OnPermissionGranted; + _permissionCallbacks.PermissionDenied -= PermissionDenied; + _permissionCallbacks.PermissionDeniedAndDontAskAgain -= PermissionDenied; + _permissionCallbacks = null; + } + } + + /// + /// 请求通过 + /// + /// + private void OnPermissionGranted(string permissionName) + { + if (permissionName == PERMISSION_POST_NOTIFICATION) + { + _notiStatus = STATUS_GRANTED; + _onPermissionCallback?.Invoke(_notiStatus); + } + + DisposePermissionCallbacks(); + } + + /// + /// 请求拒绝 + /// + /// + private void PermissionDenied(string permissionName) + { + if (permissionName == PERMISSION_POST_NOTIFICATION) + { + _notiStatus = STATUS_DENIDED; + _onPermissionCallback?.Invoke(_notiStatus); + } + + DisposePermissionCallbacks(); + } + + private int GetAndroidSDKVersion() + { + int sdkInt = 999; + TryExecute(() => + { + using (AndroidJavaClass jc = new AndroidJavaClass("android.os.Build$VERSION")) + { + sdkInt = jc.GetStatic("SDK_INT"); + Debug.LogWarning($"[SDK] --- Android SDK Version:{sdkInt}"); + } + }); + return sdkInt; + } + + +#endif + + + } +} \ No newline at end of file diff --git a/Runtime/GuruNoification/Manager/NotificationAgentAndroid.cs.meta b/Runtime/GuruNoification/Manager/NotificationAgentAndroid.cs.meta new file mode 100644 index 0000000..686f47b --- /dev/null +++ b/Runtime/GuruNoification/Manager/NotificationAgentAndroid.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 554fcea56ce74a80a2424e5c037ce6c0 +timeCreated: 1718844764 \ No newline at end of file diff --git a/Runtime/GuruNoification/Manager/NotificationAgentIOS.cs b/Runtime/GuruNoification/Manager/NotificationAgentIOS.cs new file mode 100644 index 0000000..0f6d928 --- /dev/null +++ b/Runtime/GuruNoification/Manager/NotificationAgentIOS.cs @@ -0,0 +1,129 @@ + + +namespace Guru.Notification +{ + using System; + using UnityEngine; +#if UNITY_IOS + using System.Threading.Tasks; + using Unity.Notifications.iOS; +#endif + + public class NotificationAgentIOS : INotificationAgent + { + + private const string STATUS_GRANTED = "granted"; + private const string STATUS_DENIDED = "denied"; + private const string STATUS_PROVISIONAL = "provisional"; + private const string STATUS_NOT_DETERMINED = "not_determined"; + + private static bool _initOnce; + private static int _waitSeconds = 30; + + + private string _notiStatus; + + public void Init() + { + if (_initOnce) return; + _initOnce = true; + + _notiStatus = STATUS_NOT_DETERMINED; +#if UNITY_IOS + InitPlugins(); +#endif + } + + public string GetStatus() + { + if (!_initOnce) Init(); +#if UNITY_IOS + UpdateStatus(); +#endif + return _notiStatus; + } + + public bool IsAllowed() + { + return _notiStatus == STATUS_GRANTED; + } + + public void RequestPermission(Action callback = null) + { + if (!_initOnce) Init(); +#if UNITY_IOS + RequestIOSPermission(callback); +#endif + } + + +#if UNITY_IOS + + private void InitPlugins() + { + UpdateStatus(); + + } + + + /// + /// 更新状态 + /// + private void UpdateStatus() + { + var status = iOSNotificationCenter.GetNotificationSettings().AuthorizationStatus; + switch (status) + { + case AuthorizationStatus.Authorized: + _notiStatus = STATUS_GRANTED; + break; + case AuthorizationStatus.Denied: + _notiStatus = STATUS_DENIDED; + break; + case AuthorizationStatus.NotDetermined: + _notiStatus = STATUS_NOT_DETERMINED; + break; + case AuthorizationStatus.Provisional: + _notiStatus = STATUS_PROVISIONAL; + break; + default: + Debug.Log($"[SDK][Noti][iOS] --- Unmarked AuthorizationStatus: {status}"); + break; + } + + } + + /// + /// 请求 IOS 的推送 + /// + /// + private async void RequestIOSPermission(Action callback = null) + { + Debug.Log($"[SDK][Noti][iOS] --- RequestIOSPermission start"); + int timePassed = 0; + using (var req = new AuthorizationRequest(AuthorizationOption.Alert | AuthorizationOption.Badge, true)) + { + while (!req.IsFinished && timePassed < _waitSeconds) + { + timePassed++; + await Task.Delay(1000); + }; + + if (timePassed >= _waitSeconds) + { + Debug.LogWarning($"[SDK][Noti][iOS] --- RequestIOSPermission timeout"); + } + + UpdateStatus(); + callback?.Invoke(_notiStatus); + Debug.Log($"[SDK][Noti][iOS] --- User Selected: {_notiStatus}"); + } + } + +#endif + + + + + } +} \ No newline at end of file diff --git a/Runtime/GuruNoification/Manager/NotificationAgentIOS.cs.meta b/Runtime/GuruNoification/Manager/NotificationAgentIOS.cs.meta new file mode 100644 index 0000000..a2b3121 --- /dev/null +++ b/Runtime/GuruNoification/Manager/NotificationAgentIOS.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 5068c6e238e74251955320761c96182b +timeCreated: 1718871877 \ No newline at end of file diff --git a/Runtime/GuruNoification/Manager/NotificationAgentStub.cs b/Runtime/GuruNoification/Manager/NotificationAgentStub.cs new file mode 100644 index 0000000..c472584 --- /dev/null +++ b/Runtime/GuruNoification/Manager/NotificationAgentStub.cs @@ -0,0 +1,58 @@ + + +namespace Guru.Notification +{ + + using System; + using System.Threading.Tasks; + using UnityEngine; + + /// + /// For Editor to use Notifications + /// + public class NotificationAgentStub: INotificationAgent + { + private const string STATUS_GRANTED = "granted"; + private const string STATUS_DENIDED = "denied"; + private const string STATUS_NOT_DETERMINED = "not_determined"; + + private Action _onPermissionCallback; + private float _delaySeconds = 1.0f; + + private string EditorGrantedStatus + { + get => PlayerPrefs.GetString(nameof(EditorGrantedStatus), STATUS_NOT_DETERMINED); + set => PlayerPrefs.SetString(nameof(EditorGrantedStatus), value); + } + + public void Init() + { + Debug.Log($"[SDK][Noti][EDT] --- NotificationAgentStub Init: {EditorGrantedStatus}"); + } + + public string GetStatus() => EditorGrantedStatus; + + public bool IsAllowed() + { + return EditorGrantedStatus == STATUS_GRANTED; + } + + public void RequestPermission(Action callback = null) + { + Debug.Log($"[SDK][Noti][EDT] --- RequestPermission ---"); + _onPermissionCallback = callback; + DelayCallPermissionHandle(); + } + + /// + /// 延迟模拟回调 + /// + private async void DelayCallPermissionHandle() + { + await Task.Delay((int)(1000 * _delaySeconds)); + EditorGrantedStatus = STATUS_GRANTED; + _onPermissionCallback?.Invoke(EditorGrantedStatus); + } + + } +} \ No newline at end of file diff --git a/Runtime/GuruNoification/Manager/NotificationAgentStub.cs.meta b/Runtime/GuruNoification/Manager/NotificationAgentStub.cs.meta new file mode 100644 index 0000000..1dcab22 --- /dev/null +++ b/Runtime/GuruNoification/Manager/NotificationAgentStub.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 6550e85d25634c46b7a57c490dcea173 +timeCreated: 1718850440 \ No newline at end of file diff --git a/Runtime/GuruNoification/Manager/NotificationService.cs b/Runtime/GuruNoification/Manager/NotificationService.cs new file mode 100644 index 0000000..52d1e45 --- /dev/null +++ b/Runtime/GuruNoification/Manager/NotificationService.cs @@ -0,0 +1,98 @@ +namespace Guru.Notification +{ + using UnityEngine; + using System; + + + + /// + /// 消息管理器 + /// + public class NotificationService + { + // 服务版本号 + public const string Version = "0.0.1"; + + + // 初始化标志位 + private static bool _initOnce; + + private static string DEFAULT_USER_STATUS = "not_determined"; + + + #region 初始化 + + private static INotificationAgent _agent; + + internal static INotificationAgent Agent + { + get + { + if (_agent == null) + { + _agent = GetAgent(); + } + return _agent; + } + } + + private static INotificationAgent GetAgent() + { +#if UNITY_EDITOR + return new NotificationAgentStub(); +#elif UNITY_ANDROID + return new NotificationAgentAndroid(); +#elif UNITY_IOS + return new NotificationAgentIOS(); +#endif + return null; + } + + /// + /// 初始化 + /// + public static void Initialize() + { + if (_initOnce) return; + _initOnce = true; + Agent?.Init(); // 初始化代理 + } + + + + #endregion + + + + #region 接口 + + /// + /// 拉起 Noti 请求 + /// + /// + public static void RequestPermission(Action callback = null) + { + if (Agent != null) + { + Agent.RequestPermission(callback); + return; + } + callback?.Invoke(DEFAULT_USER_STATUS); + } + + public static bool IsPermissionGranted() + { + return Agent?.IsAllowed() ?? false; + } + + public static string GetStatus() + { + return Agent?.GetStatus() ?? DEFAULT_USER_STATUS; + } + + #endregion + + + + } +} \ No newline at end of file diff --git a/Runtime/GuruNoification/Manager/NotificationService.cs.meta b/Runtime/GuruNoification/Manager/NotificationService.cs.meta new file mode 100644 index 0000000..04a52c3 --- /dev/null +++ b/Runtime/GuruNoification/Manager/NotificationService.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: e0f31750e8bc48a6a5e4f56ee2d5e1cc +timeCreated: 1718846076 \ No newline at end of file diff --git a/package.json b/package.json index 38e393d..41524dd 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ "dependencies": { "com.unity.ads.ios-support": "1.2.0", "com.unity.editorcoroutines": "1.0.0", + "com.unity.mobile.notifications": "2.2.2", "com.unity.mobile.android-logcat": "1.3.2", "com.unity.purchasing": "4.10.0" }