406 lines
12 KiB
C#
406 lines
12 KiB
C#
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
|
|
}
|