(61d00a474) v0.9.7.1

This commit is contained in:
Regalis
2020-03-04 13:04:10 +01:00
parent 3c50efa5c9
commit 3c09ebe02f
5086 changed files with 786063 additions and 295871 deletions
@@ -0,0 +1,514 @@
using System;
#if UNITY
using UnityEngine;
using GameAnalyticsSDK.Net.Store;
#endif
using System.IO;
using System.Text.RegularExpressions;
using GameAnalyticsSDK.Net.Logging;
#if WINDOWS_UWP || WINDOWS_WSA
using Windows.System.Profile;
using Windows.Security.ExchangeActiveSyncProvisioning;
using Windows.Storage;
using Windows.System.UserProfile;
using Windows.Foundation.Metadata;
using Windows.Storage.Streams;
#endif
namespace GameAnalyticsSDK.Net.Device
{
internal static class GADevice
{
#if WINDOWS_UWP
private const string _sdkWrapperVersion = "uwp 2.1.7";
#elif WINDOWS_WSA
private const string _sdkWrapperVersion = "wsa 2.1.7";
#else
private const string _sdkWrapperVersion = "mono 2.1.7";
#endif
#if UNITY
private static readonly string _buildPlatform = UnityRuntimePlatformToString(Application.platform);
private static readonly string _deviceModel = SystemInfo.deviceType.ToString().ToLowerInvariant();
private static string _writablepath = GAStore.InMemory ? "" : GetPersistentPath();
#else
private static readonly string _buildPlatform = RuntimePlatformToString();
#if WINDOWS_UWP || WINDOWS_WSA
private static readonly string _deviceModel = GetDeviceModel();
private static readonly string _advertisingId = AdvertisingManager.AdvertisingId;
private static string _deviceId = GetDeviceId();
#else
private static readonly string _deviceModel = "unknown";
#endif
private static string _writablepath = GetPersistentPath();
#endif
private static readonly string _osVersion = GetOSVersionString();
#if WINDOWS_UWP || WINDOWS_WSA
private static readonly string _deviceManufacturer = GetDeviceManufacturer();
#else
private static readonly string _deviceManufacturer = "unknown";
#endif
public static void Touch()
{
}
public static string SdkGameEngineVersion
{
private get;
set;
}
public static string GameEngineVersion
{
get;
set;
}
public static string ConnectionType
{
get;
set;
}
public static string RelevantSdkVersion
{
get
{
if(!string.IsNullOrEmpty(SdkGameEngineVersion))
{
return SdkGameEngineVersion;
}
return _sdkWrapperVersion;
}
}
public static string BuildPlatform
{
get
{
return _buildPlatform;
}
}
public static string OSVersion
{
get
{
return _osVersion;
}
}
public static string DeviceModel
{
get
{
return _deviceModel;
}
}
public static string DeviceManufacturer
{
get
{
return _deviceManufacturer;
}
}
public static string WritablePath
{
get
{
return _writablepath;
}
}
#if WINDOWS_UWP
public static string AdvertisingId
{
get
{
return _advertisingId;
}
}
public static string DeviceId
{
get
{
return _deviceId;
}
}
#endif
#if UNITY
public static void UpdateConnectionType()
{
switch(Application.internetReachability)
{
case NetworkReachability.ReachableViaCarrierDataNetwork:
{
ConnectionType = "wwan";
}
break;
case NetworkReachability.ReachableViaLocalAreaNetwork:
{
ConnectionType = "lan";
}
break;
default:
{
ConnectionType = "offline";
}
break;
}
}
private static string GetOSVersionString()
{
string osVersion = SystemInfo.operatingSystem;
GALogger.D("GetOSVersionString: " + osVersion);
// Capture and process OS version information
// For Windows
Match regexResult = Regex.Match(osVersion, @"Windows.*?\((\d{0,5}\.\d{0,5}\.(\d{0,5}))\)");
if(regexResult.Success)
{
string versionNumberString = regexResult.Groups[1].Value;
string buildNumberString = regexResult.Groups[2].Value;
// Fix a bug in older versions of Unity where Windows 10 isn't recognised properly
int buildNumber = 0;
Int32.TryParse(buildNumberString, out buildNumber);
if(buildNumber > 10000)
{
versionNumberString = "10.0." + buildNumberString;
}
return "windows " + versionNumberString;
}
// For OS X
regexResult = Regex.Match(osVersion, @"Mac OS X (\d{0,5}\.\d{0,5}\.\d{0,5})");
if(regexResult.Success)
{
return "mac_osx " + regexResult.Captures[0].Value.Replace("Mac OS X ", "");
}
regexResult = Regex.Match(osVersion, @"Mac OS X (\d{0,5}_\d{0,5}_\d{0,5})");
if(regexResult.Success)
{
return "mac_osx " + regexResult.Captures[0].Value.Replace("Mac OS X ", "").Replace("_", ".");
}
// Not supporting other OS yet. The default version string won't be accepted by GameAnalytics
return UnityRuntimePlatformToString(Application.platform) + " 0.0.0";
}
private static string GetPersistentPath()
{
string result = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData) + Path.DirectorySeparatorChar + "GameAnalytics" + Path.DirectorySeparatorChar + System.AppDomain.CurrentDomain.FriendlyName;
if (!Directory.Exists(result))
{
Directory.CreateDirectory(result);
}
return result;
}
private static string UnityRuntimePlatformToString(RuntimePlatform platform)
{
switch(platform)
{
case RuntimePlatform.LinuxPlayer:
{
return "linux";
}
case RuntimePlatform.OSXPlayer:
case RuntimePlatform.OSXDashboardPlayer:
{
return "mac_osx";
}
case RuntimePlatform.PS3:
{
return "ps3";
}
case RuntimePlatform.PS4:
{
return "ps4";
}
case RuntimePlatform.PSP2:
{
return "vita";
}
case RuntimePlatform.WindowsPlayer:
{
return "windows";
}
#if UNITY_5
case RuntimePlatform.PSM:
{
return "psm";
}
case RuntimePlatform.WiiU:
{
return "wiiu";
}
case RuntimePlatform.WebGLPlayer:
{
return "webgl";
}
case RuntimePlatform.WSAPlayerARM:
case RuntimePlatform.WSAPlayerX64:
case RuntimePlatform.WSAPlayerX86:
{
switch(SystemInfo.deviceType)
{
case DeviceType.Desktop:
{
return "uwp_desktop";
}
case DeviceType.Handheld:
{
return "uwp_mobile";
}
case DeviceType.Console:
{
return "uwp_console";
}
default:
{
return "uwp_desktop";
}
}
}
#endif
case RuntimePlatform.WP8Player:
{
return "windows_phone";
}
case RuntimePlatform.XBOX360:
{
return "xbox360";
}
case RuntimePlatform.XboxOne:
{
return "xboxone";
}
case RuntimePlatform.TizenPlayer:
{
return "tizen";
}
case RuntimePlatform.SamsungTVPlayer:
{
return "samsung_tv";
}
default:
{
return "unknown";
}
}
}
#else
public static void UpdateConnectionType()
{
ConnectionType = "lan";
}
private static string GetOSVersionString()
{
#if WINDOWS_UWP
string deviceFamilyVersion = AnalyticsInfo.VersionInfo.DeviceFamilyVersion;
ulong version = ulong.Parse(deviceFamilyVersion);
ulong major = (version & 0xFFFF000000000000L) >> 48;
ulong minor = (version & 0x0000FFFF00000000L) >> 32;
ulong build = (version & 0x00000000FFFF0000L) >> 16;
return BuildPlatform + string.Format(" {0}.{1}.{2}", major, minor, build);
#elif WINDOWS_WSA
// Always 8.1 on Universal Windows 8.1
return BuildPlatform + " 8";
#else
Version v = Environment.OSVersion.Version;
return BuildPlatform + string.Format(" {0}.{1}.{2}", v.Major, v.Minor, v.Build);
#endif
}
#if WINDOWS_UWP || WINDOWS_WSA
private static string GetDeviceId()
{
string result = "";
#if WINDOWS_UWP
if(ApiInformation.IsTypePresent("Windows.System.Profile.HardwareIdentification"))
#endif
{
var token = HardwareIdentification.GetPackageSpecificToken(null);
var hardwareId = token.Id;
var dataReader = DataReader.FromBuffer(hardwareId);
byte[] bytes = new byte[hardwareId.Length];
dataReader.ReadBytes(bytes);
result = BitConverter.ToString(bytes).Replace("-", "");
}
return result;
}
private static string GetDeviceManufacturer()
{
EasClientDeviceInformation eas = new EasClientDeviceInformation();
return eas.SystemManufacturer;
}
private static string GetDeviceModel()
{
EasClientDeviceInformation eas = new EasClientDeviceInformation();
return eas.SystemProductName;
}
#if WINDOWS_UWP
private static string RuntimePlatformToString()
{
switch(AnalyticsInfo.VersionInfo.DeviceFamily)
{
case "Windows.Mobile":
{
return "uwp_mobile";
}
case "Windows.Desktop":
{
return "uwp_desktop";
}
case "Windows.Universal":
{
return "uwp_iot";
}
case "Windows.Xbox":
{
return "uwp_console";
}
case "Windows.Team":
{
return "uwp_surfacehub";
}
case "Windows.Holographic":
{
return "uwp_holographic";
}
default:
{
return AnalyticsInfo.VersionInfo.DeviceFamily;
}
}
}
#else
private static string RuntimePlatformToString()
{
EasClientDeviceInformation eas = new EasClientDeviceInformation();
switch(eas.OperatingSystem.ToUpperInvariant())
{
case "WINDOWSPHONE":
{
return "windows_phone";
}
case "WINDOWS":
{
return "windows";
}
default:
{
return eas.OperatingSystem;
}
}
}
#endif
#else
private static string RuntimePlatformToString()
{
switch(Environment.OSVersion.Platform)
{
case PlatformID.Unix:
{
// Well, there are chances MacOSX is reported as Unix instead of MacOSX.
// Instead of platform check, we'll do a feature checks (Mac specific root folders)
if(Directory.Exists("/Applications") && Directory.Exists("/System") && Directory.Exists("/Users") && Directory.Exists("/Volumes"))
{
return "mac_osx";
}
else
{
return "linux";
}
}
case PlatformID.MacOSX:
{
return "mac_osx";
}
case PlatformID.Win32NT:
case PlatformID.Win32S:
case PlatformID.Win32Windows:
case PlatformID.WinCE:
{
return "windows";
}
case PlatformID.Xbox:
{
return "xbox360";
}
default:
{
return "unknown";
}
}
}
#endif
private static string GetPersistentPath()
{
#if WINDOWS_UWP || WINDOWS_WSA
System.Threading.Tasks.Task<StorageFolder> gaFolderTask = System.Threading.Tasks.Task.Run<StorageFolder>(async() => await ApplicationData.Current.LocalFolder.CreateFolderAsync("GameAnalytics", CreationCollisionOption.OpenIfExists));
return gaFolderTask.GetAwaiter().GetResult().Path;
#else
string result = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData) + Path.DirectorySeparatorChar + "GameAnalytics" + Path.DirectorySeparatorChar + System.AppDomain.CurrentDomain.FriendlyName;
if(!Directory.Exists(result))
{
Directory.CreateDirectory(result);
}
return result;
#endif
}
#endif
}
}
@@ -0,0 +1,13 @@
namespace GameAnalyticsSDK.Net
{
public enum EGAErrorSeverity
{
Undefined = 0,
Debug = 1,
Info = 2,
Warning = 3,
Error = 4,
Critical = 5
}
}
@@ -0,0 +1,10 @@
namespace GameAnalyticsSDK.Net
{
public enum EGAGender
{
Undefined = 0,
Male = 1,
Female = 2
}
}
@@ -0,0 +1,11 @@
namespace GameAnalyticsSDK.Net
{
public enum EGALoggerMessageType
{
Error,
Warning,
Info,
Debug
}
}
@@ -0,0 +1,11 @@
namespace GameAnalyticsSDK.Net
{
public enum EGAProgressionStatus
{
Undefined = 0,
Start = 1,
Complete = 2,
Fail = 3
}
}
@@ -0,0 +1,10 @@
namespace GameAnalyticsSDK.Net
{
public enum EGAResourceFlowType
{
Undefined = 0,
Source = 1,
Sink = 2
}
}
@@ -0,0 +1,853 @@
using System;
using GameAnalyticsSDK.Net.Threading;
using GameAnalyticsSDK.Net.Logging;
using System.Collections.Generic;
using GameAnalyticsSDK.Net.Store;
using GameAnalyticsSDK.Net.Utilities;
using GameAnalyticsSDK.Net.Http;
using GameAnalyticsSDK.Net.State;
using GameAnalyticsSDK.Net.Validators;
using System.Globalization;
namespace GameAnalyticsSDK.Net.Events
{
internal class GAEvents
{
#region Fields and properties
private static readonly GAEvents _instance = new GAEvents();
private const string CategorySessionStart = "user";
private const string CategorySessionEnd = "session_end";
private const string CategoryDesign = "design";
private const string CategoryBusiness = "business";
private const string CategoryProgression = "progression";
private const string CategoryResource = "resource";
private const string CategoryError = "error";
private bool isRunning;
private bool keepRunning;
private const double ProcessEventsIntervalInSeconds = 8.0;
private const int MaxEventCount = 500;
private static GAEvents Instance
{
get
{
return _instance;
}
}
#endregion // Fields and properties
private GAEvents()
{
}
#region Public methods
public static void StopEventQueue()
{
Instance.keepRunning = false;
}
public static void EnsureEventQueueIsRunning()
{
Instance.keepRunning = true;
if(!Instance.isRunning)
{
Instance.isRunning = true;
GAThreading.ScheduleTimer(ProcessEventsIntervalInSeconds, "processEventQueue", ProcessEventQueue);
}
}
public static void AddSessionStartEvent()
{
if(!GAState.IsEventSubmissionEnabled)
{
return;
}
string categorySessionStart = CategorySessionStart;
// Event specific data
JSONObject eventDict = new JSONObject();
eventDict["category"] = categorySessionStart;
// Increment session number and persist
GAState.IncrementSessionNum();
GAStore.SetState(GAState.SessionNumKey, GAState.SessionNum.ToString(CultureInfo.InvariantCulture));
// Add custom dimensions
AddDimensionsToEvent(eventDict);
// Add to store
AddEventToStore(eventDict);
// Log
GALogger.I("Add SESSION START event");
// Send event right away
ProcessEvents(categorySessionStart, false);
}
public static void AddSessionEndEvent()
{
if(!GAState.IsEventSubmissionEnabled)
{
return;
}
long session_start_ts = GAState.SessionStart;
long client_ts_adjusted = GAState.GetClientTsAdjusted();
long sessionLength = client_ts_adjusted - session_start_ts;
if(sessionLength < 0)
{
// Should never happen.
// Could be because of edge cases regarding time altering on device.
GALogger.W("Session length was calculated to be less then 0. Should not be possible. Resetting to 0.");
sessionLength = 0;
}
// Event specific data
JSONObject eventDict = new JSONObject();
eventDict["category"] = CategorySessionEnd;
eventDict.Add("length", new JSONNumber(sessionLength));
// Add custom dimensions
AddDimensionsToEvent(eventDict);
// Add to store
AddEventToStore(eventDict);
// Log
GALogger.I("Add SESSION END event.");
// Send all event right away
ProcessEvents("", false);
}
public static void AddBusinessEvent(
string currency,
int amount,
string itemType,
string itemId,
string cartType,
IDictionary<string, object> fields
)
{
if(!GAState.IsEventSubmissionEnabled)
{
return;
}
// Validate event params
if (!GAValidator.ValidateBusinessEvent(currency, amount, cartType, itemType, itemId))
{
//GAHTTPApi.Instance.SendSdkErrorEvent(EGASdkErrorType.Rejected);
return;
}
// Create empty eventData
JSONObject eventDict = new JSONObject();
// Increment transaction number and persist
GAState.IncrementTransactionNum();
GAStore.SetState(GAState.TransactionNumKey, GAState.TransactionNum.ToString(CultureInfo.InvariantCulture));
// Required
eventDict["event_id"] = itemType + ":" + itemId;
eventDict["category"] = CategoryBusiness;
eventDict["currency"] = currency;
eventDict.Add("amount", new JSONNumber(amount));
eventDict.Add(GAState.TransactionNumKey, new JSONNumber(GAState.TransactionNum));
// Optional
if (!string.IsNullOrEmpty(cartType))
{
eventDict.Add("cart_type", cartType);
}
// Add custom dimensions
AddDimensionsToEvent(eventDict);
// Add custom fields
AddFieldsToEvent(eventDict, GAState.ValidateAndCleanCustomFields(fields));
// Log
GALogger.I("Add BUSINESS event: {currency:" + currency + ", amount:" + amount + ", itemType:" + itemType + ", itemId:" + itemId + ", cartType:" + cartType + "}");
// Send to store
AddEventToStore(eventDict);
}
public static void AddResourceEvent(EGAResourceFlowType flowType, string currency, double amount, string itemType, string itemId, IDictionary<string, object> fields)
{
if(!GAState.IsEventSubmissionEnabled)
{
return;
}
// Validate event params
if (!GAValidator.ValidateResourceEvent(flowType, currency, (long)amount, itemType, itemId))
{
//GAHTTPApi.Instance.SendSdkErrorEvent(EGASdkErrorType.Rejected);
return;
}
// If flow type is sink reverse amount
if (flowType == EGAResourceFlowType.Sink)
{
amount *= -1;
}
// Create empty eventData
JSONObject eventDict = new JSONObject();
// insert event specific values
string flowTypeString = ResourceFlowTypeToString(flowType);
eventDict["event_id"] = flowTypeString + ":" + currency + ":" + itemType + ":" + itemId;
eventDict["category"] = CategoryResource;
eventDict.Add("amount", new JSONNumber(amount));
// Add custom dimensions
AddDimensionsToEvent(eventDict);
// Add custom fields
AddFieldsToEvent(eventDict, GAState.ValidateAndCleanCustomFields(fields));
// Log
GALogger.I("Add RESOURCE event: {currency:" + currency + ", amount:" + amount + ", itemType:" + itemType + ", itemId:" + itemId + "}");
// Send to store
AddEventToStore(eventDict);
}
public static void AddProgressionEvent(EGAProgressionStatus progressionStatus, string progression01, string progression02, string progression03, double score, bool sendScore, IDictionary<string, object> fields)
{
if(!GAState.IsEventSubmissionEnabled)
{
return;
}
string progressionStatusString = ProgressionStatusToString(progressionStatus);
// Validate event params
if (!GAValidator.ValidateProgressionEvent(progressionStatus, progression01, progression02, progression03))
{
//GAHTTPApi.Instance.SendSdkErrorEvent(EGASdkErrorType.Rejected);
return;
}
// Create empty eventData
JSONObject eventDict = new JSONObject();
// Progression identifier
string progressionIdentifier;
if (string.IsNullOrEmpty(progression02))
{
progressionIdentifier = progression01;
}
else if (string.IsNullOrEmpty(progression03))
{
progressionIdentifier = progression01 + ":" + progression02;
}
else
{
progressionIdentifier = progression01 + ":" + progression02 + ":" + progression03;
}
// Append event specifics
eventDict["category"] = CategoryProgression;
eventDict["event_id"] = progressionStatusString + ":" + progressionIdentifier;
// Attempt
double attempt_num = 0;
// Add score if specified and status is not start
if (sendScore && progressionStatus != EGAProgressionStatus.Start)
{
eventDict.Add("score", new JSONNumber(score));
}
// Count attempts on each progression fail and persist
if (progressionStatus == EGAProgressionStatus.Fail)
{
// Increment attempt number
GAState.IncrementProgressionTries(progressionIdentifier);
}
// increment and add attempt_num on complete and delete persisted
if (progressionStatus == EGAProgressionStatus.Complete)
{
// Increment attempt number
GAState.IncrementProgressionTries(progressionIdentifier);
// Add to event
attempt_num = GAState.GetProgressionTries(progressionIdentifier);
eventDict.Add("attempt_num", new JSONNumber(attempt_num));
// Clear
GAState.ClearProgressionTries(progressionIdentifier);
}
// Add custom dimensions
AddDimensionsToEvent(eventDict);
// Add custom fields
AddFieldsToEvent(eventDict, GAState.ValidateAndCleanCustomFields(fields));
// Log
GALogger.I("Add PROGRESSION event: {status:" + progressionStatusString + ", progression01:" + progression01 + ", progression02:" + progression02 + ", progression03:" + progression03 + ", score:" + score + ", attempt:" + attempt_num + "}");
// Send to store
AddEventToStore(eventDict);
}
public static void AddDesignEvent(string eventId, double value, bool sendValue, IDictionary<string, object> fields)
{
if(!GAState.IsEventSubmissionEnabled)
{
return;
}
// Validate
if (!GAValidator.ValidateDesignEvent(eventId, value))
{
//GAHTTPApi.Instance.SendSdkErrorEvent(EGASdkErrorType.Rejected);
return;
}
// Create empty eventData
JSONObject eventData = new JSONObject();
// Append event specifics
eventData["category"] = CategoryDesign;
eventData["event_id"] = eventId;
if(sendValue)
{
eventData.Add("value", new JSONNumber(value));
}
// Add custom dimensions
AddDimensionsToEvent(eventData);
// Add custom fields
AddFieldsToEvent(eventData, GAState.ValidateAndCleanCustomFields(fields));
// Log
GALogger.I("Add DESIGN event: {eventId:" + eventId + ", value:" + value + "}");
// Send to store
AddEventToStore(eventData);
}
public static void AddErrorEvent(EGAErrorSeverity severity, string message, IDictionary<string, object> fields)
{
if(!GAState.IsEventSubmissionEnabled)
{
return;
}
string severityString = ErrorSeverityToString(severity);
// Validate
if (!GAValidator.ValidateErrorEvent(severity, message))
{
//GAHTTPApi.Instance.SendSdkErrorEvent(EGASdkErrorType.Rejected);
return;
}
// Create empty eventData
JSONObject eventData = new JSONObject();
// Append event specifics
eventData["category"] = CategoryError;
eventData["severity"] = severityString;
eventData["message"] = message;
// Add custom dimensions
AddDimensionsToEvent(eventData);
// Add custom fields
AddFieldsToEvent(eventData, GAState.ValidateAndCleanCustomFields(fields));
// Log
GALogger.I("Add ERROR event: {severity:" + severityString + ", message:" + message + "}");
// Send to store
AddEventToStore(eventData);
}
#endregion // Public methods
#region Private methods
private static void ProcessEventQueue()
{
ProcessEvents("", true);
if(Instance.keepRunning)
{
GAThreading.ScheduleTimer(ProcessEventsIntervalInSeconds, "processEventQueue", ProcessEventQueue);
}
else
{
Instance.isRunning = false;
}
}
#if WINDOWS_UWP || WINDOWS_WSA
private async static void ProcessEvents(string category, bool performCleanUp)
#else
private static void ProcessEvents(string category, bool performCleanUp)
#endif
{
if(!GAState.IsEventSubmissionEnabled)
{
return;
}
try
{
string requestIdentifier = Guid.NewGuid().ToString();
string selectSql;
string updateSql;
string deleteSql = "DELETE FROM ga_events WHERE status = '" + requestIdentifier + "'";
string putbackSql = "UPDATE ga_events SET status = 'new' WHERE status = '" + requestIdentifier + "';";
// Cleanup
if(performCleanUp)
{
CleanupEvents();
FixMissingSessionEndEvents();
}
// Prepare SQL
string andCategory = "";
if(!string.IsNullOrEmpty(category))
{
andCategory = " AND category='" + category + "' ";
}
selectSql = "SELECT event FROM ga_events WHERE status = 'new' " + andCategory + ";";
updateSql = "UPDATE ga_events SET status = '" + requestIdentifier + "' WHERE status = 'new' " + andCategory + ";";
// Get events to process
JSONArray events = GAStore.ExecuteQuerySync(selectSql);
// Check for errors or empty
if(events == null || events.Count == 0)
{
GALogger.I("Event queue: No events to send");
UpdateSessionTime();
return;
}
// Check number of events and take some action if there are too many?
if(events.Count > MaxEventCount)
{
// Make a limit request
selectSql = "SELECT client_ts FROM ga_events WHERE status = 'new' " + andCategory + " ORDER BY client_ts ASC LIMIT 0," + MaxEventCount + ";";
events = GAStore.ExecuteQuerySync(selectSql);
if(events == null)
{
return;
}
// Get last timestamp
JSONNode lastItem = events[events.Count - 1];
string lastTimestamp = lastItem["client_ts"].Value;
// Select again
selectSql = "SELECT event FROM ga_events WHERE status = 'new' " + andCategory + " AND client_ts<='" + lastTimestamp + "';";
events = GAStore.ExecuteQuerySync(selectSql);
if (events == null)
{
return;
}
// Update sql
updateSql = "UPDATE ga_events SET status='" + requestIdentifier + "' WHERE status='new' " + andCategory + " AND client_ts<='" + lastTimestamp + "';";
}
// Log
GALogger.I("Event queue: Sending " + events.Count + " events.");
// Set status of events to 'sending' (also check for error)
if (GAStore.ExecuteQuerySync(updateSql) == null)
{
return;
}
// Create payload data from events
List<JSONNode> payloadArray = new List<JSONNode>();
for(int i = 0; i < events.Count; ++i)
{
JSONNode ev = events[i];
JSONNode eventDict = null;
try
{
eventDict = JSONNode.LoadFromBinaryBase64(ev["event"].Value);
}
catch(Exception)
{
//GALogger.E("ProcessEvents: Error decoding json, " + e);
}
if (eventDict != null && eventDict.Count != 0)
{
payloadArray.Add(eventDict);
}
}
// send events
#if WINDOWS_UWP || WINDOWS_WSA
KeyValuePair<EGAHTTPApiResponse, JSONNode> result = await GAHTTPApi.Instance.SendEventsInArray(payloadArray);
#else
KeyValuePair<EGAHTTPApiResponse, JSONNode> result = GAHTTPApi.Instance.SendEventsInArray(payloadArray);
#endif
ProcessEvents(result.Key, result.Value, putbackSql, deleteSql, payloadArray.Count);
}
catch (Exception e)
{
GALogger.E("Error during ProcessEvents(): " + e);
}
}
public static void ProcessEvents(EGAHTTPApiResponse responseEnum, JSONNode dataDict, string putbackSql, string deleteSql, int eventCount)
{
if(responseEnum == EGAHTTPApiResponse.Ok)
{
// Delete events
GAStore.ExecuteQuerySync(deleteSql);
GALogger.I("Event queue: " + eventCount + " events sent.");
}
else
{
// Put events back (Only in case of no response)
if(responseEnum == EGAHTTPApiResponse.NoResponse)
{
GALogger.W("Event queue: Failed to send events to collector - Retrying next time");
GAStore.ExecuteQuerySync(putbackSql);
// Delete events (When getting some anwser back always assume events are processed)
}
else
{
if(dataDict != null)
{
JSONNode json = null;
IEnumerator<JSONNode> enumerator = dataDict.Children.GetEnumerator();
if(enumerator.MoveNext())
{
json = enumerator.Current;
}
if(responseEnum == EGAHTTPApiResponse.BadRequest && json is JSONArray)
{
GALogger.W("Event queue: " + eventCount + " events sent. " + dataDict.Count + " events failed GA server validation.");
}
else
{
GALogger.W("Event queue: Failed to send events.");
}
}
else
{
GALogger.W("Event queue: Failed to send events.");
}
GAStore.ExecuteQuerySync(deleteSql);
}
}
}
private static void CleanupEvents()
{
GAStore.ExecuteQuerySync("UPDATE ga_events SET status = 'new';");
}
private static void FixMissingSessionEndEvents()
{
if(!GAState.IsEventSubmissionEnabled)
{
return;
}
// Get all sessions that are not current
Dictionary<string, object> parameters = new Dictionary<string, object>();
parameters.Add("$session_id", GAState.SessionId);
string sql = "SELECT timestamp, event FROM ga_session WHERE session_id != $session_id;";
JSONArray sessions = GAStore.ExecuteQuerySync(sql, parameters);
if (sessions == null || sessions.Count == 0)
{
return;
}
GALogger.I(sessions.Count + " session(s) located with missing session_end event.");
// Add missing session_end events
for (int i = 0; i < sessions.Count; ++i)
{
JSONNode session = sessions[i];
JSONNode sessionEndEvent = null;
try
{
sessionEndEvent = JSONNode.LoadFromBinaryBase64(session["event"].Value);
}
catch(Exception)
{
//GALogger.E("FixMissingSessionEndEvents: Error decoding json, " + e);
}
if(sessionEndEvent != null)
{
long event_ts = sessionEndEvent["client_ts"].AsLong;
long start_ts = session["timestamp"].AsLong;
long length = event_ts - start_ts;
length = Math.Max(0, length);
GALogger.D("fixMissingSessionEndEvents length calculated: " + length);
sessionEndEvent["category"] = CategorySessionEnd;
sessionEndEvent.Add("length", new JSONNumber(length));
// Add to store
AddEventToStore(sessionEndEvent.AsObject);
}
else
{
GALogger.I("Problem decoding session_end event. Skipping this session_end event.");
}
}
}
#if WINDOWS_WSA
private async static void AddEventToStore(JSONObject eventData)
#else
private static void AddEventToStore(JSONObject eventData)
#endif
{
// Check if datastore is available
if (!GAStore.IsTableReady)
{
GALogger.W("Could not add event: SDK datastore error");
return;
}
// Check if we are initialized
if (!GAState.Initialized)
{
GALogger.W("Could not add event: SDK is not initialized");
return;
}
try
{
// Check db size limits (10mb)
// If database is too large block all except user, session and business
if (GAStore.IsDbTooLargeForEvents && !GAUtilities.StringMatch(eventData["category"].Value, "^(user|session_end|business)$"))
{
GALogger.W("Database too large. Event has been blocked.");
return;
}
// Get default annotations
JSONObject ev = GAState.GetEventAnnotations();
// Create json with only default annotations
string jsonDefaults = ev.SaveToBinaryBase64();
// Merge with eventData
foreach(KeyValuePair<string,JSONNode> pair in eventData)
{
ev.Add(pair.Key, pair.Value);
}
// Create json string representation
string json = ev.ToString();
// output if VERBOSE LOG enabled
GALogger.II("Event added to queue: " + json);
// Add to store
Dictionary<string, object> parameters = new Dictionary<string, object>();
parameters.Add("$status", "new");
parameters.Add("$category", ev["category"].Value);
parameters.Add("$session_id", ev["session_id"].Value);
parameters.Add("$client_ts", ev["client_ts"].Value);
parameters.Add("$event", ev.SaveToBinaryBase64());
string sql = "INSERT INTO ga_events (status, category, session_id, client_ts, event) VALUES($status, $category, $session_id, $client_ts, $event);";
GAStore.ExecuteQuerySync(sql, parameters);
// Add to session store if not last
if (eventData["category"].Value.Equals(CategorySessionEnd))
{
parameters.Clear();
parameters.Add("$session_id", ev["session_id"].Value);
sql = "DELETE FROM ga_session WHERE session_id = $session_id;";
GAStore.ExecuteQuerySync(sql, parameters);
}
else
{
sql = "INSERT OR REPLACE INTO ga_session(session_id, timestamp, event) VALUES($session_id, $timestamp, $event);";
parameters.Clear();
parameters.Add("$session_id", ev["session_id"].Value);
parameters.Add("$timestamp", GAState.SessionStart);
parameters.Add("$event", jsonDefaults);
GAStore.ExecuteQuerySync(sql, parameters);
}
}
catch (Exception e)
{
GALogger.E("addEventToStoreWithEventData: error using json");
GALogger.E(e.ToString());
}
}
private static void AddDimensionsToEvent(JSONObject eventData)
{
if (eventData == null)
{
return;
}
// add to dict (if not nil)
if (!string.IsNullOrEmpty(GAState.CurrentCustomDimension01))
{
eventData["custom_01"] = GAState.CurrentCustomDimension01;
}
if (!string.IsNullOrEmpty(GAState.CurrentCustomDimension02))
{
eventData["custom_02"] = GAState.CurrentCustomDimension02;
}
if (!string.IsNullOrEmpty(GAState.CurrentCustomDimension03))
{
eventData["custom_03"] = GAState.CurrentCustomDimension03;
}
}
private static void AddFieldsToEvent(JSONObject eventData, JSONObject fields)
{
if (eventData == null)
{
return;
}
if(fields != null && fields.Count > 0)
{
eventData["custom_fields"] = fields;
}
}
private static string ResourceFlowTypeToString(EGAResourceFlowType value)
{
switch(value)
{
case EGAResourceFlowType.Source:
{
return "Source";
}
case EGAResourceFlowType.Sink:
{
return "Sink";
}
default:
{
return "";
}
}
}
private static string ProgressionStatusToString(EGAProgressionStatus value)
{
switch(value)
{
case EGAProgressionStatus.Start:
{
return "Start";
}
case EGAProgressionStatus.Complete:
{
return "Complete";
}
case EGAProgressionStatus.Fail:
{
return "Fail";
}
default:
{
return "";
}
}
}
private static void UpdateSessionTime()
{
if(GAState.SessionIsStarted())
{
JSONObject ev = GAState.GetEventAnnotations();
string jsonDefaults = ev.SaveToBinaryBase64();
string sql = "INSERT OR REPLACE INTO ga_session(session_id, timestamp, event) VALUES($session_id, $timestamp, $event);";
Dictionary<string, object> parameters = new Dictionary<string, object>();
parameters.Add("$session_id", ev["session_id"].Value);
parameters.Add("$timestamp", GAState.SessionStart);
parameters.Add("$event", jsonDefaults);
GAStore.ExecuteQuerySync(sql, parameters);
}
}
private static string ErrorSeverityToString(EGAErrorSeverity value)
{
switch(value)
{
case EGAErrorSeverity.Debug:
{
return "debug";
}
case EGAErrorSeverity.Info:
{
return "info";
}
case EGAErrorSeverity.Warning:
{
return "warning";
}
case EGAErrorSeverity.Error:
{
return "error";
}
case EGAErrorSeverity.Critical:
{
return "critical";
}
default:
{
return "";
}
}
}
#endregion // Private methods
}
}
@@ -0,0 +1,407 @@
using System;
using System.Collections;
using System.Collections.Generic;
#if UNITY
using UnityEngine;
#endif
#if WINDOWS_WSA || WINDOWS_UWP
using Windows.System.Threading;
#else
using System.Threading;
#endif
namespace Foundation.Tasks
{
/// <summary>
/// Describes the Tasks State
/// </summary>
public enum TaskStatus
{
/// <summary>
/// Working
/// </summary>
Pending,
/// <summary>
/// Exception as thrown or otherwise stopped early
/// </summary>
Faulted,
/// <summary>
/// Complete without error
/// </summary>
Success,
}
/// <summary>
/// Execution strategy for the Task
/// </summary>
public enum TaskStrategy
{
/// <summary>
/// Dispatches the task to a background thread
/// </summary>
BackgroundThread,
/// <summary>
/// Dispatches the task to the main thread
/// </summary>
MainThread,
/// <summary>
/// Dispatches the task to the current thread
/// </summary>
CurrentThread,
/// <summary>
/// Runs the task as a coroutine
/// </summary>
Coroutine,
/// <summary>
/// Does nothing. For custom tasks.
/// </summary>
Custom,
}
/// <summary>
/// A task encapsulates future work that may be waited on.
/// - Support running actions in background threads
/// - Supports running coroutines with return results
/// - Use the WaitForRoutine method to wait for the task in a coroutine
/// </summary>
/// <example>
/// <code>
/// var task = Task.Run(() =>
/// {
/// //Debug.Log does not work in
/// Debug.Log("Sleeping...");
/// Task.Delay(2000);
/// Debug.Log("Slept");
/// });
/// // wait for it
/// yield return task;
///
/// // check exceptions
/// if(task.IsFaulted)
/// Debug.LogException(task.Exception)
///</code>
///</example>
public partial class AsyncTask :
#if UNITY_5
CustomYieldInstruction,
#endif
IDisposable
{
#region options
/// <summary>
/// Forces use of a single thread for debugging
/// </summary>
public static bool DisableMultiThread = false;
/// <summary>
/// Logs Exceptions
/// </summary>
public static bool LogErrors = false;
#endregion
#region properties
/// <summary>
/// Run execution path
/// </summary>
public TaskStrategy Strategy;
/// <summary>
/// Error
/// </summary>
public Exception Exception { get; set; }
/// <summary>
/// Run State
/// </summary>
public TaskStatus Status { get; set; }
#if UNITY_5
/// <summary>
/// Custom Yield
/// </summary>
public override bool keepWaiting
{
get { return !IsCompleted; }
}
#endif
public bool IsRunning
{
get { return Status == TaskStatus.Pending; }
}
public bool IsCompleted
{
get { return (Status == TaskStatus.Success || Status == TaskStatus.Faulted) && !HasContinuations; }
}
public bool IsFaulted
{
get { return Status == TaskStatus.Faulted; }
}
public bool IsSuccess
{
get { return Status == TaskStatus.Success; }
}
public bool HasContinuations { get; protected set; }
#endregion
#region private
protected TaskStatus _status;
protected Action _action;
protected IEnumerator _routine;
List<Delegate> _completeList;
#endregion
#region constructor
static AsyncTask()
{
#if UNITY
TaskManager.ConfirmInit();
#endif
}
/// <summary>
/// Creates a new task
/// </summary>
public AsyncTask()
{
}
/// <summary>
/// Creates a new task
/// </summary>
public AsyncTask(TaskStrategy mode)
{
Strategy = mode;
}
/// <summary>
/// Creates a new Task in a Faulted state
/// </summary>
/// <param name="ex"></param>
public AsyncTask(Exception ex)
{
Exception = ex;
Strategy = TaskStrategy.Custom;
Status = TaskStatus.Faulted;
}
/// <summary>
/// Creates a new background task
/// </summary>
/// <param name="action"></param>
public AsyncTask(Action action)
{
_action = action;
Strategy = TaskStrategy.BackgroundThread;
}
/// <summary>
/// Creates a new Task
/// </summary>
/// <param name="action"></param>
/// <param name="mode"></param>
public AsyncTask(Action action, TaskStrategy mode)
: this()
{
if (mode == TaskStrategy.Coroutine)
throw new ArgumentException("Action tasks may not be coroutines");
_action = action;
Strategy = mode;
}
/// <summary>
/// Creates a new Coroutine Task
/// </summary>
/// <param name="action"></param>
public AsyncTask(IEnumerator action)
: this()
{
if (action == null)
throw new ArgumentNullException("action");
_routine = action;
Strategy = TaskStrategy.Coroutine;
}
#endregion
#region Private
protected virtual void Execute()
{
try
{
if (_action != null)
{
_action();
}
Status = TaskStatus.Success;
OnTaskComplete();
}
catch (Exception ex)
{
Exception = ex;
Status = TaskStatus.Faulted;
#if UNITY
if (LogErrors)
Debug.LogException(ex);
#endif
}
}
#if WINDOWS_WSA || WINDOWS_UWP
protected async void RunOnBackgroundThread()
{
Status = TaskStatus.Pending;
await ThreadPool.RunAsync(o => Execute());
#else
protected void RunOnBackgroundThread()
{
Status = TaskStatus.Pending;
ThreadPool.QueueUserWorkItem(state => Execute());
#endif
}
protected void RunOnCurrentThread()
{
Status = TaskStatus.Pending;
Execute();
}
#if UNITY
protected void RunOnMainThread()
{
Status = TaskStatus.Pending;
TaskManager.RunOnMainThread(Execute);
}
protected void RunAsCoroutine()
{
Status = TaskStatus.Pending;
TaskManager.StartRoutine(new TaskManager.CoroutineCommand
{
Coroutine = _routine,
OnComplete = OnRoutineComplete
});
}
#endif
protected virtual void OnTaskComplete()
{
if (_completeList != null)
{
foreach (var d in _completeList)
{
if (d != null)
d.DynamicInvoke(this);
}
_completeList = null;
}
HasContinuations = false;
}
protected void OnRoutineComplete()
{
if (Status == TaskStatus.Pending)
{
Status = TaskStatus.Success;
OnTaskComplete();
}
}
#endregion
#region public methods
/// <summary>
/// Runs complete logic, for custom tasks
/// </summary>
public virtual void Complete(Exception ex = null)
{
if (ex == null)
{
Exception = null;
Status = TaskStatus.Success;
OnTaskComplete();
}
else
{
Exception = ex;
Status = TaskStatus.Faulted;
OnTaskComplete();
}
}
/// <summary>
/// Executes the task
/// </summary>
public virtual void Start()
{
Status = TaskStatus.Pending;
switch (Strategy)
{
case TaskStrategy.Custom:
break;
#if UNITY
case TaskStrategy.Coroutine:
RunAsCoroutine();
break;
#endif
case TaskStrategy.BackgroundThread:
if (DisableMultiThread)
RunOnCurrentThread();
else
RunOnBackgroundThread();
break;
case TaskStrategy.CurrentThread:
RunOnCurrentThread();
break;
#if UNITY
case TaskStrategy.MainThread:
RunOnMainThread();
break;
#endif
}
}
public virtual void Dispose()
{
Status = TaskStatus.Pending;
Exception = null;
_action = null;
_routine = null;
_completeList = null;
HasContinuations = false;
}
public void AddContinue(Delegate action)
{
HasContinuations = true;
if (_completeList == null)
{
_completeList = new List<Delegate>();
}
_completeList.Add(action);
}
#endregion
}
}
@@ -0,0 +1,68 @@
#if UNITY
using System;
using System.Collections;
using UnityEngine;
namespace Foundation.Tasks
{
public static class TaskExtensions
{
/// <summary>
/// will throw if faulted
/// </summary>
/// <returns></returns>
public static T ThrowIfFaulted<T>(this T self) where T : AsyncTask
{
if (self.IsFaulted)
throw self.Exception;
return self;
}
/// <summary>
/// Waits for the task to complete
/// </summary>
public static T ContinueWith<T>(this T self, Action<T> continuation) where T : AsyncTask
{
if (self.IsCompleted)
{
continuation(self);
}
else
{
self.AddContinue(continuation);
}
return self;
}
/// <summary>
/// Adds a timeout to the task. Will raise an exception if still running
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="self"></param>
/// <param name="seconds"></param>
/// <param name="onTimeout"></param>
/// <returns></returns>
public static T AddTimeout<T>(this T self, int seconds, Action<AsyncTask> onTimeout = null) where T : AsyncTask
{
TaskManager.StartRoutine(TimeOutAsync(self, seconds, onTimeout));
return self;
}
static IEnumerator TimeOutAsync(AsyncTask task, int seconds, Action<AsyncTask> onTimeout = null)
{
yield return new WaitForSeconds(seconds);
if (task.IsRunning)
{
if (onTimeout != null)
{
onTimeout(task);
}
task.Complete(new Exception("Timeout"));
}
}
}
}
#endif
@@ -0,0 +1,211 @@
using System;
using System.Collections;
namespace Foundation.Tasks
{
/// <summary>
/// A task encapsulates future work that may be waited on.
/// - Support running actions in background threads
/// - Supports running coroutines with return results
/// - Use the WaitForRoutine method to wait for the task in a coroutine
/// </summary>
/// <example>
/// <code>
/// var task = Task.Run(() =>
/// {
/// //Debug.Log does not work in
/// Debug.Log("Sleeping...");
/// Task.Delay(2000);
/// Debug.Log("Slept");
/// });
/// // wait for it
/// yield return task;
///
/// // check exceptions
/// if(task.IsFaulted)
/// Debug.LogException(task.Exception)
///</code>
///</example>
public partial class AsyncTask
{
#region Task
/// <summary>
/// Creates a new running task
/// </summary>
public static AsyncTask Run(Action action)
{
var task = new AsyncTask(action);
task.Start();
return task;
}
/// <summary>
/// Creates a new running task
/// </summary>
public static AsyncTask RunOnMain(Action action)
{
var task = new AsyncTask(action, TaskStrategy.MainThread);
task.Start();
return task;
}
/// <summary>
/// Creates a new running task
/// </summary>
public static AsyncTask RunOnCurrent(Action action)
{
var task = new AsyncTask(action, TaskStrategy.CurrentThread);
task.Start();
return task;
}
#endregion
#region Coroutine
/// <summary>
/// Creates a new running task
/// </summary>
public static AsyncTask RunCoroutine(IEnumerator function)
{
var task = new AsyncTask(function);
task.Start();
return task;
}
/// <summary>
/// Creates a new running task
/// </summary>
public static AsyncTask RunCoroutine(Func<IEnumerator> function)
{
var task = new AsyncTask(function());
task.Start();
return task;
}
/// <summary>
/// Creates a new running task
/// </summary>
public static AsyncTask RunCoroutine(Func<AsyncTask, IEnumerator> function)
{
var task = new AsyncTask();
task.Strategy = TaskStrategy.Coroutine;
task._routine = function(task);
task.Start();
return task;
}
#endregion
#if UNITY
#region Task With Result
/// <summary>
/// Creates a new running task
/// </summary>
public static AsyncTask<TResult> Run<TResult>(Func<TResult> function)
{
var task = new AsyncTask<TResult>(function);
task.Start();
return task;
}
/// <summary>
/// Creates a new running task
/// </summary>
public static AsyncTask<TResult> RunOnMain<TResult>(Func<TResult> function)
{
var task = new AsyncTask<TResult>(function, TaskStrategy.MainThread);
task.Start();
return task;
}
/// <summary>
/// Creates a new running task
/// </summary>
public static AsyncTask<TResult> RunOnCurrent<TResult>(Func<TResult> function)
{
var task = new AsyncTask<TResult>(function, TaskStrategy.CurrentThread);
task.Start();
return task;
}
/// <summary>
/// Creates a new running task
/// </summary>
public static AsyncTask<TResult> RunCoroutine<TResult>(IEnumerator function)
{
var task = new AsyncTask<TResult>(function);
task.Start();
return task;
}
/// <summary>
/// Creates a task which passes the task as a parameter
/// </summary>
public static AsyncTask<TResult> RunCoroutine<TResult>(Func<AsyncTask<TResult>, IEnumerator> function)
{
var task = new AsyncTask<TResult>();
task.Strategy = TaskStrategy.Coroutine;
task._routine = function(task);
task.Start();
return task;
}
#endregion
#region success / fails
/// <summary>
/// A default task in the success state
/// </summary>
static AsyncTask _successTask = new AsyncTask(TaskStrategy.Custom) { Status = TaskStatus.Success };
/// <summary>
/// A default task in the success state
/// </summary>
public static AsyncTask<T> SuccessTask<T>(T result)
{
return new AsyncTask<T>(TaskStrategy.Custom) { Status = TaskStatus.Success, Result = result };
}
/// <summary>
/// A default task in the faulted state
/// </summary>
public static AsyncTask SuccessTask()
{
return _successTask;
}
/// <summary>
/// A default task in the faulted state
/// </summary>
public static AsyncTask FailedTask(string exception)
{
return FailedTask(new Exception(exception));
}
/// <summary>
/// A default task in the faulted state
/// </summary>
public static AsyncTask FailedTask(Exception ex)
{
return new AsyncTask(TaskStrategy.Custom) { Status = TaskStatus.Faulted, Exception = ex };
}
/// <summary>
/// A default task in the faulted state
/// </summary>
public static AsyncTask<T> FailedTask<T>(string exception)
{
return FailedTask<T>(new Exception(exception));
}
/// <summary>
/// A default task in the faulted state
/// </summary>
public static AsyncTask<T> FailedTask<T>(Exception ex)
{
return new AsyncTask<T>(TaskStrategy.Custom) { Status = TaskStatus.Faulted, Exception = ex };
}
#endregion
#endif
}
}
@@ -0,0 +1,32 @@
# if !WINDOWS_WSA && !WINDOWS_UWP
using System.Threading;
namespace Foundation.Tasks
{
public partial class TaskManager
{ /// <summary>
/// Checks if this is the main thread
/// </summary>
public static bool IsMainThread
{
get { return Thread.CurrentThread == MainThread; }
}
/// <summary>
/// The Main Thread
/// </summary>
public static Thread MainThread { get; protected set; }
/// <summary>
/// The Current Thread
/// </summary>
public static Thread CurrentThread
{
get
{
return Thread.CurrentThread;
}
}
}
}
#endif
@@ -0,0 +1,30 @@
#if WINDOWS_WSA || WINDOWS_UWP
using System;
namespace Foundation.Tasks
{
public partial class TaskManager
{
/// <summary>
/// Checks if this is the main thread
/// </summary>
public static bool IsMainThread
{
get { return Environment.CurrentManagedThreadId == MainThread; }
}
/// <summary>
/// The Main Thread
/// </summary>
public static int MainThread { get; protected set; }
/// <summary>
/// The Current Thread
/// </summary>
public static int CurrentThread
{
get { return Environment.CurrentManagedThreadId; }
}
}
}
#endif
@@ -0,0 +1,308 @@
#if UNITY
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace Foundation.Tasks
{
/// <summary>
/// Manager for running coroutines and scheduling actions to runs in the main thread.
/// </summary>
/// <remarks>
/// Self instantiating. No need to add to scene.
/// </remarks>
[AddComponentMenu("Foundation/TaskManager")]
[ExecuteInEditMode]
public partial class TaskManager : MonoBehaviour
{
#region sub
/// <summary>
/// Thread Safe logger command
/// </summary>
public struct LogCommand
{
/// <summary>
/// Color Code
/// </summary>
public LogType Type;
/// <summary>
/// Text
/// </summary>
public object Message;
}
/// <summary>
/// Thread safe coroutine command
/// </summary>
public struct CoroutineCommand
{
/// <summary>
/// The IEnumerator Coroutine
/// </summary>
public IEnumerator Coroutine;
/// <summary>
/// Called on complete
/// </summary>
public Action OnComplete;
}
#endregion
/// <summary>
/// Static Accessor
/// </summary>
public static TaskManager Instance
{
get
{
ConfirmInit();
return _instance;
}
}
/// <summary>
/// Confirms the instance is ready for use
/// </summary>
public static void ConfirmInit()
{
if (_instance == null)
{
var old = FindObjectsOfType<TaskManager>();
foreach (var manager in old)
{
if (Application.isEditor)
DestroyImmediate(manager.gameObject);
else
Destroy(manager.gameObject);
}
var go = new GameObject("_TaskManager");
DontDestroyOnLoad(go);
_instance = go.AddComponent<TaskManager>();
MainThread = CurrentThread;
}
}
/// <summary>
/// Scheduled the routine to run (on the main thread)
/// </summary>
public static Coroutine WaitForSeconds(int seconds)
{
return Instance.StartCoroutine(Instance.WaitForSecondsInternal(seconds));
}
/// <summary>
/// Scheduled the routine to run (on the main thread)
/// </summary>
public static Coroutine StartRoutine(IEnumerator coroutine)
{
if (IsApplicationQuit)
return null;
//Make sure we are in the main thread
if (!IsMainThread)
{
lock (syncRoot)
{
PendingAdd.Add(coroutine);
//Debug.LogWarning("Running coroutines from background thread are not awaitable. Use CoroutineInfo");
return null;
}
}
return Instance.StartCoroutine(coroutine);
}
/// <summary>
/// Scheduled the routine to run (on the main thread)
/// </summary>
public static void StartRoutine(CoroutineCommand info)
{
if (IsApplicationQuit)
return;
//Make sure we are in the main thread
if (!IsMainThread)
{
lock (syncRoot)
{
PendingCoroutineInfo.Add(info);
}
}
else
{
Instance.StartCoroutine(Instance.RunCoroutineInfo(info));
}
}
/// <summary>
/// Scheduled the routine to run (on the main thread)
/// </summary>
public static void StopRoutine(IEnumerator coroutine)
{
if (IsApplicationQuit)
return;
//Make sure we are in the main thread
if (!IsMainThread)
{
lock (syncRoot)
{
PendingRemove.Add(coroutine);
}
}
else
{
Instance.StopCoroutine(coroutine);
}
}
/// <summary>
/// Schedules the action to run on the main thread
/// </summary>
/// <param name="action"></param>
public static void RunOnMainThread(Action action)
{
if (IsApplicationQuit)
return;
//Make sure we are in the main thread
if (!IsMainThread)
{
lock (syncRoot)
{
PendingActions.Add(action);
}
}
else
{
action();
}
}
/// <summary>
/// A thread safe logger
/// </summary>
/// <param name="m"></param>
public static void Log(LogCommand m)
{
if (!IsMainThread)
{
lock (syncRoot)
{
PendingLogs.Add(m);
}
}
else
{
Write(m);
}
}
static void Write(LogCommand m)
{
switch (m.Type)
{
case LogType.Warning:
Debug.LogWarning(m.Message);
break;
case LogType.Error:
case LogType.Exception:
Debug.LogError(m.Message);
break;
case LogType.Log:
case LogType.Assert:
Debug.Log(m.Message);
break;
}
}
private static TaskManager _instance;
private static object syncRoot = new object();
protected static readonly List<CoroutineCommand> PendingCoroutineInfo = new List<CoroutineCommand>();
protected static readonly List<IEnumerator> PendingAdd = new List<IEnumerator>();
protected static readonly List<IEnumerator> PendingRemove = new List<IEnumerator>();
protected static readonly List<Action> PendingActions = new List<Action>();
protected static readonly List<LogCommand> PendingLogs = new List<LogCommand>();
protected static bool IsApplicationQuit;
protected void Awake()
{
if (_instance == null)
_instance = this;
}
protected void Update()
{
if (IsApplicationQuit)
return;
if (PendingAdd.Count == 0 && PendingRemove.Count == 0 && PendingActions.Count == 0 && PendingLogs.Count == 0 && PendingCoroutineInfo.Count == 0)
return;
lock (syncRoot)
{
for (int i = 0;i < PendingLogs.Count;i++)
{
Write(PendingLogs[i]);
}
for (int i = 0;i < PendingAdd.Count;i++)
{
StartCoroutine(PendingAdd[i]);
}
for (int i = 0;i < PendingRemove.Count;i++)
{
StopCoroutine(PendingRemove[i]);
}
for (int i = 0;i < PendingCoroutineInfo.Count;i++)
{
StartCoroutine(RunCoroutineInfo(PendingCoroutineInfo[i]));
}
for (int i = 0;i < PendingActions.Count;i++)
{
PendingActions[i]();
}
PendingAdd.Clear();
PendingRemove.Clear();
PendingActions.Clear();
PendingLogs.Clear();
PendingCoroutineInfo.Clear();
}
}
IEnumerator RunCoroutineInfo(CoroutineCommand info)
{
yield return StartCoroutine(info.Coroutine);
if (info.OnComplete != null)
info.OnComplete();
}
protected void OnApplicationQuit()
{
IsApplicationQuit = true;
}
IEnumerator WaitForSecondsInternal(int seconds)
{
if(seconds <= 0)
yield break;
var delta = 0f;
while (delta < seconds)
{
delta += Time.unscaledDeltaTime;
yield return 1;
}
}
}
}
#endif
@@ -0,0 +1,173 @@
#if UNITY
using System;
using System.Collections;
using UnityEngine;
namespace Foundation.Tasks
{
/// <summary>
/// A task encapsulates future work that may be waited on.
/// - Support running actions in background threads
/// - Supports running coroutines with return results
/// - Use the WaitForRoutine method to wait for the task in a coroutine
/// </summary>
/// <example>
/// <code>
/// var task = Task.Run(() =>
/// {
/// //Debug.Log does not work in
/// Debug.Log("Sleeping...");
/// Task.Delay(2000);
/// Debug.Log("Slept");
/// });
/// // wait for it
/// yield return task;
///
/// // check exceptions
/// if(task.IsFaulted)
/// Debug.LogException(task.Exception)
///</code>
///</example>
public class AsyncTask<TResult> : AsyncTask
{
#region public fields
/// <summary>
/// get the result of the task. Blocking. It is recommended you yield on the wait before accessing this value
/// </summary>
public TResult Result;
#endregion
#region ctor
Func<TResult> _function;
public AsyncTask()
{
}
/// <summary>
/// Returns the task in the Success state.
/// </summary>
/// <param name="result"></param>
public AsyncTask(TResult result)
: this()
{
Status = TaskStatus.Success;
Strategy = TaskStrategy.Custom;
Result = result;
}
/// <summary>
/// Creates a new background Task strategy
/// </summary>
/// <param name="function"></param>
public AsyncTask(Func<TResult> function)
: this()
{
if (function == null)
throw new ArgumentNullException("function");
_function = function;
}
/// <summary>
/// Creates a new task with a specific strategy
/// </summary>
public AsyncTask(Func<TResult> function, TaskStrategy mode)
: this()
{
if (function == null)
throw new ArgumentNullException("function");
if (mode == TaskStrategy.Coroutine)
throw new ArgumentException("Mode can not be coroutine");
_function = function;
Strategy = mode;
}
/// <summary>
/// Creates a new Coroutine task
/// </summary>
public AsyncTask(IEnumerator routine)
{
if (routine == null)
throw new ArgumentNullException("routine");
_routine = routine;
Strategy = TaskStrategy.Coroutine;
}
/// <summary>
/// Creates a new Task in a Faulted state
/// </summary>
/// <param name="ex"></param>
public AsyncTask(Exception ex)
{
Exception = ex;
Strategy = TaskStrategy.Custom;
Status = TaskStatus.Faulted;
}
/// <summary>
/// Creates a new task
/// </summary>
public AsyncTask(TaskStrategy mode)
: this()
{
Strategy = mode;
}
#endregion
#region protected methods
/// <summary>
/// Runs complete logic, for custom tasks
/// </summary>
public override void Complete(Exception ex = null)
{
Result = default(TResult);
base.Complete(ex);
}
/// <summary>
/// Runs complete logic, for custom tasks
/// </summary>
public void Complete(TResult result)
{
Result = result;
base.Complete();
}
public override void Start()
{
Result = default(TResult);
base.Start();
}
protected override void Execute()
{
try
{
if (_function != null)
{
Result = _function();
}
Status = TaskStatus.Success;
OnTaskComplete();
}
catch (Exception ex)
{
Exception = ex;
Status = TaskStatus.Faulted;
if (LogErrors)
Debug.LogException(ex);
}
}
#endregion
}
}
#endif
@@ -0,0 +1,43 @@
<?xml version="1.0" encoding="utf-8"?>
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<MSBuildAllProjects>$(MSBuildAllProjects);$(MSBuildThisFileFullPath)</MSBuildAllProjects>
<HasSharedItems>true</HasSharedItems>
<SharedGUID>1577d2e0-959b-4106-a7d8-d77b41c09f0c</SharedGUID>
</PropertyGroup>
<PropertyGroup Label="Configuration">
<Import_RootNamespace>GA_SDK_MONO_SHARED</Import_RootNamespace>
</PropertyGroup>
<ItemGroup>
<Compile Include="$(MSBuildThisFileDirectory)Device\GADevice.cs" />
<Compile Include="$(MSBuildThisFileDirectory)EGAErrorSeverity.cs" />
<Compile Include="$(MSBuildThisFileDirectory)EGAGender.cs" />
<Compile Include="$(MSBuildThisFileDirectory)EGALoggerMessageType.cs" />
<Compile Include="$(MSBuildThisFileDirectory)EGAProgressionStatus.cs" />
<Compile Include="$(MSBuildThisFileDirectory)EGAResourceFlowType.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Events\GAEvents.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Foundation\Tasks\Task.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Foundation\Tasks\TaskExtensions.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Foundation\Tasks\TaskFactory.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Foundation\Tasks\TaskManager.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Foundation\Tasks\TaskManager.Net.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Foundation\Tasks\TaskManager.WSA.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Foundation\Tasks\TaskTResult.cs" />
<Compile Include="$(MSBuildThisFileDirectory)GameAnalytics.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Http\EGAHTTPApiResponse.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Http\EGASdkErrorType.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Http\GAHTTPApi.cs" />
<Compile Include="$(MSBuildThisFileDirectory)ICommandCenterListener.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Logging\GALogger.cs" />
<Compile Include="$(MSBuildThisFileDirectory)State\GAState.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Store\GAStore.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Tasks\SdkErrorTask.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Threading\GAThreading.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Threading\PriorityQueue.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Threading\TimedBlock.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Utilities\GAUtilities.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Utilities\SimpleJSON.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Utilities\SimpleJSONBinary.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Validators\GAValidator.cs" />
</ItemGroup>
</Project>
@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="14.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup Label="Globals">
<ProjectGuid>1577d2e0-959b-4106-a7d8-d77b41c09f0c</ProjectGuid>
<MinimumVisualStudioVersion>14.0</MinimumVisualStudioVersion>
</PropertyGroup>
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<Import Project="$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)\CodeSharing\Microsoft.CodeSharing.Common.Default.props" />
<Import Project="$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)\CodeSharing\Microsoft.CodeSharing.Common.props" />
<PropertyGroup />
<Import Project="GA-SDK-MONO-SHARED.projitems" Label="Shared" />
<Import Project="$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)\CodeSharing\Microsoft.CodeSharing.CSharp.targets" />
</Project>
@@ -0,0 +1,839 @@
using System;
using System.Collections.Generic;
using GameAnalyticsSDK.Net.Threading;
using GameAnalyticsSDK.Net.Logging;
using GameAnalyticsSDK.Net.State;
using GameAnalyticsSDK.Net.Validators;
using GameAnalyticsSDK.Net.Device;
using GameAnalyticsSDK.Net.Events;
using GameAnalyticsSDK.Net.Store;
#if WINDOWS_UWP || WINDOWS_WSA
using Windows.ApplicationModel;
using Windows.ApplicationModel.Core;
using System.Threading.Tasks;
#else
using System.Threading;
#endif
namespace GameAnalyticsSDK.Net
{
public static class GameAnalytics
{
private static bool _endThread;
static GameAnalytics()
{
_endThread = false;
GADevice.Touch();
}
#if !UNITY && !MONO
public static event Action<string, EGALoggerMessageType> OnMessageLogged;
internal static void MessageLogged(string message, EGALoggerMessageType type)
{
OnMessageLogged?.Invoke(message, type);
}
#endif
#region CONFIGURE
public static void ConfigureAvailableCustomDimensions01(params string[] customDimensions)
{
if(_endThread)
{
return;
}
GAThreading.PerformTaskOnGAThread("configureAvailableCustomDimensions01", () =>
{
if (IsSdkReady(true, false))
{
GALogger.W("Available custom dimensions must be set before SDK is initialized");
return;
}
GAState.AvailableCustomDimensions01 = customDimensions;
});
}
public static void ConfigureAvailableCustomDimensions02(params string[] customDimensions)
{
if(_endThread)
{
return;
}
GAThreading.PerformTaskOnGAThread("configureAvailableCustomDimensions02", () =>
{
if (IsSdkReady(true, false))
{
GALogger.W("Available custom dimensions must be set before SDK is initialized");
return;
}
GAState.AvailableCustomDimensions02 = customDimensions;
});
}
public static void ConfigureAvailableCustomDimensions03(params string[] customDimensions)
{
if(_endThread)
{
return;
}
GAThreading.PerformTaskOnGAThread("configureAvailableCustomDimensions03", () =>
{
if (IsSdkReady(true, false))
{
GALogger.W("Available custom dimensions must be set before SDK is initialized");
return;
}
GAState.AvailableCustomDimensions03 = customDimensions;
});
}
public static void ConfigureAvailableResourceCurrencies(params string[] resourceCurrencies)
{
if(_endThread)
{
return;
}
GAThreading.PerformTaskOnGAThread("configureAvailableResourceCurrencies", () =>
{
if (IsSdkReady(true, false))
{
GALogger.W("Available resource currencies must be set before SDK is initialized");
return;
}
GAState.AvailableResourceCurrencies = resourceCurrencies;
});
}
public static void ConfigureAvailableResourceItemTypes(params string[] resourceItemTypes)
{
if(_endThread)
{
return;
}
GAThreading.PerformTaskOnGAThread("configureAvailableResourceItemTypes", () =>
{
if (IsSdkReady(true, false))
{
GALogger.W("Available resource item types must be set before SDK is initialized");
return;
}
GAState.AvailableResourceItemTypes = resourceItemTypes;
});
}
public static void ConfigureBuild(string build)
{
if(_endThread)
{
return;
}
GAThreading.PerformTaskOnGAThread("configureBuild", () =>
{
if (IsSdkReady(true, false))
{
GALogger.W("Build version must be set before SDK is initialized.");
return;
}
if (!GAValidator.ValidateBuild(build))
{
GALogger.I("Validation fail - configure build: Cannot be null, empty or above 32 length. String: " + build);
return;
}
GAState.Build = build;
});
}
public static void ConfigureSdkGameEngineVersion(string sdkGameEngineVersion)
{
if(_endThread)
{
return;
}
GAThreading.PerformTaskOnGAThread("configureSdkGameEngineVersion", () =>
{
if (IsSdkReady(true, false))
{
return;
}
if (!GAValidator.ValidateSdkWrapperVersion(sdkGameEngineVersion))
{
GALogger.I("Validation fail - configure sdk version: Sdk version not supported. String: " + sdkGameEngineVersion);
return;
}
GADevice.SdkGameEngineVersion = sdkGameEngineVersion;
});
}
public static void ConfigureGameEngineVersion(string gameEngineVersion)
{
if(_endThread)
{
return;
}
GAThreading.PerformTaskOnGAThread("configureGameEngineVersion", () =>
{
if (IsSdkReady(true, false))
{
return;
}
if (!GAValidator.ValidateEngineVersion(gameEngineVersion))
{
GALogger.I("Validation fail - configure sdk version: Sdk version not supported. String: " + gameEngineVersion);
return;
}
GADevice.GameEngineVersion = gameEngineVersion;
});
}
public static void ConfigureUserId(string uId)
{
if(_endThread)
{
return;
}
GAThreading.PerformTaskOnGAThread("configureUserId", () =>
{
if (IsSdkReady(true, false))
{
GALogger.W("A custom user id must be set before SDK is initialized.");
return;
}
if (!GAValidator.ValidateUserId(uId))
{
GALogger.I("Validation fail - configure user_id: Cannot be null, empty or above 64 length. Will use default user_id method. Used string: " + uId);
return;
}
GAState.UserId = uId;
});
}
#endregion // CONFIGURE
#region INITIALIZE
public static void Initialize(string gameKey, string gameSecret)
{
if(_endThread)
{
return;
}
#if WINDOWS_UWP || WINDOWS_WSA
CoreApplication.Suspending += OnSuspending;
CoreApplication.Resuming += OnResuming;
#endif
GADevice.UpdateConnectionType();
GAThreading.PerformTaskOnGAThread("initialize", () =>
{
if (IsSdkReady(true, false))
{
GALogger.W("SDK already initialized. Can only be called once.");
return;
}
if (!GAValidator.ValidateKeys(gameKey, gameSecret))
{
GALogger.W("SDK failed initialize. Game key or secret key is invalid. Can only contain characters A-z 0-9, gameKey is 32 length, gameSecret is 40 length. Failed keys - gameKey: " + gameKey + ", secretKey: " + gameSecret);
return;
}
GAState.SetKeys(gameKey, gameSecret);
if (!GAStore.EnsureDatabase(false, gameKey))
{
GALogger.W("Could not ensure/validate local event database: " + GADevice.WritablePath);
}
GAState.InternalInitialize();
});
}
#if WINDOWS_UWP || WINDOWS_WSA
private static async void OnSuspending(object sender, SuspendingEventArgs e)
{
var deferral = e.SuspendingOperation.GetDeferral();
await WaitOnSuspend();
deferral.Complete();
}
private static async Task WaitOnSuspend()
{
if (!GAState.UseManualSessionHandling)
{
OnSuspend();
while (!GAThreading.IsThreadFinished())
{
await Task.Delay(100);
}
}
else
{
GALogger.I("OnSuspending: Not calling GameAnalytics.OnStop() as using manual session handling");
}
}
private static void OnResuming(object sender, object e)
{
GAThreading.PerformTaskOnGAThread("onResuming", () =>
{
if(!GAState.UseManualSessionHandling)
{
OnResume();
}
else
{
GALogger.I("OnResuming: Not calling GameAnalytics.OnResume() as using manual session handling");
}
});
}
#endif
#endregion // INITIALIZE
#region ADD EVENTS
public static void AddBusinessEvent(string currency, int amount, string itemType, string itemId, string cartType/*, IDictionary<string, object> fields = null*/)
{
if(_endThread)
{
return;
}
GADevice.UpdateConnectionType();
GAThreading.PerformTaskOnGAThread("addBusinessEvent", () =>
{
if (!IsSdkReady(true, true, "Could not add business event"))
{
return;
}
// Send to events
GAEvents.AddBusinessEvent(currency, amount, itemType, itemId, cartType, null);
});
}
public static void AddResourceEvent(EGAResourceFlowType flowType, string currency, float amount, string itemType, string itemId/*, IDictionary<string, object> fields = null*/)
{
if(_endThread)
{
return;
}
GADevice.UpdateConnectionType();
GAThreading.PerformTaskOnGAThread("addResourceEvent", () =>
{
if (!IsSdkReady(true, true, "Could not add resource event"))
{
return;
}
GAEvents.AddResourceEvent(flowType, currency, amount, itemType, itemId, null);
});
}
public static void AddProgressionEvent(EGAProgressionStatus progressionStatus, string progression01/*, IDictionary<string, object> fields = null*/)
{
AddProgressionEvent(progressionStatus, progression01, "", ""/*, fields*/);
}
public static void AddProgressionEvent(EGAProgressionStatus progressionStatus, string progression01, double score/*, IDictionary<string, object> fields = null*/)
{
AddProgressionEvent(progressionStatus, progression01, "", "", score/*, fields*/);
}
public static void AddProgressionEvent(EGAProgressionStatus progressionStatus, string progression01, string progression02/*, IDictionary<string, object> fields = null*/)
{
AddProgressionEvent(progressionStatus, progression01, progression02, ""/*, fields*/);
}
public static void AddProgressionEvent(EGAProgressionStatus progressionStatus, string progression01, string progression02, double score/*, IDictionary<string, object> fields = null*/)
{
AddProgressionEvent(progressionStatus, progression01, progression02, "", score/*, fields*/);
}
public static void AddProgressionEvent(EGAProgressionStatus progressionStatus, string progression01, string progression02, string progression03/*, IDictionary<string, object> fields = null*/)
{
if(_endThread)
{
return;
}
GADevice.UpdateConnectionType();
GAThreading.PerformTaskOnGAThread("addProgressionEvent", () =>
{
if(!IsSdkReady(true, true, "Could not add progression event"))
{
return;
}
// Send to events
// TODO(nikolaj): check if this cast from int to double is OK
GAEvents.AddProgressionEvent(progressionStatus, progression01, progression02, progression03, 0, false, null);
});
}
public static void AddProgressionEvent(EGAProgressionStatus progressionStatus, string progression01, string progression02, string progression03, double score/*, IDictionary<string, object> fields = null*/)
{
if(_endThread)
{
return;
}
GADevice.UpdateConnectionType();
GAThreading.PerformTaskOnGAThread("addProgressionEvent", () =>
{
if (!IsSdkReady(true, true, "Could not add progression event"))
{
return;
}
// Send to events
// TODO(nikolaj): check if this cast from int to double is OK
GAEvents.AddProgressionEvent(progressionStatus, progression01, progression02, progression03, score, true, null);
});
}
public static void AddDesignEvent(string eventId, IDictionary<string, object> fields = null)
{
if(_endThread)
{
return;
}
GADevice.UpdateConnectionType();
GAThreading.PerformTaskOnGAThread("addDesignEvent", () =>
{
if(!IsSdkReady(true, true, "Could not add design event"))
{
return;
}
GAEvents.AddDesignEvent(eventId, 0, false, fields);
});
}
public static void AddDesignEvent(string eventId, double value/*, IDictionary<string, object> fields = null*/)
{
if(_endThread)
{
return;
}
GADevice.UpdateConnectionType();
GAThreading.PerformTaskOnGAThread("addDesignEvent", () =>
{
if (!IsSdkReady(true, true, "Could not add design event"))
{
return;
}
GAEvents.AddDesignEvent(eventId, value, true, null);
});
}
public static void AddErrorEvent(EGAErrorSeverity severity, string message/*, IDictionary<string, object> fields = null*/)
{
if(_endThread)
{
return;
}
GADevice.UpdateConnectionType();
GAThreading.PerformTaskOnGAThread("addErrorEvent", () =>
{
if (!IsSdkReady(true, true, "Could not add error event"))
{
return;
}
GAEvents.AddErrorEvent(severity, message, null);
});
}
#endregion // ADD EVENTS
#region SET STATE CHANGES WHILE RUNNING
public static void SetEnabledInfoLog(bool flag)
{
if(_endThread)
{
return;
}
GAThreading.PerformTaskOnGAThread("setEnabledInfoLog", () =>
{
if (flag)
{
GALogger.InfoLog = flag;
GALogger.I("Info logging enabled");
}
else
{
GALogger.I("Info logging disabled");
GALogger.InfoLog = flag;
}
});
}
public static void SetEnabledVerboseLog(bool flag)
{
if(_endThread)
{
return;
}
GAThreading.PerformTaskOnGAThread("setEnabledVerboseLog", () =>
{
if (flag)
{
GALogger.VerboseLog = flag;
GALogger.I("Verbose logging enabled");
}
else
{
GALogger.I("Verbose logging disabled");
GALogger.VerboseLog = flag;
}
});
}
public static void SetEnabledManualSessionHandling(bool flag)
{
if(_endThread)
{
return;
}
GAThreading.PerformTaskOnGAThread("setEnabledManualSessionHandling", () =>
{
GAState.SetManualSessionHandling(flag);
});
}
public static void SetEnabledEventSubmission(bool flag)
{
if(_endThread)
{
return;
}
GAThreading.PerformTaskOnGAThread("setEnabledEventSubmission", () =>
{
if (flag)
{
GAState.SetEnabledEventSubmission(flag);
GALogger.I("Event submission enabled");
}
else
{
GALogger.I("Event submission disabled");
GAState.SetEnabledEventSubmission(flag);
}
});
}
public static void SetCustomDimension01(string dimension)
{
if(_endThread)
{
return;
}
GAThreading.PerformTaskOnGAThread("setCustomDimension01", () =>
{
if (!GAValidator.ValidateDimension01(dimension))
{
GALogger.W("Could not set custom01 dimension value to '" + dimension + "'. Value not found in available custom01 dimension values");
return;
}
GAState.SetCustomDimension01(dimension);
});
}
public static void SetCustomDimension02(string dimension)
{
if(_endThread)
{
return;
}
GAThreading.PerformTaskOnGAThread("setCustomDimension02", () =>
{
if (!GAValidator.ValidateDimension02(dimension))
{
GALogger.W("Could not set custom02 dimension value to '" + dimension + "'. Value not found in available custom02 dimension values");
return;
}
GAState.SetCustomDimension02(dimension);
});
}
public static void SetCustomDimension03(string dimension)
{
if(_endThread)
{
return;
}
GAThreading.PerformTaskOnGAThread("setCustomDimension03", () =>
{
if (!GAValidator.ValidateDimension03(dimension))
{
GALogger.W("Could not set custom03 dimension value to '" + dimension + "'. Value not found in available custom03 dimension values");
return;
}
GAState.SetCustomDimension03(dimension);
});
}
public static void SetFacebookId(string facebookId)
{
if(_endThread)
{
return;
}
GAThreading.PerformTaskOnGAThread("setFacebookId", () =>
{
if (GAValidator.ValidateFacebookId(facebookId))
{
GAState.SetFacebookId(facebookId);
}
});
}
public static void SetGender(EGAGender gender)
{
if(_endThread)
{
return;
}
GAThreading.PerformTaskOnGAThread("setGender", () =>
{
if (GAValidator.ValidateGender(gender))
{
GAState.SetGender(gender);
}
});
}
public static void SetBirthYear(int birthYear)
{
if(_endThread)
{
return;
}
GAThreading.PerformTaskOnGAThread("setBirthYear", () =>
{
if (GAValidator.ValidateBirthyear(birthYear))
{
GAState.SetBirthYear(birthYear);
}
});
}
#endregion // SET STATE CHANGES WHILE RUNNING
public static void StartSession()
{
if(_endThread)
{
return;
}
GAThreading.PerformTaskOnGAThread("startSession", () =>
{
#if WINDOWS_UWP || WINDOWS_WSA
if(GAState.UseManualSessionHandling)
#endif
{
if(!GAState.Initialized)
{
return;
}
if(GAState.IsEnabled() && GAState.SessionIsStarted())
{
GAState.EndSessionAndStopQueue(false);
}
GAState.ResumeSessionAndStartQueue();
}
});
}
public static void EndSession()
{
#if WINDOWS_UWP || WINDOWS_WSA
if(GAState.UseManualSessionHandling)
#endif
{
OnSuspend();
}
}
public static void OnResume()
{
if(_endThread)
{
return;
}
GALogger.D("OnResume() called");
GAThreading.PerformTaskOnGAThread("onResume", () =>
{
GAState.ResumeSessionAndStartQueue();
});
}
public static void OnSuspend()
{
if(_endThread)
{
return;
}
GALogger.D("OnSuspend() called");
GAThreading.PerformTaskOnGAThread("onSuspend", () =>
{
try
{
GAState.EndSessionAndStopQueue(false);
}
catch(Exception)
{
}
});
}
public static void OnQuit()
{
if(_endThread)
{
return;
}
GALogger.D("OnQuit() called");
GAThreading.PerformTaskOnGAThread("onQuit", () =>
{
try
{
_endThread = true;
GAState.EndSessionAndStopQueue(true);
}
catch(Exception)
{
}
});
}
#region COMMAND CENTER
public static string GetCommandCenterValueAsString(string key, string defaultValue = null)
{
return GAState.GetConfigurationStringValue(key, defaultValue);
}
public static bool IsCommandCenterReady()
{
return GAState.IsCommandCenterReady();
}
public static void AddCommandCenterListener(ICommandCenterListener listener)
{
GAState.AddCommandCenterListener(listener);
}
public static void RemoveCommandCenterListener(ICommandCenterListener listener)
{
GAState.RemoveCommandCenterListener(listener);
}
public static string GetConfigurationsAsString()
{
return GAState.GetConfigurationsAsString();
}
#endregion // COMMAND CENTER
#region PRIVATE HELPERS
private static bool IsSdkReady(bool needsInitialized)
{
return IsSdkReady(needsInitialized, true);
}
private static bool IsSdkReady(bool needsInitialized, bool warn)
{
return IsSdkReady(needsInitialized, warn, "");
}
private static bool IsSdkReady(bool needsInitialized, bool warn, String message)
{
if (!string.IsNullOrEmpty(message))
{
message = message + ": ";
}
// Make sure database is ready
if (!GAStore.IsTableReady)
{
if (warn)
{
GALogger.W(message + "Datastore not initialized");
}
return false;
}
// Is SDK initialized
if (needsInitialized && !GAState.Initialized)
{
if (warn)
{
GALogger.W(message + "SDK is not initialized");
}
return false;
}
// Is SDK enabled
if (needsInitialized && !GAState.IsEnabled())
{
if (warn)
{
GALogger.W(message + "SDK is disabled");
}
return false;
}
// Is session started
if (needsInitialized && !GAState.SessionIsStarted())
{
if (warn)
{
GALogger.W(message + "Session has not started yet");
}
return false;
}
return true;
}
#endregion // PRIVATE HELPERS
}
}
@@ -0,0 +1,19 @@
namespace GameAnalyticsSDK.Net.Http
{
internal enum EGAHTTPApiResponse
{
// client
NoResponse,
BadResponse,
RequestTimeout, // 408
JsonEncodeFailed,
JsonDecodeFailed,
// server
InternalServerError,
BadRequest, // 400
Unauthorized, // 401
UnknownResponseCode,
Ok
}
}
@@ -0,0 +1,9 @@
namespace GameAnalyticsSDK.Net.Http
{
internal enum EGASdkErrorType
{
Undefined = 0,
Rejected = 1
}
}
@@ -0,0 +1,553 @@
using System;
using System.Net;
using System.IO;
using System.Text;
using GameAnalyticsSDK.Net.Logging;
using GameAnalyticsSDK.Net.Utilities;
using GameAnalyticsSDK.Net.State;
using GameAnalyticsSDK.Net.Validators;
using System.Collections.Generic;
using GameAnalyticsSDK.Net.Tasks;
#if !WINDOWS_UWP && !WINDOWS_WSA
using System.Security.Cryptography.X509Certificates;
using System.Net.Security;
#endif
#if WINDOWS_UWP || WINDOWS_WSA
using System.Threading.Tasks;
using Windows.Networking.Connectivity;
using GameAnalyticsSDK.Net.Device;
#endif
namespace GameAnalyticsSDK.Net.Http
{
internal class GAHTTPApi
{
#region Fields and properties
private static readonly GAHTTPApi _instance = new GAHTTPApi();
// base url settings
private static string protocol = "https";
private static string hostName = "api.gameanalytics.com";
private static string version = "v2";
private static string baseUrl = getBaseUrl();
private static string initializeUrlPath = "init";
private static string eventsUrlPath = "events";
private bool useGzip;
private static string getBaseUrl()
{
return protocol + "://" + hostName + "/" + version;
}
public static GAHTTPApi Instance
{
get
{
return _instance;
}
}
#endregion // Fields and properties
// Constructor - setup the basic information for HTTP
private GAHTTPApi()
{
this.useGzip = true;
#if DEBUG
this.useGzip = false;
#endif
#if !WINDOWS_UWP && !WINDOWS_WSA
ServicePointManager.ServerCertificateValidationCallback = MyRemoteCertificateValidationCallback;
#endif
#if WINDOWS_UWP || WINDOWS_WSA
NetworkInformation.NetworkStatusChanged += NetworkInformationOnNetworkStatusChanged;
CheckInternetAccess();
#endif
}
#if WINDOWS_UWP || WINDOWS_WSA
private static void NetworkInformationOnNetworkStatusChanged(object sender)
{
CheckInternetAccess();
}
private static void CheckInternetAccess()
{
var connectionProfile = NetworkInformation.GetInternetConnectionProfile();
bool hasInternetAccess = (connectionProfile != null && connectionProfile.GetNetworkConnectivityLevel() == NetworkConnectivityLevel.InternetAccess);
if (hasInternetAccess)
{
if (connectionProfile.IsWlanConnectionProfile)
{
GADevice.ConnectionType = "wifi";
}
else if (connectionProfile.IsWwanConnectionProfile)
{
GADevice.ConnectionType = "wwan";
}
else
{
GADevice.ConnectionType = "lan";
}
}
else
{
GADevice.ConnectionType = "offline";
}
}
#endif
#if !WINDOWS_UWP && !WINDOWS_WSA
private bool MyRemoteCertificateValidationCallback(System.Object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors)
{
bool isOk = true;
// If there are errors in the certificate chain, look at each error to determine the cause.
if (sslPolicyErrors != SslPolicyErrors.None)
{
for (int i = 0; i < chain.ChainStatus.Length; i++)
{
if (chain.ChainStatus[i].Status != X509ChainStatusFlags.RevocationStatusUnknown)
{
chain.ChainPolicy.RevocationFlag = X509RevocationFlag.EntireChain;
chain.ChainPolicy.RevocationMode = X509RevocationMode.Online;
chain.ChainPolicy.UrlRetrievalTimeout = new TimeSpan(0, 1, 0);
chain.ChainPolicy.VerificationFlags = X509VerificationFlags.AllFlags;
bool chainIsValid = chain.Build((X509Certificate2)certificate);
if (!chainIsValid)
{
isOk = false;
}
}
}
}
return isOk;
}
#endif
#region Public methods
#if WINDOWS_UWP || WINDOWS_WSA
public async Task<KeyValuePair<EGAHTTPApiResponse, JSONObject>> RequestInitReturningDict()
#else
public KeyValuePair<EGAHTTPApiResponse, JSONObject> RequestInitReturningDict()
#endif
{
JSONObject json;
EGAHTTPApiResponse result = EGAHTTPApiResponse.NoResponse;
string gameKey = GAState.GameKey;
// Generate URL
string url = baseUrl + "/" + gameKey + "/" + initializeUrlPath;
url = "https://rubick.gameanalytics.com/v2/command_center?game_key=" + gameKey + "&interval_seconds=1000000";
//url = "https://requestb.in/1fvbe2g1";
GALogger.D("Sending 'init' URL: " + url);
JSONObject initAnnotations = GAState.GetInitAnnotations();
// make JSON string from data
string JSONstring = initAnnotations.ToString();
if (string.IsNullOrEmpty(JSONstring))
{
result = EGAHTTPApiResponse.JsonEncodeFailed;
json = null;
return new KeyValuePair<EGAHTTPApiResponse, JSONObject>(result, json);
}
string body = "";
HttpStatusCode responseCode = (HttpStatusCode)0;
string responseDescription = "";
string authorization = "";
try
{
byte[] payloadData = CreatePayloadData(JSONstring, false);
HttpWebRequest request = CreateRequest(url, payloadData, false);
authorization = request.Headers[HttpRequestHeader.Authorization];
#if WINDOWS_UWP || WINDOWS_WSA
using (Stream dataStream = await request.GetRequestStreamAsync())
#else
using(Stream dataStream = request.GetRequestStream())
#endif
{
dataStream.Write(payloadData, 0, payloadData.Length);
}
#if WINDOWS_UWP || WINDOWS_WSA
using (HttpWebResponse response = await request.GetResponseAsync() as HttpWebResponse)
#else
using(HttpWebResponse response = request.GetResponse() as HttpWebResponse)
#endif
{
using (Stream dataStream = response.GetResponseStream())
{
using (StreamReader reader = new StreamReader(dataStream))
{
string responseString = reader.ReadToEnd();
responseCode = response.StatusCode;
responseDescription = response.StatusDescription;
// print result
body = responseString;
}
}
}
}
catch (WebException e)
{
if(e.Response != null)
{
using (HttpWebResponse response = (HttpWebResponse)e.Response)
{
using (Stream streamResponse = response.GetResponseStream())
{
using (StreamReader streamRead = new StreamReader(streamResponse))
{
string responseString = streamRead.ReadToEnd();
responseCode = response.StatusCode;
responseDescription = response.StatusDescription;
body = responseString;
}
}
}
}
}
catch(Exception e)
{
GALogger.E(e.ToString());
}
// process the response
GALogger.D("init request content : " + body);
JSONNode requestJsonDict = JSON.Parse(body);
EGAHTTPApiResponse requestResponseEnum = ProcessRequestResponse(responseCode, responseDescription, body, "Init");
// if not 200 result
if (requestResponseEnum != EGAHTTPApiResponse.Ok && requestResponseEnum != EGAHTTPApiResponse.BadRequest)
{
GALogger.D("Failed Init Call. URL: " + url + ", Authorization: " + authorization + ", JSONString: " + JSONstring);
result = requestResponseEnum;
json = null;
return new KeyValuePair<EGAHTTPApiResponse, JSONObject>(result, json);
}
if (requestJsonDict == null)
{
GALogger.D("Failed Init Call. Json decoding failed");
result = EGAHTTPApiResponse.JsonDecodeFailed;
json = null;
return new KeyValuePair<EGAHTTPApiResponse, JSONObject>(result, json);
}
// print reason if bad request
if (requestResponseEnum == EGAHTTPApiResponse.BadRequest)
{
GALogger.D("Failed Init Call. Bad request. Response: " + requestJsonDict.ToString());
// return bad request result
result = requestResponseEnum;
json = null;
return new KeyValuePair<EGAHTTPApiResponse, JSONObject>(result, json);
}
// validate Init call values
JSONObject validatedInitValues = GAValidator.ValidateAndCleanInitRequestResponse(requestJsonDict);
if (validatedInitValues == null)
{
result = EGAHTTPApiResponse.BadResponse;
json = null;
return new KeyValuePair<EGAHTTPApiResponse, JSONObject>(result, json);
}
// all ok
result = EGAHTTPApiResponse.Ok;
json = validatedInitValues;
return new KeyValuePair<EGAHTTPApiResponse, JSONObject>(result, json);
}
#if WINDOWS_UWP || WINDOWS_WSA
public async Task<KeyValuePair<EGAHTTPApiResponse, JSONNode>> SendEventsInArray(List<JSONNode> eventArray)
#else
public KeyValuePair<EGAHTTPApiResponse, JSONNode> SendEventsInArray(List<JSONNode> eventArray)
#endif
{
JSONNode json;
if (eventArray.Count == 0)
{
GALogger.D("sendEventsInArray called with missing eventArray");
}
EGAHTTPApiResponse result = EGAHTTPApiResponse.NoResponse;
string gameKey = GAState.GameKey;
// Generate URL
string url = baseUrl + "/" + gameKey + "/" + eventsUrlPath;
GALogger.D("Sending 'events' URL: " + url);
// make JSON string from data
string JSONstring = GAUtilities.ArrayOfObjectsToJsonString(eventArray);
if (JSONstring.Length == 0)
{
GALogger.D("sendEventsInArray JSON encoding failed of eventArray");
json = null;
result = EGAHTTPApiResponse.JsonEncodeFailed;
return new KeyValuePair<EGAHTTPApiResponse, JSONNode>(result, json);
}
string body = "";
HttpStatusCode responseCode = (HttpStatusCode)0;
string responseDescription = "";
string authorization = "";
try
{
byte[] payloadData = CreatePayloadData(JSONstring, useGzip);
HttpWebRequest request = CreateRequest(url, payloadData, useGzip);
authorization = request.Headers[HttpRequestHeader.Authorization];
#if WINDOWS_UWP || WINDOWS_WSA
using (Stream dataStream = await request.GetRequestStreamAsync())
#else
using(Stream dataStream = request.GetRequestStream())
#endif
{
dataStream.Write(payloadData, 0, payloadData.Length);
}
#if WINDOWS_UWP || WINDOWS_WSA
using (HttpWebResponse response = await request.GetResponseAsync() as HttpWebResponse)
#else
using(HttpWebResponse response = request.GetResponse() as HttpWebResponse)
#endif
{
using (Stream dataStream = response.GetResponseStream())
{
using (StreamReader reader = new StreamReader(dataStream))
{
string responseString = reader.ReadToEnd();
responseCode = response.StatusCode;
responseDescription = response.StatusDescription;
// print result
body = responseString;
}
}
}
}
catch (WebException e)
{
if(e.Response != null)
{
using (HttpWebResponse response = (HttpWebResponse)e.Response)
{
using (Stream streamResponse = response.GetResponseStream())
{
using (StreamReader streamRead = new StreamReader(streamResponse))
{
string responseString = streamRead.ReadToEnd();
responseCode = response.StatusCode;
responseDescription = response.StatusDescription;
body = responseString;
}
}
}
}
}
catch(Exception e)
{
GALogger.E(e.ToString());
}
GALogger.D("events request content: " + body);
EGAHTTPApiResponse requestResponseEnum = ProcessRequestResponse(responseCode, responseDescription, body, "Events");
// if not 200 result
if (requestResponseEnum != EGAHTTPApiResponse.Ok && requestResponseEnum != EGAHTTPApiResponse.BadRequest)
{
GALogger.D("Failed events Call. URL: " + url + ", Authorization: " + authorization + ", JSONString: " + JSONstring);
json = null;
result = requestResponseEnum;
return new KeyValuePair<EGAHTTPApiResponse, JSONNode>(result, json);
}
// decode JSON
JSONNode requestJsonDict = JSON.Parse(body);
if (requestJsonDict == null)
{
json = null;
result = EGAHTTPApiResponse.JsonDecodeFailed;
return new KeyValuePair<EGAHTTPApiResponse, JSONNode>(result, json);
}
// print reason if bad request
if (requestResponseEnum == EGAHTTPApiResponse.BadRequest)
{
GALogger.D("Failed Events Call. Bad request. Response: " + requestJsonDict.ToString());
}
// return response
json = requestJsonDict;
result = requestResponseEnum;
return new KeyValuePair<EGAHTTPApiResponse, JSONNode>(result, json);
}
public void SendSdkErrorEvent(EGASdkErrorType type)
{
if(!GAState.IsEventSubmissionEnabled)
{
return;
}
string gameKey = GAState.GameKey;
string secretKey = GAState.GameSecret;
// Validate
if (!GAValidator.ValidateSdkErrorEvent(gameKey, secretKey, type))
{
return;
}
// Generate URL
string url = baseUrl + "/" + gameKey + "/" + eventsUrlPath;
GALogger.D("Sending 'events' URL: " + url);
string payloadJSONString = "";
JSONObject json = GAState.GetSdkErrorEventAnnotations();
string typeString = SdkErrorTypeToString(type);
json.Add("type", typeString);
List<JSONNode> eventArray = new List<JSONNode>();
eventArray.Add(json);
payloadJSONString = GAUtilities.ArrayOfObjectsToJsonString(eventArray);
if(string.IsNullOrEmpty(payloadJSONString))
{
GALogger.W("sendSdkErrorEvent: JSON encoding failed.");
return;
}
GALogger.D("sendSdkErrorEvent json: " + payloadJSONString);
byte[] payloadData = Encoding.UTF8.GetBytes(payloadJSONString);
SdkErrorTask sdkErrorTask = new SdkErrorTask(type, payloadData, secretKey);
sdkErrorTask.Execute(url);
}
#endregion // Public methods
#region Private methods
private byte[] CreatePayloadData(string payload, bool gzip)
{
byte[] payloadData;
if(gzip)
{
payloadData = GAUtilities.GzipCompress(payload);
GALogger.D("Gzip stats. Size: " + Encoding.UTF8.GetBytes(payload).Length + ", Compressed: " + payloadData.Length + ", Content: " + payload);
}
else
{
payloadData = Encoding.UTF8.GetBytes(payload);
}
return payloadData;
}
private static string SdkErrorTypeToString(EGASdkErrorType value)
{
switch(value)
{
case EGASdkErrorType.Rejected:
{
return "rejected";
}
default:
{
return "";
}
}
}
private HttpWebRequest CreateRequest(string url, byte[] payloadData, bool gzip)
{
HttpWebRequest request = WebRequest.Create(url) as HttpWebRequest;
request.Method = "POST";
#if WINDOWS_UWP
request.Headers[HttpRequestHeader.ContentLength] = payloadData.Length.ToString();
#elif WINDOWS_WSA
//request.Headers[HttpRequestHeader.ContentLength] = payloadData.Length.ToString();
// Bug setting Content Length on WSA
#else
request.ContentLength = payloadData.Length;
#endif
if (gzip)
{
request.Headers[HttpRequestHeader.ContentEncoding] = "gzip";
}
// create authorization hash
String key = GAState.GameSecret;
request.Headers[HttpRequestHeader.Authorization] = GAUtilities.HmacWithKey(key, payloadData);
request.ContentType = "application/json";
return request;
}
private EGAHTTPApiResponse ProcessRequestResponse(HttpStatusCode responseCode, string responseMessage, string body, string requestId)
{
// if no result - often no connection
if(string.IsNullOrEmpty(body))
{
GALogger.D(requestId + " request. failed. Might be no connection. Description: " + responseMessage + ", Status code: " + responseCode);
return EGAHTTPApiResponse.NoResponse;
}
// ok
if (responseCode == HttpStatusCode.OK)
{
return EGAHTTPApiResponse.Ok;
}
// 401 can return 0 status
if (responseCode == (HttpStatusCode)0 || responseCode == HttpStatusCode.Unauthorized)
{
GALogger.D(requestId + " request. 401 - Unauthorized.");
return EGAHTTPApiResponse.Unauthorized;
}
if (responseCode == HttpStatusCode.BadRequest)
{
GALogger.D(requestId + " request. 400 - Bad Request.");
return EGAHTTPApiResponse.BadRequest;
}
if (responseCode == HttpStatusCode.InternalServerError)
{
GALogger.D(requestId + " request. 500 - Internal Server Error.");
return EGAHTTPApiResponse.InternalServerError;
}
return EGAHTTPApiResponse.UnknownResponseCode;
}
#endregion // Private methods
}
}
@@ -0,0 +1,11 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace GameAnalyticsSDK.Net
{
public interface ICommandCenterListener
{
void OnCommandCenterUpdated();
}
}
@@ -0,0 +1,233 @@
using System;
#if WINDOWS_UWP || WINDOWS_WSA
using Windows.Foundation.Diagnostics;
using MetroLog;
using MetroLog.Targets;
#elif MONO
using NLog;
using NLog.Config;
using NLog.Targets;
using GameAnalyticsSDK.Net.Device;
using System.IO;
#endif
namespace GameAnalyticsSDK.Net.Logging
{
internal class GALogger
{
#region Fields and properties
private static readonly GALogger _instance = new GALogger();
private bool infoLogEnabled;
private bool infoLogVerboseEnabled;
#pragma warning disable 0649
private static bool debugEnabled;
#pragma warning restore 0649
private const string Tag = "GameAnalytics";
#if WINDOWS_UWP || WINDOWS_WSA
private IFileLoggingSession session;
private ILoggingChannel logger;
private ILogger log;
#elif MONO
private static ILogger logger;
#elif !UNITY
private ILogger logger;
#endif
private static GALogger Instance
{
get
{
return _instance;
}
}
public static bool InfoLog
{
set
{
Instance.infoLogEnabled = value;
}
}
public static bool VerboseLog
{
set
{
Instance.infoLogVerboseEnabled = value;
}
}
#endregion // Fields and properties
private GALogger()
{
#if DEBUG
debugEnabled = true;
#endif
#if WINDOWS_UWP || WINDOWS_WSA
session = new FileLoggingSession("ga-session");
#if WINDOWS_UWP
var options = new LoggingChannelOptions();
logger = new LoggingChannel("ga-channel", options);
#else
logger = new LoggingChannel("ga-channel");
#endif
session.AddLoggingChannel(logger);
LogManagerFactory.DefaultConfiguration.AddTarget(LogLevel.Trace, LogLevel.Fatal, new StreamingFileTarget());
log = LogManagerFactory.DefaultLogManager.GetLogger<GALogger>();
#elif MONO
logger = LogManager.GetCurrentClassLogger();
var config = new LoggingConfiguration();
var consoleTarget = new ColoredConsoleTarget();
config.AddTarget("console", consoleTarget);
var fileTarget = new FileTarget();
config.AddTarget("file", fileTarget);
//consoleTarget.Layout = @"${date:format=HH\:mm\:ss} ${logger} ${message}";
fileTarget.FileName = GADevice.WritablePath + Path.DirectorySeparatorChar + "ga_log.txt";
fileTarget.Layout = "${message}";
var rule1 = new LoggingRule("*", LogLevel.Debug, consoleTarget);
config.LoggingRules.Add(rule1);
var rule2 = new LoggingRule("*", LogLevel.Debug, fileTarget);
config.LoggingRules.Add(rule2);
LogManager.Configuration = config;
#endif
}
#region Public methods
public static void I(String format)
{
if(!Instance.infoLogEnabled)
{
return;
}
string message = "Info/" + Tag + ": " + format;
Instance.SendNotificationMessage(message, EGALoggerMessageType.Info);
}
public static void W(String format)
{
string message = "Warning/" + Tag + ": " + format;
Instance.SendNotificationMessage(message, EGALoggerMessageType.Warning);
}
public static void E(String format)
{
string message = "Error/" + Tag + ": " + format;
Instance.SendNotificationMessage(message, EGALoggerMessageType.Error);
}
public static void II(String format)
{
if(!Instance.infoLogVerboseEnabled)
{
return;
}
string message = "Verbose/" + Tag + ": " + format;
Instance.SendNotificationMessage(message, EGALoggerMessageType.Info);
}
public static void D(String format)
{
if(!debugEnabled)
{
return;
}
string message = "Debug/" + Tag + ": " + format;
Instance.SendNotificationMessage(message, EGALoggerMessageType.Debug);
}
#endregion // Public methods
#region Private methods
private void SendNotificationMessage(string message, EGALoggerMessageType type)
{
switch(type)
{
case EGALoggerMessageType.Error:
{
#if UNITY
UnityEngine.Debug.LogError(message);
#elif WINDOWS_UWP || WINDOWS_WSA
logger.LogMessage(message, LoggingLevel.Error);
log.Error(message);
GameAnalytics.MessageLogged(message, type);
#elif MONO
logger.Error(message);
#else
logger.LogError(message);
GameAnalytics.MessageLogged(message, type);
#endif
}
break;
case EGALoggerMessageType.Warning:
{
#if UNITY
UnityEngine.Debug.LogWarning(message);
#elif WINDOWS_UWP || WINDOWS_WSA
logger.LogMessage(message, LoggingLevel.Warning);
log.Warn(message);
GameAnalytics.MessageLogged(message, type);
#elif MONO
logger.Warn(message);
#else
logger.LogWarning(message);
GameAnalytics.MessageLogged(message, type);
#endif
}
break;
case EGALoggerMessageType.Debug:
{
#if UNITY
UnityEngine.Debug.Log(message);
#elif WINDOWS_UWP || WINDOWS_WSA
logger.LogMessage(message, LoggingLevel.Information);
log.Debug(message);
GameAnalytics.MessageLogged(message, type);
#elif MONO
logger.Debug(message);
#else
logger.LogDebug(message);
GameAnalytics.MessageLogged(message, type);
#endif
}
break;
case EGALoggerMessageType.Info:
{
#if UNITY
UnityEngine.Debug.Log(message);
#elif WINDOWS_UWP || WINDOWS_WSA
logger.LogMessage(message, LoggingLevel.Information);
log.Info(message);
GameAnalytics.MessageLogged(message, type);
#elif MONO
logger.Info(message);
#else
logger.LogInformation(message);
GameAnalytics.MessageLogged(message, type);
#endif
}
break;
}
}
#endregion // Private methods
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,478 @@
using System;
#if MONO
using SqliteConnection = System.Data.SQLite.SQLiteConnection;
using SqliteTransaction = System.Data.SQLite.SQLiteTransaction;
using SqliteCommand = System.Data.SQLite.SQLiteCommand;
using SqliteDataReader = System.Data.SQLite.SQLiteDataReader;
using SqliteException = System.Data.SQLite.SQLiteException;
using SqliteConnectionStringBuilder = System.Data.SQLite.SQLiteConnectionStringBuilder;
#elif WINDOWS_WSA || !UNITY || WINDOWS_UWP
using Microsoft.Data.Sqlite;
using System.Reflection;
#else
using Mono.Data.Sqlite;
#endif
using System.Collections.Generic;
using GameAnalyticsSDK.Net.Utilities;
using GameAnalyticsSDK.Net.Logging;
using GameAnalyticsSDK.Net.Device;
using System.IO;
#if UNITY_SAMSUNGTV
using System.Runtime.InteropServices;
#endif
#if WINDOWS_WSA
using Windows.Storage;
using Windows.Storage.FileProperties;
using System.Threading.Tasks;
#endif
namespace GameAnalyticsSDK.Net.Store
{
internal class GAStore
{
#region Fields and properties
#if UNITY_SAMSUNGTV
[DllImport("__Internal")]
private static extern long sqlite3_memory_used();
#endif
#if UNITY_SAMSUNGTV
public const bool InMemory = true;
private const long MaxDbSizeBytes = 2097152;
private const long MaxDbSizeBytesBeforeTrim = 2621440;
#else
public const bool InMemory = false;
private const long MaxDbSizeBytes = 6291456;
private const long MaxDbSizeBytesBeforeTrim = 5242880;
#endif
private static readonly GAStore _instance = new GAStore();
private static GAStore Instance
{
get
{
return _instance;
}
}
// set when calling "ensureDatabase"
// using a "writablePath" that needs to be set into the C++ component before
private string dbPath = "";
// local pointer to database
private SqliteConnection SqlDatabase
{
get;
set;
}
private bool DbReady
{
get;
set;
}
private bool _tableReady;
public static bool IsTableReady
{
get { return Instance._tableReady; }
private set { Instance._tableReady = value; }
}
public static bool IsDbTooLargeForEvents
{
get { return DbSizeBytes > MaxDbSizeBytes; }
}
#endregion // Fields and properties
private GAStore()
{
}
#region Public methods
public static JSONArray ExecuteQuerySync(string sql)
{
return ExecuteQuerySync(sql, new Dictionary<string, object>());
}
public static JSONArray ExecuteQuerySync(string sql, Dictionary<string, object> parameters)
{
return ExecuteQuerySync(sql, parameters, false);
}
public static JSONArray ExecuteQuerySync(string sql, Dictionary<string, object> parameters, bool useTransaction)
{
// Force transaction if it is an update, insert or delete.
if (GAUtilities.StringMatch(sql.ToUpperInvariant(), "^(UPDATE|INSERT|DELETE)"))
{
useTransaction = true;
}
// Get database connection from singelton sharedInstance
SqliteConnection sqlDatabasePtr = Instance.SqlDatabase;
// Create mutable array for results
JSONArray results = new JSONArray();
SqliteTransaction transaction = null;
SqliteCommand command = null;
try
{
if (useTransaction)
{
transaction = sqlDatabasePtr.BeginTransaction();
}
command = sqlDatabasePtr.CreateCommand();
if (useTransaction)
{
command.Transaction = transaction;
}
command.CommandText = sql;
command.Prepare();
// Bind parameters
if (parameters.Count != 0)
{
foreach(KeyValuePair<string, object> pair in parameters)
{
command.Parameters.AddWithValue(pair.Key, pair.Value);
}
}
using (SqliteDataReader reader = command.ExecuteReader())
{
// Loop through results
while (reader.Read())
{
// get columns count
int columnCount = reader.FieldCount;
JSONObject row = new JSONObject();
for (int i = 0; i < columnCount; i++)
{
string column = reader.GetName(i);
if(string.IsNullOrEmpty(column))
{
continue;
}
row[column] = reader.GetValue(i).ToString();
}
results.Add(row);
}
}
if (useTransaction)
{
transaction.Commit();
}
}
catch (SqliteException e)
{
// TODO(nikolaj): Should we do a db validation to see if the db is corrupt here?
GALogger.E("SQLITE3 ERROR: " + e);
results = null;
if (useTransaction)
{
if(transaction != null)
{
try
{
transaction.Rollback();
}
catch (SqliteException ex)
{
GALogger.E("SQLITE3 ROLLBACK ERROR: " + ex);
}
finally
{
transaction.Dispose();
}
}
}
}
finally
{
if(command != null)
{
command.Dispose();
}
if(transaction != null)
{
transaction.Dispose();
}
}
// Return results
return results;
}
public static bool EnsureDatabase(bool dropDatabase, string key)
{
// lazy creation of db path
if(string.IsNullOrEmpty(Instance.dbPath))
{
// initialize db path
#pragma warning disable 0429
#if WINDOWS_UWP || WINDOWS_WSA
Instance.dbPath = InMemory ? ":memory:" : Path.Combine(GADevice.WritablePath, "ga.sqlite3");
#else
Instance.dbPath = InMemory ? ":memory:" : Path.Combine(Path.Combine(GADevice.WritablePath, key), "ga.sqlite3");
if (!InMemory)
{
string d = Path.Combine(GADevice.WritablePath, key);
if (!Directory.Exists(d))
{
Directory.CreateDirectory(d);
}
}
#endif
#pragma warning restore 0429
GALogger.D("Database path set to: " + Instance.dbPath);
}
// Open database
try
{
#if UNITY
Instance.SqlDatabase = new SqliteConnection("URI=file:" + Instance.dbPath + ";Version=3");
#else
Instance.SqlDatabase = new SqliteConnection(new SqliteConnectionStringBuilder
{
DataSource = Instance.dbPath
}.ConnectionString);
#endif
Instance.SqlDatabase.Open();
Instance.DbReady = true;
GALogger.I("Database opened: " + Instance.dbPath);
}
catch (Exception e)
{
Instance.DbReady = false;
GALogger.W("Could not open database: " + Instance.dbPath + " " + e);
return false;
}
if (dropDatabase)
{
GALogger.D("Drop tables");
ExecuteQuerySync("DROP TABLE ga_events");
ExecuteQuerySync("DROP TABLE ga_state");
ExecuteQuerySync("DROP TABLE ga_session");
ExecuteQuerySync("DROP TABLE ga_progression");
ExecuteQuerySync("VACUUM");
}
// Create statements
string sql_ga_events = "CREATE TABLE IF NOT EXISTS ga_events(status CHAR(50) NOT NULL, category CHAR(50) NOT NULL, session_id CHAR(50) NOT NULL, client_ts CHAR(50) NOT NULL, event TEXT NOT NULL);";
string sql_ga_session = "CREATE TABLE IF NOT EXISTS ga_session(session_id CHAR(50) PRIMARY KEY NOT NULL, timestamp CHAR(50) NOT NULL, event TEXT NOT NULL);";
string sql_ga_state = "CREATE TABLE IF NOT EXISTS ga_state(key CHAR(255) PRIMARY KEY NOT NULL, value TEXT);";
string sql_ga_progression = "CREATE TABLE IF NOT EXISTS ga_progression(progression CHAR(255) PRIMARY KEY NOT NULL, tries CHAR(255));";
JSONArray results = ExecuteQuerySync(sql_ga_events);
if (results == null)
{
return false;
}
if (ExecuteQuerySync("SELECT status FROM ga_events LIMIT 0,1") == null)
{
GALogger.D("ga_events corrupt, recreating.");
ExecuteQuerySync("DROP TABLE ga_events");
results = ExecuteQuerySync(sql_ga_events);
if (results == null)
{
GALogger.W("ga_events corrupt, could not recreate it.");
return false;
}
}
results = ExecuteQuerySync(sql_ga_session);
if (results == null)
{
return false;
}
if (ExecuteQuerySync("SELECT session_id FROM ga_session LIMIT 0,1") == null)
{
GALogger.D("ga_session corrupt, recreating.");
ExecuteQuerySync("DROP TABLE ga_session");
results = ExecuteQuerySync(sql_ga_session);
if (results == null)
{
GALogger.W("ga_session corrupt, could not recreate it.");
return false;
}
}
results = ExecuteQuerySync(sql_ga_state);
if (results == null)
{
return false;
}
if (ExecuteQuerySync("SELECT key FROM ga_state LIMIT 0,1") == null)
{
GALogger.D("ga_state corrupt, recreating.");
ExecuteQuerySync("DROP TABLE ga_state");
results = ExecuteQuerySync(sql_ga_state);
if (results == null)
{
GALogger.W("ga_state corrupt, could not recreate it.");
return false;
}
}
results = ExecuteQuerySync(sql_ga_progression);
if (results == null)
{
return false;
}
if (ExecuteQuerySync("SELECT progression FROM ga_progression LIMIT 0,1") == null)
{
GALogger.D("ga_progression corrupt, recreating.");
ExecuteQuerySync("DROP TABLE ga_progression");
results = ExecuteQuerySync(sql_ga_progression);
if (results == null)
{
GALogger.W("ga_progression corrupt, could not recreate it.");
return false;
}
}
// All good
TrimEventTable();
IsTableReady = true;
GALogger.D("Database tables ensured present");
return true;
}
#pragma warning disable 0162
public static void SetState(string key, string value)
{
if (value == null)
{
if (InMemory)
{
#if UNITY
if(UnityEngine.PlayerPrefs.HasKey(State.GAState.InMemoryPrefix + key))
{
UnityEngine.PlayerPrefs.DeleteKey(State.GAState.InMemoryPrefix + key);
}
#else
GALogger.W("SetState: No implementation yet for InMemory=true");
#endif
}
else
{
Dictionary<string, object> parameterArray = new Dictionary<string, object>();
parameterArray.Add("$key", key);
ExecuteQuerySync("DELETE FROM ga_state WHERE key = $key;", parameterArray);
}
}
else
{
if (InMemory)
{
#if UNITY
UnityEngine.PlayerPrefs.SetString(State.GAState.InMemoryPrefix + key, value);
#else
GALogger.W("SetState: No implementation yet for InMemory=true");
#endif
}
else
{
Dictionary<string, object> parameterArray = new Dictionary<string, object>();
parameterArray.Add("$key", key);
parameterArray.Add("$value", value);
ExecuteQuerySync("INSERT OR REPLACE INTO ga_state (key, value) VALUES($key, $value);", parameterArray, true);
}
}
}
#pragma warning restore 0162
public static long DbSizeBytes
{
get
{
#if WINDOWS_WSA
Task<StorageFile> fileTask = Task.Run<StorageFile>(async () => await StorageFile.GetFileFromPathAsync(Instance.dbPath));
StorageFile file = fileTask.GetAwaiter().GetResult();
Task<BasicProperties> propertiesTask = Task.Run<BasicProperties>(async () => await file.GetBasicPropertiesAsync());
BasicProperties properties = propertiesTask.GetAwaiter().GetResult();
return (long)properties.Size;
#elif UNITY_SAMSUNGTV
long result = 0;
try
{
result = sqlite3_memory_used();
}
catch(Exception)
{
GALogger.W("DbSizeBytes: sqlite3_memory_used failed using DbSizeBytes=0");
}
return result;
#else
return InMemory ? 0 : new FileInfo(Instance.dbPath).Length;
#endif
}
}
#endregion // Public methods
#region Private methods
private static void TrimEventTable()
{
if (DbSizeBytes > MaxDbSizeBytesBeforeTrim)
{
JSONArray resultSessionArray = ExecuteQuerySync("SELECT session_id, Max(client_ts) FROM ga_events GROUP BY session_id ORDER BY client_ts LIMIT 3");
if(resultSessionArray != null && resultSessionArray.Count > 0)
{
string sessionDeleteString = "";
for(int i = 0; i < resultSessionArray.Count; ++i)
{
sessionDeleteString += resultSessionArray[i].Value;
if(i < resultSessionArray.Count - 1)
{
sessionDeleteString += ",";
}
}
string deleteOldSessionSql = "DELETE FROM ga_events WHERE session_id IN (\"" + sessionDeleteString + "\");";
GALogger.W("Database too large when initializing. Deleting the oldest 3 sessions.");
ExecuteQuerySync(deleteOldSessionSql);
ExecuteQuerySync("VACUUM");
}
}
}
#endregion
}
}
@@ -0,0 +1,152 @@
using System;
using GameAnalyticsSDK.Net.Http;
using System.Collections.Generic;
using GameAnalyticsSDK.Net.Utilities;
using System.Net;
using System.IO;
using GameAnalyticsSDK.Net.Logging;
using Foundation.Tasks;
#if WINDOWS_UWP || WINDOWS_WSA
using System.Threading.Tasks;
#endif
namespace GameAnalyticsSDK.Net.Tasks
{
internal class SdkErrorTask
{
protected EGASdkErrorType type;
protected byte[] payloadData;
protected string hashHmac;
protected string body = "";
private const int MaxCount = 10;
private static Dictionary<EGASdkErrorType, int> countMap = new Dictionary<EGASdkErrorType, int>();
public SdkErrorTask(EGASdkErrorType type, byte[] payloadData, string secretKey)
{
this.type = type;
this.payloadData = payloadData;
this.hashHmac = GAUtilities.HmacWithKey(secretKey, payloadData);
}
public void Execute(string url)
{
AsyncTask.Run(() =>
{
#if WINDOWS_UWP || WINDOWS_WSA
DoInBackground(url).Wait();
#else
DoInBackground(url);
#endif
});
}
#if WINDOWS_UWP || WINDOWS_WSA
protected async Task DoInBackground(string url)
#else
protected void DoInBackground(string url)
#endif
{
if(!countMap.ContainsKey(this.type))
{
countMap.Add(this.type, 0);
}
if(countMap[this.type] >= MaxCount)
{
return;
}
HttpStatusCode responseCode = (HttpStatusCode)0;
string responseDescription = "";
try
{
HttpWebRequest request = WebRequest.Create(url) as HttpWebRequest;
request.Method = "POST";
#if WINDOWS_UWP || WINDOWS_WSA
request.Headers[HttpRequestHeader.ContentLength] = this.payloadData.Length.ToString();
#else
request.ContentLength = payloadData.Length;
#endif
request.Headers[HttpRequestHeader.Authorization] = this.hashHmac;
request.ContentType = "application/json";
#if WINDOWS_UWP || WINDOWS_WSA
using (Stream dataStream = await request.GetRequestStreamAsync())
#else
using (Stream dataStream = request.GetRequestStream())
#endif
{
dataStream.Write(this.payloadData, 0, payloadData.Length);
}
#if WINDOWS_UWP || WINDOWS_WSA
using (HttpWebResponse response = await request.GetResponseAsync() as HttpWebResponse)
#else
using(HttpWebResponse response = request.GetResponse() as HttpWebResponse)
#endif
{
using (Stream dataStream = response.GetResponseStream())
{
using (StreamReader reader = new StreamReader(dataStream))
{
string responseString = reader.ReadToEnd();
body = responseString;
}
}
}
}
catch (WebException e)
{
if(e.Response != null)
{
using (HttpWebResponse response = (HttpWebResponse)e.Response)
{
using (Stream streamResponse = response.GetResponseStream())
{
using (StreamReader streamRead = new StreamReader(streamResponse))
{
string responseString = streamRead.ReadToEnd();
responseCode = response.StatusCode;
responseDescription = response.StatusDescription;
body = responseString;
}
}
}
}
}
catch(Exception e)
{
GALogger.E(e.ToString());
}
// process the response
GALogger.D("sdk error request content : " + body);
OnPostExecute(responseCode, responseDescription);
}
protected void OnPostExecute(HttpStatusCode responseCode, string responseDescription)
{
if(string.IsNullOrEmpty(this.body))
{
GALogger.D("sdk error failed. Might be no connection. Description: " + responseDescription + ", Status code: " + responseCode);
return;
}
if(responseCode != HttpStatusCode.OK)
{
GALogger.W("sdk error failed. response code not 200. status code: " + responseCode + ", description: " + responseDescription + ", body: " + this.body);
return;
}
else
{
countMap[this.type] = countMap[this.type] + 1;
}
}
}
}
@@ -0,0 +1,232 @@
using System;
using System.Collections.Generic;
#if WINDOWS_WSA || WINDOWS_UWP
using Windows.System.Threading;
using Windows.Foundation;
using System.Threading.Tasks;
#else
using System.Threading;
#endif
using GameAnalyticsSDK.Net.Logging;
namespace GameAnalyticsSDK.Net.Threading
{
public class GAThreading
{
private static bool endThread = false;
private static DateTime threadDeadline;
private static readonly GAThreading _instance = new GAThreading ();
private const int ThreadWaitTimeInMs = 1000;
private readonly PriorityQueue<long, TimedBlock> blocks = new PriorityQueue<long, TimedBlock>();
private readonly object threadLock = new object();
private TimedBlock scheduledBlock;
private bool hasScheduledBlockRun;
#if WINDOWS_WSA || WINDOWS_UWP
private IAsyncAction thread;
#else
private Thread thread;
#endif
private GAThreading()
{
threadDeadline = DateTime.Now;
hasScheduledBlockRun = true;
}
~GAThreading()
{
StopThread();
}
private static GAThreading Instance
{
get
{
return _instance;
}
}
private static void RunBlocks()
{
TimedBlock timedBlock;
while ((timedBlock = GetNextBlock()) != null)
{
timedBlock.block();
}
if ((timedBlock = GetScheduledBlock()) != null)
{
timedBlock.block();
}
}
public static void Run()
{
GALogger.D("Starting GA thread");
try
{
while(!endThread && threadDeadline.CompareTo(DateTime.Now) > 0)
{
RunBlocks();
#if WINDOWS_WSA || WINDOWS_UWP
Task.Delay(ThreadWaitTimeInMs).Wait();
#else
Thread.Sleep(ThreadWaitTimeInMs);
#endif
}
// run any last blocks added
RunBlocks();
if (!endThread)
{
GALogger.D("Ending GA thread");
}
}
catch(Exception)
{
//GALogger.E("Error on GA thread");
//GALogger.E(e.ToString());
}
}
public static void PerformTaskOnGAThread(string blockName, Action taskBlock)
{
PerformTaskOnGAThread(blockName, taskBlock, 0);
}
public static void PerformTaskOnGAThread(string blockName, Action taskBlock, long delayInSeconds)
{
if(endThread)
{
return;
}
lock(Instance.threadLock)
{
DateTime time = DateTime.Now;
time = time.AddSeconds(delayInSeconds);
TimedBlock timedBlock = new TimedBlock(time, taskBlock, blockName);
Instance.AddTimedBlock(timedBlock);
threadDeadline = time.AddSeconds(10);
#if WINDOWS_WSA || WINDOWS_UWP
if (IsThreadFinished())
{
StartThread();
}
#else
if (IsThreadFinished())
{
if(Instance.thread != null)
{
Instance.thread.Join();
}
StartThread();
}
#endif
}
}
public static void ScheduleTimer(double interval, string blockName, Action callback)
{
if (endThread)
{
return;
}
lock (Instance.threadLock)
{
if(Instance.hasScheduledBlockRun)
{
DateTime time = DateTime.Now;
time = time.AddSeconds(interval);
Instance.scheduledBlock = new TimedBlock(time, callback, blockName);
Instance.hasScheduledBlockRun = false;
threadDeadline = time.AddSeconds(2);
#if WINDOWS_WSA || WINDOWS_UWP
if (IsThreadFinished())
{
StartThread();
}
#else
if (IsThreadFinished())
{
if(Instance.thread != null)
{
Instance.thread.Join();
}
StartThread();
}
#endif
}
}
}
private void AddTimedBlock(TimedBlock timedBlock)
{
this.blocks.Enqueue(timedBlock.deadline.Ticks, timedBlock);
}
private static TimedBlock GetNextBlock()
{
lock(Instance.threadLock)
{
DateTime now = DateTime.Now;
if(Instance.blocks.HasItems && Instance.blocks.Peek().deadline.CompareTo(now) <= 0)
{
return Instance.blocks.Dequeue();
}
return null;
}
}
private static TimedBlock GetScheduledBlock()
{
lock (Instance.threadLock)
{
DateTime now = DateTime.Now;
if (!Instance.hasScheduledBlockRun && Instance.scheduledBlock != null && Instance.scheduledBlock.deadline.CompareTo(now) <= 0)
{
Instance.hasScheduledBlockRun = true;
return Instance.scheduledBlock;
}
return null;
}
}
public static void StartThread()
{
#if WINDOWS_WSA || WINDOWS_UWP
Instance.thread = ThreadPool.RunAsync(o => Run());
#else
Instance.thread = new Thread(new ThreadStart(Run));
Instance.thread.Priority = ThreadPriority.Lowest;
Instance.thread.Start();
#endif
}
public static void StopThread()
{
endThread = true;
}
public static bool IsThreadFinished()
{
#if WINDOWS_WSA || WINDOWS_UWP
return Instance.thread == null || Instance.thread.Status != AsyncStatus.Started;
#else
return Instance.thread == null || !Instance.thread.IsAlive;
#endif
}
}
}
@@ -0,0 +1,71 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace GameAnalyticsSDK.Net.Threading
{
internal class PriorityQueue<TPriority, TItem>
{
readonly SortedDictionary<TPriority, Queue<TItem>> _subqueues;
public PriorityQueue(IComparer<TPriority> priorityComparer)
{
_subqueues = new SortedDictionary<TPriority, Queue<TItem>>(priorityComparer);
}
public PriorityQueue() : this(Comparer<TPriority>.Default) { }
public void Enqueue(TPriority priority, TItem item)
{
if (!_subqueues.ContainsKey(priority))
{
AddQueueOfPriority(priority);
}
_subqueues[priority].Enqueue(item);
}
private void AddQueueOfPriority(TPriority priority)
{
_subqueues.Add(priority, new Queue<TItem>());
}
public TItem Peek()
{
if (HasItems)
return _subqueues.First().Value.Peek();
else
throw new InvalidOperationException("The queue is empty");
}
public bool HasItems
{
get { return _subqueues.Any(); }
}
public TItem Dequeue()
{
if (_subqueues.Any())
return DequeueFromHighPriorityQueue();
else
throw new InvalidOperationException("The queue is empty");
}
private TItem DequeueFromHighPriorityQueue()
{
KeyValuePair<TPriority, Queue<TItem>> first = _subqueues.First();
TItem nextItem = first.Value.Dequeue();
if (!first.Value.Any())
{
_subqueues.Remove(first.Key);
}
return nextItem;
}
public int Count
{
get { return _subqueues.Sum(q => q.Value.Count); }
}
}
}
@@ -0,0 +1,29 @@
using System;
namespace GameAnalyticsSDK.Net
{
internal class TimedBlock : IComparable<TimedBlock>
{
public readonly DateTime deadline;
public readonly Action block;
public readonly string blockName;
public TimedBlock (DateTime deadline, Action block, string blockName)
{
this.deadline = deadline;
this.block = block;
this.blockName = blockName;
}
public int CompareTo(TimedBlock other)
{
return this.deadline.CompareTo (other.deadline);
}
public override string ToString ()
{
return "{TimedBlock: deadLine=" + this.deadline.Ticks + ", block=" + this.blockName + "}";
}
}
}
@@ -0,0 +1,154 @@
using System;
using System.Text.RegularExpressions;
using System.Text;
using System.Collections.Generic;
#if UNITY
using GameAnalyticsSDK.Net.Utilities.Zip.GZip;
#else
using System.IO.Compression;
#endif
#if WINDOWS_UWP || WINDOWS_WSA
using Windows.Security.Cryptography.Core;
using Windows.Security.Cryptography;
using System.Runtime.InteropServices.WindowsRuntime;
#else
using System.Security.Cryptography;
#endif
using System.IO;
namespace GameAnalyticsSDK.Net.Utilities
{
internal static class GAUtilities
{
private static readonly DateTime origin = new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc);
public static byte[] GzipCompress(string data)
{
if (string.IsNullOrEmpty(data))
{
return new byte[0];
}
byte[] result;
#if !UNITY
using (MemoryStream msi = new MemoryStream(Encoding.UTF8.GetBytes(data)))
{
using (MemoryStream mso = new MemoryStream())
{
using (GZipStream gs = new GZipStream(mso, CompressionMode.Compress))
{
msi.CopyTo(gs);
}
result = mso.ToArray();
}
}
#else
using (MemoryStream outStream = new MemoryStream())
{
using (GZipOutputStream tinyStream = new GZipOutputStream(outStream))
using (MemoryStream mStream = new MemoryStream(Encoding.UTF8.GetBytes(data)))
{
mStream.CopyTo(tinyStream);
}
result = outStream.ToArray();
}
#endif
return result;
}
public static string HmacWithKey(string key, byte[] data)
{
byte[] keyByte = Encoding.UTF8.GetBytes(key);
#if WINDOWS_UWP || WINDOWS_WSA
var hmacsha256 = MacAlgorithmProvider.OpenAlgorithm(MacAlgorithmNames.HmacSha256);
#else
using (var hmacsha256 = new HMACSHA256(keyByte))
#endif
{
#if WINDOWS_UWP || WINDOWS_WSA
var input = data.AsBuffer();
var signatureKey = hmacsha256.CreateKey(keyByte.AsBuffer());
var cypherMac = CryptographicEngine.Sign(signatureKey, input);
return CryptographicBuffer.EncodeToBase64String(cypherMac);
#else
byte[] hashmessage = hmacsha256.ComputeHash(data);
return Convert.ToBase64String(hashmessage);
#endif
}
}
public static bool StringMatch(string s, string pattern)
{
if(s == null || pattern == null)
{
return false;
}
return Regex.IsMatch(s, pattern);
}
public static string JoinStringArray(string[] v, string delimiter)
{
StringBuilder sbStr = new StringBuilder();
for (int i = 0, il = v.Length; i < il; i++)
{
if (i > 0)
{
sbStr.Append(delimiter);
}
sbStr.Append(v[i]);
}
return sbStr.ToString();
}
public static bool StringArrayContainsString(string[] array, string search)
{
if (array.Length == 0)
{
return false;
}
foreach(string s in array)
{
if(s.Equals(search))
{
return true;
}
}
return false;
}
public static long TimeIntervalSince1970()
{
TimeSpan interval = DateTime.Now.ToUniversalTime() - origin;
return (long)interval.TotalSeconds;
}
public static string ArrayOfObjectsToJsonString(List<JSONNode> arr)
{
JSONArray json_array = new JSONArray();
foreach (JSONNode x in arr)
{
json_array.Add(x);
}
return json_array.ToString();
}
public static void CopyTo(this Stream input, Stream output)
{
byte[] buffer = new byte[16 * 1024]; // Fairly arbitrary size
int bytesRead;
while ((bytesRead = input.Read(buffer, 0, buffer.Length)) > 0)
{
output.Write(buffer, 0, bytesRead);
}
}
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,297 @@
//#define USE_SharpZipLib
/* * * * *
* This is an extension of the SimpleJSON framework to provide methods to
* serialize a JSON object tree into a compact binary format. Optionally the
* binary stream can be compressed with the SharpZipLib when using the define
* "USE_SharpZipLib"
*
* Those methods where originally part of the framework but since it's rarely
* used I've extracted this part into this seperate module file.
*
* You can use the define "SimpleJSON_ExcludeBinary" to selectively disable
* this extension without the need to remove the file from the project.
*
*
* The MIT License (MIT)
*
* Copyright (c) 2012-2017 Markus Göbel (Bunny83)
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*
* * * * */
using System;
namespace GameAnalyticsSDK.Net.Utilities
{
#if !SimpleJSON_ExcludeBinary
public abstract partial class JSONNode
{
public abstract void SerializeBinary(System.IO.BinaryWriter aWriter);
public void SaveToBinaryStream(System.IO.Stream aData)
{
var W = new System.IO.BinaryWriter(aData);
SerializeBinary(W);
}
#if USE_SharpZipLib
public void SaveToCompressedStream(System.IO.Stream aData)
{
using (var gzipOut = new ICSharpCode.SharpZipLib.BZip2.BZip2OutputStream(aData))
{
gzipOut.IsStreamOwner = false;
SaveToBinaryStream(gzipOut);
gzipOut.Close();
}
}
public void SaveToCompressedFile(string aFileName)
{
System.IO.Directory.CreateDirectory((new System.IO.FileInfo(aFileName)).Directory.FullName);
using(var F = System.IO.File.OpenWrite(aFileName))
{
SaveToCompressedStream(F);
}
}
public string SaveToCompressedBase64()
{
using (var stream = new System.IO.MemoryStream())
{
SaveToCompressedStream(stream);
stream.Position = 0;
return System.Convert.ToBase64String(stream.ToArray());
}
}
#else
public void SaveToCompressedStream(System.IO.Stream aData)
{
throw new Exception("Can't use compressed functions. You need include the SharpZipLib and uncomment the define at the top of SimpleJSON");
}
public void SaveToCompressedFile(string aFileName)
{
throw new Exception("Can't use compressed functions. You need include the SharpZipLib and uncomment the define at the top of SimpleJSON");
}
public string SaveToCompressedBase64()
{
throw new Exception("Can't use compressed functions. You need include the SharpZipLib and uncomment the define at the top of SimpleJSON");
}
#endif
/*public void SaveToBinaryFile(string aFileName)
{
System.IO.Directory.CreateDirectory((new System.IO.FileInfo(aFileName)).Directory.FullName);
using (var F = System.IO.File.OpenWrite(aFileName))
{
SaveToBinaryStream(F);
}
}*/
public string SaveToBinaryBase64()
{
using (var stream = new System.IO.MemoryStream())
{
SaveToBinaryStream(stream);
stream.Position = 0;
return System.Convert.ToBase64String(stream.ToArray());
}
}
public static JSONNode DeserializeBinary(System.IO.BinaryReader aReader)
{
JSONNodeType type = (JSONNodeType)aReader.ReadByte();
switch (type)
{
case JSONNodeType.Array:
{
int count = aReader.ReadInt32();
JSONArray tmp = new JSONArray();
for (int i = 0; i < count; i++)
tmp.Add(DeserializeBinary(aReader));
return tmp;
}
case JSONNodeType.Object:
{
int count = aReader.ReadInt32();
JSONObject tmp = new JSONObject();
for (int i = 0; i < count; i++)
{
string key = aReader.ReadString();
var val = DeserializeBinary(aReader);
tmp.Add(key, val);
}
return tmp;
}
case JSONNodeType.String:
{
return new JSONString(aReader.ReadString());
}
case JSONNodeType.Number:
{
return new JSONNumber(aReader.ReadDouble());
}
case JSONNodeType.Boolean:
{
return new JSONBool(aReader.ReadBoolean());
}
case JSONNodeType.NullValue:
{
return JSONNull.CreateOrGet();
}
default:
{
throw new Exception("Error deserializing JSON. Unknown tag: " + type);
}
}
}
#if USE_SharpZipLib
public static JSONNode LoadFromCompressedStream(System.IO.Stream aData)
{
var zin = new ICSharpCode.SharpZipLib.BZip2.BZip2InputStream(aData);
return LoadFromStream(zin);
}
public static JSONNode LoadFromCompressedFile(string aFileName)
{
using(var F = System.IO.File.OpenRead(aFileName))
{
return LoadFromCompressedStream(F);
}
}
public static JSONNode LoadFromCompressedBase64(string aBase64)
{
var tmp = System.Convert.FromBase64String(aBase64);
var stream = new System.IO.MemoryStream(tmp);
stream.Position = 0;
return LoadFromCompressedStream(stream);
}
#else
public static JSONNode LoadFromCompressedFile(string aFileName)
{
throw new Exception("Can't use compressed functions. You need include the SharpZipLib and uncomment the define at the top of SimpleJSON");
}
public static JSONNode LoadFromCompressedStream(System.IO.Stream aData)
{
throw new Exception("Can't use compressed functions. You need include the SharpZipLib and uncomment the define at the top of SimpleJSON");
}
public static JSONNode LoadFromCompressedBase64(string aBase64)
{
throw new Exception("Can't use compressed functions. You need include the SharpZipLib and uncomment the define at the top of SimpleJSON");
}
#endif
public static JSONNode LoadFromBinaryStream(System.IO.Stream aData)
{
using (var R = new System.IO.BinaryReader(aData))
{
return DeserializeBinary(R);
}
}
/*public static JSONNode LoadFromBinaryFile(string aFileName)
{
using (var F = System.IO.File.OpenRead(aFileName))
{
return LoadFromBinaryStream(F);
}
}*/
public static JSONNode LoadFromBinaryBase64(string aBase64)
{
var tmp = System.Convert.FromBase64String(aBase64);
var stream = new System.IO.MemoryStream(tmp);
stream.Position = 0;
return LoadFromBinaryStream(stream);
}
}
public partial class JSONArray : JSONNode
{
public override void SerializeBinary(System.IO.BinaryWriter aWriter)
{
aWriter.Write((byte)JSONNodeType.Array);
aWriter.Write(m_List.Count);
for (int i = 0; i < m_List.Count; i++)
{
m_List[i].SerializeBinary(aWriter);
}
}
}
public partial class JSONObject : JSONNode
{
public override void SerializeBinary(System.IO.BinaryWriter aWriter)
{
aWriter.Write((byte)JSONNodeType.Object);
aWriter.Write(m_Dict.Count);
foreach (string K in m_Dict.Keys)
{
aWriter.Write(K);
m_Dict[K].SerializeBinary(aWriter);
}
}
}
public partial class JSONString : JSONNode
{
public override void SerializeBinary(System.IO.BinaryWriter aWriter)
{
aWriter.Write((byte)JSONNodeType.String);
aWriter.Write(m_Data);
}
}
public partial class JSONNumber : JSONNode
{
public override void SerializeBinary(System.IO.BinaryWriter aWriter)
{
aWriter.Write((byte)JSONNodeType.Number);
aWriter.Write(m_Data);
}
}
public partial class JSONBool : JSONNode
{
public override void SerializeBinary(System.IO.BinaryWriter aWriter)
{
aWriter.Write((byte)JSONNodeType.Boolean);
aWriter.Write(m_Data);
}
}
public partial class JSONNull : JSONNode
{
public override void SerializeBinary(System.IO.BinaryWriter aWriter)
{
aWriter.Write((byte)JSONNodeType.NullValue);
}
}
internal partial class JSONLazyCreator : JSONNode
{
public override void SerializeBinary(System.IO.BinaryWriter aWriter)
{
}
}
#endif
}
@@ -0,0 +1,631 @@
using System;
using GameAnalyticsSDK.Net.Logging;
using GameAnalyticsSDK.Net.Utilities;
using GameAnalyticsSDK.Net.State;
using GameAnalyticsSDK.Net.Http;
namespace GameAnalyticsSDK.Net.Validators
{
internal static class GAValidator
{
#region Public methods
public static bool ValidateBusinessEvent(string currency, long amount, string cartType, string itemType, string itemId)
{
// validate currency
if (!ValidateCurrency(currency))
{
GALogger.W("Validation fail - business event - currency: Cannot be (null) and need to be A-Z, 3 characters and in the standard at openexchangerates.org. Failed currency: " + currency);
return false;
}
if (amount < 0)
{
GALogger.W("Validation fail - business event - amount. Cannot be less than 0. Failed amount: " + amount);
return false;
}
// validate cartType
if (!ValidateShortString(cartType, true))
{
GALogger.W("Validation fail - business event - cartType. Cannot be above 32 length. String: " + cartType);
return false;
}
// validate itemType length
if (!ValidateEventPartLength(itemType, false))
{
GALogger.W("Validation fail - business event - itemType: Cannot be (null), empty or above 64 characters. String: " + itemType);
return false;
}
// validate itemType chars
if (!ValidateEventPartCharacters(itemType))
{
GALogger.W("Validation fail - business event - itemType: Cannot contain other characters than A-z, 0-9, -_., ()!?. String: " + itemType);
return false;
}
// validate itemId
if (!ValidateEventPartLength(itemId, false))
{
GALogger.W("Validation fail - business event - itemId. Cannot be (null), empty or above 64 characters. String: " + itemId);
return false;
}
if (!ValidateEventPartCharacters(itemId))
{
GALogger.W("Validation fail - business event - itemId: Cannot contain other characters than A-z, 0-9, -_., ()!?. String: " + itemId);
return false;
}
return true;
}
public static bool ValidateResourceEvent(EGAResourceFlowType flowType, string currency, long amount, string itemType, string itemId)
{
if (flowType == EGAResourceFlowType.Undefined)
{
GALogger.W("Validation fail - resource event - flowType: Invalid flow type.");
return false;
}
if (string.IsNullOrEmpty(currency))
{
GALogger.W("Validation fail - resource event - currency: Cannot be (null)");
return false;
}
if (!GAState.HasAvailableResourceCurrency(currency))
{
GALogger.W("Validation fail - resource event - currency: Not found in list of pre-defined available resource currencies. String: " + currency);
return false;
}
if (!(amount > 0))
{
GALogger.W("Validation fail - resource event - amount: Float amount cannot be 0 or negative. Value: " + amount);
return false;
}
if (string.IsNullOrEmpty(itemType))
{
GALogger.W("Validation fail - resource event - itemType: Cannot be (null)");
return false;
}
if (!ValidateEventPartLength(itemType, false))
{
GALogger.W("Validation fail - resource event - itemType: Cannot be (null), empty or above 64 characters. String: " + itemType);
return false;
}
if (!ValidateEventPartCharacters(itemType))
{
GALogger.W("Validation fail - resource event - itemType: Cannot contain other characters than A-z, 0-9, -_., ()!?. String: " + itemType);
return false;
}
if (!GAState.HasAvailableResourceItemType(itemType))
{
GALogger.W("Validation fail - resource event - itemType: Not found in list of pre-defined available resource itemTypes. String: " + itemType);
return false;
}
if (!ValidateEventPartLength(itemId, false))
{
GALogger.W("Validation fail - resource event - itemId: Cannot be (null), empty or above 64 characters. String: " + itemId);
return false;
}
if (!ValidateEventPartCharacters(itemId))
{
GALogger.W("Validation fail - resource event - itemId: Cannot contain other characters than A-z, 0-9, -_., ()!?. String: " + itemId);
return false;
}
return true;
}
public static bool ValidateProgressionEvent(EGAProgressionStatus progressionStatus, string progression01, string progression02, string progression03)
{
if (progressionStatus == EGAProgressionStatus.Undefined)
{
GALogger.W("Validation fail - progression event: Invalid progression status.");
return false;
}
// Make sure progressions are defined as either 01, 01+02 or 01+02+03
if (!string.IsNullOrEmpty(progression03) && !(!string.IsNullOrEmpty(progression02) || string.IsNullOrEmpty(progression01)))
{
GALogger.W("Validation fail - progression event: 03 found but 01+02 are invalid. Progression must be set as either 01, 01+02 or 01+02+03.");
return false;
}
else if (!string.IsNullOrEmpty(progression02) && string.IsNullOrEmpty(progression01))
{
GALogger.W("Validation fail - progression event: 02 found but not 01. Progression must be set as either 01, 01+02 or 01+02+03");
return false;
}
else if (string.IsNullOrEmpty(progression01))
{
GALogger.W("Validation fail - progression event: progression01 not valid. Progressions must be set as either 01, 01+02 or 01+02+03");
return false;
}
// progression01 (required)
if (!ValidateEventPartLength(progression01, false))
{
GALogger.W("Validation fail - progression event - progression01: Cannot be (null), empty or above 64 characters. String: " + progression01);
return false;
}
if (!ValidateEventPartCharacters(progression01))
{
GALogger.W("Validation fail - progression event - progression01: Cannot contain other characters than A-z, 0-9, -_., ()!?. String: " + progression01);
return false;
}
// progression02
if (!string.IsNullOrEmpty(progression02))
{
if (!ValidateEventPartLength(progression02, true))
{
GALogger.W("Validation fail - progression event - progression02: Cannot be empty or above 64 characters. String: " + progression02);
return false;
}
if (!ValidateEventPartCharacters(progression02))
{
GALogger.W("Validation fail - progression event - progression02: Cannot contain other characters than A-z, 0-9, -_., ()!?. String: " + progression02);
return false;
}
}
// progression03
if (!string.IsNullOrEmpty(progression03))
{
if (!ValidateEventPartLength(progression03, true))
{
GALogger.W("Validation fail - progression event - progression03: Cannot be empty or above 64 characters. String: " + progression03);
return false;
}
if (!ValidateEventPartCharacters(progression03))
{
GALogger.W("Validation fail - progression event - progression03: Cannot contain other characters than A-z, 0-9, -_., ()!?. String: " + progression03);
return false;
}
}
return true;
}
public static bool ValidateDesignEvent(string eventId, double value)
{
if (!ValidateEventIdLength(eventId))
{
GALogger.W("Validation fail - design event - eventId: Cannot be (null) or empty. Only 5 event parts allowed seperated by :. Each part need to be 32 characters or less. String: " + eventId);
return false;
}
if (!ValidateEventIdCharacters(eventId))
{
GALogger.W("Validation fail - design event - eventId: Non valid characters. Only allowed A-z, 0-9, -_., ()!?. String: " + eventId);
return false;
}
// value: allow 0, negative and nil (not required)
return true;
}
public static bool ValidateErrorEvent(EGAErrorSeverity severity, string message)
{
if (severity == EGAErrorSeverity.Undefined)
{
GALogger.W("Validation fail - error event - severity: Severity was unsupported value.");
return false;
}
if (!ValidateLongString(message, true))
{
GALogger.W("Validation fail - error event - message: Message cannot be above 8192 characters.");
return false;
}
return true;
}
public static bool ValidateSdkErrorEvent(string gameKey, string gameSecret, EGASdkErrorType type)
{
if(!ValidateKeys(gameKey, gameSecret))
{
return false;
}
if (type == EGASdkErrorType.Undefined)
{
GALogger.W("Validation fail - sdk error event - type: Type was unsupported value.");
return false;
}
return true;
}
public static bool ValidateKeys(string gameKey, string gameSecret)
{
if (GAUtilities.StringMatch(gameKey, "^[A-z0-9]{32}$"))
{
if (GAUtilities.StringMatch(gameSecret, "^[A-z0-9]{40}$"))
{
return true;
}
}
return false;
}
public static bool ValidateCurrency(string currency)
{
if (string.IsNullOrEmpty(currency))
{
return false;
}
if (!GAUtilities.StringMatch(currency, "^[A-Z]{3}$"))
{
return false;
}
return true;
}
public static bool ValidateEventPartLength(string eventPart, bool allowNull)
{
if (allowNull == true && string.IsNullOrEmpty(eventPart))
{
return true;
}
if (string.IsNullOrEmpty(eventPart))
{
return false;
}
if (eventPart.Length > 64)
{
return false;
}
return true;
}
public static bool ValidateEventPartCharacters(string eventPart)
{
if (!GAUtilities.StringMatch(eventPart, "^[A-Za-z0-9\\s\\-_\\.\\(\\)\\!\\?]{1,64}$"))
{
return false;
}
return true;
}
public static bool ValidateEventIdLength(string eventId)
{
if (string.IsNullOrEmpty(eventId))
{
return false;
}
if (!GAUtilities.StringMatch(eventId, "^[^:]{1,64}(?::[^:]{1,64}){0,4}$"))
{
return false;
}
return true;
}
public static bool ValidateEventIdCharacters(string eventId)
{
if (string.IsNullOrEmpty(eventId))
{
return false;
}
if (!GAUtilities.StringMatch(eventId, "^[A-Za-z0-9\\s\\-_\\.\\(\\)\\!\\?]{1,64}(:[A-Za-z0-9\\s\\-_\\.\\(\\)\\!\\?]{1,64}){0,4}$"))
{
return false;
}
return true;
}
public static JSONObject ValidateAndCleanInitRequestResponse(JSONNode initResponse)
{
// make sure we have a valid dict
if (initResponse == null)
{
GALogger.W("validateInitRequestResponse failed - no response dictionary.");
return null;
}
JSONObject validatedDict = new JSONObject();
// validate enabled field
try
{
validatedDict.Add("enabled", new JSONBool(initResponse["enabled"].IsBoolean ? initResponse["enabled"].AsBool : true));
}
catch (Exception)
{
GALogger.W("validateInitRequestResponse failed - invalid type in 'enabled' field.");
return null;
}
// validate server_ts
try
{
long serverTsNumber = initResponse["server_ts"].IsNumber ? initResponse["server_ts"].AsLong : -1;
if (serverTsNumber > 0)
{
validatedDict.Add("server_ts", new JSONNumber(serverTsNumber));
}
}
catch (Exception e)
{
GALogger.W("validateInitRequestResponse failed - invalid type in 'server_ts' field. type=" + initResponse["server_ts"].GetType() + ", value=" + initResponse["server_ts"] + ", " + e);
return null;
}
// validate configurations field
try
{
validatedDict.Add("configurations", initResponse["configurations"].IsArray ? initResponse["configurations"].AsArray : new JSONArray());
}
catch (Exception e)
{
GALogger.W("validateInitRequestResponse failed - invalid type in 'configurations' field. type=" + initResponse["configurations"].GetType() + ", value=" + initResponse["configurations"] + ", " + e);
return null;
}
return validatedDict;
}
public static bool ValidateBuild(string build)
{
if (!ValidateShortString(build, false))
{
return false;
}
return true;
}
public static bool ValidateSdkWrapperVersion(string wrapperVersion)
{
if (!GAUtilities.StringMatch(wrapperVersion, "^(unity) [0-9]{0,5}(\\.[0-9]{0,5}){0,2}$"))
{
return false;
}
return true;
}
public static bool ValidateEngineVersion(string engineVersion)
{
if (engineVersion == null || !GAUtilities.StringMatch(engineVersion, "^(unity) [0-9]{0,5}(\\.[0-9]{0,5}){0,2}$"))
{
return false;
}
return true;
}
public static bool ValidateUserId(string uId)
{
if (!ValidateString(uId, false))
{
GALogger.W("Validation fail - user id: id cannot be (null), empty or above 64 characters.");
return false;
}
return true;
}
public static bool ValidateShortString(string shortString, bool canBeEmpty)
{
// String is allowed to be empty or nil
if (canBeEmpty && string.IsNullOrEmpty(shortString))
{
return true;
}
if (string.IsNullOrEmpty(shortString) || shortString.Length > 32)
{
return false;
}
return true;
}
public static bool ValidateString(string s, bool canBeEmpty)
{
// String is allowed to be empty or nil
if (canBeEmpty && string.IsNullOrEmpty(s))
{
return true;
}
if (string.IsNullOrEmpty(s) || s.Length > 64)
{
return false;
}
return true;
}
public static bool ValidateLongString(string longString, bool canBeEmpty)
{
// String is allowed to be empty
if (canBeEmpty && string.IsNullOrEmpty(longString))
{
return true;
}
if (string.IsNullOrEmpty(longString) || longString.Length > 8192)
{
return false;
}
return true;
}
public static bool ValidateConnectionType(string connectionType)
{
return GAUtilities.StringMatch(connectionType, "^(wwan|wifi|lan|offline)$");
}
public static bool ValidateCustomDimensions(params string[] customDimensions)
{
return ValidateArrayOfStrings(20, 32, false, "custom dimensions", customDimensions);
}
public static bool ValidateResourceCurrencies(params string[] resourceCurrencies)
{
if (!ValidateArrayOfStrings(20, 64, false, "resource currencies", resourceCurrencies))
{
return false;
}
// validate each string for regex
foreach (string resourceCurrency in resourceCurrencies)
{
if (!GAUtilities.StringMatch(resourceCurrency, "^[A-Za-z]+$"))
{
GALogger.W("resource currencies validation failed: a resource currency can only be A-Z, a-z. String was: " + resourceCurrency);
return false;
}
}
return true;
}
public static bool ValidateResourceItemTypes(params string[] resourceItemTypes)
{
if (!ValidateArrayOfStrings(20, 32, false, "resource item types", resourceItemTypes))
{
return false;
}
// validate each resourceItemType for eventpart validation
foreach (string resourceItemType in resourceItemTypes)
{
if (!ValidateEventPartCharacters(resourceItemType))
{
GALogger.W("resource item types validation failed: a resource item type cannot contain other characters than A-z, 0-9, -_., ()!?. String was: " + resourceItemType);
return false;
}
}
return true;
}
public static bool ValidateDimension01(string dimension01)
{
// allow nil
if (string.IsNullOrEmpty(dimension01))
{
return true;
}
if (!GAState.HasAvailableCustomDimensions01(dimension01))
{
return false;
}
return true;
}
public static bool ValidateDimension02(string dimension02)
{
// allow nil
if (string.IsNullOrEmpty(dimension02))
{
return true;
}
if (!GAState.HasAvailableCustomDimensions02(dimension02))
{
return false;
}
return true;
}
public static bool ValidateDimension03(string dimension03)
{
// allow nil
if (string.IsNullOrEmpty(dimension03))
{
return true;
}
if (!GAState.HasAvailableCustomDimensions03(dimension03))
{
return false;
}
return true;
}
public static bool ValidateArrayOfStrings(long maxCount, long maxStringLength, bool allowNoValues, string logTag, params string[] arrayOfStrings)
{
string arrayTag = logTag;
// use arrayTag to annotate warning log
if (string.IsNullOrEmpty(arrayTag))
{
arrayTag = "Array";
}
if(arrayOfStrings == null)
{
GALogger.W(arrayTag + " validation failed: array cannot be null. ");
return false;
}
// check if empty
if (allowNoValues == false && arrayOfStrings.Length == 0)
{
GALogger.W(arrayTag + " validation failed: array cannot be empty. ");
return false;
}
// check if exceeding max count
if (maxCount > 0 && arrayOfStrings.Length > maxCount)
{
GALogger.W(arrayTag + " validation failed: array cannot exceed " + maxCount + " values. It has " + arrayOfStrings.Length + " values.");
return false;
}
// validate each string
foreach (string arrayString in arrayOfStrings)
{
int stringLength = arrayString == null ? 0 : arrayString.Length;
// check if empty (not allowed)
if (stringLength == 0)
{
GALogger.W(arrayTag + " validation failed: contained an empty string.");
return false;
}
// check if exceeding max length
if (maxStringLength > 0 && stringLength > maxStringLength)
{
GALogger.W(arrayTag + " validation failed: a string exceeded max allowed length (which is: " + maxStringLength + "). String was: " + arrayString);
return false;
}
}
return true;
}
public static bool ValidateFacebookId(string facebookId)
{
if (!ValidateString(facebookId, false))
{
GALogger.W("Validation fail - facebook id: id cannot be (null), empty or above 64 characters.");
return false;
}
return true;
}
public static bool ValidateGender(EGAGender gender)
{
if (gender == EGAGender.Undefined || !(gender == EGAGender.Male || gender == EGAGender.Female))
{
GALogger.W("Validation fail - gender: Has to be 'male' or 'female'.");
return false;
}
return true;
}
public static bool ValidateBirthyear(long birthYear)
{
if (birthYear < 0 || birthYear > 9999)
{
GALogger.W("Validation fail - birthYear: Cannot be (null) or invalid range.");
return false;
}
return true;
}
public static bool ValidateClientTs(long clientTs)
{
// TODO(nikolaj): validate other way? (instead of max possible)
if (clientTs < (long.MinValue+1) || clientTs > (long.MaxValue-1))
{
return false;
}
return true;
}
#endregion // Public methods
}
}