(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

View File

@@ -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
}
}

View File

@@ -0,0 +1,13 @@
namespace GameAnalyticsSDK.Net
{
public enum EGAErrorSeverity
{
Undefined = 0,
Debug = 1,
Info = 2,
Warning = 3,
Error = 4,
Critical = 5
}
}

View File

@@ -0,0 +1,10 @@
namespace GameAnalyticsSDK.Net
{
public enum EGAGender
{
Undefined = 0,
Male = 1,
Female = 2
}
}

View File

@@ -0,0 +1,11 @@
namespace GameAnalyticsSDK.Net
{
public enum EGALoggerMessageType
{
Error,
Warning,
Info,
Debug
}
}

View File

@@ -0,0 +1,11 @@
namespace GameAnalyticsSDK.Net
{
public enum EGAProgressionStatus
{
Undefined = 0,
Start = 1,
Complete = 2,
Fail = 3
}
}

View File

@@ -0,0 +1,10 @@
namespace GameAnalyticsSDK.Net
{
public enum EGAResourceFlowType
{
Undefined = 0,
Source = 1,
Sink = 2
}
}

View File

@@ -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
}
}

View File

@@ -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
}
}

View File

@@ -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

View File

@@ -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
}
}

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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>

View File

@@ -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>

View File

@@ -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
}
}

View File

@@ -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
}
}

View File

@@ -0,0 +1,9 @@
namespace GameAnalyticsSDK.Net.Http
{
internal enum EGASdkErrorType
{
Undefined = 0,
Rejected = 1
}
}

View File

@@ -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
}
}

View File

@@ -0,0 +1,11 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace GameAnalyticsSDK.Net
{
public interface ICommandCenterListener
{
void OnCommandCenterUpdated();
}
}

View File

@@ -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

View File

@@ -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
}
}

View File

@@ -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;
}
}
}
}

View File

@@ -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
}
}
}

View File

@@ -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); }
}
}
}

View File

@@ -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 + "}";
}
}
}

View File

@@ -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

View File

@@ -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
}

View File

@@ -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
}
}

View File

@@ -0,0 +1,35 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.1</TargetFramework>
<AssemblyName>GameAnalytics.NetStandard</AssemblyName>
<RootNamespace>GameAnalytics.Net</RootNamespace>
<Platforms>AnyCPU;x64</Platforms>
<Authors>Game Analytics</Authors>
<Copyright>Copyright (c) 2016 Game Analytics</Copyright>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
<DefineConstants>TRACE;MONO</DefineConstants>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
<DefineConstants>TRACE;MONO</DefineConstants>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
<DefineConstants>TRACE;MONO</DefineConstants>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
<DefineConstants>TRACE;MONO</DefineConstants>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="NLog" Version="4.6.7" />
<PackageReference Include="System.Data.SQLite" Version="1.0.111" />
</ItemGroup>
<Import Project="..\GA-SDK-MONO-SHARED\GA-SDK-MONO-SHARED.projitems" Label="Shared" />
</Project>

View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2016 Game Analytics
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.