using System; using System.Collections; using System.Collections.Generic; using UnityEngine; #if UNITY_ANDROID && !UNITY_EDITOR using Firebase.Extensions; using Firebase.Messaging; #endif /// /// Android FCM registration for the player client. The Editor and dedicated server builds do not talk to Firebase. /// public class PushNotificationService : MonoBehaviour { public const string BroadcastTopic = "kk_broadcast"; public const string ChannelId = "support"; public static PushNotificationService instance { get; private set; } static string _pendingTicketId; readonly List _mainThread = new List(); readonly object _mainLock = new object(); #if UNITY_ANDROID && !UNITY_EDITOR const string RegisteredFidKey = "WalkInvest.SupportPushFid"; const string RegisteredUserKey = "WalkInvest.SupportPushUserId"; const string PostNotificationsPermission = "android.permission.POST_NOTIFICATIONS"; const float UnregisterTimeoutSeconds = 5f; string _fid; string _lastRegisteredFid; int _lastRegisteredUserId; bool _sessionReady; bool _registerQueued; bool _registerInFlight; bool _topicSubscribed; bool _subscribeInFlight; bool _firebaseReady; Firebase.FirebaseApp _firebaseApp; #endif public static void EnsureCreated() { #if UNITY_ANDROID && !UNITY_EDITOR if (instance != null) return; var go = new GameObject(nameof(PushNotificationService)); go.AddComponent(); #endif } public static void OnGameSessionReady() { #if UNITY_ANDROID && !UNITY_EDITOR EnsureCreated(); if (instance == null) return; instance._sessionReady = true; instance.RequestRegister(); #endif } /// /// Deletes this install on the admin API, then runs . /// A failed or timed-out delete still continues. /// public static void UnregisterThen(Action next) { #if UNITY_ANDROID && !UNITY_EDITOR if (instance != null && instance.CanUnregister) { instance.StartCoroutine(instance.UnregisterAndContinue(next)); return; } if (instance != null) instance.ForgetLocalRegistration(); #endif next?.Invoke(); } public static string ConsumePendingTicket() { string ticketId = _pendingTicketId; _pendingTicketId = null; return ticketId; } void Awake() { if (instance != null && instance != this) { Destroy(gameObject); return; } instance = this; DontDestroyOnLoad(gameObject); #if UNITY_ANDROID && !UNITY_EDITOR _lastRegisteredFid = PlayerPrefs.GetString(RegisteredFidKey, ""); _lastRegisteredUserId = PlayerPrefs.GetInt(RegisteredUserKey, 0); InitializeFirebase(); #endif } void OnDestroy() { if (instance == this) instance = null; #if UNITY_ANDROID && !UNITY_EDITOR if (_firebaseReady) { FirebaseMessaging.RegistrationReceived -= OnRegistrationReceived; FirebaseMessaging.MessageReceived -= OnMessageReceived; } #endif } void Update() { Action[] batch = null; lock (_mainLock) { if (_mainThread.Count == 0) return; batch = _mainThread.ToArray(); _mainThread.Clear(); } for (int i = 0; i < batch.Length; i++) batch[i]?.Invoke(); } #if UNITY_ANDROID && !UNITY_EDITOR bool CanUnregister { get { return _sessionReady && !string.IsNullOrEmpty(_fid) && SupportClient.IsSignedIn; } } void InitializeFirebase() { Firebase.FirebaseApp.CheckAndFixDependenciesAsync().ContinueWithOnMainThread(task => { if (instance != this) return; if (task.IsFaulted || task.IsCanceled) { Logger.Log("Firebase dependency check failed."); return; } var status = task.Result; if (status != Firebase.DependencyStatus.Available) { Logger.Log("Firebase dependencies unavailable: " + status); return; } _firebaseApp = Firebase.FirebaseApp.DefaultInstance; if (_firebaseApp == null) { Logger.Log("Firebase app missing."); return; } _firebaseReady = true; EnsureNotificationChannel(); RequestNotificationPermission(); FirebaseMessaging.RegistrationReceived += OnRegistrationReceived; FirebaseMessaging.MessageReceived += OnMessageReceived; FirebaseMessaging.RegisterAsync().ContinueWithOnMainThread(register => { if (instance != this) return; if (register.IsFaulted) Logger.Log("FCM register failed: " + register.Exception.GetBaseException().Message); }); }); } void OnRegistrationReceived(object sender, RegistrationReceivedEventArgs args) { string fid = args != null ? args.InstallationId : null; RunOnMain(() => HandleRegistration(fid)); } void OnMessageReceived(object sender, MessageReceivedEventArgs args) { string type = null; string ticketId = null; if (args != null && args.Message != null && args.Message.Data != null) { args.Message.Data.TryGetValue("type", out type); args.Message.Data.TryGetValue("ticketId", out ticketId); } string typeCopy = type; string ticketCopy = ticketId; RunOnMain(() => HandleMessage(typeCopy, ticketCopy)); } void HandleRegistration(string fid) { if (string.IsNullOrEmpty(fid)) return; if (fid != _fid) { _topicSubscribed = false; _subscribeInFlight = false; } _fid = fid; if (!_topicSubscribed && !_subscribeInFlight) { _subscribeInFlight = true; StartCoroutine(SubscribeBroadcast()); } RequestRegister(); } void HandleMessage(string type, string ticketId) { if (!string.Equals(type, "support_reply", StringComparison.Ordinal)) return; if (string.IsNullOrEmpty(ticketId)) return; CustomerSupportCanvas canvas = CustomerSupportCanvas.instance; if (canvas != null && canvas.isActiveAndEnabled) canvas.OpenFromNotification(ticketId); else _pendingTicketId = ticketId; } IEnumerator SubscribeBroadcast() { var task = FirebaseMessaging.SubscribeAsync(BroadcastTopic); while (!task.IsCompleted) yield return null; _subscribeInFlight = false; if (task.IsFaulted || task.IsCanceled) { Logger.Log("FCM topic subscribe failed."); yield break; } _topicSubscribed = true; } void RequestRegister() { if (!_sessionReady || string.IsNullOrEmpty(_fid)) return; _registerQueued = true; if (!_registerInFlight) StartCoroutine(RegisterLoop()); } IEnumerator RegisterLoop() { _registerInFlight = true; while (_registerQueued && _sessionReady) { _registerQueued = false; yield return RegisterDeviceBody(); } _registerInFlight = false; } IEnumerator RegisterDeviceBody() { if (!_sessionReady || string.IsNullOrEmpty(_fid)) yield break; if (!SupportClient.IsSignedIn) { string username = LoginManager.SessionUsername; string password = LoginManager.SessionPassword; if (string.IsNullOrEmpty(username) || string.IsNullOrEmpty(password)) { Logger.Log("Push registration skipped: support session is not available."); yield break; } SupportCallResult login = null; yield return SupportClient.Login(username, password, r => login = r); if (!_sessionReady) yield break; if (login == null || !login.ok) { Logger.Log("Push registration skipped: support login failed."); yield break; } } int userId = SupportClient.UserId; if (_fid == _lastRegisteredFid && userId != 0 && userId == _lastRegisteredUserId) yield break; string fid = _fid; SupportCallResult result = null; yield return SupportClient.RegisterDevice(fid, "android", r => result = r); if (!_sessionReady || fid != _fid) yield break; if (result == null || !result.ok) { Logger.Log("Push registration failed: " + (result != null ? result.error : "no result")); yield break; } _lastRegisteredFid = fid; _lastRegisteredUserId = SupportClient.UserId; PlayerPrefs.SetString(RegisteredFidKey, _lastRegisteredFid ?? ""); PlayerPrefs.SetInt(RegisteredUserKey, _lastRegisteredUserId); PlayerPrefs.Save(); } IEnumerator UnregisterAndContinue(Action next) { _sessionReady = false; bool done = false; StartCoroutine(FinishUnregister(_fid, () => done = true)); float start = Time.unscaledTime; while (!done && Time.unscaledTime - start < UnregisterTimeoutSeconds) yield return null; if (!done) Logger.Log("Push unregister timed out."); ForgetLocalRegistration(); next?.Invoke(); } IEnumerator FinishUnregister(string fid, Action done) { SupportCallResult result = null; yield return SupportClient.UnregisterDevice(fid, r => result = r); if (result == null || !result.ok) Logger.Log("Push unregister failed: " + (result != null ? result.error : "no result")); done?.Invoke(); } void ForgetLocalRegistration() { _lastRegisteredFid = null; _lastRegisteredUserId = 0; PlayerPrefs.DeleteKey(RegisteredFidKey); PlayerPrefs.DeleteKey(RegisteredUserKey); PlayerPrefs.Save(); } void RunOnMain(Action action) { if (action == null) return; lock (_mainLock) _mainThread.Add(action); } static void RequestNotificationPermission() { try { using (var version = new AndroidJavaClass("android.os.Build$VERSION")) { if (version.GetStatic("SDK_INT") < 33) return; } if (!UnityEngine.Android.Permission.HasUserAuthorizedPermission(PostNotificationsPermission)) UnityEngine.Android.Permission.RequestUserPermission(PostNotificationsPermission); } catch (Exception e) { Logger.Log("Notification permission request failed: " + e.Message); } } static void EnsureNotificationChannel() { try { using (var version = new AndroidJavaClass("android.os.Build$VERSION")) { if (version.GetStatic("SDK_INT") < 26) return; } using (var unityPlayer = new AndroidJavaClass("com.unity3d.player.UnityPlayer")) using (var activity = unityPlayer.GetStatic("currentActivity")) using (var channel = new AndroidJavaObject("android.app.NotificationChannel", ChannelId, "Support", 4)) using (var manager = activity.Call("getSystemService", "notification")) { channel.Call("setDescription", "Support replies and announcements"); manager.Call("createNotificationChannel", channel); } } catch (Exception e) { Logger.Log("Notification channel setup failed: " + e.Message); } } #endif }