push notifications
This commit is contained in:
@@ -0,0 +1,405 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
#if UNITY_ANDROID && !UNITY_EDITOR
|
||||
using Firebase.Extensions;
|
||||
using Firebase.Messaging;
|
||||
#endif
|
||||
|
||||
/// <summary>
|
||||
/// Android FCM registration for the player client. The Editor and dedicated server builds do not talk to Firebase.
|
||||
/// </summary>
|
||||
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<Action> _mainThread = new List<Action>();
|
||||
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<PushNotificationService>();
|
||||
#endif
|
||||
}
|
||||
|
||||
public static void OnGameSessionReady()
|
||||
{
|
||||
#if UNITY_ANDROID && !UNITY_EDITOR
|
||||
EnsureCreated();
|
||||
if (instance == null)
|
||||
return;
|
||||
instance._sessionReady = true;
|
||||
instance.RequestRegister();
|
||||
#endif
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deletes this install on the admin API, then runs <paramref name="next"/>.
|
||||
/// A failed or timed-out delete still continues.
|
||||
/// </summary>
|
||||
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<SupportLoginResponse> 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<SupportDeviceResponse> 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<SupportDeviceResponse> 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<int>("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<int>("SDK_INT") < 26)
|
||||
return;
|
||||
}
|
||||
|
||||
using (var unityPlayer = new AndroidJavaClass("com.unity3d.player.UnityPlayer"))
|
||||
using (var activity = unityPlayer.GetStatic<AndroidJavaObject>("currentActivity"))
|
||||
using (var channel = new AndroidJavaObject("android.app.NotificationChannel", ChannelId, "Support", 4))
|
||||
using (var manager = activity.Call<AndroidJavaObject>("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
|
||||
}
|
||||
Reference in New Issue
Block a user