using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using UnityEditor;
using UnityEngine;
using UnityEngine.Networking;
namespace JustTrack
{
///
/// Utility class for justtrack SDK functionality.
///
internal class JustTrackUtils
{
///
/// The download page URL for the External Dependency Manager.
///
internal const string EdmDownloadPage = "https://developers.google.com/unity/archive#external_dependency_manager_for_unity";
///
/// The direct download URL for the External Dependency Manager package.
///
internal const string EdmPackageUrl = "https://github.com/googlesamples/unity-jar-resolver/raw/v1.2.183/external-dependency-manager-1.2.183.unitypackage";
///
/// Gets the JustTrack settings from the asset database.
///
/// The JustTrack settings or null if not found.
internal static JustTrackSettings GetSettings()
{
return AssetDatabase.LoadAssetAtPath(JustTrackSettings.JustTrackSettingsPath);
}
///
/// Gets existing settings or creates new ones if they don't exist.
///
/// The JustTrack settings or null if they couldn't be loaded.
internal static JustTrackSettings? GetOrCreateSettings()
{
var settings = GetSettings();
if (settings == null)
{
if (File.Exists(JustTrackSettings.JustTrackSettingsPath))
{
return null;
}
settings = ScriptableObject.CreateInstance();
settings.AndroidApiToken = string.Empty;
settings.IosApiToken = string.Empty;
PersistSettings(settings);
return settings;
}
return settings;
}
private static void PersistSettings(JustTrackSettings settings)
{
Directory.CreateDirectory(JustTrackSettings.JustTrackSettingsDirectory);
AssetDatabase.CreateAsset(settings, JustTrackSettings.JustTrackSettingsPath);
AssetDatabase.SaveAssets();
}
///
/// Gets a serialized object for the JustTrack settings.
///
/// Serialized settings object or null if settings couldn't be loaded.
internal static SerializedObject? GetSerializedSettings()
{
var settings = GetOrCreateSettings();
if (settings == null)
{
return null;
}
return new SerializedObject(settings);
}
///
/// Sets the always update injected code flag in settings.
///
/// True to always update injected code, false otherwise.
internal static void SetAlwaysUpdateInjectedCode(bool value)
{
var serializedObject = GetSerializedSettings();
if (serializedObject == null)
{
Debug.LogError("no justtrack settings could be loaded");
return;
}
SerializedProperty alwaysUpdateInjectedCode = serializedObject.FindProperty("AlwaysUpdateInjectedCode");
alwaysUpdateInjectedCode.boolValue = value;
serializedObject.ApplyModifiedProperties();
AssetDatabase.SaveAssets();
}
///
/// Result of a validation operation.
///
internal struct ValidationResult
{
///
/// List of validation warnings.
///
internal List Warnings;
///
/// List of validation errors.
///
internal List Errors;
}
///
/// Validation mode for different platforms.
///
internal enum ValidationMode
{
///
/// Validate Android platform only.
///
ValidateAndroid,
///
/// Validate iOS platform only.
///
ValidateIOS,
///
/// Validate all platforms.
///
ValidateAll,
}
///
/// Validates JustTrack settings for all platforms.
///
/// The settings to validate.
/// Callback to invoke with validation results.
internal static void Validate(JustTrackSettings settings, Action callback)
{
CoroutineRuntime.StartCoroutine(ValidateAsync(settings, ValidationMode.ValidateAll, callback));
}
///
/// Asynchronously validates JustTrack settings.
///
/// The settings to validate.
/// The validation mode to use.
/// Callback to invoke with validation results.
/// Coroutine enumerator.
internal static IEnumerator ValidateAsync(JustTrackSettings settings, ValidationMode mode, Action callback)
{
ValidationResult result;
result.Warnings = new List();
result.Errors = new List();
var androidErrors = mode == ValidationMode.ValidateIOS ? result.Warnings : result.Errors;
var iosErrors = mode == ValidationMode.ValidateAndroid ? result.Warnings : result.Errors;
// Use custom bundle ID if set, otherwise fall back to PlayerSettings
string androidPackageId = !string.IsNullOrEmpty(settings.AndroidBundleId) ? settings.AndroidBundleId : PlayerSettings.GetApplicationIdentifier(BuildTargetGroup.Android);
string iosPackageId = !string.IsNullOrEmpty(settings.IosBundleId) ? settings.IosBundleId : PlayerSettings.GetApplicationIdentifier(BuildTargetGroup.iOS);
foreach (var step in CoroutineRuntime.ToEnumerable(ValidateToken(androidPackageId, settings.AndroidApiToken, "Android", result.Warnings, androidErrors)))
{
yield return step;
}
foreach (var step in CoroutineRuntime.ToEnumerable(ValidateToken(iosPackageId, settings.IosApiToken, "iOS", result.Warnings, iosErrors)))
{
yield return step;
}
if (string.IsNullOrEmpty(settings.AndroidApiToken) && string.IsNullOrEmpty(settings.IosApiToken))
{
result.Errors.Add("Neither Android nor iOS API tokens are configured. You need to configure an API token for each platform to use the justtrack SDK");
}
ValidateTrackingSettings(!string.IsNullOrEmpty(settings.IosApiToken), settings.IosTrackingSettings, result.Warnings, iosErrors);
if (!DetectExternalDependencyManager())
{
result.Errors.Add("The External Dependency Manager for Unity is missing. This is a required dependency of the justtrack SDK. Please go to " + EdmDownloadPage + " and download version 1.2.167 or later.");
}
callback(result);
yield break;
}
private static IEnumerator ValidateToken(string packageId, string token, string platform, List warnings, List errors)
{
if (string.IsNullOrEmpty(token))
{
yield break;
}
var parts = token.Split('-');
if (parts.Length != 2)
{
if (token.Length != 64)
{
errors.Add(platform + " API token has length " + token.Length + ", expected a string of length 64");
}
}
else if (parts[0] != "prod" && parts[0] != "sandbox")
{
errors.Add(platform + " API token starts with unknown environment '" + parts[0] + "', expected 'prod' or 'sandbox'");
}
else if (parts[1].Length != 64)
{
errors.Add(platform + " API token has " + parts[1].Length + " instead of 64 characters of token data");
}
foreach (var step in CoroutineRuntime.ToEnumerable(DoVerifyApiToken(packageId, token, (success, valid, errorMessage) =>
{
if (!success)
{
warnings.Add(platform + " API token could not be verified. Are you offline?");
}
else if (!valid)
{
string errorDetail = string.IsNullOrEmpty(errorMessage) ?
"Please follow the instructions at https://docs.justtrack.io/sdk/unity-sdk-readme#getting-an-api-token to obtain a valid API token" :
errorMessage;
errors.Add(platform + " API token is not valid. " + errorDetail);
}
})))
{
yield return step;
}
}
private static void ValidateTrackingSettings(bool activeOnIOS, iOSTrackingSettings settings, List warnings, List errors)
{
if (!activeOnIOS && settings.RequestTrackingPermission)
{
errors.Add("You configured automatically requesting App Tracking Transparency opt-in, but there is no API token configured for iOS");
}
else if (settings.RequestTrackingPermission && string.IsNullOrEmpty(settings.TrackingPermissionDescription) && !settings.TrackingPermissionDescriptionManaged)
{
warnings.Add("You configured automatically requesting App Tracking Transparency opt-in, but there isn't a description configured. You have to set NSUserTrackingUsageDescription yourself.");
}
else if (!settings.RequestTrackingPermission && !string.IsNullOrEmpty(settings.TrackingPermissionDescription))
{
warnings.Add("You have not configured automatically requesting App Tracking Transparency opt-in, but there is a description configured. This will overwrite NSUserTrackingUsageDescription.");
}
switch (settings.FacebookAudienceNetworkIntegration)
{
case FacebookAudienceNetworkIntegration.NoIntegration:
if (settings.RequestTrackingPermission && Reflection.HasFacebookAudienceNetworkUnity())
{
errors.Add("You have not enabled the automatic integration with the Unity version of the Facebook Audience Network, but it was deteted in your game.");
}
break;
case FacebookAudienceNetworkIntegration.UnityIntegration:
if (!settings.RequestTrackingPermission)
{
errors.Add("The automatic integration with the Unity version of the Facebook Audience Network will not be performed. Enable requesting the tracking permission for it to be performed.");
}
else if (!Reflection.HasFacebookAudienceNetworkUnity())
{
errors.Add("The automatic integration with the Unity version of the Facebook Audience Network might not work. No assembly was found containing the code of the FAN.");
}
break;
case FacebookAudienceNetworkIntegration.NativeIntegration:
if (!settings.RequestTrackingPermission)
{
errors.Add("The automatic integration with the native version of the Facebook Audience Network will not be performed. Enable requesting the tracking permission for it to be performed.");
}
else if (Reflection.HasFacebookAudienceNetworkUnity())
{
errors.Add("You have chosen to integrate with the native version of the Facebook Audience Network, but the Unity version of it was detected.");
}
break;
}
}
///
/// Called when Unity is loaded to initialize justtrack SDK.
///
/// The JustTrack settings to use.
internal static void OnUnityLoaded(JustTrackSettings settings)
{
RunCodeGenerator(settings, GetFacebookAudienceNetworkIntegration(settings), false);
JustTrackCodeGenerator.GenerateDependencyFile();
}
///
/// Runs the code generator with specified settings.
///
/// The JustTrack settings to use.
/// Facebook Audience Network integration settings.
/// True to force code generation, false otherwise.
internal static void RunCodeGenerator(JustTrackSettings settings, FacebookAudienceNetworkIntegration facebookAudienceNetworkIntegration, bool force)
{
JustTrackCodeGenerator.Run(facebookAudienceNetworkIntegration, settings, force);
}
///
/// Configures preprocessor defines for the specified build target group.
///
/// The build target group to configure defines for.
internal static void ConfigurePreprocessorDefines(BuildTargetGroup targetGroup)
{
string defines = PlayerSettings.GetScriptingDefineSymbolsForGroup(targetGroup);
string newDefines = GeneratePreprocessorDefines(defines, Enum.GetName(typeof(BuildTargetGroup), targetGroup));
if (newDefines != defines)
{
PlayerSettings.SetScriptingDefineSymbolsForGroup(targetGroup, newDefines);
try
{
// Sometimes the defines are not updated directly... we have to press ctrl+s first. Avoid that need by doing it automatically.
EditorApplication.ExecuteMenuItem("File/Save Project");
}
catch (Exception)
{
Debug.LogError("Could not save project automatically. Please save project by hand to ensure preprocessor defines are up to date.");
}
}
}
private static string GeneratePreprocessorDefines(string currentDefines, string platform)
{
return BuildPreprocessorDefines(currentDefines, platform, new Dictionary
{
// keep them for now to ensure we drop those defines again, remove in 2024 or so
["JUSTTRACK_APPSFLYER_INTEGRATION"] = false,
["JUSTTRACK_IRONSOURCE_INTEGRATION"] = false,
});
}
private static string BuildPreprocessorDefines(string currentDefines, string platform, Dictionary wantedDefines)
{
List defines = new List();
foreach (var define in currentDefines.Split(';'))
{
if (wantedDefines.ContainsKey(define))
{
// define managed by us
if (wantedDefines[define])
{
// we want to keep it
defines.Add(define);
}
else
{
Debug.Log("Dropping " + define + " preprocessor symbol for platform " + platform);
}
// remove it in any case, afterwards we don't need to iterate over the defines again
wantedDefines.Remove(define);
}
else
{
// unknown define, keep it as it is
defines.Add(define);
}
}
foreach (var wantedDefine in wantedDefines)
{
if (wantedDefine.Value)
{
// we wanted to have this, but didn't find it yet, so add it
defines.Add(wantedDefine.Key);
Debug.Log("Adding " + wantedDefine.Key + " preprocessor symbol for platform " + platform);
}
}
return string.Join(";", defines.ToArray());
}
///
/// Gets the Facebook Audience Network integration setting from settings.
///
/// The JustTrack settings to read from.
/// The Facebook Audience Network integration setting.
internal static FacebookAudienceNetworkIntegration GetFacebookAudienceNetworkIntegration(JustTrackSettings settings)
{
if (string.IsNullOrEmpty(settings.IosApiToken))
{
return FacebookAudienceNetworkIntegration.NoIntegration;
}
return settings.IosTrackingSettings.FacebookAudienceNetworkIntegration;
}
///
/// Checks if IL2CPP scripting backend is used for the specified platform.
///
/// True to check Android platform, false for iOS.
/// True if IL2CPP is used, false otherwise.
internal static bool IsIL2CPP(bool isAndroid)
{
var backend = PlayerSettings.GetScriptingBackend(isAndroid ? BuildTargetGroup.Android : BuildTargetGroup.iOS);
return backend == ScriptingImplementation.IL2CPP;
}
///
/// Detects if the External Dependency Manager is present.
///
/// True if External Dependency Manager is detected, false otherwise.
internal static bool DetectExternalDependencyManager()
{
var edmAssemblies = new List(new string[]
{
"Google.PackageManagerResolver",
"Google.VersionHandler",
"Google.JarResolver",
"Google.IOSResolver",
"Google.VersionHandlerImpl",
});
try
{
var assemblies = AppDomain.CurrentDomain.GetAssemblies();
foreach (var assembly in assemblies)
{
if (edmAssemblies.Contains(assembly.GetName().Name))
{
return true;
}
}
return false;
}
catch (System.Exception)
{
return false;
}
}
///
/// Imports a Unity package from the specified URL.
///
/// The URL to download the package from.
/// Coroutine enumerator.
internal static IEnumerator ImportPackageFromUrl(string url)
{
UnityWebRequest request = UnityWebRequest.Get(url);
yield return request.SendWebRequest();
while (!request.isDone || request.responseCode == -1)
{
yield return null; // what we yield is ignored anyway
}
if (request.responseCode < 200 || request.responseCode > 299)
{
Debug.LogError("Failed to download url: " + request.error + " with status " + request.responseCode);
yield break;
}
string tempFile = Path.GetTempFileName();
File.WriteAllBytes(tempFile, request.downloadHandler.data);
AssetDatabase.ImportPackage(tempFile, true);
// sadly we can't clean up the temp file as importing the EDM will cause a recompile of the scripts, causing this
// routine to be unloaded before we can do anything else...
}
///
/// Cache of API token verification results.
///
#pragma warning disable SA1401 // Fields should be private
internal static Dictionary VerificationResults = new Dictionary();
#pragma warning restore SA1401
///
/// Verifies an API token for the specified package ID.
///
/// The package ID to verify for.
/// The API token to verify.
/// Action to call when verification completes.
/// The current verification result.
internal static VerificationResultData VerifyApiToken(string packageId, string apiToken, Action repaint)
{
if (string.IsNullOrEmpty(packageId) || string.IsNullOrEmpty(apiToken))
{
return VerificationResultData.Missing();
}
var key = packageId + "\u2063" + apiToken;
lock (VerificationResults)
{
if (VerificationResults.ContainsKey(key))
{
return VerificationResults[key];
}
VerificationResults[key] = VerificationResultData.Pending();
}
CoroutineRuntime.StartCoroutine(DoVerifyApiToken(packageId, apiToken, (success, valid, errorMessage) =>
{
lock (VerificationResults)
{
if (success)
{
VerificationResults[key] = valid ? VerificationResultData.Valid() : VerificationResultData.Invalid(errorMessage);
}
else
{
VerificationResults[key] = VerificationResultData.Unknown();
}
}
repaint();
}));
return VerificationResultData.Pending();
}
#pragma warning disable SA1401 // Fields should be private (required for JSON serialization)
[System.Serializable]
private class ApiErrorResponse
{
public string Err = string.Empty;
}
#pragma warning restore SA1401
private static IEnumerator DoVerifyApiToken(string packageId, string apiToken, Action callback)
{
string url, token;
if (apiToken.StartsWith("prod-"))
{
url = "https://ipv4.justtrack.io/v0/sign";
token = apiToken.Substring("prod-".Length);
}
else if (apiToken.StartsWith("sandbox-"))
{
url = $"https://ipv4.{LocalCredentials.SandboxRoot}/v0/sign";
token = apiToken.Substring("sandbox-".Length);
}
else
{
url = "https://ipv4.justtrack.io/v0/sign";
token = apiToken;
}
if (apiToken.Contains(" "))
{
callback(true, false, "API token contains spaces");
yield break;
}
var request = UnityWebRequest.Get(url);
request.SetRequestHeader("X-CLIENT-ID", packageId);
request.SetRequestHeader("X-CLIENT-TOKEN", token);
yield return request.SendWebRequest();
while (!request.isDone || request.responseCode == -1)
{
yield return null; // what we yield is ignored anyway
}
var success = request.result == UnityWebRequest.Result.Success || request.result == UnityWebRequest.Result.ProtocolError;
var valid = request.responseCode >= 200 && request.responseCode <= 299 && request.result == UnityWebRequest.Result.Success;
string errorMessage = "Your API token is invalid. Please follow the instructions at https://docs.justtrack.io/sdk/latest/overview/find-your-justtrack-token to obtain a valid API token.";
if (!valid && !string.IsNullOrEmpty(request.downloadHandler.text))
{
try
{
ApiErrorResponse errorResponse = JsonUtility.FromJson(request.downloadHandler.text);
if (errorResponse != null && !string.IsNullOrEmpty(errorResponse.Err))
{
errorMessage = errorResponse.Err;
}
}
catch (Exception)
{
errorMessage += " Failed to parse request body.";
}
}
callback(success, valid, errorMessage);
}
}
}