commit a0532a5cedea5d4cd244441e36943b404c4eca8b Author: HuYufei Date: Tue Dec 26 12:22:19 2023 +0800 init commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e6c2593 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +# Mac auto-generated system files +*.DS_Store* diff --git a/ChangeLog.md b/ChangeLog.md new file mode 100644 index 0000000..25d2d57 --- /dev/null +++ b/ChangeLog.md @@ -0,0 +1,65 @@ +# Guru Template Change Log + +## 2.0.3 + +- Firebase + - 整体升级到 10.1.1. 作为Unity组统一标准 +- Max 广告聚合 + - 升级Max聚合的所有渠道, 对齐[中台需求](https://docs.google.com/spreadsheets/d/161UnDimGerqetIYNiMCfUBmJ7qozht8z1baxnxRdCnI/edit#gid=311231520) +- GuruBuildTools + - 新增 Deps Reporter, 用于在Jenkins环境, 上报打包依赖 (Android/iOS). 理论上报告工具不影响本地打包, 不影响打包机打包. 出问题的话也不会中断打包流程. + - 修复 IOS 打包成功后, 上传TF时报Info.plist内没有设置版本号的报错 +- GuruAds + - 根据中台的需求, 新增广告属性打点: `bads_imp`, `bads_loaded` + - 去掉使用三方广告SDK, 需要预先设置 AD_AMAZON 和 AD_PUBMATIC 的流程. 目前默认全部接入 +- GuruAnalytics + - 更新和添加 Worker 支持, 预期可让用户在线时长更加精准 +- DeviceUtil + - 更新Android端获取系统版本号的的接口, 方便程序判断 Android API 33 以及后继的响应 + + +## 2.0.2 + +各模块对应的内容更新 + +- FBService + - 添加IOS平台打点开启语句 `FB.Mobile.SetAdvertiserTrackingEnabled(true);` +- AdjustService + - 自动创建生命周期对象, 优化部分逻辑 +- Firebase + - 修复 Firebase(8.1.0) 无法生成 google-service.json 的问题. 替换了Firebase.Editor.dll +- Guru L10n 更新 + - 更新GuruL10N -> 0.4.0 新增单独翻译google sheet的设置 + - 更新插件 backup.csv 缓存路径, 请各个项目将 **`backup.csv`** 提交到代码内进行跟踪! + - 更新了语言编码去重识别, 根据Alpha表重排的功能 +- Guru Analytics + - 广告打点中加入了 Waterfall 信息上报 + - 添加了用户属性缓存表 +- Guru Build Tools + - 新增 DebugView 联调参数注入, 需要项目安装验证 + - 更新打包管线相关逻辑, IOS Target Version 升级为 13.0 +- RemoteConfig + - 修复了AROConfig的初始化逻辑, 避免初始化时报空引发报错 +- IAPService + - 添加了本地的订单数据缓存, 防止特殊操作的时候, iOS的订单重复上报的问题. + + +## 2.0.1 +- 更新自打点库原生依赖 + - Android 原生库版本 (0.2.10) + - IOS 原生库版本 (0.2.16) +- 更新 L10n 版本 (3.2.0) + - 更新了导入表格会把code行也导入进来的BUG + - 更新配置多语言时 nb-NO 语言的识别功能 + + +## 1.6.0 +- 添加了 Amazon (1.0.0) 广告扩展,使用说明详见: [GuruAds 扩展 Amazon安装说明](GuruAds/README.md) +- 添加了 GuruAnalytics (1.0.3) 广告自打点插件,使用说明详见: [Guru Unity Analytics 自打点插件](GuruAnalytics/README.md) +- 添加了 StandardProperties 标准属性点类, 用于记录游戏内的标准事件和属性 +- 添加了 GuruBuildTool 模块, 收集和整理了目前为止所有 BuildTool 相关的逻辑 +- 扩展了GuruSettings的配置格式(Amazon广告配置相关) +- 在 AdjustService 内添加获取 AdjustId 以及 FirebaseID 的逻辑 +- 在 AdjustService 内添加上报 DeviceID 的逻辑 +- 修复了若干插件显示层以及构建管线的BUG +- 调整了框架整体的文件结构 \ No newline at end of file diff --git a/ChangeLog.md.meta b/ChangeLog.md.meta new file mode 100644 index 0000000..6d7194a --- /dev/null +++ b/ChangeLog.md.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 6acd89dd2c454676834609a8981c136a +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Editor.meta b/Editor.meta new file mode 100644 index 0000000..66fee68 --- /dev/null +++ b/Editor.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: e3ff74f6837b4366b71c8bfe322a4562 +timeCreated: 1671010724 \ No newline at end of file diff --git a/Editor/PostBuildProcess.meta b/Editor/PostBuildProcess.meta new file mode 100644 index 0000000..c8d6987 --- /dev/null +++ b/Editor/PostBuildProcess.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: c2bda7db652148e7a73fd2179e22be09 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Editor/PostBuildProcess/AND_POST_PROGUARD.meta b/Editor/PostBuildProcess/AND_POST_PROGUARD.meta new file mode 100644 index 0000000..04d17c9 --- /dev/null +++ b/Editor/PostBuildProcess/AND_POST_PROGUARD.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 347116c7f46946f5945ca097a6e964db +timeCreated: 1687241187 \ No newline at end of file diff --git a/Editor/PostBuildProcess/AND_POST_PROGUARD/UserProguardHelper.cs b/Editor/PostBuildProcess/AND_POST_PROGUARD/UserProguardHelper.cs new file mode 100644 index 0000000..1d5b186 --- /dev/null +++ b/Editor/PostBuildProcess/AND_POST_PROGUARD/UserProguardHelper.cs @@ -0,0 +1,83 @@ +#if UNITY_ANDROID +namespace Guru +{ + using System.Collections.Generic; + using System.IO; + using System.Linq; + using UnityEditor; + using UnityEditor.Build; + using UnityEditor.Build.Reporting; + using UnityEngine; + + /// + /// Android混淆器内容填充 + /// 于应用构建前执行 + /// + public class UserProguardHelper: IPreprocessBuildWithReport + { + public int callbackOrder { get; } = 0; + public void OnPreprocessBuild(BuildReport report) + { + // 修复ProguardFile + OnApplyProguardFiles(); + } + + private static void OnApplyProguardFiles() + { + string proguardPath = $"{Application.dataPath}/Plugins/Android/proguard-user.txt"; + if (File.Exists(proguardPath)) + { + List keeps = new List(); + DirectoryInfo dir = new DirectoryInfo(Application.dataPath); + string raw = File.ReadAllText(proguardPath); + + if (dir.Exists) + { + var editors = dir.GetDirectories("Editor", SearchOption.AllDirectories); + List files = new List(); + foreach (var e in editors) + { + files.AddRange(e.GetFiles("*Proguards.txt", SearchOption.AllDirectories)); + } + + // Debug.Log($"--- Proguard Files: {files.Length}"); + + string[] lens = null; + string l = ""; + for (int i = 0; i < files.Count; i++) + { + lens = File.ReadAllLines(files[i].FullName); + foreach (var s in lens) + { + l = s.TrimStart(); + if(string.IsNullOrEmpty(l)) continue; + if(raw.Contains(l)) continue; + keeps.Add(l); + Debug.Log($"--- ✏️ Apply: [ {l} ]"); + } + } + } + + if (keeps.Count == 0) return; // 无注入文件则退出 + + List lines = File.ReadAllLines(proguardPath).ToList(); + lines.Add(""); + lines.AddRange(keeps); + + File.WriteAllLines(proguardPath, lines.ToArray()); + Debug.Log($"--- Update proguard-user.txt done! ☀️ ---"); + + } + } + + + [MenuItem("Tools/Android/Add proguard-user")] + private static void EditorAddProguardUser() + { + OnApplyProguardFiles(); + } + + } +} + +#endif \ No newline at end of file diff --git a/Editor/PostBuildProcess/AND_POST_PROGUARD/UserProguardHelper.cs.meta b/Editor/PostBuildProcess/AND_POST_PROGUARD/UserProguardHelper.cs.meta new file mode 100644 index 0000000..5c6bff9 --- /dev/null +++ b/Editor/PostBuildProcess/AND_POST_PROGUARD/UserProguardHelper.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 548800163ca249de8edcfa039836449d +timeCreated: 1687241187 \ No newline at end of file diff --git a/Editor/UniWebView.meta b/Editor/UniWebView.meta new file mode 100644 index 0000000..cdc38fb --- /dev/null +++ b/Editor/UniWebView.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: c4d05b56a8b854c2c93c1db55c5bc13b +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Editor/UniWebView/settings.asset b/Editor/UniWebView/settings.asset new file mode 100644 index 0000000..6d7fb62 --- /dev/null +++ b/Editor/UniWebView/settings.asset @@ -0,0 +1,22 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!114 &11400000 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: a403a09e241a0480a957591ea60fb785, type: 3} + m_Name: settings + m_EditorClassIdentifier: + usesCleartextTraffic: 0 + writeExternalStorage: 0 + accessFineLocation: 0 + addsKotlin: 0 + addsAndroidBrowser: 0 + enableJetifier: 1 + authCallbackUrls: [] + supportLINELogin: 0 diff --git a/Editor/UniWebView/settings.asset.meta b/Editor/UniWebView/settings.asset.meta new file mode 100644 index 0000000..5011366 --- /dev/null +++ b/Editor/UniWebView/settings.asset.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 0e00e1700888048628304b0c69a1c61e +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Keystore.meta b/Keystore.meta new file mode 100644 index 0000000..1dcad42 --- /dev/null +++ b/Keystore.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: a99ba0cf57ee14adbb2be18231643025 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Keystore/guru.jks b/Keystore/guru.jks new file mode 100644 index 0000000..99c7ccb Binary files /dev/null and b/Keystore/guru.jks differ diff --git a/Keystore/guru.jks.meta b/Keystore/guru.jks.meta new file mode 100644 index 0000000..e09df29 --- /dev/null +++ b/Keystore/guru.jks.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: a2b9d9fa87e634e99b4932bcaaec2f40 +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/LitJson.meta b/LitJson.meta new file mode 100644 index 0000000..7d8bb1b --- /dev/null +++ b/LitJson.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 0c5075eff6f44aad872f5c8160b94132 +timeCreated: 1703228621 \ No newline at end of file diff --git a/LitJson/Guru.LitJson.asmdef b/LitJson/Guru.LitJson.asmdef new file mode 100644 index 0000000..707abf6 --- /dev/null +++ b/LitJson/Guru.LitJson.asmdef @@ -0,0 +1,14 @@ +{ + "name": "Guru.LitJson", + "rootNamespace": "Guru.LitJson", + "references": [], + "includePlatforms": [], + "excludePlatforms": [], + "allowUnsafeCode": false, + "overrideReferences": false, + "precompiledReferences": [], + "autoReferenced": true, + "defineConstraints": [], + "versionDefines": [], + "noEngineReferences": false +} \ No newline at end of file diff --git a/LitJson/Guru.LitJson.asmdef.meta b/LitJson/Guru.LitJson.asmdef.meta new file mode 100644 index 0000000..9e05214 --- /dev/null +++ b/LitJson/Guru.LitJson.asmdef.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 2bd307385b74f48c2bdf31bd24d78056 +AssemblyDefinitionImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/LitJson/Runtime.meta b/LitJson/Runtime.meta new file mode 100644 index 0000000..2aebb8f --- /dev/null +++ b/LitJson/Runtime.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 63a6db46b50de451a9008227af468125 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/LitJson/Runtime/AssemblyInfo.cs.DISABLED.in b/LitJson/Runtime/AssemblyInfo.cs.DISABLED.in new file mode 100644 index 0000000..2cd1f90 --- /dev/null +++ b/LitJson/Runtime/AssemblyInfo.cs.DISABLED.in @@ -0,0 +1,18 @@ +using System; +using System.Reflection; +using System.Runtime.CompilerServices; + + +[assembly: CLSCompliant (true)] + +[assembly: AssemblyTitle ("LitJson")] +[assembly: AssemblyDescription ("LitJSON library")] +[assembly: AssemblyConfiguration ("")] +[assembly: AssemblyCompany ("")] +[assembly: AssemblyProduct ("LitJSON")] +[assembly: AssemblyCopyright ( + "The authors disclaim copyright to this source code")] +[assembly: AssemblyTrademark ("")] +[assembly: AssemblyCulture ("")] + +[assembly: AssemblyVersion ("@ASSEMBLY_VERSION@")] diff --git a/LitJson/Runtime/AssemblyInfo.cs.DISABLED.in.meta b/LitJson/Runtime/AssemblyInfo.cs.DISABLED.in.meta new file mode 100644 index 0000000..b830e27 --- /dev/null +++ b/LitJson/Runtime/AssemblyInfo.cs.DISABLED.in.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: b7cf5dc50b2d64dca9830e9144c06b2c +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/LitJson/Runtime/IJsonWrapper.cs b/LitJson/Runtime/IJsonWrapper.cs new file mode 100644 index 0000000..53e6505 --- /dev/null +++ b/LitJson/Runtime/IJsonWrapper.cs @@ -0,0 +1,60 @@ +#region Header +/** + * IJsonWrapper.cs + * Interface that represents a type capable of handling all kinds of JSON + * data. This is mainly used when mapping objects through JsonMapper, and + * it's implemented by JsonData. + * + * The authors disclaim copyright to this source code. For more details, see + * the COPYING file included with this distribution. + **/ +#endregion + + +using System.Collections; +using System.Collections.Specialized; + + +namespace Guru.LitJson +{ + public enum JsonType + { + None, + + Object, + Array, + String, + Int, + Long, + Double, + Boolean + } + + public interface IJsonWrapper : IList, IOrderedDictionary + { + bool IsArray { get; } + bool IsBoolean { get; } + bool IsDouble { get; } + bool IsInt { get; } + bool IsLong { get; } + bool IsObject { get; } + bool IsString { get; } + + bool GetBoolean (); + double GetDouble (); + int GetInt (); + JsonType GetJsonType (); + long GetLong (); + string GetString (); + + void SetBoolean (bool val); + void SetDouble (double val); + void SetInt (int val); + void SetJsonType (JsonType type); + void SetLong (long val); + void SetString (string val); + + string ToJson (); + void ToJson (JsonWriter writer); + } +} diff --git a/LitJson/Runtime/IJsonWrapper.cs.meta b/LitJson/Runtime/IJsonWrapper.cs.meta new file mode 100644 index 0000000..642a486 --- /dev/null +++ b/LitJson/Runtime/IJsonWrapper.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 394f32e20d93e474487e7f8a07415298 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/LitJson/Runtime/JsonData.cs b/LitJson/Runtime/JsonData.cs new file mode 100644 index 0000000..c464553 --- /dev/null +++ b/LitJson/Runtime/JsonData.cs @@ -0,0 +1,1059 @@ +#region Header +/** + * JsonData.cs + * Generic type to hold JSON data (objects, arrays, and so on). This is + * the default type returned by JsonMapper.ToObject(). + * + * The authors disclaim copyright to this source code. For more details, see + * the COPYING file included with this distribution. + **/ +#endregion + + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.IO; + + +namespace Guru.LitJson +{ + public class JsonData : IJsonWrapper, IEquatable + { + #region Fields + private IList inst_array; + private bool inst_boolean; + private double inst_double; + private int inst_int; + private long inst_long; + private IDictionary inst_object; + private string inst_string; + private string json; + private JsonType type; + + // Used to implement the IOrderedDictionary interface + private IList> object_list; + #endregion + + + #region Properties + public int Count { + get { return EnsureCollection ().Count; } + } + + public bool IsArray { + get { return type == JsonType.Array; } + } + + public bool IsBoolean { + get { return type == JsonType.Boolean; } + } + + public bool IsDouble { + get { return type == JsonType.Double; } + } + + public bool IsInt { + get { return type == JsonType.Int; } + } + + public bool IsLong { + get { return type == JsonType.Long; } + } + + public bool IsObject { + get { return type == JsonType.Object; } + } + + public bool IsString { + get { return type == JsonType.String; } + } + + public ICollection Keys { + get { EnsureDictionary (); return inst_object.Keys; } + } + + /// + /// Determines whether the json contains an element that has the specified key. + /// + /// The key to locate in the json. + /// true if the json contains an element that has the specified key; otherwise, false. + public Boolean ContainsKey(String key) { + EnsureDictionary(); + return this.inst_object.Keys.Contains(key); + } + #endregion + + + #region ICollection Properties + int ICollection.Count { + get { + return Count; + } + } + + bool ICollection.IsSynchronized { + get { + return EnsureCollection ().IsSynchronized; + } + } + + object ICollection.SyncRoot { + get { + return EnsureCollection ().SyncRoot; + } + } + #endregion + + + #region IDictionary Properties + bool IDictionary.IsFixedSize { + get { + return EnsureDictionary ().IsFixedSize; + } + } + + bool IDictionary.IsReadOnly { + get { + return EnsureDictionary ().IsReadOnly; + } + } + + ICollection IDictionary.Keys { + get { + EnsureDictionary (); + IList keys = new List (); + + foreach (KeyValuePair entry in + object_list) { + keys.Add (entry.Key); + } + + return (ICollection) keys; + } + } + + ICollection IDictionary.Values { + get { + EnsureDictionary (); + IList values = new List (); + + foreach (KeyValuePair entry in + object_list) { + values.Add (entry.Value); + } + + return (ICollection) values; + } + } + #endregion + + + + #region IJsonWrapper Properties + bool IJsonWrapper.IsArray { + get { return IsArray; } + } + + bool IJsonWrapper.IsBoolean { + get { return IsBoolean; } + } + + bool IJsonWrapper.IsDouble { + get { return IsDouble; } + } + + bool IJsonWrapper.IsInt { + get { return IsInt; } + } + + bool IJsonWrapper.IsLong { + get { return IsLong; } + } + + bool IJsonWrapper.IsObject { + get { return IsObject; } + } + + bool IJsonWrapper.IsString { + get { return IsString; } + } + #endregion + + + #region IList Properties + bool IList.IsFixedSize { + get { + return EnsureList ().IsFixedSize; + } + } + + bool IList.IsReadOnly { + get { + return EnsureList ().IsReadOnly; + } + } + #endregion + + + #region IDictionary Indexer + object IDictionary.this[object key] { + get { + return EnsureDictionary ()[key]; + } + + set { + if (! (key is String)) + throw new ArgumentException ( + "The key has to be a string"); + + JsonData data = ToJsonData (value); + + this[(string) key] = data; + } + } + #endregion + + + #region IOrderedDictionary Indexer + object IOrderedDictionary.this[int idx] { + get { + EnsureDictionary (); + return object_list[idx].Value; + } + + set { + EnsureDictionary (); + JsonData data = ToJsonData (value); + + KeyValuePair old_entry = object_list[idx]; + + inst_object[old_entry.Key] = data; + + KeyValuePair entry = + new KeyValuePair (old_entry.Key, data); + + object_list[idx] = entry; + } + } + #endregion + + + #region IList Indexer + object IList.this[int index] { + get { + return EnsureList ()[index]; + } + + set { + EnsureList (); + JsonData data = ToJsonData (value); + + this[index] = data; + } + } + #endregion + + + #region Public Indexers + public JsonData this[string prop_name] { + get { + EnsureDictionary (); + return inst_object[prop_name]; + } + + set { + EnsureDictionary (); + + KeyValuePair entry = + new KeyValuePair (prop_name, value); + + if (inst_object.ContainsKey (prop_name)) { + for (int i = 0; i < object_list.Count; i++) { + if (object_list[i].Key == prop_name) { + object_list[i] = entry; + break; + } + } + } else + object_list.Add (entry); + + inst_object[prop_name] = value; + + json = null; + } + } + + public JsonData this[int index] { + get { + EnsureCollection (); + + if (type == JsonType.Array) + return inst_array[index]; + + return object_list[index].Value; + } + + set { + EnsureCollection (); + + if (type == JsonType.Array) + inst_array[index] = value; + else { + KeyValuePair entry = object_list[index]; + KeyValuePair new_entry = + new KeyValuePair (entry.Key, value); + + object_list[index] = new_entry; + inst_object[entry.Key] = value; + } + + json = null; + } + } + #endregion + + + #region Constructors + public JsonData () + { + } + + public JsonData (bool boolean) + { + type = JsonType.Boolean; + inst_boolean = boolean; + } + + public JsonData (double number) + { + type = JsonType.Double; + inst_double = number; + } + + public JsonData (int number) + { + type = JsonType.Int; + inst_int = number; + } + + public JsonData (long number) + { + type = JsonType.Long; + inst_long = number; + } + + public JsonData (object obj) + { + if (obj is Boolean) { + type = JsonType.Boolean; + inst_boolean = (bool) obj; + return; + } + + if (obj is Double) { + type = JsonType.Double; + inst_double = (double) obj; + return; + } + + if (obj is Int32) { + type = JsonType.Int; + inst_int = (int) obj; + return; + } + + if (obj is Int64) { + type = JsonType.Long; + inst_long = (long) obj; + return; + } + + if (obj is String) { + type = JsonType.String; + inst_string = (string) obj; + return; + } + + throw new ArgumentException ( + "Unable to wrap the given object with JsonData"); + } + + public JsonData (string str) + { + type = JsonType.String; + inst_string = str; + } + #endregion + + + #region Implicit Conversions + public static implicit operator JsonData (Boolean data) + { + return new JsonData (data); + } + + public static implicit operator JsonData (Double data) + { + return new JsonData (data); + } + + public static implicit operator JsonData (Int32 data) + { + return new JsonData (data); + } + + public static implicit operator JsonData (Int64 data) + { + return new JsonData (data); + } + + public static implicit operator JsonData (String data) + { + return new JsonData (data); + } + #endregion + + + #region Explicit Conversions + public static explicit operator Boolean (JsonData data) + { + if (data.type != JsonType.Boolean) + throw new InvalidCastException ( + "Instance of JsonData doesn't hold a double"); + + return data.inst_boolean; + } + + public static explicit operator Double (JsonData data) + { + if (data.type != JsonType.Double) + throw new InvalidCastException ( + "Instance of JsonData doesn't hold a double"); + + return data.inst_double; + } + + public static explicit operator Int32(JsonData data) + { + if (data.type != JsonType.Int && data.type != JsonType.Long) + { + throw new InvalidCastException( + "Instance of JsonData doesn't hold an int"); + } + + // cast may truncate data... but that's up to the user to consider + return data.type == JsonType.Int ? data.inst_int : (int)data.inst_long; + } + + public static explicit operator Int64(JsonData data) + { + if (data.type != JsonType.Long && data.type != JsonType.Int) + { + throw new InvalidCastException( + "Instance of JsonData doesn't hold a long"); + } + + return data.type == JsonType.Long ? data.inst_long : data.inst_int; + } + + public static explicit operator String (JsonData data) + { + if (data.type != JsonType.String) + throw new InvalidCastException ( + "Instance of JsonData doesn't hold a string"); + + return data.inst_string; + } + #endregion + + + #region ICollection Methods + void ICollection.CopyTo (Array array, int index) + { + EnsureCollection ().CopyTo (array, index); + } + #endregion + + + #region IDictionary Methods + void IDictionary.Add (object key, object value) + { + JsonData data = ToJsonData (value); + + EnsureDictionary ().Add (key, data); + + KeyValuePair entry = + new KeyValuePair ((string) key, data); + object_list.Add (entry); + + json = null; + } + + void IDictionary.Clear () + { + EnsureDictionary ().Clear (); + object_list.Clear (); + json = null; + } + + bool IDictionary.Contains (object key) + { + return EnsureDictionary ().Contains (key); + } + + IDictionaryEnumerator IDictionary.GetEnumerator () + { + return ((IOrderedDictionary) this).GetEnumerator (); + } + + void IDictionary.Remove (object key) + { + EnsureDictionary ().Remove (key); + + for (int i = 0; i < object_list.Count; i++) { + if (object_list[i].Key == (string) key) { + object_list.RemoveAt (i); + break; + } + } + + json = null; + } + #endregion + + + #region IEnumerable Methods + IEnumerator IEnumerable.GetEnumerator () + { + return EnsureCollection ().GetEnumerator (); + } + #endregion + + + #region IJsonWrapper Methods + bool IJsonWrapper.GetBoolean () + { + if (type != JsonType.Boolean) + throw new InvalidOperationException ( + "JsonData instance doesn't hold a boolean"); + + return inst_boolean; + } + + double IJsonWrapper.GetDouble () + { + if (type != JsonType.Double) + throw new InvalidOperationException ( + "JsonData instance doesn't hold a double"); + + return inst_double; + } + + int IJsonWrapper.GetInt () + { + if (type != JsonType.Int) + throw new InvalidOperationException ( + "JsonData instance doesn't hold an int"); + + return inst_int; + } + + long IJsonWrapper.GetLong () + { + if (type != JsonType.Long) + throw new InvalidOperationException ( + "JsonData instance doesn't hold a long"); + + return inst_long; + } + + string IJsonWrapper.GetString () + { + if (type != JsonType.String) + throw new InvalidOperationException ( + "JsonData instance doesn't hold a string"); + + return inst_string; + } + + void IJsonWrapper.SetBoolean (bool val) + { + type = JsonType.Boolean; + inst_boolean = val; + json = null; + } + + void IJsonWrapper.SetDouble (double val) + { + type = JsonType.Double; + inst_double = val; + json = null; + } + + void IJsonWrapper.SetInt (int val) + { + type = JsonType.Int; + inst_int = val; + json = null; + } + + void IJsonWrapper.SetLong (long val) + { + type = JsonType.Long; + inst_long = val; + json = null; + } + + void IJsonWrapper.SetString (string val) + { + type = JsonType.String; + inst_string = val; + json = null; + } + + string IJsonWrapper.ToJson () + { + return ToJson (); + } + + void IJsonWrapper.ToJson (JsonWriter writer) + { + ToJson (writer); + } + #endregion + + + #region IList Methods + int IList.Add (object value) + { + return Add (value); + } + + void IList.Clear () + { + EnsureList ().Clear (); + json = null; + } + + bool IList.Contains (object value) + { + return EnsureList ().Contains (value); + } + + int IList.IndexOf (object value) + { + return EnsureList ().IndexOf (value); + } + + void IList.Insert (int index, object value) + { + EnsureList ().Insert (index, value); + json = null; + } + + void IList.Remove (object value) + { + EnsureList ().Remove (value); + json = null; + } + + void IList.RemoveAt (int index) + { + EnsureList ().RemoveAt (index); + json = null; + } + #endregion + + + #region IOrderedDictionary Methods + IDictionaryEnumerator IOrderedDictionary.GetEnumerator () + { + EnsureDictionary (); + + return new OrderedDictionaryEnumerator ( + object_list.GetEnumerator ()); + } + + void IOrderedDictionary.Insert (int idx, object key, object value) + { + string property = (string) key; + JsonData data = ToJsonData (value); + + this[property] = data; + + KeyValuePair entry = + new KeyValuePair (property, data); + + object_list.Insert (idx, entry); + } + + void IOrderedDictionary.RemoveAt (int idx) + { + EnsureDictionary (); + + inst_object.Remove (object_list[idx].Key); + object_list.RemoveAt (idx); + } + #endregion + + + #region Private Methods + private ICollection EnsureCollection () + { + if (type == JsonType.Array) + return (ICollection) inst_array; + + if (type == JsonType.Object) + return (ICollection) inst_object; + + throw new InvalidOperationException ( + "The JsonData instance has to be initialized first"); + } + + private IDictionary EnsureDictionary () + { + if (type == JsonType.Object) + return (IDictionary) inst_object; + + if (type != JsonType.None) + throw new InvalidOperationException ( + "Instance of JsonData is not a dictionary"); + + type = JsonType.Object; + inst_object = new Dictionary (); + object_list = new List> (); + + return (IDictionary) inst_object; + } + + private IList EnsureList () + { + if (type == JsonType.Array) + return (IList) inst_array; + + if (type != JsonType.None) + throw new InvalidOperationException ( + "Instance of JsonData is not a list"); + + type = JsonType.Array; + inst_array = new List (); + + return (IList) inst_array; + } + + private JsonData ToJsonData (object obj) + { + if (obj == null) + return null; + + if (obj is JsonData) + return (JsonData) obj; + + return new JsonData (obj); + } + + private static void WriteJson (IJsonWrapper obj, JsonWriter writer) + { + if (obj == null) { + writer.Write (null); + return; + } + + if (obj.IsString) { + writer.Write (obj.GetString ()); + return; + } + + if (obj.IsBoolean) { + writer.Write (obj.GetBoolean ()); + return; + } + + if (obj.IsDouble) { + writer.Write (obj.GetDouble ()); + return; + } + + if (obj.IsInt) { + writer.Write (obj.GetInt ()); + return; + } + + if (obj.IsLong) { + writer.Write (obj.GetLong ()); + return; + } + + if (obj.IsArray) { + writer.WriteArrayStart (); + foreach (object elem in (IList) obj) + WriteJson ((JsonData) elem, writer); + writer.WriteArrayEnd (); + + return; + } + + if (obj.IsObject) { + writer.WriteObjectStart (); + + foreach (DictionaryEntry entry in ((IDictionary) obj)) { + writer.WritePropertyName ((string) entry.Key); + WriteJson ((JsonData) entry.Value, writer); + } + writer.WriteObjectEnd (); + + return; + } + } + #endregion + + + public int Add (object value) + { + JsonData data = ToJsonData (value); + + json = null; + + return EnsureList ().Add (data); + } + + public bool Remove(object obj) + { + json = null; + if(IsObject) + { + JsonData value = null; + if (inst_object.TryGetValue((string)obj, out value)) + return inst_object.Remove((string)obj) && object_list.Remove(new KeyValuePair((string)obj, value)); + else + throw new KeyNotFoundException("The specified key was not found in the JsonData object."); + } + if(IsArray) + { + return inst_array.Remove(ToJsonData(obj)); + } + throw new InvalidOperationException ( + "Instance of JsonData is not an object or a list."); + } + + public void Clear () + { + if (IsObject) { + ((IDictionary) this).Clear (); + return; + } + + if (IsArray) { + ((IList) this).Clear (); + return; + } + } + + public bool Equals (JsonData x) + { + if (x == null) + return false; + + if (x.type != this.type) + { + // further check to see if this is a long to int comparison + if ((x.type != JsonType.Int && x.type != JsonType.Long) + || (this.type != JsonType.Int && this.type != JsonType.Long)) + { + return false; + } + } + + switch (this.type) { + case JsonType.None: + return true; + + case JsonType.Object: + return this.inst_object.Equals (x.inst_object); + + case JsonType.Array: + return this.inst_array.Equals (x.inst_array); + + case JsonType.String: + return this.inst_string.Equals (x.inst_string); + + case JsonType.Int: + { + if (x.IsLong) + { + if (x.inst_long < Int32.MinValue || x.inst_long > Int32.MaxValue) + return false; + return this.inst_int.Equals((int)x.inst_long); + } + return this.inst_int.Equals(x.inst_int); + } + + case JsonType.Long: + { + if (x.IsInt) + { + if (this.inst_long < Int32.MinValue || this.inst_long > Int32.MaxValue) + return false; + return x.inst_int.Equals((int)this.inst_long); + } + return this.inst_long.Equals(x.inst_long); + } + + case JsonType.Double: + return this.inst_double.Equals (x.inst_double); + + case JsonType.Boolean: + return this.inst_boolean.Equals (x.inst_boolean); + } + + return false; + } + + public JsonType GetJsonType () + { + return type; + } + + public void SetJsonType (JsonType type) + { + if (this.type == type) + return; + + switch (type) { + case JsonType.None: + break; + + case JsonType.Object: + inst_object = new Dictionary (); + object_list = new List> (); + break; + + case JsonType.Array: + inst_array = new List (); + break; + + case JsonType.String: + inst_string = default (String); + break; + + case JsonType.Int: + inst_int = default (Int32); + break; + + case JsonType.Long: + inst_long = default (Int64); + break; + + case JsonType.Double: + inst_double = default (Double); + break; + + case JsonType.Boolean: + inst_boolean = default (Boolean); + break; + } + + this.type = type; + } + + public string ToJson () + { + if (json != null) + return json; + + StringWriter sw = new StringWriter (); + JsonWriter writer = new JsonWriter (sw); + writer.Validate = false; + + WriteJson (this, writer); + json = sw.ToString (); + + return json; + } + + public void ToJson (JsonWriter writer) + { + bool old_validate = writer.Validate; + + writer.Validate = false; + + WriteJson (this, writer); + + writer.Validate = old_validate; + } + + public override string ToString () + { + switch (type) { + case JsonType.Array: + return "JsonData array"; + + case JsonType.Boolean: + return inst_boolean.ToString (); + + case JsonType.Double: + return inst_double.ToString (); + + case JsonType.Int: + return inst_int.ToString (); + + case JsonType.Long: + return inst_long.ToString (); + + case JsonType.Object: + return "JsonData object"; + + case JsonType.String: + return inst_string; + } + + return "Uninitialized JsonData"; + } + } + + + internal class OrderedDictionaryEnumerator : IDictionaryEnumerator + { + IEnumerator> list_enumerator; + + + public object Current { + get { return Entry; } + } + + public DictionaryEntry Entry { + get { + KeyValuePair curr = list_enumerator.Current; + return new DictionaryEntry (curr.Key, curr.Value); + } + } + + public object Key { + get { return list_enumerator.Current.Key; } + } + + public object Value { + get { return list_enumerator.Current.Value; } + } + + + public OrderedDictionaryEnumerator ( + IEnumerator> enumerator) + { + list_enumerator = enumerator; + } + + + public bool MoveNext () + { + return list_enumerator.MoveNext (); + } + + public void Reset () + { + list_enumerator.Reset (); + } + } +} diff --git a/LitJson/Runtime/JsonData.cs.meta b/LitJson/Runtime/JsonData.cs.meta new file mode 100644 index 0000000..d0abdc4 --- /dev/null +++ b/LitJson/Runtime/JsonData.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 5559251be0dd54c669a5b225ad53126e +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/LitJson/Runtime/JsonException.cs b/LitJson/Runtime/JsonException.cs new file mode 100644 index 0000000..544db4c --- /dev/null +++ b/LitJson/Runtime/JsonException.cs @@ -0,0 +1,65 @@ +#region Header +/** + * JsonException.cs + * Base class throwed by LitJSON when a parsing error occurs. + * + * The authors disclaim copyright to this source code. For more details, see + * the COPYING file included with this distribution. + **/ +#endregion + + +using System; + + +namespace Guru.LitJson +{ + public class JsonException : +#if NETSTANDARD1_5 + Exception +#else + ApplicationException +#endif + { + public JsonException () : base () + { + } + + internal JsonException (ParserToken token) : + base (String.Format ( + "Invalid token '{0}' in input string", token)) + { + } + + internal JsonException (ParserToken token, + Exception inner_exception) : + base (String.Format ( + "Invalid token '{0}' in input string", token), + inner_exception) + { + } + + internal JsonException (int c) : + base (String.Format ( + "Invalid character '{0}' in input string", (char) c)) + { + } + + internal JsonException (int c, Exception inner_exception) : + base (String.Format ( + "Invalid character '{0}' in input string", (char) c), + inner_exception) + { + } + + + public JsonException (string message) : base (message) + { + } + + public JsonException (string message, Exception inner_exception) : + base (message, inner_exception) + { + } + } +} diff --git a/LitJson/Runtime/JsonException.cs.meta b/LitJson/Runtime/JsonException.cs.meta new file mode 100644 index 0000000..edcbbb9 --- /dev/null +++ b/LitJson/Runtime/JsonException.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 35e97fa5a8f99447a90bffb890cfd55d +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/LitJson/Runtime/JsonMapper.cs b/LitJson/Runtime/JsonMapper.cs new file mode 100644 index 0000000..36f8a94 --- /dev/null +++ b/LitJson/Runtime/JsonMapper.cs @@ -0,0 +1,995 @@ +#region Header +/** + * JsonMapper.cs + * JSON to .Net object and object to JSON conversions. + * + * The authors disclaim copyright to this source code. For more details, see + * the COPYING file included with this distribution. + **/ +#endregion + + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Reflection; + + +namespace Guru.LitJson +{ + internal struct PropertyMetadata + { + public MemberInfo Info; + public bool IsField; + public Type Type; + } + + + internal struct ArrayMetadata + { + private Type element_type; + private bool is_array; + private bool is_list; + + + public Type ElementType { + get { + if (element_type == null) + return typeof (JsonData); + + return element_type; + } + + set { element_type = value; } + } + + public bool IsArray { + get { return is_array; } + set { is_array = value; } + } + + public bool IsList { + get { return is_list; } + set { is_list = value; } + } + } + + + internal struct ObjectMetadata + { + private Type element_type; + private bool is_dictionary; + + private IDictionary properties; + + + public Type ElementType { + get { + if (element_type == null) + return typeof (JsonData); + + return element_type; + } + + set { element_type = value; } + } + + public bool IsDictionary { + get { return is_dictionary; } + set { is_dictionary = value; } + } + + public IDictionary Properties { + get { return properties; } + set { properties = value; } + } + } + + + internal delegate void ExporterFunc (object obj, JsonWriter writer); + public delegate void ExporterFunc (T obj, JsonWriter writer); + + internal delegate object ImporterFunc (object input); + public delegate TValue ImporterFunc (TJson input); + + public delegate IJsonWrapper WrapperFactory (); + + + public class JsonMapper + { + #region Fields + private static readonly int max_nesting_depth; + + private static readonly IFormatProvider datetime_format; + + private static readonly IDictionary base_exporters_table; + private static readonly IDictionary custom_exporters_table; + private static readonly object custom_exporters_table_lock = new Object(); + + private static readonly IDictionary> base_importers_table; + private static readonly IDictionary> custom_importers_table; + private static readonly object custom_importers_table_lock = new Object(); + + private static readonly IDictionary array_metadata; + private static readonly object array_metadata_lock = new Object (); + + private static readonly IDictionary> conv_ops; + private static readonly object conv_ops_lock = new Object (); + + private static readonly IDictionary object_metadata; + private static readonly object object_metadata_lock = new Object (); + + private static readonly IDictionary> type_properties; + private static readonly object type_properties_lock = new Object (); + + private static readonly JsonWriter static_writer; + private static readonly object static_writer_lock = new Object (); + #endregion + + + #region Constructors + static JsonMapper () + { + max_nesting_depth = 100; + + array_metadata = new Dictionary (); + conv_ops = new Dictionary> (); + object_metadata = new Dictionary (); + type_properties = new Dictionary> (); + + static_writer = new JsonWriter (); + + datetime_format = DateTimeFormatInfo.InvariantInfo; + + base_exporters_table = new Dictionary (); + custom_exporters_table = new Dictionary (); + + base_importers_table = new Dictionary> (); + custom_importers_table = new Dictionary> (); + + RegisterBaseExporters (); + RegisterBaseImporters (); + } + #endregion + + + #region Private Methods + private static void AddArrayMetadata (Type type) + { + if (array_metadata.ContainsKey (type)) + return; + + ArrayMetadata data = new ArrayMetadata (); + + data.IsArray = type.IsArray; + + if (type.GetInterface ("System.Collections.IList") != null) + data.IsList = true; + + foreach (PropertyInfo p_info in type.GetProperties ()) { + if (p_info.Name != "Item") + continue; + + ParameterInfo[] parameters = p_info.GetIndexParameters (); + + if (parameters.Length != 1) + continue; + + if (parameters[0].ParameterType == typeof (int)) + data.ElementType = p_info.PropertyType; + } + + lock (array_metadata_lock) { + try { + array_metadata.Add (type, data); + } catch (ArgumentException) { + return; + } + } + } + + private static void AddObjectMetadata (Type type) + { + if (object_metadata.ContainsKey (type)) + return; + + ObjectMetadata data = new ObjectMetadata (); + + if (type.GetInterface ("System.Collections.IDictionary") != null) + data.IsDictionary = true; + + data.Properties = new Dictionary (); + + foreach (PropertyInfo p_info in type.GetProperties ()) { + if (p_info.Name == "Item") { + ParameterInfo[] parameters = p_info.GetIndexParameters (); + + if (parameters.Length != 1) + continue; + + if (parameters[0].ParameterType == typeof (string)) + data.ElementType = p_info.PropertyType; + + continue; + } + + PropertyMetadata p_data = new PropertyMetadata (); + p_data.Info = p_info; + p_data.Type = p_info.PropertyType; + + data.Properties.Add (p_info.Name, p_data); + } + + foreach (FieldInfo f_info in type.GetFields ()) { + PropertyMetadata p_data = new PropertyMetadata (); + p_data.Info = f_info; + p_data.IsField = true; + p_data.Type = f_info.FieldType; + + data.Properties.Add (f_info.Name, p_data); + } + + lock (object_metadata_lock) { + try { + object_metadata.Add (type, data); + } catch (ArgumentException) { + return; + } + } + } + + private static void AddTypeProperties (Type type) + { + if (type_properties.ContainsKey (type)) + return; + + IList props = new List (); + + foreach (PropertyInfo p_info in type.GetProperties ()) { + if (p_info.Name == "Item") + continue; + + PropertyMetadata p_data = new PropertyMetadata (); + p_data.Info = p_info; + p_data.IsField = false; + props.Add (p_data); + } + + foreach (FieldInfo f_info in type.GetFields ()) { + PropertyMetadata p_data = new PropertyMetadata (); + p_data.Info = f_info; + p_data.IsField = true; + + props.Add (p_data); + } + + lock (type_properties_lock) { + try { + type_properties.Add (type, props); + } catch (ArgumentException) { + return; + } + } + } + + private static MethodInfo GetConvOp (Type t1, Type t2) + { + lock (conv_ops_lock) { + if (! conv_ops.ContainsKey (t1)) + conv_ops.Add (t1, new Dictionary ()); + } + + if (conv_ops[t1].ContainsKey (t2)) + return conv_ops[t1][t2]; + + MethodInfo op = t1.GetMethod ( + "op_Implicit", new Type[] { t2 }); + + lock (conv_ops_lock) { + try { + conv_ops[t1].Add (t2, op); + } catch (ArgumentException) { + return conv_ops[t1][t2]; + } + } + + return op; + } + + private static object ReadValue (Type inst_type, JsonReader reader) + { + reader.Read (); + + if (reader.Token == JsonToken.ArrayEnd) + return null; + + Type underlying_type = Nullable.GetUnderlyingType(inst_type); + Type value_type = underlying_type ?? inst_type; + + if (reader.Token == JsonToken.Null) { + #if NETSTANDARD1_5 + if (inst_type.IsClass() || underlying_type != null) { + return null; + } + #else + if (inst_type.IsClass || underlying_type != null) { + return null; + } + #endif + + throw new JsonException (String.Format ( + "Can't assign null to an instance of type {0}", + inst_type)); + } + + if (reader.Token == JsonToken.Double || + reader.Token == JsonToken.Int || + reader.Token == JsonToken.Long || + reader.Token == JsonToken.String || + reader.Token == JsonToken.Boolean) { + + Type json_type = reader.Value.GetType (); + + if (value_type.IsAssignableFrom (json_type)) + return reader.Value; + + // If there's a custom importer that fits, use it + lock (custom_importers_table_lock) { + if (custom_importers_table.TryGetValue(json_type, out IDictionary customImporterTablesValue) && + customImporterTablesValue.TryGetValue(value_type, out ImporterFunc customImporter)) { + + return customImporter(reader.Value); + } + } + + // Maybe there's a base importer that works + if (base_importers_table.TryGetValue(json_type, out IDictionary baseImporterTablesValue) && + baseImporterTablesValue.TryGetValue(value_type, out ImporterFunc baseImporter)) { + + return baseImporter(reader.Value); + } + + // Maybe it's an enum + #if NETSTANDARD1_5 + if (value_type.IsEnum()) + return Enum.ToObject (value_type, reader.Value); + #else + if (value_type.IsEnum) + return Enum.ToObject (value_type, reader.Value); + #endif + // Try using an implicit conversion operator + MethodInfo conv_op = GetConvOp (value_type, json_type); + + if (conv_op != null) + return conv_op.Invoke (null, + new object[] { reader.Value }); + + // No luck + throw new JsonException (String.Format ( + "Can't assign value '{0}' (type {1}) to type {2}", + reader.Value, json_type, inst_type)); + } + + object instance = null; + + if (reader.Token == JsonToken.ArrayStart) { + + AddArrayMetadata (inst_type); + ArrayMetadata t_data = array_metadata[inst_type]; + + if (! t_data.IsArray && ! t_data.IsList) + throw new JsonException (String.Format ( + "Type {0} can't act as an array", + inst_type)); + + IList list; + Type elem_type; + + if (! t_data.IsArray) { + list = (IList) Activator.CreateInstance (inst_type); + elem_type = t_data.ElementType; + } else { + list = new ArrayList (); + elem_type = inst_type.GetElementType (); + } + + list.Clear(); + + while (true) { + object item = ReadValue (elem_type, reader); + if (item == null && reader.Token == JsonToken.ArrayEnd) + break; + + list.Add (item); + } + + if (t_data.IsArray) { + int n = list.Count; + instance = Array.CreateInstance (elem_type, n); + + for (int i = 0; i < n; i++) + ((Array) instance).SetValue (list[i], i); + } else + instance = list; + + } else if (reader.Token == JsonToken.ObjectStart) { + AddObjectMetadata (value_type); + ObjectMetadata t_data = object_metadata[value_type]; + + instance = Activator.CreateInstance (value_type); + + while (true) { + reader.Read (); + + if (reader.Token == JsonToken.ObjectEnd) + break; + + string property = (string) reader.Value; + + if (t_data.Properties.ContainsKey (property)) { + PropertyMetadata prop_data = + t_data.Properties[property]; + + if (prop_data.IsField) { + ((FieldInfo) prop_data.Info).SetValue ( + instance, ReadValue (prop_data.Type, reader)); + } else { + PropertyInfo p_info = + (PropertyInfo) prop_data.Info; + + if (p_info.CanWrite) + p_info.SetValue ( + instance, + ReadValue (prop_data.Type, reader), + null); + else + ReadValue (prop_data.Type, reader); + } + + } else { + if (! t_data.IsDictionary) { + + if (! reader.SkipNonMembers) { + throw new JsonException (String.Format ( + "The type {0} doesn't have the " + + "property '{1}'", + inst_type, property)); + } else { + ReadSkip (reader); + continue; + } + } + + ((IDictionary) instance).Add ( + property, ReadValue ( + t_data.ElementType, reader)); + } + + } + + } + + return instance; + } + + private static IJsonWrapper ReadValue (WrapperFactory factory, + JsonReader reader) + { + reader.Read (); + + if (reader.Token == JsonToken.ArrayEnd || + reader.Token == JsonToken.Null) + return null; + + IJsonWrapper instance = factory (); + + if (reader.Token == JsonToken.String) { + instance.SetString ((string) reader.Value); + return instance; + } + + if (reader.Token == JsonToken.Double) { + instance.SetDouble ((double) reader.Value); + return instance; + } + + if (reader.Token == JsonToken.Int) { + instance.SetInt ((int) reader.Value); + return instance; + } + + if (reader.Token == JsonToken.Long) { + instance.SetLong ((long) reader.Value); + return instance; + } + + if (reader.Token == JsonToken.Boolean) { + instance.SetBoolean ((bool) reader.Value); + return instance; + } + + if (reader.Token == JsonToken.ArrayStart) { + instance.SetJsonType (JsonType.Array); + + while (true) { + IJsonWrapper item = ReadValue (factory, reader); + if (item == null && reader.Token == JsonToken.ArrayEnd) + break; + + ((IList) instance).Add (item); + } + } + else if (reader.Token == JsonToken.ObjectStart) { + instance.SetJsonType (JsonType.Object); + + while (true) { + reader.Read (); + + if (reader.Token == JsonToken.ObjectEnd) + break; + + string property = (string) reader.Value; + + ((IDictionary) instance)[property] = ReadValue ( + factory, reader); + } + + } + + return instance; + } + + private static void ReadSkip (JsonReader reader) + { + ToWrapper ( + delegate { return new JsonMockWrapper (); }, reader); + } + + private static void RegisterBaseExporters () + { + // This method is only called from the static initializer, + // so there is no need to explicitly lock any static members here + base_exporters_table[typeof (byte)] = + delegate (object obj, JsonWriter writer) { + writer.Write (Convert.ToInt32 ((byte) obj)); + }; + + base_exporters_table[typeof (char)] = + delegate (object obj, JsonWriter writer) { + writer.Write (Convert.ToString ((char) obj)); + }; + + base_exporters_table[typeof (DateTime)] = + delegate (object obj, JsonWriter writer) { + writer.Write (Convert.ToString ((DateTime) obj, + datetime_format)); + }; + + base_exporters_table[typeof (decimal)] = + delegate (object obj, JsonWriter writer) { + writer.Write ((decimal) obj); + }; + + base_exporters_table[typeof (sbyte)] = + delegate (object obj, JsonWriter writer) { + writer.Write (Convert.ToInt32 ((sbyte) obj)); + }; + + base_exporters_table[typeof (short)] = + delegate (object obj, JsonWriter writer) { + writer.Write (Convert.ToInt32 ((short) obj)); + }; + + base_exporters_table[typeof (ushort)] = + delegate (object obj, JsonWriter writer) { + writer.Write (Convert.ToInt32 ((ushort) obj)); + }; + + base_exporters_table[typeof (uint)] = + delegate (object obj, JsonWriter writer) { + writer.Write (Convert.ToUInt64 ((uint) obj)); + }; + + base_exporters_table[typeof (ulong)] = + delegate (object obj, JsonWriter writer) { + writer.Write ((ulong) obj); + }; + + base_exporters_table[typeof(DateTimeOffset)] = + delegate (object obj, JsonWriter writer) { + writer.Write(((DateTimeOffset)obj).ToString("yyyy-MM-ddTHH:mm:ss.fffffffzzz", datetime_format)); + }; + } + + private static void RegisterBaseImporters () + { + // This method is only called from the static initializer, + // so there is no need to explicitly lock any static members here + ImporterFunc importer; + + importer = delegate (object input) { + return Convert.ToByte ((int) input); + }; + RegisterImporter (base_importers_table, typeof (int), + typeof (byte), importer); + + importer = delegate (object input) { + return Convert.ToUInt64 ((int) input); + }; + RegisterImporter (base_importers_table, typeof (int), + typeof (ulong), importer); + + importer = delegate (object input) { + return Convert.ToInt64((int)input); + }; + RegisterImporter(base_importers_table, typeof(int), + typeof(long), importer); + + importer = delegate (object input) { + return Convert.ToSByte ((int) input); + }; + RegisterImporter (base_importers_table, typeof (int), + typeof (sbyte), importer); + + importer = delegate (object input) { + return Convert.ToInt16 ((int) input); + }; + RegisterImporter (base_importers_table, typeof (int), + typeof (short), importer); + + importer = delegate (object input) { + return Convert.ToUInt16 ((int) input); + }; + RegisterImporter (base_importers_table, typeof (int), + typeof (ushort), importer); + + importer = delegate (object input) { + return Convert.ToUInt32 ((int) input); + }; + RegisterImporter (base_importers_table, typeof (int), + typeof (uint), importer); + + importer = delegate (object input) { + return Convert.ToSingle ((int) input); + }; + RegisterImporter (base_importers_table, typeof (int), + typeof (float), importer); + + importer = delegate (object input) { + return Convert.ToDouble ((int) input); + }; + RegisterImporter (base_importers_table, typeof (int), + typeof (double), importer); + + importer = delegate (object input) { + return Convert.ToDecimal ((double) input); + }; + RegisterImporter (base_importers_table, typeof (double), + typeof (decimal), importer); + + importer = delegate (object input) { + return Convert.ToSingle((double)input); + }; + RegisterImporter(base_importers_table, typeof(double), + typeof(float), importer); + + importer = delegate (object input) { + return Convert.ToUInt32 ((long) input); + }; + RegisterImporter (base_importers_table, typeof (long), + typeof (uint), importer); + + importer = delegate (object input) { + return Convert.ToChar ((string) input); + }; + RegisterImporter (base_importers_table, typeof (string), + typeof (char), importer); + + importer = delegate (object input) { + return Convert.ToDateTime ((string) input, datetime_format); + }; + RegisterImporter (base_importers_table, typeof (string), + typeof (DateTime), importer); + + importer = delegate (object input) { + return DateTimeOffset.Parse((string)input, datetime_format); + }; + RegisterImporter(base_importers_table, typeof(string), + typeof(DateTimeOffset), importer); + } + + private static void RegisterImporter ( + IDictionary> table, + Type json_type, Type value_type, ImporterFunc importer) + { + if (! table.ContainsKey (json_type)) + table.Add (json_type, new Dictionary ()); + + table[json_type][value_type] = importer; + } + + private static void WriteValue (object obj, JsonWriter writer, + bool writer_is_private, + int depth) + { + if (depth > max_nesting_depth) + throw new JsonException ( + String.Format ("Max allowed object depth reached while " + + "trying to export from type {0}", + obj.GetType ())); + + if (obj == null) { + writer.Write (null); + return; + } + + if (obj is IJsonWrapper) { + if (writer_is_private) + writer.TextWriter.Write (((IJsonWrapper) obj).ToJson ()); + else + ((IJsonWrapper) obj).ToJson (writer); + + return; + } + + if (obj is String) { + writer.Write ((string) obj); + return; + } + + if (obj is Double) { + writer.Write ((double) obj); + return; + } + + if (obj is Single) + { + writer.Write((float)obj); + return; + } + + if (obj is Int32) { + writer.Write ((int) obj); + return; + } + + if (obj is Boolean) { + writer.Write ((bool) obj); + return; + } + + if (obj is Int64) { + writer.Write ((long) obj); + return; + } + + if (obj is Array) { + writer.WriteArrayStart (); + + foreach (object elem in (Array) obj) + WriteValue (elem, writer, writer_is_private, depth + 1); + + writer.WriteArrayEnd (); + + return; + } + + if (obj is IList) { + writer.WriteArrayStart (); + foreach (object elem in (IList) obj) + WriteValue (elem, writer, writer_is_private, depth + 1); + writer.WriteArrayEnd (); + + return; + } + + if (obj is IDictionary dictionary) { + writer.WriteObjectStart (); + foreach (DictionaryEntry entry in dictionary) { + var propertyName = entry.Key is string key ? + key + : Convert.ToString(entry.Key, CultureInfo.InvariantCulture); + writer.WritePropertyName (propertyName); + WriteValue (entry.Value, writer, writer_is_private, + depth + 1); + } + writer.WriteObjectEnd (); + + return; + } + + Type obj_type = obj.GetType (); + + // See if there's a custom exporter for the object + lock (custom_exporters_table_lock) { + if (custom_exporters_table.TryGetValue(obj_type, out ExporterFunc customExporter)) { + customExporter(obj, writer); + + return; + } + } + + // If not, maybe there's a base exporter + if (base_exporters_table.TryGetValue(obj_type, out ExporterFunc baseExporter)) { + baseExporter(obj, writer); + + return; + } + + // Last option, let's see if it's an enum + if (obj is Enum) { + Type e_type = Enum.GetUnderlyingType (obj_type); + + if (e_type == typeof (long)) + writer.Write ((long) obj); + else if (e_type == typeof (uint)) + writer.Write ((uint) obj); + else if (e_type == typeof (ulong)) + writer.Write ((ulong) obj); + else if (e_type == typeof(ushort)) + writer.Write ((ushort)obj); + else if (e_type == typeof(short)) + writer.Write ((short)obj); + else if (e_type == typeof(byte)) + writer.Write ((byte)obj); + else if (e_type == typeof(sbyte)) + writer.Write ((sbyte)obj); + else + writer.Write ((int) obj); + + return; + } + + // Okay, so it looks like the input should be exported as an + // object + AddTypeProperties (obj_type); + IList props = type_properties[obj_type]; + + writer.WriteObjectStart (); + foreach (PropertyMetadata p_data in props) { + if (p_data.IsField) { + writer.WritePropertyName (p_data.Info.Name); + WriteValue (((FieldInfo) p_data.Info).GetValue (obj), + writer, writer_is_private, depth + 1); + } + else { + PropertyInfo p_info = (PropertyInfo) p_data.Info; + + if (p_info.CanRead) { + writer.WritePropertyName (p_data.Info.Name); + WriteValue (p_info.GetValue (obj, null), + writer, writer_is_private, depth + 1); + } + } + } + writer.WriteObjectEnd (); + } + #endregion + + + public static string ToJson (object obj) + { + lock (static_writer_lock) { + static_writer.Reset (); + + WriteValue (obj, static_writer, true, 0); + + return static_writer.ToString (); + } + } + + public static void ToJson (object obj, JsonWriter writer) + { + WriteValue (obj, writer, false, 0); + } + + public static JsonData ToObject (JsonReader reader) + { + return (JsonData) ToWrapper ( + delegate { return new JsonData (); }, reader); + } + + public static JsonData ToObject (TextReader reader) + { + JsonReader json_reader = new JsonReader (reader); + + return (JsonData) ToWrapper ( + delegate { return new JsonData (); }, json_reader); + } + + public static JsonData ToObject (string json) + { + return (JsonData) ToWrapper ( + delegate { return new JsonData (); }, json); + } + + public static T ToObject (JsonReader reader) + { + return (T) ReadValue (typeof (T), reader); + } + + public static T ToObject (TextReader reader) + { + JsonReader json_reader = new JsonReader (reader); + + return (T) ReadValue (typeof (T), json_reader); + } + + public static T ToObject (string json) + { + JsonReader reader = new JsonReader (json); + + return (T) ReadValue (typeof (T), reader); + } + + public static object ToObject(string json, Type ConvertType ) + { + JsonReader reader = new JsonReader(json); + + return ReadValue(ConvertType, reader); + } + + public static IJsonWrapper ToWrapper (WrapperFactory factory, + JsonReader reader) + { + return ReadValue (factory, reader); + } + + public static IJsonWrapper ToWrapper (WrapperFactory factory, + string json) + { + JsonReader reader = new JsonReader (json); + + return ReadValue (factory, reader); + } + + public static void RegisterExporter (ExporterFunc exporter) + { + ExporterFunc exporter_wrapper = + delegate (object obj, JsonWriter writer) { + exporter ((T) obj, writer); + }; + + lock (custom_exporters_table_lock) { + custom_exporters_table[typeof (T)] = exporter_wrapper; + } + } + + public static void RegisterImporter ( + ImporterFunc importer) + { + ImporterFunc importer_wrapper = + delegate (object input) { + return importer ((TJson) input); + }; + + lock (custom_importers_table_lock) { + RegisterImporter (custom_importers_table, typeof (TJson), + typeof (TValue), importer_wrapper); + } + } + + public static void UnregisterExporters () + { + lock (custom_exporters_table_lock) { + custom_exporters_table.Clear(); + } + } + + public static void UnregisterImporters () + { + lock (custom_importers_table_lock) { + custom_importers_table.Clear(); + } + } + } +} diff --git a/LitJson/Runtime/JsonMapper.cs.meta b/LitJson/Runtime/JsonMapper.cs.meta new file mode 100644 index 0000000..f2eabd5 --- /dev/null +++ b/LitJson/Runtime/JsonMapper.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 3e14ed6947460448ea1c1512947ced5d +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/LitJson/Runtime/JsonMockWrapper.cs b/LitJson/Runtime/JsonMockWrapper.cs new file mode 100644 index 0000000..ebb54e0 --- /dev/null +++ b/LitJson/Runtime/JsonMockWrapper.cs @@ -0,0 +1,105 @@ +#region Header +/** + * JsonMockWrapper.cs + * Mock object implementing IJsonWrapper, to facilitate actions like + * skipping data more efficiently. + * + * The authors disclaim copyright to this source code. For more details, see + * the COPYING file included with this distribution. + **/ +#endregion + + +using System; +using System.Collections; +using System.Collections.Specialized; + + +namespace Guru.LitJson +{ + public class JsonMockWrapper : IJsonWrapper + { + public bool IsArray { get { return false; } } + public bool IsBoolean { get { return false; } } + public bool IsDouble { get { return false; } } + public bool IsInt { get { return false; } } + public bool IsLong { get { return false; } } + public bool IsObject { get { return false; } } + public bool IsString { get { return false; } } + + public bool GetBoolean () { return false; } + public double GetDouble () { return 0.0; } + public int GetInt () { return 0; } + public JsonType GetJsonType () { return JsonType.None; } + public long GetLong () { return 0L; } + public string GetString () { return ""; } + + public void SetBoolean (bool val) {} + public void SetDouble (double val) {} + public void SetInt (int val) {} + public void SetJsonType (JsonType type) {} + public void SetLong (long val) {} + public void SetString (string val) {} + + public string ToJson () { return ""; } + public void ToJson (JsonWriter writer) {} + + + bool IList.IsFixedSize { get { return true; } } + bool IList.IsReadOnly { get { return true; } } + + object IList.this[int index] { + get { return null; } + set {} + } + + int IList.Add (object value) { return 0; } + void IList.Clear () {} + bool IList.Contains (object value) { return false; } + int IList.IndexOf (object value) { return -1; } + void IList.Insert (int i, object v) {} + void IList.Remove (object value) {} + void IList.RemoveAt (int index) {} + + + int ICollection.Count { get { return 0; } } + bool ICollection.IsSynchronized { get { return false; } } + object ICollection.SyncRoot { get { return null; } } + + void ICollection.CopyTo (Array array, int index) {} + + + IEnumerator IEnumerable.GetEnumerator () { return null; } + + + bool IDictionary.IsFixedSize { get { return true; } } + bool IDictionary.IsReadOnly { get { return true; } } + + ICollection IDictionary.Keys { get { return null; } } + ICollection IDictionary.Values { get { return null; } } + + object IDictionary.this[object key] { + get { return null; } + set {} + } + + void IDictionary.Add (object k, object v) {} + void IDictionary.Clear () {} + bool IDictionary.Contains (object key) { return false; } + void IDictionary.Remove (object key) {} + + IDictionaryEnumerator IDictionary.GetEnumerator () { return null; } + + + object IOrderedDictionary.this[int idx] { + get { return null; } + set {} + } + + IDictionaryEnumerator IOrderedDictionary.GetEnumerator () { + return null; + } + void IOrderedDictionary.Insert (int i, object k, object v) {} + void IOrderedDictionary.RemoveAt (int i) {} + } +} diff --git a/LitJson/Runtime/JsonMockWrapper.cs.meta b/LitJson/Runtime/JsonMockWrapper.cs.meta new file mode 100644 index 0000000..ac7cc47 --- /dev/null +++ b/LitJson/Runtime/JsonMockWrapper.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: b54da0524de4e4164893c964b0b7b443 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/LitJson/Runtime/JsonReader.cs b/LitJson/Runtime/JsonReader.cs new file mode 100644 index 0000000..d9ef09c --- /dev/null +++ b/LitJson/Runtime/JsonReader.cs @@ -0,0 +1,478 @@ +#region Header +/** + * JsonReader.cs + * Stream-like access to JSON text. + * + * The authors disclaim copyright to this source code. For more details, see + * the COPYING file included with this distribution. + **/ +#endregion + + +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Text; + + +namespace Guru.LitJson +{ + public enum JsonToken + { + None, + + ObjectStart, + PropertyName, + ObjectEnd, + + ArrayStart, + ArrayEnd, + + Int, + Long, + Double, + + String, + + Boolean, + Null + } + + + public class JsonReader + { + #region Fields + private static readonly IDictionary> parse_table; + + private Stack automaton_stack; + private int current_input; + private int current_symbol; + private bool end_of_json; + private bool end_of_input; + private Lexer lexer; + private bool parser_in_string; + private bool parser_return; + private bool read_started; + private TextReader reader; + private bool reader_is_owned; + private bool skip_non_members; + private object token_value; + private JsonToken token; + #endregion + + + #region Public Properties + public bool AllowComments { + get { return lexer.AllowComments; } + set { lexer.AllowComments = value; } + } + + public bool AllowSingleQuotedStrings { + get { return lexer.AllowSingleQuotedStrings; } + set { lexer.AllowSingleQuotedStrings = value; } + } + + public bool SkipNonMembers { + get { return skip_non_members; } + set { skip_non_members = value; } + } + + public bool EndOfInput { + get { return end_of_input; } + } + + public bool EndOfJson { + get { return end_of_json; } + } + + public JsonToken Token { + get { return token; } + } + + public object Value { + get { return token_value; } + } + #endregion + + + #region Constructors + static JsonReader () + { + parse_table = PopulateParseTable (); + } + + public JsonReader (string json_text) : + this (new StringReader (json_text), true) + { + } + + public JsonReader (TextReader reader) : + this (reader, false) + { + } + + private JsonReader (TextReader reader, bool owned) + { + if (reader == null) + throw new ArgumentNullException ("reader"); + + parser_in_string = false; + parser_return = false; + + read_started = false; + automaton_stack = new Stack (); + automaton_stack.Push ((int) ParserToken.End); + automaton_stack.Push ((int) ParserToken.Text); + + lexer = new Lexer (reader); + + end_of_input = false; + end_of_json = false; + + skip_non_members = true; + + this.reader = reader; + reader_is_owned = owned; + } + #endregion + + + #region Static Methods + private static IDictionary> PopulateParseTable () + { + // See section A.2. of the manual for details + IDictionary> parse_table = new Dictionary> (); + + TableAddRow (parse_table, ParserToken.Array); + TableAddCol (parse_table, ParserToken.Array, '[', + '[', + (int) ParserToken.ArrayPrime); + + TableAddRow (parse_table, ParserToken.ArrayPrime); + TableAddCol (parse_table, ParserToken.ArrayPrime, '"', + (int) ParserToken.Value, + + (int) ParserToken.ValueRest, + ']'); + TableAddCol (parse_table, ParserToken.ArrayPrime, '[', + (int) ParserToken.Value, + (int) ParserToken.ValueRest, + ']'); + TableAddCol (parse_table, ParserToken.ArrayPrime, ']', + ']'); + TableAddCol (parse_table, ParserToken.ArrayPrime, '{', + (int) ParserToken.Value, + (int) ParserToken.ValueRest, + ']'); + TableAddCol (parse_table, ParserToken.ArrayPrime, (int) ParserToken.Number, + (int) ParserToken.Value, + (int) ParserToken.ValueRest, + ']'); + TableAddCol (parse_table, ParserToken.ArrayPrime, (int) ParserToken.True, + (int) ParserToken.Value, + (int) ParserToken.ValueRest, + ']'); + TableAddCol (parse_table, ParserToken.ArrayPrime, (int) ParserToken.False, + (int) ParserToken.Value, + (int) ParserToken.ValueRest, + ']'); + TableAddCol (parse_table, ParserToken.ArrayPrime, (int) ParserToken.Null, + (int) ParserToken.Value, + (int) ParserToken.ValueRest, + ']'); + + TableAddRow (parse_table, ParserToken.Object); + TableAddCol (parse_table, ParserToken.Object, '{', + '{', + (int) ParserToken.ObjectPrime); + + TableAddRow (parse_table, ParserToken.ObjectPrime); + TableAddCol (parse_table, ParserToken.ObjectPrime, '"', + (int) ParserToken.Pair, + (int) ParserToken.PairRest, + '}'); + TableAddCol (parse_table, ParserToken.ObjectPrime, '}', + '}'); + + TableAddRow (parse_table, ParserToken.Pair); + TableAddCol (parse_table, ParserToken.Pair, '"', + (int) ParserToken.String, + ':', + (int) ParserToken.Value); + + TableAddRow (parse_table, ParserToken.PairRest); + TableAddCol (parse_table, ParserToken.PairRest, ',', + ',', + (int) ParserToken.Pair, + (int) ParserToken.PairRest); + TableAddCol (parse_table, ParserToken.PairRest, '}', + (int) ParserToken.Epsilon); + + TableAddRow (parse_table, ParserToken.String); + TableAddCol (parse_table, ParserToken.String, '"', + '"', + (int) ParserToken.CharSeq, + '"'); + + TableAddRow (parse_table, ParserToken.Text); + TableAddCol (parse_table, ParserToken.Text, '[', + (int) ParserToken.Array); + TableAddCol (parse_table, ParserToken.Text, '{', + (int) ParserToken.Object); + + TableAddRow (parse_table, ParserToken.Value); + TableAddCol (parse_table, ParserToken.Value, '"', + (int) ParserToken.String); + TableAddCol (parse_table, ParserToken.Value, '[', + (int) ParserToken.Array); + TableAddCol (parse_table, ParserToken.Value, '{', + (int) ParserToken.Object); + TableAddCol (parse_table, ParserToken.Value, (int) ParserToken.Number, + (int) ParserToken.Number); + TableAddCol (parse_table, ParserToken.Value, (int) ParserToken.True, + (int) ParserToken.True); + TableAddCol (parse_table, ParserToken.Value, (int) ParserToken.False, + (int) ParserToken.False); + TableAddCol (parse_table, ParserToken.Value, (int) ParserToken.Null, + (int) ParserToken.Null); + + TableAddRow (parse_table, ParserToken.ValueRest); + TableAddCol (parse_table, ParserToken.ValueRest, ',', + ',', + (int) ParserToken.Value, + (int) ParserToken.ValueRest); + TableAddCol (parse_table, ParserToken.ValueRest, ']', + (int) ParserToken.Epsilon); + + return parse_table; + } + + private static void TableAddCol (IDictionary> parse_table, ParserToken row, int col, + params int[] symbols) + { + parse_table[(int) row].Add (col, symbols); + } + + private static void TableAddRow (IDictionary> parse_table, ParserToken rule) + { + parse_table.Add ((int) rule, new Dictionary ()); + } + #endregion + + + #region Private Methods + private void ProcessNumber (string number) + { + if (number.IndexOf ('.') != -1 || + number.IndexOf ('e') != -1 || + number.IndexOf ('E') != -1) { + + double n_double; + if (double.TryParse (number, NumberStyles.Any, CultureInfo.InvariantCulture, out n_double)) { + token = JsonToken.Double; + token_value = n_double; + + return; + } + } + + int n_int32; + if (int.TryParse (number, NumberStyles.Integer, CultureInfo.InvariantCulture, out n_int32)) { + token = JsonToken.Int; + token_value = n_int32; + + return; + } + + long n_int64; + if (long.TryParse (number, NumberStyles.Integer, CultureInfo.InvariantCulture, out n_int64)) { + token = JsonToken.Long; + token_value = n_int64; + + return; + } + + ulong n_uint64; + if (ulong.TryParse(number, NumberStyles.Integer, CultureInfo.InvariantCulture, out n_uint64)) + { + token = JsonToken.Long; + token_value = n_uint64; + + return; + } + + // Shouldn't happen, but just in case, return something + token = JsonToken.Int; + token_value = 0; + } + + private void ProcessSymbol () + { + if (current_symbol == '[') { + token = JsonToken.ArrayStart; + parser_return = true; + + } else if (current_symbol == ']') { + token = JsonToken.ArrayEnd; + parser_return = true; + + } else if (current_symbol == '{') { + token = JsonToken.ObjectStart; + parser_return = true; + + } else if (current_symbol == '}') { + token = JsonToken.ObjectEnd; + parser_return = true; + + } else if (current_symbol == '"') { + if (parser_in_string) { + parser_in_string = false; + + parser_return = true; + + } else { + if (token == JsonToken.None) + token = JsonToken.String; + + parser_in_string = true; + } + + } else if (current_symbol == (int) ParserToken.CharSeq) { + token_value = lexer.StringValue; + + } else if (current_symbol == (int) ParserToken.False) { + token = JsonToken.Boolean; + token_value = false; + parser_return = true; + + } else if (current_symbol == (int) ParserToken.Null) { + token = JsonToken.Null; + parser_return = true; + + } else if (current_symbol == (int) ParserToken.Number) { + ProcessNumber (lexer.StringValue); + + parser_return = true; + + } else if (current_symbol == (int) ParserToken.Pair) { + token = JsonToken.PropertyName; + + } else if (current_symbol == (int) ParserToken.True) { + token = JsonToken.Boolean; + token_value = true; + parser_return = true; + + } + } + + private bool ReadToken () + { + if (end_of_input) + return false; + + lexer.NextToken (); + + if (lexer.EndOfInput) { + Close (); + + return false; + } + + current_input = lexer.Token; + + return true; + } + #endregion + + + public void Close () + { + if (end_of_input) + return; + + end_of_input = true; + end_of_json = true; + + if (reader_is_owned) + { + using(reader){} + } + + reader = null; + } + + public bool Read () + { + if (end_of_input) + return false; + + if (end_of_json) { + end_of_json = false; + automaton_stack.Clear (); + automaton_stack.Push ((int) ParserToken.End); + automaton_stack.Push ((int) ParserToken.Text); + } + + parser_in_string = false; + parser_return = false; + + token = JsonToken.None; + token_value = null; + + if (! read_started) { + read_started = true; + + if (! ReadToken ()) + return false; + } + + + int[] entry_symbols; + + while (true) { + if (parser_return) { + if (automaton_stack.Peek () == (int) ParserToken.End) + end_of_json = true; + + return true; + } + + current_symbol = automaton_stack.Pop (); + + ProcessSymbol (); + + if (current_symbol == current_input) { + if (! ReadToken ()) { + if (automaton_stack.Peek () != (int) ParserToken.End) + throw new JsonException ( + "Input doesn't evaluate to proper JSON text"); + + if (parser_return) + return true; + + return false; + } + + continue; + } + + try { + + entry_symbols = + parse_table[current_symbol][current_input]; + + } catch (KeyNotFoundException e) { + throw new JsonException ((ParserToken) current_input, e); + } + + if (entry_symbols[0] == (int) ParserToken.Epsilon) + continue; + + for (int i = entry_symbols.Length - 1; i >= 0; i--) + automaton_stack.Push (entry_symbols[i]); + } + } + + } +} diff --git a/LitJson/Runtime/JsonReader.cs.meta b/LitJson/Runtime/JsonReader.cs.meta new file mode 100644 index 0000000..82e6f64 --- /dev/null +++ b/LitJson/Runtime/JsonReader.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: a486d94a05da74fb4a2e1d9d1eb53455 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/LitJson/Runtime/JsonWriter.cs b/LitJson/Runtime/JsonWriter.cs new file mode 100644 index 0000000..322ff0a --- /dev/null +++ b/LitJson/Runtime/JsonWriter.cs @@ -0,0 +1,484 @@ +#region Header +/** + * JsonWriter.cs + * Stream-like facility to output JSON text. + * + * The authors disclaim copyright to this source code. For more details, see + * the COPYING file included with this distribution. + **/ +#endregion + + +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Text; + + +namespace Guru.LitJson +{ + internal enum Condition + { + InArray, + InObject, + NotAProperty, + Property, + Value + } + + internal class WriterContext + { + public int Count; + public bool InArray; + public bool InObject; + public bool ExpectingValue; + public int Padding; + } + + public class JsonWriter + { + #region Fields + private static readonly NumberFormatInfo number_format; + + private WriterContext context; + private Stack ctx_stack; + private bool has_reached_end; + private char[] hex_seq; + private int indentation; + private int indent_value; + private StringBuilder inst_string_builder; + private bool pretty_print; + private bool validate; + private bool lower_case_properties; + private TextWriter writer; + #endregion + + + #region Properties + public int IndentValue { + get { return indent_value; } + set { + indentation = (indentation / indent_value) * value; + indent_value = value; + } + } + + public bool PrettyPrint { + get { return pretty_print; } + set { pretty_print = value; } + } + + public TextWriter TextWriter { + get { return writer; } + } + + public bool Validate { + get { return validate; } + set { validate = value; } + } + + public bool LowerCaseProperties { + get { return lower_case_properties; } + set { lower_case_properties = value; } + } + #endregion + + + #region Constructors + static JsonWriter () + { + number_format = NumberFormatInfo.InvariantInfo; + } + + public JsonWriter () + { + inst_string_builder = new StringBuilder (); + writer = new StringWriter (inst_string_builder); + + Init (); + } + + public JsonWriter (StringBuilder sb) : + this (new StringWriter (sb)) + { + } + + public JsonWriter (TextWriter writer) + { + if (writer == null) + throw new ArgumentNullException ("writer"); + + this.writer = writer; + + Init (); + } + #endregion + + + #region Private Methods + private void DoValidation (Condition cond) + { + if (! context.ExpectingValue) + context.Count++; + + if (! validate) + return; + + if (has_reached_end) + throw new JsonException ( + "A complete JSON symbol has already been written"); + + switch (cond) { + case Condition.InArray: + if (! context.InArray) + throw new JsonException ( + "Can't close an array here"); + break; + + case Condition.InObject: + if (! context.InObject || context.ExpectingValue) + throw new JsonException ( + "Can't close an object here"); + break; + + case Condition.NotAProperty: + if (context.InObject && ! context.ExpectingValue) + throw new JsonException ( + "Expected a property"); + break; + + case Condition.Property: + if (! context.InObject || context.ExpectingValue) + throw new JsonException ( + "Can't add a property here"); + break; + + case Condition.Value: + if (! context.InArray && + (! context.InObject || ! context.ExpectingValue)) + throw new JsonException ( + "Can't add a value here"); + + break; + } + } + + private void Init () + { + has_reached_end = false; + hex_seq = new char[4]; + indentation = 0; + indent_value = 4; + pretty_print = false; + validate = true; + lower_case_properties = false; + + ctx_stack = new Stack (); + context = new WriterContext (); + ctx_stack.Push (context); + } + + private static void IntToHex (int n, char[] hex) + { + int num; + + for (int i = 0; i < 4; i++) { + num = n % 16; + + if (num < 10) + hex[3 - i] = (char) ('0' + num); + else + hex[3 - i] = (char) ('A' + (num - 10)); + + n >>= 4; + } + } + + private void Indent () + { + if (pretty_print) + indentation += indent_value; + } + + + private void Put (string str) + { + if (pretty_print && ! context.ExpectingValue) + for (int i = 0; i < indentation; i++) + writer.Write (' '); + + writer.Write (str); + } + + private void PutNewline () + { + PutNewline (true); + } + + private void PutNewline (bool add_comma) + { + if (add_comma && ! context.ExpectingValue && + context.Count > 1) + writer.Write (','); + + if (pretty_print && ! context.ExpectingValue) + writer.Write (Environment.NewLine); + } + + private void PutString (string str) + { + Put (String.Empty); + + writer.Write ('"'); + + int n = str.Length; + for (int i = 0; i < n; i++) { + switch (str[i]) { + case '\n': + writer.Write ("\\n"); + continue; + + case '\r': + writer.Write ("\\r"); + continue; + + case '\t': + writer.Write ("\\t"); + continue; + + case '"': + case '\\': + writer.Write ('\\'); + writer.Write (str[i]); + continue; + + case '\f': + writer.Write ("\\f"); + continue; + + case '\b': + writer.Write ("\\b"); + continue; + } + + if ((int) str[i] >= 32 && (int) str[i] <= 126) { + writer.Write (str[i]); + continue; + } + + // Default, turn into a \uXXXX sequence + IntToHex ((int) str[i], hex_seq); + writer.Write ("\\u"); + writer.Write (hex_seq); + } + + writer.Write ('"'); + } + + private void Unindent () + { + if (pretty_print) + indentation -= indent_value; + } + #endregion + + + public override string ToString () + { + if (inst_string_builder == null) + return String.Empty; + + return inst_string_builder.ToString (); + } + + public void Reset () + { + has_reached_end = false; + + ctx_stack.Clear (); + context = new WriterContext (); + ctx_stack.Push (context); + + if (inst_string_builder != null) + inst_string_builder.Remove (0, inst_string_builder.Length); + } + + public void Write (bool boolean) + { + DoValidation (Condition.Value); + PutNewline (); + + Put (boolean ? "true" : "false"); + + context.ExpectingValue = false; + } + + public void Write (decimal number) + { + DoValidation (Condition.Value); + PutNewline (); + + Put (Convert.ToString (number, number_format)); + + context.ExpectingValue = false; + } + + public void Write (double number) + { + DoValidation (Condition.Value); + PutNewline (); + + string str = Convert.ToString (number, number_format); + Put (str); + + if (str.IndexOf ('.') == -1 && + str.IndexOf ('E') == -1) + writer.Write (".0"); + + context.ExpectingValue = false; + } + + public void Write(float number) + { + DoValidation(Condition.Value); + PutNewline(); + + string str = Convert.ToString(number, number_format); + Put(str); + + context.ExpectingValue = false; + } + + public void Write (int number) + { + DoValidation (Condition.Value); + PutNewline (); + + Put (Convert.ToString (number, number_format)); + + context.ExpectingValue = false; + } + + public void Write (long number) + { + DoValidation (Condition.Value); + PutNewline (); + + Put (Convert.ToString (number, number_format)); + + context.ExpectingValue = false; + } + + public void Write (string str) + { + DoValidation (Condition.Value); + PutNewline (); + + if (str == null) + Put ("null"); + else + PutString (str); + + context.ExpectingValue = false; + } + + [CLSCompliant(false)] + public void Write (ulong number) + { + DoValidation (Condition.Value); + PutNewline (); + + Put (Convert.ToString (number, number_format)); + + context.ExpectingValue = false; + } + + public void WriteArrayEnd () + { + DoValidation (Condition.InArray); + PutNewline (false); + + ctx_stack.Pop (); + if (ctx_stack.Count == 1) + has_reached_end = true; + else { + context = ctx_stack.Peek (); + context.ExpectingValue = false; + } + + Unindent (); + Put ("]"); + } + + public void WriteArrayStart () + { + DoValidation (Condition.NotAProperty); + PutNewline (); + + Put ("["); + + context = new WriterContext (); + context.InArray = true; + ctx_stack.Push (context); + + Indent (); + } + + public void WriteObjectEnd () + { + DoValidation (Condition.InObject); + PutNewline (false); + + ctx_stack.Pop (); + if (ctx_stack.Count == 1) + has_reached_end = true; + else { + context = ctx_stack.Peek (); + context.ExpectingValue = false; + } + + Unindent (); + Put ("}"); + } + + public void WriteObjectStart () + { + DoValidation (Condition.NotAProperty); + PutNewline (); + + Put ("{"); + + context = new WriterContext (); + context.InObject = true; + ctx_stack.Push (context); + + Indent (); + } + + public void WritePropertyName (string property_name) + { + DoValidation (Condition.Property); + PutNewline (); + string propertyName = (property_name == null || !lower_case_properties) + ? property_name + : property_name.ToLowerInvariant(); + + PutString (propertyName); + + if (pretty_print) { + if (propertyName.Length > context.Padding) + context.Padding = propertyName.Length; + + for (int i = context.Padding - propertyName.Length; + i >= 0; i--) + writer.Write (' '); + + writer.Write (": "); + } else + writer.Write (':'); + + context.ExpectingValue = true; + } + } +} diff --git a/LitJson/Runtime/JsonWriter.cs.meta b/LitJson/Runtime/JsonWriter.cs.meta new file mode 100644 index 0000000..d1b4d63 --- /dev/null +++ b/LitJson/Runtime/JsonWriter.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: b5c3c60bc9e974025813d454b53835e1 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/LitJson/Runtime/Lexer.cs b/LitJson/Runtime/Lexer.cs new file mode 100644 index 0000000..0c7e5c9 --- /dev/null +++ b/LitJson/Runtime/Lexer.cs @@ -0,0 +1,912 @@ +#region Header +/** + * Lexer.cs + * JSON lexer implementation based on a finite state machine. + * + * The authors disclaim copyright to this source code. For more details, see + * the COPYING file included with this distribution. + **/ +#endregion + + +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; + + +namespace Guru.LitJson +{ + internal class FsmContext + { + public bool Return; + public int NextState; + public Lexer L; + public int StateStack; + } + + + internal class Lexer + { + #region Fields + private delegate bool StateHandler (FsmContext ctx); + + private static readonly int[] fsm_return_table; + private static readonly StateHandler[] fsm_handler_table; + + private bool allow_comments; + private bool allow_single_quoted_strings; + private bool end_of_input; + private FsmContext fsm_context; + private int input_buffer; + private int input_char; + private TextReader reader; + private int state; + private StringBuilder string_buffer; + private string string_value; + private int token; + private int unichar; + #endregion + + + #region Properties + public bool AllowComments { + get { return allow_comments; } + set { allow_comments = value; } + } + + public bool AllowSingleQuotedStrings { + get { return allow_single_quoted_strings; } + set { allow_single_quoted_strings = value; } + } + + public bool EndOfInput { + get { return end_of_input; } + } + + public int Token { + get { return token; } + } + + public string StringValue { + get { return string_value; } + } + #endregion + + + #region Constructors + static Lexer () + { + PopulateFsmTables (out fsm_handler_table, out fsm_return_table); + } + + public Lexer (TextReader reader) + { + allow_comments = true; + allow_single_quoted_strings = true; + + input_buffer = 0; + string_buffer = new StringBuilder (128); + state = 1; + end_of_input = false; + this.reader = reader; + + fsm_context = new FsmContext (); + fsm_context.L = this; + } + #endregion + + + #region Static Methods + private static int HexValue (int digit) + { + switch (digit) { + case 'a': + case 'A': + return 10; + + case 'b': + case 'B': + return 11; + + case 'c': + case 'C': + return 12; + + case 'd': + case 'D': + return 13; + + case 'e': + case 'E': + return 14; + + case 'f': + case 'F': + return 15; + + default: + return digit - '0'; + } + } + + private static void PopulateFsmTables (out StateHandler[] fsm_handler_table, out int[] fsm_return_table) + { + // See section A.1. of the manual for details of the finite + // state machine. + fsm_handler_table = new StateHandler[28] { + State1, + State2, + State3, + State4, + State5, + State6, + State7, + State8, + State9, + State10, + State11, + State12, + State13, + State14, + State15, + State16, + State17, + State18, + State19, + State20, + State21, + State22, + State23, + State24, + State25, + State26, + State27, + State28 + }; + + fsm_return_table = new int[28] { + (int) ParserToken.Char, + 0, + (int) ParserToken.Number, + (int) ParserToken.Number, + 0, + (int) ParserToken.Number, + 0, + (int) ParserToken.Number, + 0, + 0, + (int) ParserToken.True, + 0, + 0, + 0, + (int) ParserToken.False, + 0, + 0, + (int) ParserToken.Null, + (int) ParserToken.CharSeq, + (int) ParserToken.Char, + 0, + 0, + (int) ParserToken.CharSeq, + (int) ParserToken.Char, + 0, + 0, + 0, + 0 + }; + } + + private static char ProcessEscChar (int esc_char) + { + switch (esc_char) { + case '"': + case '\'': + case '\\': + case '/': + return Convert.ToChar (esc_char); + + case 'n': + return '\n'; + + case 't': + return '\t'; + + case 'r': + return '\r'; + + case 'b': + return '\b'; + + case 'f': + return '\f'; + + default: + // Unreachable + return '?'; + } + } + + private static bool State1 (FsmContext ctx) + { + while (ctx.L.GetChar ()) { + if (ctx.L.input_char == ' ' || + ctx.L.input_char >= '\t' && ctx.L.input_char <= '\r') + continue; + + if (ctx.L.input_char >= '1' && ctx.L.input_char <= '9') { + ctx.L.string_buffer.Append ((char) ctx.L.input_char); + ctx.NextState = 3; + return true; + } + + switch (ctx.L.input_char) { + case '"': + ctx.NextState = 19; + ctx.Return = true; + return true; + + case ',': + case ':': + case '[': + case ']': + case '{': + case '}': + ctx.NextState = 1; + ctx.Return = true; + return true; + + case '-': + ctx.L.string_buffer.Append ((char) ctx.L.input_char); + ctx.NextState = 2; + return true; + + case '0': + ctx.L.string_buffer.Append ((char) ctx.L.input_char); + ctx.NextState = 4; + return true; + + case 'f': + ctx.NextState = 12; + return true; + + case 'n': + ctx.NextState = 16; + return true; + + case 't': + ctx.NextState = 9; + return true; + + case '\'': + if (! ctx.L.allow_single_quoted_strings) + return false; + + ctx.L.input_char = '"'; + ctx.NextState = 23; + ctx.Return = true; + return true; + + case '/': + if (! ctx.L.allow_comments) + return false; + + ctx.NextState = 25; + return true; + + default: + return false; + } + } + + return true; + } + + private static bool State2 (FsmContext ctx) + { + ctx.L.GetChar (); + + if (ctx.L.input_char >= '1' && ctx.L.input_char<= '9') { + ctx.L.string_buffer.Append ((char) ctx.L.input_char); + ctx.NextState = 3; + return true; + } + + switch (ctx.L.input_char) { + case '0': + ctx.L.string_buffer.Append ((char) ctx.L.input_char); + ctx.NextState = 4; + return true; + + default: + return false; + } + } + + private static bool State3 (FsmContext ctx) + { + while (ctx.L.GetChar ()) { + if (ctx.L.input_char >= '0' && ctx.L.input_char <= '9') { + ctx.L.string_buffer.Append ((char) ctx.L.input_char); + continue; + } + + if (ctx.L.input_char == ' ' || + ctx.L.input_char >= '\t' && ctx.L.input_char <= '\r') { + ctx.Return = true; + ctx.NextState = 1; + return true; + } + + switch (ctx.L.input_char) { + case ',': + case ']': + case '}': + ctx.L.UngetChar (); + ctx.Return = true; + ctx.NextState = 1; + return true; + + case '.': + ctx.L.string_buffer.Append ((char) ctx.L.input_char); + ctx.NextState = 5; + return true; + + case 'e': + case 'E': + ctx.L.string_buffer.Append ((char) ctx.L.input_char); + ctx.NextState = 7; + return true; + + default: + return false; + } + } + return true; + } + + private static bool State4 (FsmContext ctx) + { + ctx.L.GetChar (); + + if (ctx.L.input_char == ' ' || + ctx.L.input_char >= '\t' && ctx.L.input_char <= '\r') { + ctx.Return = true; + ctx.NextState = 1; + return true; + } + + switch (ctx.L.input_char) { + case ',': + case ']': + case '}': + ctx.L.UngetChar (); + ctx.Return = true; + ctx.NextState = 1; + return true; + + case '.': + ctx.L.string_buffer.Append ((char) ctx.L.input_char); + ctx.NextState = 5; + return true; + + case 'e': + case 'E': + ctx.L.string_buffer.Append ((char) ctx.L.input_char); + ctx.NextState = 7; + return true; + + default: + return false; + } + } + + private static bool State5 (FsmContext ctx) + { + ctx.L.GetChar (); + + if (ctx.L.input_char >= '0' && ctx.L.input_char <= '9') { + ctx.L.string_buffer.Append ((char) ctx.L.input_char); + ctx.NextState = 6; + return true; + } + + return false; + } + + private static bool State6 (FsmContext ctx) + { + while (ctx.L.GetChar ()) { + if (ctx.L.input_char >= '0' && ctx.L.input_char <= '9') { + ctx.L.string_buffer.Append ((char) ctx.L.input_char); + continue; + } + + if (ctx.L.input_char == ' ' || + ctx.L.input_char >= '\t' && ctx.L.input_char <= '\r') { + ctx.Return = true; + ctx.NextState = 1; + return true; + } + + switch (ctx.L.input_char) { + case ',': + case ']': + case '}': + ctx.L.UngetChar (); + ctx.Return = true; + ctx.NextState = 1; + return true; + + case 'e': + case 'E': + ctx.L.string_buffer.Append ((char) ctx.L.input_char); + ctx.NextState = 7; + return true; + + default: + return false; + } + } + + return true; + } + + private static bool State7 (FsmContext ctx) + { + ctx.L.GetChar (); + + if (ctx.L.input_char >= '0' && ctx.L.input_char<= '9') { + ctx.L.string_buffer.Append ((char) ctx.L.input_char); + ctx.NextState = 8; + return true; + } + + switch (ctx.L.input_char) { + case '+': + case '-': + ctx.L.string_buffer.Append ((char) ctx.L.input_char); + ctx.NextState = 8; + return true; + + default: + return false; + } + } + + private static bool State8 (FsmContext ctx) + { + while (ctx.L.GetChar ()) { + if (ctx.L.input_char >= '0' && ctx.L.input_char<= '9') { + ctx.L.string_buffer.Append ((char) ctx.L.input_char); + continue; + } + + if (ctx.L.input_char == ' ' || + ctx.L.input_char >= '\t' && ctx.L.input_char<= '\r') { + ctx.Return = true; + ctx.NextState = 1; + return true; + } + + switch (ctx.L.input_char) { + case ',': + case ']': + case '}': + ctx.L.UngetChar (); + ctx.Return = true; + ctx.NextState = 1; + return true; + + default: + return false; + } + } + + return true; + } + + private static bool State9 (FsmContext ctx) + { + ctx.L.GetChar (); + + switch (ctx.L.input_char) { + case 'r': + ctx.NextState = 10; + return true; + + default: + return false; + } + } + + private static bool State10 (FsmContext ctx) + { + ctx.L.GetChar (); + + switch (ctx.L.input_char) { + case 'u': + ctx.NextState = 11; + return true; + + default: + return false; + } + } + + private static bool State11 (FsmContext ctx) + { + ctx.L.GetChar (); + + switch (ctx.L.input_char) { + case 'e': + ctx.Return = true; + ctx.NextState = 1; + return true; + + default: + return false; + } + } + + private static bool State12 (FsmContext ctx) + { + ctx.L.GetChar (); + + switch (ctx.L.input_char) { + case 'a': + ctx.NextState = 13; + return true; + + default: + return false; + } + } + + private static bool State13 (FsmContext ctx) + { + ctx.L.GetChar (); + + switch (ctx.L.input_char) { + case 'l': + ctx.NextState = 14; + return true; + + default: + return false; + } + } + + private static bool State14 (FsmContext ctx) + { + ctx.L.GetChar (); + + switch (ctx.L.input_char) { + case 's': + ctx.NextState = 15; + return true; + + default: + return false; + } + } + + private static bool State15 (FsmContext ctx) + { + ctx.L.GetChar (); + + switch (ctx.L.input_char) { + case 'e': + ctx.Return = true; + ctx.NextState = 1; + return true; + + default: + return false; + } + } + + private static bool State16 (FsmContext ctx) + { + ctx.L.GetChar (); + + switch (ctx.L.input_char) { + case 'u': + ctx.NextState = 17; + return true; + + default: + return false; + } + } + + private static bool State17 (FsmContext ctx) + { + ctx.L.GetChar (); + + switch (ctx.L.input_char) { + case 'l': + ctx.NextState = 18; + return true; + + default: + return false; + } + } + + private static bool State18 (FsmContext ctx) + { + ctx.L.GetChar (); + + switch (ctx.L.input_char) { + case 'l': + ctx.Return = true; + ctx.NextState = 1; + return true; + + default: + return false; + } + } + + private static bool State19 (FsmContext ctx) + { + while (ctx.L.GetChar ()) { + switch (ctx.L.input_char) { + case '"': + ctx.L.UngetChar (); + ctx.Return = true; + ctx.NextState = 20; + return true; + + case '\\': + ctx.StateStack = 19; + ctx.NextState = 21; + return true; + + default: + ctx.L.string_buffer.Append ((char) ctx.L.input_char); + continue; + } + } + + return true; + } + + private static bool State20 (FsmContext ctx) + { + ctx.L.GetChar (); + + switch (ctx.L.input_char) { + case '"': + ctx.Return = true; + ctx.NextState = 1; + return true; + + default: + return false; + } + } + + private static bool State21 (FsmContext ctx) + { + ctx.L.GetChar (); + + switch (ctx.L.input_char) { + case 'u': + ctx.NextState = 22; + return true; + + case '"': + case '\'': + case '/': + case '\\': + case 'b': + case 'f': + case 'n': + case 'r': + case 't': + ctx.L.string_buffer.Append ( + ProcessEscChar (ctx.L.input_char)); + ctx.NextState = ctx.StateStack; + return true; + + default: + return false; + } + } + + private static bool State22 (FsmContext ctx) + { + int counter = 0; + int mult = 4096; + + ctx.L.unichar = 0; + + while (ctx.L.GetChar ()) { + + if (ctx.L.input_char >= '0' && ctx.L.input_char <= '9' || + ctx.L.input_char >= 'A' && ctx.L.input_char <= 'F' || + ctx.L.input_char >= 'a' && ctx.L.input_char <= 'f') { + + ctx.L.unichar += HexValue (ctx.L.input_char) * mult; + + counter++; + mult /= 16; + + if (counter == 4) { + ctx.L.string_buffer.Append ( + Convert.ToChar (ctx.L.unichar)); + ctx.NextState = ctx.StateStack; + return true; + } + + continue; + } + + return false; + } + + return true; + } + + private static bool State23 (FsmContext ctx) + { + while (ctx.L.GetChar ()) { + switch (ctx.L.input_char) { + case '\'': + ctx.L.UngetChar (); + ctx.Return = true; + ctx.NextState = 24; + return true; + + case '\\': + ctx.StateStack = 23; + ctx.NextState = 21; + return true; + + default: + ctx.L.string_buffer.Append ((char) ctx.L.input_char); + continue; + } + } + + return true; + } + + private static bool State24 (FsmContext ctx) + { + ctx.L.GetChar (); + + switch (ctx.L.input_char) { + case '\'': + ctx.L.input_char = '"'; + ctx.Return = true; + ctx.NextState = 1; + return true; + + default: + return false; + } + } + + private static bool State25 (FsmContext ctx) + { + ctx.L.GetChar (); + + switch (ctx.L.input_char) { + case '*': + ctx.NextState = 27; + return true; + + case '/': + ctx.NextState = 26; + return true; + + default: + return false; + } + } + + private static bool State26 (FsmContext ctx) + { + while (ctx.L.GetChar ()) { + if (ctx.L.input_char == '\n') { + ctx.NextState = 1; + return true; + } + } + + return true; + } + + private static bool State27 (FsmContext ctx) + { + while (ctx.L.GetChar ()) { + if (ctx.L.input_char == '*') { + ctx.NextState = 28; + return true; + } + } + + return true; + } + + private static bool State28 (FsmContext ctx) + { + while (ctx.L.GetChar ()) { + if (ctx.L.input_char == '*') + continue; + + if (ctx.L.input_char == '/') { + ctx.NextState = 1; + return true; + } + + ctx.NextState = 27; + return true; + } + + return true; + } + #endregion + + + private bool GetChar () + { + if ((input_char = NextChar ()) != -1) + return true; + + end_of_input = true; + return false; + } + + private int NextChar () + { + if (input_buffer != 0) { + int tmp = input_buffer; + input_buffer = 0; + + return tmp; + } + + return reader.Read (); + } + + public bool NextToken () + { + StateHandler handler; + fsm_context.Return = false; + + while (true) { + handler = fsm_handler_table[state - 1]; + + if (! handler (fsm_context)) + throw new JsonException (input_char); + + if (end_of_input) + return false; + + if (fsm_context.Return) { + string_value = string_buffer.ToString (); + string_buffer.Remove (0, string_buffer.Length); + token = fsm_return_table[state - 1]; + + if (token == (int) ParserToken.Char) + token = input_char; + + state = fsm_context.NextState; + + return true; + } + + state = fsm_context.NextState; + } + } + + private void UngetChar () + { + input_buffer = input_char; + } + } +} diff --git a/LitJson/Runtime/Lexer.cs.meta b/LitJson/Runtime/Lexer.cs.meta new file mode 100644 index 0000000..1777a28 --- /dev/null +++ b/LitJson/Runtime/Lexer.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 0e99ecf149fcd4361bafc43771b80d60 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/LitJson/Runtime/LitJSON.csproj b/LitJson/Runtime/LitJSON.csproj new file mode 100644 index 0000000..5f2fcfb --- /dev/null +++ b/LitJson/Runtime/LitJSON.csproj @@ -0,0 +1,87 @@ + + + + netstandard2.1;netstandard2.0;net45;net48;netstandard1.5;net40;net35;net20;net6.0;net8.0 + + + + false + embedded + true + true + true + README.md + + + + + + + + + + LitJson + A .NET library to handle conversions from and to JSON (JavaScript Object Notation) strings. Written in C#, and it’s intended to be small, fast and easy to use. +It's quick and lean, without external dependencies. + The authors disclaim copyright to this source code. + Leonardo Boshell, Mattias Karlsson and contributors + Leonardo Boshell, Mattias Karlsson and contributors + Unlicense + litjson.png + https://github.com/LitJSON/litjson + git + JSON;Serializer + true + + + + + + + + $(DefineConstants);LEGACY + + + + $(DefineConstants);LEGACY + + + + $(DefineConstants);LEGACY + + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers + + + all + runtime; build; native; contentfiles; analyzers + + + all + runtime; build; native; contentfiles; analyzers + + + all + runtime; build; native; contentfiles; analyzers + + + all + runtime; build; native; contentfiles; analyzers + + + + + + + + diff --git a/LitJson/Runtime/LitJSON.csproj.meta b/LitJson/Runtime/LitJSON.csproj.meta new file mode 100644 index 0000000..4320971 --- /dev/null +++ b/LitJson/Runtime/LitJSON.csproj.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: c5e8519174c484ae1903459114f61f98 +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/LitJson/Runtime/Netstandard15Polyfill.cs b/LitJson/Runtime/Netstandard15Polyfill.cs new file mode 100644 index 0000000..55b02a2 --- /dev/null +++ b/LitJson/Runtime/Netstandard15Polyfill.cs @@ -0,0 +1,24 @@ +#if NETSTANDARD1_5 +using System; +using System.Reflection; +namespace LitJson +{ + internal static class Netstandard15Polyfill + { + internal static Type GetInterface(this Type type, string name) + { + return type.GetTypeInfo().GetInterface(name); + } + + internal static bool IsClass(this Type type) + { + return type.GetTypeInfo().IsClass; + } + + internal static bool IsEnum(this Type type) + { + return type.GetTypeInfo().IsEnum; + } + } +} +#endif \ No newline at end of file diff --git a/LitJson/Runtime/Netstandard15Polyfill.cs.meta b/LitJson/Runtime/Netstandard15Polyfill.cs.meta new file mode 100644 index 0000000..be2fe01 --- /dev/null +++ b/LitJson/Runtime/Netstandard15Polyfill.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: e212e5f514d9d4ec68eed1251080cce4 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/LitJson/Runtime/ParserToken.cs b/LitJson/Runtime/ParserToken.cs new file mode 100644 index 0000000..e6437ca --- /dev/null +++ b/LitJson/Runtime/ParserToken.cs @@ -0,0 +1,44 @@ +#region Header +/** + * ParserToken.cs + * Internal representation of the tokens used by the lexer and the parser. + * + * The authors disclaim copyright to this source code. For more details, see + * the COPYING file included with this distribution. + **/ +#endregion + + +namespace Guru.LitJson +{ + internal enum ParserToken + { + // Lexer tokens (see section A.1.1. of the manual) + None = System.Char.MaxValue + 1, + Number, + True, + False, + Null, + CharSeq, + // Single char + Char, + + // Parser Rules (see section A.2.1 of the manual) + Text, + Object, + ObjectPrime, + Pair, + PairRest, + Array, + ArrayPrime, + Value, + ValueRest, + String, + + // End of input + End, + + // The empty rule + Epsilon + } +} diff --git a/LitJson/Runtime/ParserToken.cs.meta b/LitJson/Runtime/ParserToken.cs.meta new file mode 100644 index 0000000..65548cf --- /dev/null +++ b/LitJson/Runtime/ParserToken.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 6a046907ebdf54072ab870aa49b58f76 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/LitJson/Runtime/litjson.png b/LitJson/Runtime/litjson.png new file mode 100644 index 0000000..a4c15e5 Binary files /dev/null and b/LitJson/Runtime/litjson.png differ diff --git a/LitJson/Runtime/litjson.png.meta b/LitJson/Runtime/litjson.png.meta new file mode 100644 index 0000000..2447ccc --- /dev/null +++ b/LitJson/Runtime/litjson.png.meta @@ -0,0 +1,135 @@ +fileFormatVersion: 2 +guid: f3a96012ad0784fc7893f9b9eb05788e +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 12 + mipmaps: + mipMapMode: 0 + enableMipMap: 1 + sRGBTexture: 1 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + ignoreMasterTextureLimit: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 1 + aniso: 1 + mipBias: 0 + wrapU: 0 + wrapV: 0 + wrapW: 0 + nPOTScale: 1 + lightmap: 0 + compressionQuality: 50 + spriteMode: 0 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 0 + spriteTessellationDetail: -1 + textureType: 0 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 0 + cookieLightType: 0 + platformSettings: + - serializedVersion: 3 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 3 + buildTarget: Standalone + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 3 + buildTarget: Android + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 3 + buildTarget: Server + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + physicsShape: [] + bones: [] + spriteID: + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + nameFileIdTable: {} + spritePackingTag: + pSDRemoveMatte: 0 + pSDShowRemoveMatteOption: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/README.md b/README.md new file mode 100644 index 0000000..2154886 --- /dev/null +++ b/README.md @@ -0,0 +1,100 @@ +# Guru Framework + + +**Version 2.1.0** + +- 插件整体调整了文件结构,细分了相关模块的路径,添加了对应的 `asmdef` 文件 + + + + +**Version 2.0.3** + +- [升级须知](#notice) + + +## 依赖库 + +### Firebase +- 整体升级为 10.1.1 + +### AppLovin Max +- 整体升级为 11.11.3 +- 详细的广告Adapter版本, [详见这里](https://docs.google.com/spreadsheets/d/161UnDimGerqetIYNiMCfUBmJ7qozht8z1baxnxRdCnI) + +--- + +## 子模块 + +### GuruCore +GuruSDK 核心逻辑类 + +### GuruAds +GuruSDK 封装了广告服务相关的接口 +- 新增了 Moloco 和 Pubmatic 两个渠道 +- 新增了ATTManager 用于管理ATT弹出和相关事件统计 + +### GuruAnalytics +Guru自打点统计模块 +- 更新了用户时长统计修复, 修复 Worker 启动报错的问题 + +### GuruConsent +使用 Funding Choices 作为数据的启动广告隐私权限引导模块 + +### GuruBuildTool +构建工具合集 +- 更新了SKADNetwork 数据 +- 更新 Xcode15 构建支持 + +### GuruIAP +支付服务相关接口, 底层使用的是 Unity 的 In-App-Purchase 插件 + +### GuruEntry +游戏入口模块 + + +### GuruL10n +Guru的翻译模块, 内部衔接了 I2 Localization 插件, 外部衔接中台自动翻译接口 + + +### GuruRating +游戏评分模块 + +### Keywords +Max Keywords 上报模块 + + +--- + + +## 升级须知 + +### Android + +- 需要在 `BuildSettings/Player Settings.../Publishing Settings` 内, 开启使用 `Custom Main Gradle Template` +- 可以直接使用中台提供的 `launcherTemplate.gradle` 文件 +- 或者在新生成的 `Assets/Plugins/Android/launcherTemplate.gradle` 内添加如下代码: + ```groove + android { + ... + + lintOptions { + abortOnError false + checkReleaseBuilds false // <---请添加此行代码 + } + + + // 请将模版内的 **PACKAGING_OPTIONS** 替换为如下代码 + packagingOptions { + exclude("META-INF/*.kotlin_module") + } + + ... + } + ``` + +- Android 构建的最小 Target Version 为 21 + +### iOS +- 构建相关的升级已经提交至 BuildTools 内 +- 其他问题持续收集中 \ No newline at end of file diff --git a/README.md.meta b/README.md.meta new file mode 100644 index 0000000..2c0e9e6 --- /dev/null +++ b/README.md.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 9d4fe0abfc67449296f3992f2b823b78 +timeCreated: 1671010548 \ No newline at end of file diff --git a/Runtime.meta b/Runtime.meta new file mode 100644 index 0000000..5a42666 --- /dev/null +++ b/Runtime.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: f0c01eb685db4af99bbfa645d8307495 +timeCreated: 1671499352 \ No newline at end of file diff --git a/Runtime/ABTest.meta b/Runtime/ABTest.meta new file mode 100644 index 0000000..543d238 --- /dev/null +++ b/Runtime/ABTest.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 205f90aa56f64c759adf90d34e07fadf +timeCreated: 1702973536 \ No newline at end of file diff --git a/Runtime/ABTest/ABTestManager.cs b/Runtime/ABTest/ABTestManager.cs new file mode 100644 index 0000000..403f06f --- /dev/null +++ b/Runtime/ABTest/ABTestManager.cs @@ -0,0 +1,150 @@ +using System; +using System.Collections.Generic; +using Firebase.RemoteConfig; +using Guru; +using Guru.LitJson; +using UnityEngine; + +namespace Guru +{ + + + /// + /// ABTEST 管理器 + /// + public class ABTestManager : Singleton + { + + private FirebaseRemoteConfig _remoteConfig; + + private List _params; + + + #region 初始化 + + /// + /// 初始化 + /// + public static void Init() + { + try + { + Instance.Setup(); + } + catch (Exception e) + { + Debug.LogError(e); + } + + } + + /// + /// 安装服务 + /// + private void Setup() + { + Debug.Log($"[AB] --- ABTest Init"); + _params = new List(); + + _remoteConfig = FirebaseRemoteConfig.DefaultInstance; + + string strValue; + foreach (var key in _remoteConfig.Keys) + { + strValue = _remoteConfig.GetValue(key).StringValue; + AddParam(strValue); + } + + // ------- ABTest ----------- + // Debug.Log($" --- start parse test string --- "); + // var testStr = @"{""enabled"":true,""value"":2,""id"":""B"",""guru_ab_23100715"":""B""}"; + // AddParam(testStr); + + if (_params.Count > 0) + { + for (int i = 0; i < _params.Count; i++) + { + // 上报实验AB属性 + GuruAnalytics.SetUserProperty(_params[i].id, _params[i].group); +#if UNITY_EDITOR + Debug.Log($"[AB] --- Add AB Param {_params[i].ToString()}"); +#else + Debug.Log($"[AB] --- Add AB Param {_params[i].ToString()}"); +#endif + } + } + + } + + #endregion + + #region 添加AB参数 + + /// + /// 添加AB参数 + /// + /// + private void AddParam(string value) + { + if (!string.IsNullOrEmpty(value) && value.Contains("guru_ab_")) + { + _params.Add(ABParamData.Parse(value)); // 添加参数 + } + } + + #endregion + } + + [Serializable] + internal class ABParamData + { + private const int PARAM_NAME_LENGTH = 23; // 从开始"ab_" 计算, 往后20个字符 + + public string id; + public string group; + public string value; + + public static ABParamData Parse(string value) + { + Debug.Log($"--- ABParamData.Parse: {value}"); + var p = new ABParamData(); + p.value = value; + + // 发现Guru AB测试标志位 + var dict = JsonMapper.ToObject>(value); + if (null != dict) + { + foreach (var k in dict.Keys) + { + if (k.StartsWith("guru_ab")) + { + p.id = GetItemKey(k); + p.group = dict[k].ToString(); + // Debug.Log($"[AB] add property {k}: {dict[k]}"); + break; + } + } + } + + return p; + } + + private static string GetItemKey(string raw) + { + var h = raw.Replace("guru_", ""); + var key = h.Substring(0, Mathf.Min(PARAM_NAME_LENGTH, h.Length)); // 最大长度23 + return key; + } + + /// + /// 输出字符串 + /// + /// + public override string ToString() + { + return $"{id} : {group}"; + } + + } + +} \ No newline at end of file diff --git a/Runtime/ABTest/ABTestManager.cs.meta b/Runtime/ABTest/ABTestManager.cs.meta new file mode 100644 index 0000000..36cd768 --- /dev/null +++ b/Runtime/ABTest/ABTestManager.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 9da3c10a9dde46499798d47cdd48fd70 +timeCreated: 1696650618 \ No newline at end of file diff --git a/Runtime/ABTest/README.md b/Runtime/ABTest/README.md new file mode 100644 index 0000000..95a3cea --- /dev/null +++ b/Runtime/ABTest/README.md @@ -0,0 +1,46 @@ + +# ABTest 中台接口 + +- [相关实现技术文档链接](https://docs.google.com/document/d/1AG9PLq-dI0plIati2qgVpuD5QchNO2P8phY1GWr8SeQ/edit?pli=1#heading=h.906ruqltpzqz) + + +## 云控配置 +- 需要根据每个测试来配置对应的属性和ID + + ```javascript + // 参数 KEY 构成 + guru_ab_ + 231009 + 01 + ^ ^ ^ + 固定前缀 年月日 实验序号 + + // 参数值构成 + "A" 或 "B" + "C" 或 "D" + + ``` + + +- 属性字段是追加在云控参数体内的 + ```json + // 云控的json 参数数据体 + { + "id": 1, + "value": "test", + + "guru_ab_23100901": "A", // 第一个实验的分组 + "guru_ab_23100902": "C" // 第二个实验的分组 + + } + + + + ``` + +## 结果验证 + +- 对于已经切换了自打点的项目, 需要BI组配合项目组, 抽取每组实验用户的数据, 可形成有效报告 +- 项目接入后在启动时不会卡顿, 项目不会产生异常和崩溃 + + + + diff --git a/Runtime/ABTest/README.md.meta b/Runtime/ABTest/README.md.meta new file mode 100644 index 0000000..110dd35 --- /dev/null +++ b/Runtime/ABTest/README.md.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: a1e7ab6ebd2348d99444732ad4f6422b +timeCreated: 1697167744 \ No newline at end of file diff --git a/Runtime/Guru.Runtime.asmdef b/Runtime/Guru.Runtime.asmdef new file mode 100644 index 0000000..3df2c6c --- /dev/null +++ b/Runtime/Guru.Runtime.asmdef @@ -0,0 +1,30 @@ +{ + "name": "Guru.Runtime", + "rootNamespace": "Guru", + "references": [ + "Adjust", + "MaxSdk", + "MaxSdk.Scripts", + "Amazon", + "OpenWrapSDK", + "UniWebView-CSharp", + "UnityEngine.Purchasing", + "UnityEngine.Purchasing.Apple", + "UnityEngine.Purchasing.Stores", + "UnityEngine.Purchasing.Security", + "UnityEngine.Purchasing.SecurityCore", + "UnityEngine.Purchasing.SecurityStub", + "Google.Play.Review", + "Google.Play.Common", + "Guru.LitJson" + ], + "includePlatforms": [], + "excludePlatforms": [], + "allowUnsafeCode": false, + "overrideReferences": false, + "precompiledReferences": [], + "autoReferenced": true, + "defineConstraints": [], + "versionDefines": [], + "noEngineReferences": false +} \ No newline at end of file diff --git a/Runtime/Guru.Runtime.asmdef.meta b/Runtime/Guru.Runtime.asmdef.meta new file mode 100644 index 0000000..b672e70 --- /dev/null +++ b/Runtime/Guru.Runtime.asmdef.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: e241d247f550f427da05939864204192 +AssemblyDefinitionImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/GuruAds.meta b/Runtime/GuruAds.meta new file mode 100644 index 0000000..fd6e44b --- /dev/null +++ b/Runtime/GuruAds.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: a66a5db145b064b588d9f4def3d72b12 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/GuruAds/ATT.meta b/Runtime/GuruAds/ATT.meta new file mode 100644 index 0000000..0c60c00 --- /dev/null +++ b/Runtime/GuruAds/ATT.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 29a493ec2a4443b7a707065c40e88459 +timeCreated: 1694079394 \ No newline at end of file diff --git a/Runtime/GuruAds/ATT/ATTManager.cs b/Runtime/GuruAds/ATT/ATTManager.cs new file mode 100644 index 0000000..1ad0f1c --- /dev/null +++ b/Runtime/GuruAds/ATT/ATTManager.cs @@ -0,0 +1,132 @@ +#if UNITY_IOS + +namespace Guru +{ + using UnityEngine; + using System; + using Unity.Advertisement.IosSupport; + public class ATTManager + { + public const string Version = "1.0.0"; + + public const string ATT_STATUS_AUTHORIZED = "authorized"; + public const string ATT_STATUS_DENIED = "denied"; + public const string ATT_STATUS_RESTRICTED = "restricted"; + public const string ATT_STATUS_NOT_DETERMINED = "notDetermined"; + public const string ATT_STATUS_NOT_APPLICABLE = "notApplicable"; + public const int ATT_REQUIRED_MIN_OS = 14; + + //---------- 引导类型 ------------ + public const string GUIDE_TYPE_ADMOB = "admob"; + public const string GUIDE_TYPE_CUSTOM = "custom"; + public const string GUIDE_TYPE_MAX = "max"; + + /// + /// 获取状态 + /// + /// + public static string GetStatus() + { + if (!IsATTSupported()) return ATT_STATUS_NOT_APPLICABLE; + var status = GetStatusString(ATTrackingStatusBinding.GetAuthorizationTrackingStatus()); + if(!string.IsNullOrEmpty(status)) return status; + return ATT_STATUS_NOT_APPLICABLE; + } + + /// + /// 转字符串 + /// + /// + /// + public static string GetStatusString(ATTrackingStatusBinding.AuthorizationTrackingStatus status) + { + switch (status) + { + case ATTrackingStatusBinding.AuthorizationTrackingStatus.NOT_DETERMINED: + return ATT_STATUS_NOT_DETERMINED; + case ATTrackingStatusBinding.AuthorizationTrackingStatus.AUTHORIZED: + return ATT_STATUS_AUTHORIZED; + case ATTrackingStatusBinding.AuthorizationTrackingStatus.DENIED: + return ATT_STATUS_DENIED; + case ATTrackingStatusBinding.AuthorizationTrackingStatus.RESTRICTED: + return ATT_STATUS_RESTRICTED; + } + return ""; + } + + /// + /// 状态码转字符串 + /// + /// + /// + public static string GetStatusString(int value) + => GetStatusString((ATTrackingStatusBinding.AuthorizationTrackingStatus)value); + + /// + /// 是否支持ATT + /// + /// + private static bool IsATTSupported() + { + string version = UnityEngine.iOS.Device.systemVersion; + + Debug.Log($"[ATT] --- Get iOS system version: {version}"); + + string tmp = version; + if (version.Contains(" ")) + { + var a1 = version.Split(' '); + tmp = a1[a1.Length - 1]; + } + + string num = tmp; + if (tmp.Contains(".")) + { + num = tmp.Split('.')[0]; + } + + if (int.TryParse(num, out var ver)) + { + if (ver >= ATT_REQUIRED_MIN_OS) return true; + } + + return false; + } + + /// + /// 请求系统弹窗 + /// + public static void RequestATTDailog(Action callback = null) + { + if (!IsATTSupported()) + { + callback?.Invoke(ATT_STATUS_NOT_APPLICABLE); // 不支持 + return; + } + + ATTrackingStatusBinding.RequestAuthorizationTracking(status =>{ + callback?.Invoke(GetStatusString(status)); + }); + } + + /// + /// 启动时检查状态 + /// + /// + public static void CheckStatus(Action callback = null) + { + if (!IsATTSupported()) + { + callback?.Invoke(ATT_STATUS_NOT_APPLICABLE); // 不支持 + return; + } + + callback?.Invoke(GetStatus()); + } + + } + + +} + +#endif \ No newline at end of file diff --git a/Runtime/GuruAds/ATT/ATTManager.cs.meta b/Runtime/GuruAds/ATT/ATTManager.cs.meta new file mode 100644 index 0000000..00418c2 --- /dev/null +++ b/Runtime/GuruAds/ATT/ATTManager.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: c64a8943316547c88c9ace04dd5e3d05 +timeCreated: 1694079403 \ No newline at end of file diff --git a/Runtime/GuruAds/Amazon.meta b/Runtime/GuruAds/Amazon.meta new file mode 100644 index 0000000..35c05bd --- /dev/null +++ b/Runtime/GuruAds/Amazon.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 367cb7699f154c8894d8c3ca85c1fe94 +timeCreated: 1681280112 \ No newline at end of file diff --git a/Runtime/GuruAds/Amazon/Editor.meta b/Runtime/GuruAds/Amazon/Editor.meta new file mode 100644 index 0000000..a4d3573 --- /dev/null +++ b/Runtime/GuruAds/Amazon/Editor.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 9bb6c50e89674152823d45b10ce535a2 +timeCreated: 1687238820 \ No newline at end of file diff --git a/Runtime/GuruAds/Amazon/Editor/Proguards.txt b/Runtime/GuruAds/Amazon/Editor/Proguards.txt new file mode 100644 index 0000000..6ee900b --- /dev/null +++ b/Runtime/GuruAds/Amazon/Editor/Proguards.txt @@ -0,0 +1 @@ +-keep class com.amazon.device.ads.** { *; } \ No newline at end of file diff --git a/Runtime/GuruAds/Amazon/Editor/Proguards.txt.meta b/Runtime/GuruAds/Amazon/Editor/Proguards.txt.meta new file mode 100644 index 0000000..be39702 --- /dev/null +++ b/Runtime/GuruAds/Amazon/Editor/Proguards.txt.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 8ce2781a43d0405d8481e54e2a8e5789 +timeCreated: 1687238837 \ No newline at end of file diff --git a/Runtime/GuruAds/Amazon/Pacakge.meta b/Runtime/GuruAds/Amazon/Pacakge.meta new file mode 100644 index 0000000..9436562 --- /dev/null +++ b/Runtime/GuruAds/Amazon/Pacakge.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: f0fcdcfefdeb4214bb75f2cc6a8356f8 +timeCreated: 1681280138 \ No newline at end of file diff --git a/Runtime/GuruAds/Amazon/Pacakge/unity_aps_1_5_1.zip b/Runtime/GuruAds/Amazon/Pacakge/unity_aps_1_5_1.zip new file mode 100644 index 0000000..de4b21a Binary files /dev/null and b/Runtime/GuruAds/Amazon/Pacakge/unity_aps_1_5_1.zip differ diff --git a/Runtime/GuruAds/Amazon/Pacakge/unity_aps_1_5_1.zip.meta b/Runtime/GuruAds/Amazon/Pacakge/unity_aps_1_5_1.zip.meta new file mode 100644 index 0000000..669b02f --- /dev/null +++ b/Runtime/GuruAds/Amazon/Pacakge/unity_aps_1_5_1.zip.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: f5e96341271f7477280791fe0c9a4015 +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/GuruAds/Amazon/README.md b/Runtime/GuruAds/Amazon/README.md new file mode 100644 index 0000000..1e95ceb --- /dev/null +++ b/Runtime/GuruAds/Amazon/README.md @@ -0,0 +1,52 @@ +# GuruAds 扩展 Amazon安装说明 + +## Version 1.0.0 + +## 更新内容: +- 添加 Amazon 广告适配兼容 + + +## Amazon 使用说明 + +### 介绍 +- 本扩展基于 GuruCore.Ads.ADService `1.2.0` 开发, + > 低于该版本的Guru框架请先升级至[ Guru 1.8.0 ](https://raw.githubusercontent.com/castbox/unity_gurucore/main/gurucore_1.8.0.unitypackage) +- 导入本扩展后, 需要先手动安装 APS 插件 + > unitypackage 可从以下路径解压缩后自行安装: + > + > Assets/Guru/GuruAds/Editor/Pcakge/unity_aps_1.4.3.zip + > +- 安装完毕后,请打开 GuruSettings 设置对应的 Amazon 广告配置参数 ,详见 `Amazon Setting` + +### 代码调用 + +- 请直接使用 `ADServiceAPS` 来完成所有广告生命周期相关的功能, 它继承了 `IADService` +- 实际上 `ADServiceAPS` 继承自 `ADService`。 具备基础功能的同时实现了Amazon的广告请求逻辑。 +- 所有的广告申请, 加载和播放结果的回调同之前是一致的。 + ```C# + + // 定义广告实例 + var service = ADService.Instance; + + // 启动广告服务 + bool isDebugMode = true; + service.StartService(() => + { + //TODO 执行广告初始化成功后的逻辑 + }, isDebugMode); + + // 显示 Banner + service.ShowBanner(); + + // 显示 IV + service.ShowInterstitialAD("level_end"); + + // 显示 RV + service.ShowRewardedAD("add_free_coin", () => + { + UserData.Coin += 10; + }); + + + + ``` \ No newline at end of file diff --git a/Runtime/GuruAds/Amazon/README.md.meta b/Runtime/GuruAds/Amazon/README.md.meta new file mode 100644 index 0000000..3203c14 --- /dev/null +++ b/Runtime/GuruAds/Amazon/README.md.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 6f5bfcb26e384536ab6e134d2cad1c18 +timeCreated: 1680158911 \ No newline at end of file diff --git a/Runtime/GuruAds/Amazon/Runtime.meta b/Runtime/GuruAds/Amazon/Runtime.meta new file mode 100644 index 0000000..17689ac --- /dev/null +++ b/Runtime/GuruAds/Amazon/Runtime.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 36da1246d638491bb601be91b8f18091 +timeCreated: 1681280144 \ No newline at end of file diff --git a/Runtime/GuruAds/Amazon/Runtime/AdChanelAmazon.cs b/Runtime/GuruAds/Amazon/Runtime/AdChanelAmazon.cs new file mode 100644 index 0000000..0521ea7 --- /dev/null +++ b/Runtime/GuruAds/Amazon/Runtime/AdChanelAmazon.cs @@ -0,0 +1,242 @@ +/********************************************** + * Amazon 广告渠道 + **********************************************/ +namespace Guru +{ + using AmazonAds; + using UnityEngine; + using System; + + internal struct AdSize + { + public int width; + public int height; + } + + /// + /// 广告渠道: Amazon + /// + public class AdChanelAmazon: IAdChannel, IAsyncRequestChannel + { + #region 广告尺寸 + + // Banner 尺寸参数 + private static AdSize BannerSize = new AdSize() { width = 320, height = 50 }; + // Video 尺寸参数 + private static AdSize VideoSize = new AdSize() { width = 320, height = 480 }; + + #endregion + + #region SLOT IDS + + // --------------- 获取各种SlotID -------------------- + private string AmazonBannerSlotID => GuruSettings.Instance.AmazonSetting.BannerSlotID; + private string AmazonInterVideoSlotID => GuruSettings.Instance.AmazonSetting.InterSlotID; + private string AmazonRewardSlotID => GuruSettings.Instance.AmazonSetting.RewardSlotID; + private string AmazonAppID => GuruSettings.Instance.AmazonSetting.AppID; + + // ---------------- Max 广告位ID -------------------- + private string MaxBannerSlotID => GuruSettings.Instance.ADSetting.GetBannerID(); + private string MaxIVSlotID => GuruSettings.Instance.ADSetting.GetInterstitialID(); + private string MaxRVSlotID => GuruSettings.Instance.ADSetting.GetRewardedVideoID(); + + #endregion + + #region Async 回调 + + public Action OnBannerRequestOver { get; set; } + public Action OnInterstitialRequestOver { get; set; } + public Action OnRewardRequestOver { get; set; } + + #endregion + + #region 属性 + + public static readonly string ChanelName = "Amazon"; + public string Name => ChanelName; + + public Action OnRequestOver { get; set; } + + /// + /// 当前平台是否可用 + /// + public bool IsEnabled + { + get + { +#if UNITY_EDITOR + return false; +#endif + // return GuruSettings.Instance.AmazonSetting.Enable; + return true; // 常驻开启 + } + } + + #endregion + + #region 初始化 + + /// + /// 初始化平台 + /// + public void Initialize() + { +#if UNITY_EDITOR + Debug.Log($"=== Amazon will not init on Editor ==="); +#endif + if (!IsEnabled) + { + Debug.Log($"[Ads] --- Amazon is not enabled"); + return; + } + + // 初始化Amazon + Amazon.Initialize (AmazonAppID); + Amazon.SetAdNetworkInfo(new AdNetworkInfo(DTBAdNetwork.MAX)); +#if UNITY_EDITOR || DEBUG + Amazon.EnableTesting (true); // Make sure to take this off when going live. +#else + Amazon.EnableLogging (false); +#endif + +#if UNITY_IOS + Amazon.SetAPSPublisherExtendedIdFeatureEnabled(true); +#endif + } + + #endregion + + #region Banner + + private APSBannerAdRequest bannerAdRequest; + private bool _firstLoadBanner = true; + public void LoadBannerAD() + { + if (!IsEnabled) return; + + Debug.Log($"--- Amazon banner start load ---"); + if (bannerAdRequest != null) bannerAdRequest.DestroyFetchManager(); + bannerAdRequest = new APSBannerAdRequest(BannerSize.width, BannerSize.height, AmazonBannerSlotID); + bannerAdRequest.onSuccess += (adResponse) => + { + Debug.Log($"--- Amazon Banner Load Success ---"); + MaxSdk.SetBannerLocalExtraParameter(MaxBannerSlotID, + "amazon_ad_response", + adResponse.GetResponse()); + OnBannerRequestOver?.Invoke(true, _firstLoadBanner); + _firstLoadBanner = false; + }; + + bannerAdRequest.onFailedWithError += (adError) => + { + Debug.Log($"--- Amazon Banner Load Fail: [{adError.GetCode()}] {adError.GetMessage()}"); + MaxSdk.SetBannerLocalExtraParameter(MaxBannerSlotID, + "amazon_ad_error", + adError.GetAdError()); + OnBannerRequestOver?.Invoke(false, _firstLoadBanner); + }; + + bannerAdRequest.LoadAd(); + +#if UNITY_EDITOR + OnBannerRequestOver?.Invoke(true, false); +#endif + + } + #endregion + + #region Intersitial + + private bool _isFirstLoadInter = true; + private APSVideoAdRequest interstitialAdRequest; + + public void LoadInterstitialAD() + { + if (!IsEnabled) return; + + // 首次启动注入渠道参数 + if (_isFirstLoadInter) + { + Debug.Log($"--- Amazon INTER start load ---"); + interstitialAdRequest = new APSVideoAdRequest(VideoSize.width, VideoSize.height, AmazonInterVideoSlotID); + interstitialAdRequest.onSuccess += (adResponse) => + { + Debug.Log($"--- Amazon INTER Load Success ---"); + MaxSdk.SetInterstitialLocalExtraParameter(MaxIVSlotID, + "amazon_ad_response", + adResponse.GetResponse()); + OnInterstitialRequestOver?.Invoke(true, true); + _isFirstLoadInter = false; + }; + interstitialAdRequest.onFailedWithError += (adError) => + { + Debug.Log($"--- Amazon INTER Load Fail: [{adError.GetCode()}] {adError.GetMessage()}"); + MaxSdk.SetInterstitialLocalExtraParameter(MaxIVSlotID, + "amazon_ad_error", + adError.GetAdError()); + OnInterstitialRequestOver?.Invoke(false, true); // 不成功则一直请求Amazon广告 + }; + interstitialAdRequest.LoadAd(); + } + else + { + OnInterstitialRequestOver?.Invoke(true, false); // 走默认的广告加载逻辑 + } + +#if UNITY_EDITOR + OnInterstitialRequestOver?.Invoke(true, false); + _isFirstLoadInter = false; +#endif + } + + + + #endregion + + #region Reward + + private bool _firstLoadRewward = true; + private APSVideoAdRequest rewardedVideoAdRequest; + + public void LoadRewardAD() + { + if (!IsEnabled) return; + + if (_firstLoadRewward) + { + Debug.Log($"--- Amazon Reward start load ---"); + rewardedVideoAdRequest = new APSVideoAdRequest(VideoSize.width, VideoSize.height, AmazonRewardSlotID); + rewardedVideoAdRequest.onSuccess += (adResponse) => + { + Debug.Log($"--- Amazon Reward Load Success ---"); + MaxSdk.SetRewardedAdLocalExtraParameter(MaxRVSlotID, + "amazon_ad_response", + adResponse.GetResponse()); + OnRewardRequestOver?.Invoke(true, true); + _firstLoadRewward = false; + }; + rewardedVideoAdRequest.onFailedWithError += (adError) => + { + Debug.Log($"--- Amazon Reward Load Fail: [{adError.GetCode()}] {adError.GetMessage()}"); + MaxSdk.SetRewardedAdLocalExtraParameter(MaxRVSlotID, + "amazon_ad_error", + adError.GetAdError()); + OnRewardRequestOver?.Invoke(false, true); // 不成功则一直请求Amazon广告 + }; + rewardedVideoAdRequest.LoadAd(); + } + else + { + OnRewardRequestOver?.Invoke(true, false); // 走默认的广告加载逻辑 + } +#if UNITY_EDITOR + OnRewardRequestOver?.Invoke(true, false); +#endif + } + + + #endregion + + } + +} \ No newline at end of file diff --git a/Runtime/GuruAds/Amazon/Runtime/AdChanelAmazon.cs.meta b/Runtime/GuruAds/Amazon/Runtime/AdChanelAmazon.cs.meta new file mode 100644 index 0000000..2f3e05f --- /dev/null +++ b/Runtime/GuruAds/Amazon/Runtime/AdChanelAmazon.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 9c01d5cecc9140c8a4a9e98a983299a2 +timeCreated: 1681280193 \ No newline at end of file diff --git a/Runtime/GuruAds/Amazon/Runtime/GuruSettings.APS.cs b/Runtime/GuruAds/Amazon/Runtime/GuruSettings.APS.cs new file mode 100644 index 0000000..e2a0d3b --- /dev/null +++ b/Runtime/GuruAds/Amazon/Runtime/GuruSettings.APS.cs @@ -0,0 +1,57 @@ +namespace Guru +{ + using System; + using UnityEngine; + + public partial class GuruSettings + { + [Header("Amazon 广告配置")] + public AmazonSetting AmazonSetting; + } + + + /// + /// Amazon广告配置 + /// + [Serializable] + public class AmazonSetting + { + + [SerializeField] public bool Enable; + [SerializeField] private AmazonPlatformSetting Android; + [SerializeField] private AmazonPlatformSetting iOS; + + /// + /// 获取AppID + /// + /// + public AmazonPlatformSetting GetPlatform() + { +#if UNITY_IOS + return iOS; +#else + return Android; +#endif + } + + public string AppID => GetPlatform().appID; + public string BannerSlotID => GetPlatform().bannerSlotID; + public string InterSlotID => GetPlatform().interSlotID; + public string RewardSlotID => GetPlatform().rewardSlotID; + + } + + + /// + /// Amazon平台专属配置 + /// + [Serializable] + public class AmazonPlatformSetting + { + // public string name; // 平台名称 + public string appID; // AppID + public string bannerSlotID; // Banner ID + public string interSlotID; // Inter ID + public string rewardSlotID; // Reward ID + } +} \ No newline at end of file diff --git a/Runtime/GuruAds/Amazon/Runtime/GuruSettings.APS.cs.meta b/Runtime/GuruAds/Amazon/Runtime/GuruSettings.APS.cs.meta new file mode 100644 index 0000000..9190afc --- /dev/null +++ b/Runtime/GuruAds/Amazon/Runtime/GuruSettings.APS.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 074cbc5efb0c40d4856e20904a9b434d +timeCreated: 1681281263 \ No newline at end of file diff --git a/Runtime/GuruAds/Moloco.meta b/Runtime/GuruAds/Moloco.meta new file mode 100644 index 0000000..bad8dbd --- /dev/null +++ b/Runtime/GuruAds/Moloco.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: a44918376993443edbe32c4ae267ad11 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/GuruAds/Moloco/Package.meta b/Runtime/GuruAds/Moloco/Package.meta new file mode 100644 index 0000000..a66c8f2 --- /dev/null +++ b/Runtime/GuruAds/Moloco/Package.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 23cd1489a0b9410ea9e6e2f410926351 +timeCreated: 1689144991 \ No newline at end of file diff --git a/Runtime/GuruAds/Moloco/Package/AppLovin-Moloco-Adapters-Android-1.2.3.0-iOS-1.1.0.0.unitypackage.zip b/Runtime/GuruAds/Moloco/Package/AppLovin-Moloco-Adapters-Android-1.2.3.0-iOS-1.1.0.0.unitypackage.zip new file mode 100644 index 0000000..9e660a4 Binary files /dev/null and b/Runtime/GuruAds/Moloco/Package/AppLovin-Moloco-Adapters-Android-1.2.3.0-iOS-1.1.0.0.unitypackage.zip differ diff --git a/Runtime/GuruAds/Moloco/Package/AppLovin-Moloco-Adapters-Android-1.2.3.0-iOS-1.1.0.0.unitypackage.zip.meta b/Runtime/GuruAds/Moloco/Package/AppLovin-Moloco-Adapters-Android-1.2.3.0-iOS-1.1.0.0.unitypackage.zip.meta new file mode 100644 index 0000000..641a765 --- /dev/null +++ b/Runtime/GuruAds/Moloco/Package/AppLovin-Moloco-Adapters-Android-1.2.3.0-iOS-1.1.0.0.unitypackage.zip.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 31dd283a9317e403e9eb07eba271e9c7 +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/GuruAds/Moloco/Runtime.meta b/Runtime/GuruAds/Moloco/Runtime.meta new file mode 100644 index 0000000..fb36378 --- /dev/null +++ b/Runtime/GuruAds/Moloco/Runtime.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: f369f6d7d0c6425d88a3b8ec6ec1b4fc +timeCreated: 1689145012 \ No newline at end of file diff --git a/Runtime/GuruAds/Moloco/Runtime/GuruSettings.Moloco.cs b/Runtime/GuruAds/Moloco/Runtime/GuruSettings.Moloco.cs new file mode 100644 index 0000000..3b857e7 --- /dev/null +++ b/Runtime/GuruAds/Moloco/Runtime/GuruSettings.Moloco.cs @@ -0,0 +1,53 @@ +using System; +using UnityEngine; + +namespace Guru +{ + public partial class GuruSettings + { + [Header("[ Moloco ] 测试广告配置")] [Tooltip("此配置只在测试阶段验证广告时使用")] + public MolocoSetting MolocoSetting; + } + + /// + /// Moloco 广告配置 + /// + [Serializable] + public class MolocoSetting + { + [SerializeField] public bool Enable; + [SerializeField] private MolocoPlatformSetting Android; + [SerializeField] private MolocoPlatformSetting iOS; + + /// + /// 获取AppID + /// + /// + public MolocoPlatformSetting GetPlatform() + { +#if UNITY_IOS + return iOS; +#else + return Android; +#endif + } + + public string BannerTestUnitID => GetPlatform().bannerTestUnitID; + public string InterTestUnitID => GetPlatform().interTestUnitID; + public string RewardTestUnitID => GetPlatform().rewardTestUnitID; + + + + /// + /// Moloco 平台专属配置 + /// + [Serializable] + public class MolocoPlatformSetting + { + // public string name; // 平台名称 + public string bannerTestUnitID; // Banner ID + public string interTestUnitID; // Inter ID + public string rewardTestUnitID; // Reward ID + } + } +} \ No newline at end of file diff --git a/Runtime/GuruAds/Moloco/Runtime/GuruSettings.Moloco.cs.meta b/Runtime/GuruAds/Moloco/Runtime/GuruSettings.Moloco.cs.meta new file mode 100644 index 0000000..e09d20b --- /dev/null +++ b/Runtime/GuruAds/Moloco/Runtime/GuruSettings.Moloco.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 73843b3931f04824be84b4adcefdbb94 +timeCreated: 1689145037 \ No newline at end of file diff --git a/Runtime/GuruAds/Moloco/Runtime/MolocoTestAPI.cs b/Runtime/GuruAds/Moloco/Runtime/MolocoTestAPI.cs new file mode 100644 index 0000000..e7c05d6 --- /dev/null +++ b/Runtime/GuruAds/Moloco/Runtime/MolocoTestAPI.cs @@ -0,0 +1,211 @@ +using UnityEngine; + +namespace Guru +{ + /// + /// Moloco 测试接口 + /// + public class MolocoTestAPI + { + + + #region 初始化 + + + + + + #endregion + + #region Banner + + private static string _testBannerId; + private static bool _isLoadingBanner = false; + /// + /// 请求测试广告Ba + /// + public static void LoadDebugBanner(string bannerId) + { + if (_isLoadingBanner) return; + _isLoadingBanner = true; + _testBannerId = bannerId; + AddBannerCallBacks(); + MaxSdk.CreateBanner(_testBannerId, MaxSdkBase.BannerPosition.BottomCenter); + ShowToast($"Load Banner: {_testBannerId}"); + } + private static void AddBannerCallBacks() + { + MaxSdkCallbacks.Banner.OnAdLoadedEvent += OnDebugBannerLoadSuccess; + MaxSdkCallbacks.Banner.OnAdLoadFailedEvent += OnDebugBannerLoadFailed; + } + + private static void RemoveBannerCallBacks() + { + MaxSdkCallbacks.Banner.OnAdLoadedEvent -= OnDebugBannerLoadSuccess; + MaxSdkCallbacks.Banner.OnAdLoadFailedEvent -= OnDebugBannerLoadFailed; + } + + private static void OnDebugBannerLoadSuccess(string adUnitId, MaxSdkBase.AdInfo adInfo) + { + if (adUnitId == _testBannerId) + { + RemoveBannerCallBacks(); + Debug.Log($"[PM] Load Banner success => Revenue: {adInfo.Revenue}"); + MaxSdk.ShowBanner(adUnitId); + _isLoadingBanner = false; + + ShowToast($"Banner Loaded: {_testBannerId}"); + } + } + + private static void OnDebugBannerLoadFailed(string adUnitId, MaxSdkBase.ErrorInfo errorInfo) + { + if (adUnitId == _testBannerId) + { + RemoveBannerCallBacks(); + Debug.Log($"[PM] Load Banner fail => Waterfall:{errorInfo.WaterfallInfo.Name}"); + _isLoadingBanner = false; + + ShowToast($"Banner Load fail: {_testBannerId}"); + } + } + + + #endregion + + #region Interstitial + + + private static bool _isLoadingIV = false; + private static string _testIVId; + + /// + /// 请求测试广告IV + /// + public static void LoadDebugIV(string unitId) + { + if (_isLoadingIV) return; + _isLoadingIV = true; + _testIVId = unitId; + AddIvCallBacks(); + MaxSdk.LoadInterstitial(_testIVId); + + ShowToast($"Load Interstitial: {_testIVId}"); + } + + private static void AddIvCallBacks() + { + MaxSdkCallbacks.Interstitial.OnAdLoadedEvent += OnDebugIVLoadSuccess; + MaxSdkCallbacks.Interstitial.OnAdLoadFailedEvent += OnDebugIVLoadFailed; + } + + private static void RemoveIvCallBacks() + { + MaxSdkCallbacks.Interstitial.OnAdLoadedEvent -= OnDebugIVLoadSuccess; + MaxSdkCallbacks.Interstitial.OnAdLoadFailedEvent -= OnDebugIVLoadFailed; + } + + private static void OnDebugIVLoadSuccess(string adUnitId, MaxSdkBase.AdInfo adInfo) + { + if (adUnitId == _testIVId) + { + RemoveIvCallBacks(); + _isLoadingIV = false; + Debug.Log($"[PM] Load IV success => Revenue: {adInfo.Revenue}"); + string placement = "pm_test_iv"; + MaxSdk.ShowInterstitial(adUnitId, placement); + ShowToast($"Load IV Success: {_testIVId}"); + } + } + + private static void OnDebugIVLoadFailed(string adUnitId, MaxSdkBase.ErrorInfo errorInfo) + { + if (adUnitId == _testIVId) + { + RemoveIvCallBacks(); + _isLoadingIV = false; + Debug.Log($"[PM] Load IV fail => Waterfall:{errorInfo.WaterfallInfo.Name}"); + ShowToast($"Load IV Fail: {_testIVId}"); + } + } + + #endregion + + #region Reward + + + + private static string _testRVId; + private static bool _isLoadingRV = false; + /// + /// 请求测试广告RV + /// + public static void LoadDebugRV(string unitId) + { + if (_isLoadingRV) return; + _isLoadingRV = true; + _testRVId = unitId; + AddRvCallBacks(); + MaxSdk.LoadRewardedAd(_testRVId); + + ShowToast($"Load RV: {_testRVId}"); + } + + private static void AddRvCallBacks() + { + MaxSdkCallbacks.Rewarded.OnAdLoadedEvent += OnDebugRVLoadSuccess; + MaxSdkCallbacks.Rewarded.OnAdLoadFailedEvent += OnDebugRVLoadFailed; + } + + private static void RemoveRvCallBacks() + { + MaxSdkCallbacks.Rewarded.OnAdLoadedEvent -= OnDebugRVLoadSuccess; + MaxSdkCallbacks.Rewarded.OnAdLoadFailedEvent -= OnDebugRVLoadFailed; + } + + private static void OnDebugRVLoadSuccess(string adUnitId, MaxSdkBase.AdInfo adInfo) + { + if (adUnitId == _testRVId) + { + RemoveRvCallBacks(); + _isLoadingRV = false; + Debug.Log($"[PM] Load RV success => Revenue: {adInfo.Revenue}"); + string placement = "pm_test_rv"; + MaxSdk.ShowRewardedAd(adUnitId, placement); + ShowToast($"Load RV Success: {_testRVId}"); + } + } + + private static void OnDebugRVLoadFailed(string adUnitId, MaxSdkBase.ErrorInfo errorInfo) + { + if (adUnitId == _testRVId) + { + RemoveRvCallBacks(); + _isLoadingRV = false; + Debug.Log($"[PM] Load RV fail => Waterfall:{errorInfo.WaterfallInfo.Name}"); + ShowToast($"Load RV Fail: {_testRVId}"); + } + } + #endregion + + #region Debug + + + /// + /// 显示Toast信息 + /// + /// + public static void ShowToast(string msg) + { +#if UNITY_ANDROID + // U3D2Android.ShowToast(msg); +#else + Debug.Log(msg); +#endif + } + + + + #endregion + } +} \ No newline at end of file diff --git a/Runtime/GuruAds/Moloco/Runtime/MolocoTestAPI.cs.meta b/Runtime/GuruAds/Moloco/Runtime/MolocoTestAPI.cs.meta new file mode 100644 index 0000000..fcb050b --- /dev/null +++ b/Runtime/GuruAds/Moloco/Runtime/MolocoTestAPI.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 92c8ebc4f05c4729b6ef6a6bae5a34b7 +timeCreated: 1689147226 \ No newline at end of file diff --git a/Runtime/GuruAds/Pubmatic.meta b/Runtime/GuruAds/Pubmatic.meta new file mode 100644 index 0000000..bae4edc --- /dev/null +++ b/Runtime/GuruAds/Pubmatic.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 476080445a424f538e3295b6d0c39c37 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/GuruAds/Pubmatic/Editor.meta b/Runtime/GuruAds/Pubmatic/Editor.meta new file mode 100644 index 0000000..bf57a33 --- /dev/null +++ b/Runtime/GuruAds/Pubmatic/Editor.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 4f27758688594110b42e5d6cc9368f2b +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/GuruAds/Pubmatic/Editor/Proguards.txt b/Runtime/GuruAds/Pubmatic/Editor/Proguards.txt new file mode 100644 index 0000000..cc71fd8 --- /dev/null +++ b/Runtime/GuruAds/Pubmatic/Editor/Proguards.txt @@ -0,0 +1 @@ +-keep class com.pubmatic.sdk.** { *; } \ No newline at end of file diff --git a/Runtime/GuruAds/Pubmatic/Editor/Proguards.txt.meta b/Runtime/GuruAds/Pubmatic/Editor/Proguards.txt.meta new file mode 100644 index 0000000..ee946d9 --- /dev/null +++ b/Runtime/GuruAds/Pubmatic/Editor/Proguards.txt.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 337880599b034517bf8ff594099c5c1d +timeCreated: 1687158363 \ No newline at end of file diff --git a/Runtime/GuruAds/Pubmatic/Package.meta b/Runtime/GuruAds/Pubmatic/Package.meta new file mode 100644 index 0000000..c91a7a3 --- /dev/null +++ b/Runtime/GuruAds/Pubmatic/Package.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: c6fc887e2efc448b85609ea99608acdd +timeCreated: 1681289567 \ No newline at end of file diff --git a/Runtime/GuruAds/Pubmatic/Package/PubmaticSDK.zip b/Runtime/GuruAds/Pubmatic/Package/PubmaticSDK.zip new file mode 100644 index 0000000..2271cb8 Binary files /dev/null and b/Runtime/GuruAds/Pubmatic/Package/PubmaticSDK.zip differ diff --git a/Runtime/GuruAds/Pubmatic/Package/PubmaticSDK.zip.meta b/Runtime/GuruAds/Pubmatic/Package/PubmaticSDK.zip.meta new file mode 100644 index 0000000..4e4e2ad --- /dev/null +++ b/Runtime/GuruAds/Pubmatic/Package/PubmaticSDK.zip.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 3a5b29c0662724638bd2fb91e3a98972 +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/GuruAds/Pubmatic/README.md b/Runtime/GuruAds/Pubmatic/README.md new file mode 100644 index 0000000..0cf6832 --- /dev/null +++ b/Runtime/GuruAds/Pubmatic/README.md @@ -0,0 +1,31 @@ +# PubMatic 广告插件 + +## Version 1.0.0 + +使用方法: +- 先解压缩Package内的zip文档 +- 导入两个unitypackage文件 +- 将 PubMatic 对应的的 AdChannelPubMatic.cs 文件导入项目 +- 在 AdService 内注册AdChannelPubMatic. (需要升级 ADService ) +- 在 PlayerSettings 内添加 `AD_PUBMATIC` 的宏 +- 在 proguard-user.txt 内添加下面的代码: + ```yaml + # PubMatic + -keep class com.pubmatic.** { *; } + ``` + +广告申请和展示时按照正常的 MAX 广告申请接口调用 + +测试时, 需要使用专用的Debug接口直接拉起 PubMatic 测试广告 +```csharp + + Debug.Log($"--- 显示 PubMatic Banner ---"); + AdChannelPubMatic.RequestDebugBanner(); + + Debug.Log($"--- 显示 PubMatic IV ---"); + AdChannelPubMatic.LoadDebugIV(); + + Debug.Log($"--- 显示 PubMatic RV ---"); + AdChannelPubMatic.RequestDebugRV(); + +``` \ No newline at end of file diff --git a/Runtime/GuruAds/Pubmatic/README.md.meta b/Runtime/GuruAds/Pubmatic/README.md.meta new file mode 100644 index 0000000..ec2bb90 --- /dev/null +++ b/Runtime/GuruAds/Pubmatic/README.md.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 40bcecbb98df437bae363dfc4956bed4 +timeCreated: 1681451207 \ No newline at end of file diff --git a/Runtime/GuruAds/Pubmatic/Runtime.meta b/Runtime/GuruAds/Pubmatic/Runtime.meta new file mode 100644 index 0000000..9d9ab45 --- /dev/null +++ b/Runtime/GuruAds/Pubmatic/Runtime.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 22a8f46f2a1cc4ebd830e5dd9ec2a898 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/GuruAds/Pubmatic/Runtime/AdChannelPubMatic.cs b/Runtime/GuruAds/Pubmatic/Runtime/AdChannelPubMatic.cs new file mode 100644 index 0000000..f901f12 --- /dev/null +++ b/Runtime/GuruAds/Pubmatic/Runtime/AdChannelPubMatic.cs @@ -0,0 +1,370 @@ +/********************************************** + * Pubmatic 广告渠道 + **********************************************/ +namespace Guru +{ + using OpenWrapSDK; + using System; + using OpenWrapSDK.Mediation.AppLovinMAX; + using UnityEngine; + + public class AdChannelPubMatic: IAdChannel + { + #region 属性定义 + + public static readonly string ChanelName = "PubMatic"; + public string Name => ChanelName; + + public Action OnRequestOver { get; set; } + + + // --------------- 获取各种SlotID -------------------- + private static string PMBannerUnitID => GuruSettings.Instance.PubmaticSetting.BannerUnitID; + private static string PMInterUnitID => GuruSettings.Instance.PubmaticSetting.InterUnitID; + private static string PMRewardUnitID => GuruSettings.Instance.PubmaticSetting.RewardUnitID; + private static string PMStoreUrl => GuruSettings.Instance.PubmaticSetting.StoreUrl; + + public static readonly int BidRequestTimeout = 5; // 请求超时 (秒) + + /// + /// 当前平台是否可用 + /// + public bool IsEnabled + { + get + { +#if UNITY_EDITOR + return false; +#endif + // return GuruSettings.Instance.PubmaticSetting.Enable; + return true; // 常驻开启 + } + } + + #endregion + + #region 初始化 + + /* + * You must set the App Store/Google Play Store storeURL of your app + * before it can request an ad using OpenWrap SDK. + * The storeURL is the URL where users can download your app from the App Store/Google Play Store. + */ + public void Initialize() + { +#if UNITY_EDITOR + Debug.Log($"=== PubMatic will not init on Editor ==="); +#endif + if (!IsEnabled) + { + Debug.Log($"[Ads] --- PubMatic is not enabled"); + return; + } + + var appInfo = new POBApplicationInfo(); + appInfo.StoreURL = new Uri(PMStoreUrl); + POBOpenWrapSDK.SetApplicationInfo(appInfo); + } + + #endregion + + #region 基础参数设置 + + /// + /// 设置Banner参数 + /// + /// + /// + /// + private static void SetBannerParams(string adUnitId, string key, object value) + { +#if UNITY_IOS + MaxSdk.SetBannerLocalExtraParameter(adUnitId, + key, + POBMAXUtil.GetIntPtr(adUnitId, value)); +#elif UNITY_ANDROID + MaxSdk.SetBannerLocalExtraParameter(adUnitId, + key, + POBMAXUtil.GetAndroidJavaObject(value)); +#endif + } + + /// + /// 设置IV参数 + /// + /// + /// + /// + private static void SetIVParams(string adUnitId, string key, object value) + { +#if UNITY_IOS + MaxSdk.SetInterstitialLocalExtraParameter(adUnitId, + key, + POBMAXUtil.GetIntPtr(adUnitId, value)); +#elif UNITY_ANDROID + MaxSdk.SetInterstitialLocalExtraParameter(adUnitId, + key, + POBMAXUtil.GetAndroidJavaObject(value)); +#endif + } + + /// + /// 设置RV参数 + /// + /// + /// + /// + private static void SetRVParams(string adUnitId, string key, object value) + { +#if UNITY_IOS + MaxSdk.SetRewardedAdLocalExtraParameter(adUnitId, + key, + POBMAXUtil.GetIntPtr(adUnitId, value)); +#elif UNITY_ANDROID + MaxSdk.SetRewardedAdLocalExtraParameter(adUnitId, + key, + POBMAXUtil.GetAndroidJavaObject(value)); +#endif + } + + + //---------------- 设置广告超时 ------------------------- + + private void SetBannerTimeout(string adUnitId, int timeout = 5) + { + SetBannerParams(adUnitId, POBMAXConstants.NetworkTimeoutKey, timeout); + } + private void SetIVTimeout(string adUnitId, int timeout = 5) + { + SetIVParams(adUnitId, POBMAXConstants.NetworkTimeoutKey, timeout); + } + + private void SetRVTimeout(string adUnitId, int timeout = 5) + { + SetRVParams(adUnitId, POBMAXConstants.NetworkTimeoutKey, timeout); + } + + //------------------ 打开测试广告 ---------------------------- + + private void EnableBannerTestAds(string adUnitId) + { + SetBannerParams(adUnitId, POBMAXConstants.EnableTestModeKey, true); + } + private void EnableIVTestAds(string adUnitId) + { + SetIVParams(adUnitId, POBMAXConstants.EnableTestModeKey, true); + } + private void EnableRVTestAds(string adUnitId) + { + SetRVParams(adUnitId, POBMAXConstants.EnableTestModeKey, true); + } + + //------------------- 打开调试模式 --------------------------- + private static void EnableBannerDebugMode(string adUnitId) + { + SetBannerParams(adUnitId, POBMAXConstants.EnableDebugModeKey, true); + } + private static void EnableIVDebugMode(string adUnitId) + { + SetIVParams(adUnitId, POBMAXConstants.EnableDebugModeKey, true); + } + private static void EnableRVDebugMode(string adUnitId) + { + SetRVParams(adUnitId, POBMAXConstants.EnableDebugModeKey, true); + } + + + + #endregion + + #region Banner + + public void LoadBannerAD() + { + } + + + private static bool _isLoadingBanner = false; + /// + /// 请求测试广告Ba + /// + public static void RequestDebugBanner() + { + if (_isLoadingBanner) return; + _isLoadingBanner = true; + EnableBannerDebugMode(PMBannerUnitID); + AddBannerCallBacks(); + MaxSdk.CreateBanner(PMBannerUnitID, MaxSdkBase.BannerPosition.BottomCenter); + ShowToast($"Load Banner: {PMBannerUnitID}"); + } + private static void AddBannerCallBacks() + { + MaxSdkCallbacks.Banner.OnAdLoadedEvent += OnDebugBannerLoadSuccess; + MaxSdkCallbacks.Banner.OnAdLoadFailedEvent += OnDebugBannerLoadFailed; + } + + private static void RemoveBannerCallBacks() + { + MaxSdkCallbacks.Banner.OnAdLoadedEvent -= OnDebugBannerLoadSuccess; + MaxSdkCallbacks.Banner.OnAdLoadFailedEvent -= OnDebugBannerLoadFailed; + } + + private static void OnDebugBannerLoadSuccess(string adUnitId, MaxSdkBase.AdInfo adInfo) + { + if (adUnitId == PMBannerUnitID) + { + RemoveBannerCallBacks(); + Debug.Log($"[PM] Load Banner success => Revenue: {adInfo.Revenue}"); + MaxSdk.ShowBanner(adUnitId); + _isLoadingBanner = false; + + ShowToast($"Banner Loaded: {PMBannerUnitID}"); + } + } + + private static void OnDebugBannerLoadFailed(string adUnitId, MaxSdkBase.ErrorInfo errorInfo) + { + if (adUnitId == PMBannerUnitID) + { + RemoveBannerCallBacks(); + Debug.Log($"[PM] Load Banner fail => Waterfall:{errorInfo.WaterfallInfo.Name}"); + _isLoadingBanner = false; + + ShowToast($"Banner Load fail: {PMBannerUnitID}"); + } + } + + #endregion + + #region Interstitial + + public void LoadInterstitialAD() + { + } + + + private static bool _isLoadingIV = false; + /// + /// 请求测试广告IV + /// + public static void LoadDebugIV() + { + if (_isLoadingIV) return; + _isLoadingIV = true; + EnableIVDebugMode(PMInterUnitID); + AddIvCallBacks(); + MaxSdk.LoadInterstitial(PMInterUnitID); + + ShowToast($"Load Interstitial: {PMInterUnitID}"); + } + + private static void AddIvCallBacks() + { + MaxSdkCallbacks.Interstitial.OnAdLoadedEvent += OnDebugIVLoadSuccess; + MaxSdkCallbacks.Interstitial.OnAdLoadFailedEvent += OnDebugIVLoadFailed; + } + + private static void RemoveIvCallBacks() + { + MaxSdkCallbacks.Interstitial.OnAdLoadedEvent -= OnDebugIVLoadSuccess; + MaxSdkCallbacks.Interstitial.OnAdLoadFailedEvent -= OnDebugIVLoadFailed; + } + + private static void OnDebugIVLoadSuccess(string adUnitId, MaxSdkBase.AdInfo adInfo) + { + if (adUnitId == PMInterUnitID) + { + RemoveIvCallBacks(); + _isLoadingIV = false; + Debug.Log($"[PM] Load IV success => Revenue: {adInfo.Revenue}"); + string placement = "pm_test_iv"; + MaxSdk.ShowInterstitial(adUnitId, placement); + ShowToast($"Load IV Success: {PMInterUnitID}"); + } + } + + private static void OnDebugIVLoadFailed(string adUnitId, MaxSdkBase.ErrorInfo errorInfo) + { + if (adUnitId == PMInterUnitID) + { + RemoveIvCallBacks(); + _isLoadingIV = false; + Debug.Log($"[PM] Load IV fail => Waterfall:{errorInfo.WaterfallInfo.Name}"); + ShowToast($"Load IV Fail: {PMInterUnitID}"); + } + } + + #endregion + + #region Reward + + public void LoadRewardAD() + { + } + + + private static bool _isLoadingRV = false; + /// + /// 请求测试广告RV + /// + public static void RequestDebugRV() + { + if (_isLoadingRV) return; + _isLoadingRV = true; + EnableIVDebugMode(PMRewardUnitID); + AddRvCallBacks(); + MaxSdk.LoadRewardedAd(PMRewardUnitID); + + ShowToast($"Load RV: {PMInterUnitID}"); + } + + private static void AddRvCallBacks() + { + MaxSdkCallbacks.Rewarded.OnAdLoadedEvent += OnDebugRVLoadSuccess; + MaxSdkCallbacks.Rewarded.OnAdLoadFailedEvent += OnDebugRVLoadFailed; + } + + private static void RemoveRvCallBacks() + { + MaxSdkCallbacks.Rewarded.OnAdLoadedEvent -= OnDebugRVLoadSuccess; + MaxSdkCallbacks.Rewarded.OnAdLoadFailedEvent -= OnDebugRVLoadFailed; + } + + private static void OnDebugRVLoadSuccess(string adUnitId, MaxSdkBase.AdInfo adInfo) + { + if (adUnitId == PMRewardUnitID) + { + RemoveRvCallBacks(); + _isLoadingRV = false; + Debug.Log($"[PM] Load RV success => Revenue: {adInfo.Revenue}"); + string placement = "pm_test_rv"; + MaxSdk.ShowRewardedAd(adUnitId, placement); + ShowToast($"Load RV Success: {PMRewardUnitID}"); + } + } + + private static void OnDebugRVLoadFailed(string adUnitId, MaxSdkBase.ErrorInfo errorInfo) + { + if (adUnitId == PMRewardUnitID) + { + RemoveRvCallBacks(); + _isLoadingRV = false; + Debug.Log($"[PM] Load RV fail => Waterfall:{errorInfo.WaterfallInfo.Name}"); + ShowToast($"Load RV Fail: {PMRewardUnitID}"); + } + } + #endregion + + #region Debug + + private static void ShowToast(string msg) + { +#if UNITY_ANDROID + DeviceUtil.ShowToast(msg); +#endif + } + + #endregion + + } +} \ No newline at end of file diff --git a/Runtime/GuruAds/Pubmatic/Runtime/AdChannelPubMatic.cs.meta b/Runtime/GuruAds/Pubmatic/Runtime/AdChannelPubMatic.cs.meta new file mode 100644 index 0000000..bf4c7c8 --- /dev/null +++ b/Runtime/GuruAds/Pubmatic/Runtime/AdChannelPubMatic.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: d1e329abcf494bd19dfc930b04c0ba5e +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/GuruAds/Pubmatic/Runtime/GuruSettings.PubMatic.cs b/Runtime/GuruAds/Pubmatic/Runtime/GuruSettings.PubMatic.cs new file mode 100644 index 0000000..c3dbe73 --- /dev/null +++ b/Runtime/GuruAds/Pubmatic/Runtime/GuruSettings.PubMatic.cs @@ -0,0 +1,67 @@ +//================================================ +// Company Name: Castbox +// Project: Ball Sort Puzzle +// Author: HYZ +// CreateTime: 2023-04-12 21:06:02 +// Version: 1.0.0 +// Desc: +//================================================ + +using UnityEngine.Serialization; + +namespace Guru +{ + using System; + using UnityEngine; + + public partial class GuruSettings + { + [Header("Pubmatic 广告配置")] + public PubmaticSetting PubmaticSetting; + } + + + /// + /// Amazon广告配置 + /// + [Serializable] + public class PubmaticSetting + { + [SerializeField] public bool Enable; + [SerializeField] private PubmaticPlatformSetting Android; + [SerializeField] private PubmaticPlatformSetting iOS; + + /// + /// 获取AppID + /// + /// + public PubmaticPlatformSetting GetPlatform() + { +#if UNITY_IOS + return iOS; +#else + return Android; +#endif + } + + public string BannerUnitID => GetPlatform().bannerUnitID; + public string InterUnitID => GetPlatform().interUnitID; + public string RewardUnitID => GetPlatform().rewardUnitID; + public string StoreUrl => GetPlatform().storeUrl; + + } + + /// + /// Amazon平台专属配置 + /// + [Serializable] + public class PubmaticPlatformSetting + { + // public string name; // 平台名称 + public string storeUrl; // 平台商店地址 + public string bannerUnitID; // Banner ID + public string interUnitID; // Inter ID + public string rewardUnitID; // Reward ID + } + +} \ No newline at end of file diff --git a/Runtime/GuruAds/Pubmatic/Runtime/GuruSettings.PubMatic.cs.meta b/Runtime/GuruAds/Pubmatic/Runtime/GuruSettings.PubMatic.cs.meta new file mode 100644 index 0000000..18a575a --- /dev/null +++ b/Runtime/GuruAds/Pubmatic/Runtime/GuruSettings.PubMatic.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: f7d6b41f0f5449be8ac0c4d1829bb330 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/GuruAnalytics.meta b/Runtime/GuruAnalytics.meta new file mode 100644 index 0000000..f642d10 --- /dev/null +++ b/Runtime/GuruAnalytics.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: cd2ad07dfc12b418db1bcd4b3a316329 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/GuruAnalytics/Editor.meta b/Runtime/GuruAnalytics/Editor.meta new file mode 100644 index 0000000..ee08eac --- /dev/null +++ b/Runtime/GuruAnalytics/Editor.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 729cc47bf56d436ba4c20380ca1e5e59 +timeCreated: 1673860499 \ No newline at end of file diff --git a/Runtime/GuruAnalytics/Editor/Dependencies.xml b/Runtime/GuruAnalytics/Editor/Dependencies.xml new file mode 100644 index 0000000..7a9e6f5 --- /dev/null +++ b/Runtime/GuruAnalytics/Editor/Dependencies.xml @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + git@github.com:castbox/GuruSpecs.git + + + + + diff --git a/Runtime/GuruAnalytics/Editor/Dependencies.xml.meta b/Runtime/GuruAnalytics/Editor/Dependencies.xml.meta new file mode 100644 index 0000000..d8253d4 --- /dev/null +++ b/Runtime/GuruAnalytics/Editor/Dependencies.xml.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: b9cafb1d82bb45598fdd566d9b8f6a80 +timeCreated: 1674110869 \ No newline at end of file diff --git a/Runtime/GuruAnalytics/Editor/Proguards.txt b/Runtime/GuruAnalytics/Editor/Proguards.txt new file mode 100644 index 0000000..e277f50 --- /dev/null +++ b/Runtime/GuruAnalytics/Editor/Proguards.txt @@ -0,0 +1,2 @@ +-keep class com.guru.** { *; } +-keep class guru.core.** { *; } \ No newline at end of file diff --git a/Runtime/GuruAnalytics/Editor/Proguards.txt.meta b/Runtime/GuruAnalytics/Editor/Proguards.txt.meta new file mode 100644 index 0000000..d636922 --- /dev/null +++ b/Runtime/GuruAnalytics/Editor/Proguards.txt.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: fb5e4cd53ce674f5ebbb1d5d865a0127 +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/GuruAnalytics/Editor/imgs.meta b/Runtime/GuruAnalytics/Editor/imgs.meta new file mode 100644 index 0000000..3466884 --- /dev/null +++ b/Runtime/GuruAnalytics/Editor/imgs.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: cd9f52a72bd884a9382ae92fc9ec2726 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/GuruAnalytics/Editor/imgs/sc01.png b/Runtime/GuruAnalytics/Editor/imgs/sc01.png new file mode 100644 index 0000000..f8c4dd9 Binary files /dev/null and b/Runtime/GuruAnalytics/Editor/imgs/sc01.png differ diff --git a/Runtime/GuruAnalytics/Editor/imgs/sc01.png.meta b/Runtime/GuruAnalytics/Editor/imgs/sc01.png.meta new file mode 100644 index 0000000..e0208d2 --- /dev/null +++ b/Runtime/GuruAnalytics/Editor/imgs/sc01.png.meta @@ -0,0 +1,133 @@ +fileFormatVersion: 2 +guid: 82a7793d183b045eeb9591cc4c15bd76 +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 12 + mipmaps: + mipMapMode: 0 + enableMipMap: 1 + sRGBTexture: 1 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 1 + aniso: 1 + mipBias: 0 + wrapU: 0 + wrapV: 0 + wrapW: 0 + nPOTScale: 1 + lightmap: 0 + compressionQuality: 50 + spriteMode: 0 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 0 + spriteTessellationDetail: -1 + textureType: 0 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 0 + cookieLightType: 0 + platformSettings: + - serializedVersion: 3 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 3 + buildTarget: Standalone + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 3 + buildTarget: iPhone + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 3 + buildTarget: Android + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + physicsShape: [] + bones: [] + spriteID: + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + spritePackingTag: + pSDRemoveMatte: 0 + pSDShowRemoveMatteOption: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/GuruAnalytics/Plugins.meta b/Runtime/GuruAnalytics/Plugins.meta new file mode 100644 index 0000000..259aef4 --- /dev/null +++ b/Runtime/GuruAnalytics/Plugins.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: cbde866c1fe3a4961b2412feddfe68a3 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/GuruAnalytics/Plugins/Android.meta b/Runtime/GuruAnalytics/Plugins/Android.meta new file mode 100644 index 0000000..d5e9826 --- /dev/null +++ b/Runtime/GuruAnalytics/Plugins/Android.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 2b5e2e7c4d1ed49cfb4b27dcfe69ab5a +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/GuruAnalytics/Plugins/Android/U3DAnalytics-release.aar b/Runtime/GuruAnalytics/Plugins/Android/U3DAnalytics-release.aar new file mode 100644 index 0000000..1ba7c22 Binary files /dev/null and b/Runtime/GuruAnalytics/Plugins/Android/U3DAnalytics-release.aar differ diff --git a/Runtime/GuruAnalytics/Plugins/Android/U3DAnalytics-release.aar.meta b/Runtime/GuruAnalytics/Plugins/Android/U3DAnalytics-release.aar.meta new file mode 100644 index 0000000..8c5d070 --- /dev/null +++ b/Runtime/GuruAnalytics/Plugins/Android/U3DAnalytics-release.aar.meta @@ -0,0 +1,32 @@ +fileFormatVersion: 2 +guid: a336b814594434b4092d38e5ce76577a +PluginImporter: + externalObjects: {} + serializedVersion: 2 + iconMap: {} + executionOrder: {} + defineConstraints: [] + isPreloaded: 0 + isOverridable: 0 + isExplicitlyReferenced: 0 + validateReferences: 1 + platformData: + - first: + Android: Android + second: + enabled: 1 + settings: {} + - first: + Any: + second: + enabled: 0 + settings: {} + - first: + Editor: Editor + second: + enabled: 0 + settings: + DefaultValueInitialized: true + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/GuruAnalytics/Plugins/Android/guru_analytics-release.aar b/Runtime/GuruAnalytics/Plugins/Android/guru_analytics-release.aar new file mode 100644 index 0000000..b8826ec Binary files /dev/null and b/Runtime/GuruAnalytics/Plugins/Android/guru_analytics-release.aar differ diff --git a/Runtime/GuruAnalytics/Plugins/Android/guru_analytics-release.aar.meta b/Runtime/GuruAnalytics/Plugins/Android/guru_analytics-release.aar.meta new file mode 100644 index 0000000..54f128a --- /dev/null +++ b/Runtime/GuruAnalytics/Plugins/Android/guru_analytics-release.aar.meta @@ -0,0 +1,32 @@ +fileFormatVersion: 2 +guid: c5a9f9e11213b4bb78856debe4c967ca +PluginImporter: + externalObjects: {} + serializedVersion: 2 + iconMap: {} + executionOrder: {} + defineConstraints: [] + isPreloaded: 0 + isOverridable: 0 + isExplicitlyReferenced: 0 + validateReferences: 1 + platformData: + - first: + Android: Android + second: + enabled: 1 + settings: {} + - first: + Any: + second: + enabled: 0 + settings: {} + - first: + Editor: Editor + second: + enabled: 0 + settings: + DefaultValueInitialized: true + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/GuruAnalytics/Plugins/iOS.meta b/Runtime/GuruAnalytics/Plugins/iOS.meta new file mode 100644 index 0000000..adc3568 --- /dev/null +++ b/Runtime/GuruAnalytics/Plugins/iOS.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 1f7f3e184bd7d457b9e678321a69ad1b +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/GuruAnalytics/Plugins/iOS/Podfile b/Runtime/GuruAnalytics/Plugins/iOS/Podfile new file mode 100644 index 0000000..2e9bf51 --- /dev/null +++ b/Runtime/GuruAnalytics/Plugins/iOS/Podfile @@ -0,0 +1,14 @@ +source 'https://github.com/CocoaPods/Specs.git' +source 'git@github.com:castbox/GuruSpecs.git' + +platform :ios, '11.0' + +target 'UnityFramework' do + + pod 'GuruAnalyticsLib', '~>0.2.2' +# pod 'GuruAnalyticsLib', :git => 'git@github.com:castbox/GuruAnalytics_iOS.git', :branch => 'dev' +end +target 'Unity-iPhone' do +end +use_frameworks! + diff --git a/Runtime/GuruAnalytics/Plugins/iOS/Podfile.meta b/Runtime/GuruAnalytics/Plugins/iOS/Podfile.meta new file mode 100644 index 0000000..769c9b7 --- /dev/null +++ b/Runtime/GuruAnalytics/Plugins/iOS/Podfile.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 10035163694574b63b74cd99c7ee5c68 +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/GuruAnalytics/Plugins/iOS/U3DAnalytics.mm b/Runtime/GuruAnalytics/Plugins/iOS/U3DAnalytics.mm new file mode 100644 index 0000000..a485a9a --- /dev/null +++ b/Runtime/GuruAnalytics/Plugins/iOS/U3DAnalytics.mm @@ -0,0 +1,294 @@ +// ============================================== +// U3DConsent.mm +// UnityFramework +// +// Created by EricHu on 2022/11/17. +// Copyright © 2022 guru. All rights reserved. +// ============================================== + +#import +#import "UnityAppController+UnityInterface.h" +#import + +@interface U3DAnalytics : NSObject + +//+ (void)requestGDPR: (NSString *)deviceId :(int) debugGeo; +//+ (UIViewController *) getUnityViewController; + +@end + +static NSString *gameobjectName; +static NSString *callbackName; + +static GuruAnalytics *_analytics; + + +@implementation U3DAnalytics + +// Const value define +NSString * const Version = @"1.8.1"; + +static const double kUploadPeriodInSecond = 60.0; +static const int kBatchLimit = 15; +static const double kEventExpiredSeconds = 7 * 24 * 60 * 60; +static const double kInitializeTimeout = 5.0; + +static double tch001MaxValue = 0.01; +static double tch02MaxValue = 0.2; +NSString * const TchAdRevRoas001 = @"tch_ad_rev_roas_001"; +NSString * const TchAdRevRoas02 = @"tch_ad_rev_roas_02"; +NSString * const TchError = @"tch_error"; + + + ++(UnityAppController *)GetAppController { + return (UnityAppController*)[UIApplication sharedApplication].delegate; +} + ++(UIViewController *) getUnityViewController { + return UnityGetGLViewController(); +} + +// 字符串转换 ++(const char*) stringToChar: (NSString *) str{ + return [str cStringUsingEncoding:NSASCIIStringEncoding]; +} + + ++(NSString *) charToString: (const char *) c{ + return [[NSString alloc] initWithUTF8String:c]; +} + +// 获取最终字符串 ++(char*) finalChar: (NSString *) string +{ + const char *tmpChar = [string cStringUsingEncoding:NSASCIIStringEncoding]; + + if (string == NULL) + return NULL; + + char* res = (char*)malloc(strlen(tmpChar) + 1); + strcpy(res, tmpChar); + + return res; +} + +// 设置太极02的预设值 ++(void) setTch02MaxValue: (const double) value{ + tch02MaxValue = value; +} + +// 构建数据 +//+(NSString *) buildDataString: (int)status andMessage: (NSString *)msg{ +// +// NSString *jsonString = [NSString stringWithFormat: @"{\"action\":\"gdpr\",\"data\":{\"status\":%d,\"msg\":\"%@\"}}", status, msg]; +// return jsonString; +//} + +// 构建数据字典 ++(NSDictionary *) buildDataDict: (NSString *) str{ + + NSArray *raw = [str componentsSeparatedByString:@","]; + if( raw == nil || raw.count == 0) return nil; + + NSMutableDictionary* dict = [[NSMutableDictionary alloc] init]; //must init before using + + for(NSString *s in raw ){ + NSArray *kvp = [s componentsSeparatedByString:@":"]; + if(kvp == nil || kvp.count < 2){ + continue; + } + + NSString *k = kvp[0]; + NSString *t = [kvp[1] substringToIndex:1]; + NSString *v = [kvp[1] substringFromIndex:1]; + + // NSLog(@"---[iOS] parse kvp key:%@ type:%@ value:%@", k, t, v); + + //TODO 解析字符值 + if([t isEqual: @"i"]){ + // int + [dict setValue:@([v integerValue]) forKey:k]; + }else if([t isEqual: @"d"]){ + // double + [dict setValue:@([v doubleValue]) forKey:k]; + } else { + // String + [dict setValue:v forKey:k]; + } + } + return dict; +} + +// 构建Json格式的数据字典 ++(NSMutableDictionary *) buildDataWithJson: (NSString *) json andKey: (NSString *) key{ + + if( json == nil || json.length == 0) return nil; + + NSData *jsonData = [json dataUsingEncoding:NSUTF8StringEncoding]; + NSError *err; + NSMutableDictionary *dict = [NSJSONSerialization JSONObjectWithData:jsonData + options:NSJSONReadingMutableContainers + error:&err]; + + if(err || dict == nil) + { + NSLog(@"json解析失败:%@",err); + [U3DAnalytics onTchErrorEvent:@"json_error" andRaw:json andOther:@""]; + return nil; + } + + // ---------- 太极数据校验 ------------- + if(key == TchAdRevRoas001){ + // tch 001 参数修复 + [U3DAnalytics fixTchParams: dict andJson:json andMaxValue:tch001MaxValue]; + } else if(key == TchAdRevRoas02){ + // tch 02 参数修复 + [U3DAnalytics fixTchParams: dict andJson:json andMaxValue:tch02MaxValue]; + } + + return dict; +} + +// 修复自打点数据 ++(void) fixTchParams: (NSMutableDictionary *)dict andJson: (NSString *)json andMaxValue: (double) targetValue { + // --- 保存 raw数据 --- + [dict setValue: json forKey:@"raw"]; + + id _platform = [dict objectForKey:@"ad_platform"]; + id _value = [dict objectForKey:@"value"]; + + if([U3DAnalytics isNullObject: _platform]) { + // 不存在 ad_paltform + [U3DAnalytics onTchErrorEvent:@"no_ad_platform" andRaw:json andOther:@""]; + } else { + // 非IAP订单 + if(![[_platform stringValue] isEqual:@"appstore"] ) + { + if([U3DAnalytics isNullObject: _value] ){ + [dict setValue: [NSNumber numberWithDouble: targetValue] forKey:@"value"]; + [U3DAnalytics onTchErrorEvent:@"no_value" andRaw:json andOther:@""]; + } else { + if([_value doubleValue] < targetValue){ + [dict setValue: [NSNumber numberWithDouble: targetValue] forKey:@"value"]; + [U3DAnalytics onTchErrorEvent:@"value_error" + andRaw:json andOther: [_value stringValue]]; + } + } + } + } +} + + + +// 上报 tch_error 事件 ++(void) onTchErrorEvent:(NSString *) evtName andRaw: (NSString *)raw andOther: (NSString *) other{ + NSString *json = [NSString stringWithFormat:@"{\"event\":\"%@\", \"raw\":\"%@\", \"other\":\"%@\"}", evtName, raw, other]; + + [GuruAnalytics logEvent:TchError + parameters:[U3DAnalytics buildDataWithJson:json andKey:evtName]]; +} + +// 对象判空 ++ (BOOL)isNullObject:(__kindof id) obj{ + if(!obj){ + return YES; + } + + if(obj == NULL){ + return YES; + } + + if([obj isEqual:[NSNull null]]){ + return YES; + } + + return NO; +} + +@end + +//============================ UNITY PUBLIC API ============================ + +extern "C" { + + // 请求GDPR + void unityInitAnalytics(const char *appId, const char *deviceInfo, bool isDebug) + { +// NSLog(@"--- [iOS] init Analytics libs"); + [GuruAnalytics initializeLibWithUploadPeriodInSecond:kUploadPeriodInSecond + batchLimit:kBatchLimit + eventExpiredSeconds:kEventExpiredSeconds + initializeTimeout:kInitializeTimeout + saasXAPPID:[U3DAnalytics charToString:appId] + saasXDEVICEINFO:[U3DAnalytics charToString:deviceInfo] + loggerDebug:isDebug]; + } + + // 设置用户ID + void unitySetUserID(const char *uid){ + [GuruAnalytics setUserID:[U3DAnalytics charToString:uid]]; + } + + // 设置用户ID + void unitySetScreen(const char *screenName){ + [GuruAnalytics setScreen:[U3DAnalytics charToString:screenName]]; + } + + // 设置用户ADID + void unitySetAdId(const char *adId){ + [GuruAnalytics setAdId:[U3DAnalytics charToString:adId]]; + } + + // 设置用户AdjustID + void unitySetAdjustID(const char *adjustId){ + [GuruAnalytics setAdjustId:[U3DAnalytics charToString:adjustId]]; + } + + // 设置用户FirebaseID + void unitySetFirebaseId(const char *firebaseId){ + [GuruAnalytics setFirebaseId:[U3DAnalytics charToString:firebaseId]]; + } + + // 设置用户设备ID + void unitySetDeviceId(const char *did){ + [GuruAnalytics setDeviceId:[U3DAnalytics charToString:did]]; + } + + // 设置用户属性 + void unitySetUserProperty(const char *key, const char *value){ + [GuruAnalytics setUserProperty:[U3DAnalytics charToString:value] + forName:[U3DAnalytics charToString:key]]; + } + + // 上报事件 + void unityLogEvent(const char *key, const char *data){ + NSString *evtName = [U3DAnalytics charToString:key]; + NSString *json = [U3DAnalytics charToString:data]; + + [GuruAnalytics logEvent: evtName + parameters:[U3DAnalytics buildDataWithJson:json andKey:evtName]]; // JSON转换 + } + + // 上报事件 + void unitySetTch02Value(const double value){ + [U3DAnalytics setTch02MaxValue:value]; + } + + // 打点事件成功率 + void unityReportEventRate(void){ + [GuruAnalytics debug_eventsStatistics:^(NSInteger uploadedEventsCount, NSInteger loggedEventsCount) { + + // 上报事件总量 + [GuruAnalytics setUserProperty:[NSString stringWithFormat:@"%ld", loggedEventsCount] + forName:@"lgd"]; + // 上报成功数量 + [GuruAnalytics setUserProperty:[NSString stringWithFormat:@"%ld", uploadedEventsCount] + forName:@"uld"]; + }]; + } + +} + + + diff --git a/Runtime/GuruAnalytics/Plugins/iOS/U3DAnalytics.mm.meta b/Runtime/GuruAnalytics/Plugins/iOS/U3DAnalytics.mm.meta new file mode 100644 index 0000000..b4ae2ee --- /dev/null +++ b/Runtime/GuruAnalytics/Plugins/iOS/U3DAnalytics.mm.meta @@ -0,0 +1,37 @@ +fileFormatVersion: 2 +guid: 621002eb5acb547ec95af0d804cfb17f +PluginImporter: + externalObjects: {} + serializedVersion: 2 + iconMap: {} + executionOrder: {} + defineConstraints: [] + isPreloaded: 0 + isOverridable: 0 + isExplicitlyReferenced: 0 + validateReferences: 1 + platformData: + - first: + Any: + second: + enabled: 0 + settings: {} + - first: + Editor: Editor + second: + enabled: 0 + settings: + DefaultValueInitialized: true + - first: + iPhone: iOS + second: + enabled: 1 + settings: {} + - first: + tvOS: tvOS + second: + enabled: 1 + settings: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/GuruAnalytics/Plugins/iOS/U3DException.mm b/Runtime/GuruAnalytics/Plugins/iOS/U3DException.mm new file mode 100644 index 0000000..c4a9071 --- /dev/null +++ b/Runtime/GuruAnalytics/Plugins/iOS/U3DException.mm @@ -0,0 +1,70 @@ +// +// JJException.m +// UnityFramework +// +// Created by Castbox on 2023/2/28. +// + +#import +#import +#import "UnityAppController+UnityInterface.h" +#import +#import + +@interface U3DException : NSObject + + +@end + +@implementation U3DException + +static U3DException *_instance; + ++ (instancetype)sharedInstance { + static dispatch_once_t oneToken; + dispatch_once(&oneToken,^{ + _instance = [[self alloc]init]; + }); + return _instance; +} + ++ (instancetype)allocWithZone:(NSZone *)zone{ + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + _instance = [super allocWithZone:zone]; + }); + return _instance; +} + +- (void)start { + [JJException configExceptionCategory:JJExceptionGuardUnrecognizedSelector]; + [JJException startGuardException]; + [JJException registerExceptionHandle:self]; +} + +#pragma mark - Exception Delegate + +- (void)handleCrashException:(NSString*)exceptionMessage extraInfo:(NSDictionary*)info{ + NSLog(@"handleCrashException: %@ info: %@", exceptionMessage, info); + NSArray *messages = [exceptionMessage componentsSeparatedByString:@"\n"]; + NSString *domain = @"[U3DException]-handler exception"; + if (messages.count > 2) { +// messages.objectat + domain = [messages objectAtIndex:2]; + } + NSError *error = [[NSError alloc] initWithDomain:domain code:-1 userInfo:@{NSLocalizedDescriptionKey: exceptionMessage}]; + [[FIRCrashlytics crashlytics] recordError:error]; +} + +@end + + +extern "C" { + void unityInitException() { + [[U3DException sharedInstance] start]; + } + + void unityTestUnrecognizedSelectorCrash() { + [[U3DException sharedInstance] performSelector:NSSelectorFromString(@"testUnrecognizedSelectorCrash")]; + } +} diff --git a/Runtime/GuruAnalytics/Plugins/iOS/U3DException.mm.meta b/Runtime/GuruAnalytics/Plugins/iOS/U3DException.mm.meta new file mode 100644 index 0000000..ce16476 --- /dev/null +++ b/Runtime/GuruAnalytics/Plugins/iOS/U3DException.mm.meta @@ -0,0 +1,37 @@ +fileFormatVersion: 2 +guid: 66c93bfd67d2e46f887594e87fa16b39 +PluginImporter: + externalObjects: {} + serializedVersion: 2 + iconMap: {} + executionOrder: {} + defineConstraints: [] + isPreloaded: 0 + isOverridable: 0 + isExplicitlyReferenced: 0 + validateReferences: 1 + platformData: + - first: + Any: + second: + enabled: 0 + settings: {} + - first: + Editor: Editor + second: + enabled: 0 + settings: + DefaultValueInitialized: true + - first: + iPhone: iOS + second: + enabled: 1 + settings: {} + - first: + tvOS: tvOS + second: + enabled: 1 + settings: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/GuruAnalytics/README.md b/Runtime/GuruAnalytics/README.md new file mode 100644 index 0000000..acf90c0 --- /dev/null +++ b/Runtime/GuruAnalytics/README.md @@ -0,0 +1,252 @@ +# Guru Unity Analytics + +GuruAnalyticsLib 的 Unity 插件库 + +--- + +## Change Logs + +### 1.8.4 +- 优化Android 端 Worker 调用逻辑, 重启 Worker 有助于让打点数据更准确 + +### 1.8.3 +- 修复 fg 打点上报时长不正确的问题 + +### 1.8.2 +- 修复参数类型转换的BUG, param数据转换为JSON对象 + +### 1.8.1 +- 修复自打点浮点参数精度问题 +- 添加太极020数值设置接口 + +### 1.7.5 +- 删除 `androidx.appcompat:appcompat` 库依赖 + + +
+ +--- + +## Document + +- 项目整合插件后, **请一定要在各插件的初始化后上报各相关ID**: + +- 相关接口如下 + +- ### UID + + ```C# + + // ---- 需要等待中台初始化后上报: + // 上报中台返回的用户ID + string uid = IPMConfig.IPM_UID + GuruAnalytics.SetUid(uid); + + ``` + +- ### DeviceID + ```C# + // 上报设备ID + string deviceId = IPMConfig.IPM_DEVICE_ID + GuruAnalytics.SetDeviceId(DeviceID); + + ``` + +- ### FirebaseID + ```C# + + // ---- 需要Firebase Analytic 初始化后, 异步获取对应的ID: + private static async void InitFirebaseAnalytics() + { + Debug.Log($"---[ANA] IPM UID: {IPMConfig.IPM_UID}"); + + var task = FirebaseAnalytics.GetAnalyticsInstanceIdAsync(); + await task; + if (task.IsCompleted) + { + var fid = task.Result; + if (!string.IsNullOrEmpty(fid)) + { + Debug.Log($"---[ANA] Firebase ID: {fid}"); + GuruAnalytics.SetFirebaseId(fid); + } + } + else + { + Debug.LogError("---- Get Firebase Analytics Instance Fail"); + } + } + + + ``` + +- ### AdjustID + + ```C# + + // ---- Adjust 启动后调用: + string adjustID = Adjust.getAdid(); + GuruAnalytics.SetAdjustId(adjustID); + + + ``` + +- ### AdID + + ```C# + string adId = ""; + Adjust.getGoogleAdId(id => + { + Debug.Log($"---- [ADJ] ADId: {id}"); + adId = id; + GuruAnalytics.SetAdId(id); + }); + + + ``` + +- 上报用户属性: + + ```C# + + string item_category = "main"; + int level = 7; + + GuruAnalytics.SetUserProperty("item_category", item_category); + GuruAnalytics.SetUserProperty("level", level.ToString()); + + ``` + +- 上报视图名称 + + ```C# + + string screenName = "MainView"; + GuruAnalytics.SetScreen(screenName); + + ``` + + +- 上报自定义打点: + + ```C# + + string eventName = "user_get_coin"; + Dictionary data = new Dictionary() + { + { "level", 7 }, + { "user_coin", 105L }, + { "win_rate", 21.25f }, + { "b_level", 7 }, + { "result", "retry" } + }; + GuruAnalytics.LogEvent(eventName, data); + + ``` +--- + +
+ +## 依赖台配置说明 + +本项目已开始使用 `ExternalDependencyManager` 简称 `EDM` 来解决各种库的依赖问题 + +详细配置可见: [Dependencies.xml](Editor/Dependencies.xml) + +IOS 项目注意配置如下图: + +--> 取消勾选 **Link frameworks statically** + +![](Editor/imgs/sc01.png) + + +### Android 项目配置: + +于主菜单 `BuildSettings/PlayerSettings/PubishSettings:` + +开启如下选项: + +- [x] Custom Main Gradle Template +- [x] Custom Properties Gradle Template + +之后会在项目的 `Plugins/Android`内生成对应的文件. + +(A) 修改 `gradleTemplate.properties` + +添加一下内容支持 `AndroidX` + +```java +org.gradle.jvmargs=-Xmx**JVM_HEAP_SIZE**M +org.gradle.parallel=true +android.enableR8=false +unityStreamingAssets=.unity3d**STREAMING_ASSETS** +android.useAndroidX=true +android.enableJetifier=true +**ADDITIONAL_PROPERTIES** +``` + +(B) 修改 `mainTemplate.gradle` + +于 `dependency` 内添加如下依赖 (目前会自动添加, 无需手动添加) + +```java + +dependencies { + ... + + implementation 'androidx.core:core:1.7.0' + compile 'com.mapzen:on-the-road:0.8.1' + + // basicDependencies + implementation 'androidx.appcompat:appcompat:1.5.1' + implementation 'com.jakewharton.timber:timber:4.7.1' + implementation 'com.google.code.gson:gson:2.8.5' + // roomDependencies + implementation 'androidx.room:room-runtime:2.4.3' + implementation 'androidx.room:room-rxjava2:2.4.3' + // retrofitDependencies + implementation 'com.squareup.retrofit2:retrofit:2.7.1' + implementation 'com.squareup.retrofit2:converter-gson:2.7.1' + implementation 'com.squareup.retrofit2:adapter-rxjava2:2.7.1' + // okhttpDependencies + implementation 'androidx.work:work-runtime:2.7.1' + implementation 'androidx.work:work-runtime-ktx:2.7.1' + implementation 'androidx.work:work-rxjava2:2.7.1' + // process + implementation 'androidx.lifecycle:lifecycle-process:2.4.0' + // okhttp3 + implementation 'com.squareup.okhttp3:okhttp:4.9.3' + + ... +} + +``` + +最低 `minTarget` 设置为 **21** + +(D) 修改 `proguard-user.txt` 文件, 在最后追加此插件的相关代码 + +若项目使用了 ProGuard 压缩混淆, 需要修改此文件, 否则可能造成JAVA类无法被找到 + +```java + +... + +-keep class com.guru.** { *; } +-keep class guru.core.** { *; } + +``` + + +--- + +
+ + +## 示例项目 + +- 示例项目位于 [~Sample](~Sample) 目录内. 详见 [CuruAnalyticsDemo.cs](~Sample/CuruAnalyticsDemo.cs) +- 示例借用了 BallSortPuzzle 的 `AppID` 和 `BundleID` + + + diff --git a/Runtime/GuruAnalytics/README.md.meta b/Runtime/GuruAnalytics/README.md.meta new file mode 100644 index 0000000..1d541ac --- /dev/null +++ b/Runtime/GuruAnalytics/README.md.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 8def6937882714f739e50c9b1e011967 +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/GuruAnalytics/Runtime.meta b/Runtime/GuruAnalytics/Runtime.meta new file mode 100644 index 0000000..c20741d --- /dev/null +++ b/Runtime/GuruAnalytics/Runtime.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 1892c569dc49c456f9ae1d99ea76e3b9 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/GuruAnalytics/Runtime/Script.meta b/Runtime/GuruAnalytics/Runtime/Script.meta new file mode 100644 index 0000000..44aeccb --- /dev/null +++ b/Runtime/GuruAnalytics/Runtime/Script.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 30c092dbfbc6d4194b436c91ae7290b9 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/GuruAnalytics/Runtime/Script/GuruAnalytics.cs b/Runtime/GuruAnalytics/Runtime/Script/GuruAnalytics.cs new file mode 100644 index 0000000..610fd1a --- /dev/null +++ b/Runtime/GuruAnalytics/Runtime/Script/GuruAnalytics.cs @@ -0,0 +1,247 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Globalization; +using System.Text; +using Newtonsoft.Json; +using UnityEngine; + + +namespace Guru +{ + + public class GuruAnalytics + { + // Plugin Version + public const string Version = "1.8.4"; + + public static readonly string Tag = "[ANA]"; + private static IAnalyticsAgent _agent; + + public static IAnalyticsAgent Agent + { + get + { + if (_agent == null) + { + #if UNITY_EDITOR + _agent = new AnalyticsAgentStub(); + #elif UNITY_ANDROID + _agent = new AnalyticsAgentAndroid(); + #elif UNITY_IOS + _agent = new AnalyticsAgentIOS(); + #endif + } + return _agent; + } + } + + private static Dictionary _userProperties; + /// + /// 用户属性缓存字典 + /// + public static Dictionary UserProperties + { + get + { + if (_userProperties == null) + { + _userProperties = new Dictionary(10); + } + return _userProperties; + } + } + + + #region 公用接口 + + /// + /// 初始化接口 + /// + public static void Init(string appId, string deviceInfo, bool isDebug = false) + { + Agent?.Init(appId, deviceInfo, isDebug); +#if UNITY_IOS + // AnalyticsAgentIOS.TestCrashEvent(); // 测试触发一下崩溃事件 +#endif + } + + /// + /// 设置视图名称 + /// + /// + public static void SetScreen(string screenName) + { + CacheUserProperty($"screen_name", screenName); + Agent?.SetScreen(screenName); + } + + /// + /// 设置广告ID + /// + /// + public static void SetAdId(string id) + { + CacheUserProperty($"ad_id", id); + Agent?.SetAdId(id); + } + + /// + /// 设置用户属性 + /// + /// + /// + public static void SetUserProperty(string key, string value) + { + CacheUserProperty(key, value); // 添加用户属性 + Agent?.SetUserProperty(key, value); + } + /// + /// 设置Firebase ID + /// + /// + public static void SetFirebaseId(string id) + { + CacheUserProperty($"firebase_id", id); + Agent?.SetFirebaseId(id); + } + + /// + /// 设置Adjust ID + /// + /// + public static void SetAdjustId(string id) + { + CacheUserProperty($"adjust_id", id); + Agent?.SetAdjustId(id); + } + + /// + /// 设置设备ID + /// + /// + public static void SetDeviceId(string deviceId) + { + CacheUserProperty($"device_id", deviceId); + Agent?.SetDeviceId(deviceId); + } + + /// + /// 设置用户ID + /// + /// + public static void SetUid(string uid) + { + CacheUserProperty($"uid", uid); + Agent?.SetUid(uid); + } + + /// + /// 上报事件成功率 + /// + public static void ReportEventSuccessRate() => Agent?.ReportEventSuccessRate(); + + /// + /// 上报打点事件 + /// + /// 事件名称 + /// INT类型的值 + public static void LogEvent(string eventName, Dictionary data = null) + { + string raw = ""; + if (data != null && data.Count > 0) + { + raw = BuildParamsJson(data); + } + Debug.Log($"{Tag} event:{eventName} | raw: {raw}"); + Agent?.LogEvent(eventName, raw); + } + + private static string BuildParamsString(Dictionary data) + { + string raw = ""; + List strList = new List(data.Count); + foreach (var kvp in data) + { + strList.Add(BuildStringValue(kvp)); + raw = string.Join(",", strList); + } + return raw; + } + + private static string BuildParamsJson(Dictionary data) + { + try + { + // 强制转换加入国家设置 + return JsonConvert.SerializeObject(data, new JsonSerializerSettings() + { + Culture = new CultureInfo("en-US"), + }); + } + catch (Exception e) + { + Debug.LogError(e); + } + + return ""; + } + + /// + /// 构建带有类型格式的Str值 + /// + /// + /// + private static string BuildStringValue(KeyValuePair kvp) + { + if (kvp.Value is int || kvp.Value is long) + { + return $"{kvp.Key}:i{kvp.Value}"; + } + + if (kvp.Value is double || kvp.Value is float) + { + double dValue = (double)((object)kvp.Value); + return $"{kvp.Key}:d{dValue.ToString("F11", new CultureInfo("en-US"))}"; // 保留精度进行转换 + } + + return $"{kvp.Key}:s{kvp.Value}"; + } + + /// + /// 设置太极02值 + /// + /// + public static void SetTch02Value(double value) + { + Debug.Log($"{Tag} set tch_02_value:{value}"); + Agent?.SetTch02Value(value); + } + + #endregion + + #region iOS独有接口 + +#if UNITY_IOS + // 触发测试崩溃埋点 + public static void TestCrash() => AnalyticsAgentIOS.TestCrashEvent(); +#endif + + #endregion + + #region 用户属性 + + /// + /// 记录用户属性 + /// + /// + /// + private static void CacheUserProperty(string key, string value) + { + UserProperties[key] = value; + } + + #endregion + } +} + diff --git a/Runtime/GuruAnalytics/Runtime/Script/GuruAnalytics.cs.meta b/Runtime/GuruAnalytics/Runtime/Script/GuruAnalytics.cs.meta new file mode 100644 index 0000000..f340841 --- /dev/null +++ b/Runtime/GuruAnalytics/Runtime/Script/GuruAnalytics.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 09fd5c799d5d24931a3974af59d496a3 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/GuruAnalytics/Runtime/Script/IAnalyticsAgent.cs b/Runtime/GuruAnalytics/Runtime/Script/IAnalyticsAgent.cs new file mode 100644 index 0000000..70d1fa0 --- /dev/null +++ b/Runtime/GuruAnalytics/Runtime/Script/IAnalyticsAgent.cs @@ -0,0 +1,21 @@ +namespace Guru +{ + /// + /// 自打点代理接口 + /// + public interface IAnalyticsAgent + { + void Init(string appId, string deviceInfo, bool isDebug = false); + void SetScreen(string screenName); + void SetAdId(string id); + void SetUserProperty(string key, string value); + void SetFirebaseId(string id); + void SetAdjustId(string id); + void SetDeviceId(string deviceId); + void SetUid(string uid); + bool IsDebug { get; } + void LogEvent(string eventName, string parameters); + void ReportEventSuccessRate(); // 上报任务成功率 + void SetTch02Value(double value); // 设置太极02数值 + } +} \ No newline at end of file diff --git a/Runtime/GuruAnalytics/Runtime/Script/IAnalyticsAgent.cs.meta b/Runtime/GuruAnalytics/Runtime/Script/IAnalyticsAgent.cs.meta new file mode 100644 index 0000000..2b001ac --- /dev/null +++ b/Runtime/GuruAnalytics/Runtime/Script/IAnalyticsAgent.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 0541b7a79e6e44a5aa8ab5cc00218c9b +timeCreated: 1672388238 \ No newline at end of file diff --git a/Runtime/GuruAnalytics/Runtime/Script/Impl.meta b/Runtime/GuruAnalytics/Runtime/Script/Impl.meta new file mode 100644 index 0000000..4599071 --- /dev/null +++ b/Runtime/GuruAnalytics/Runtime/Script/Impl.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: e9a20f30f62248ac88252dc5958a338b +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/GuruAnalytics/Runtime/Script/Impl/AnalyticsAgentAndroid.cs b/Runtime/GuruAnalytics/Runtime/Script/Impl/AnalyticsAgentAndroid.cs new file mode 100644 index 0000000..ec2b179 --- /dev/null +++ b/Runtime/GuruAnalytics/Runtime/Script/Impl/AnalyticsAgentAndroid.cs @@ -0,0 +1,95 @@ +using System; +using UnityEngine; + +namespace Guru +{ + public class AnalyticsAgentAndroid: IAnalyticsAgent + { + +#if UNITY_ANDROID + + public static readonly string AnalyticsClassName = "com.guru.unity.analytics.Analytics"; + private static AndroidJavaClass _classAnalytics; + private static AndroidJavaClass ClassAnalytics => _classAnalytics ??= new AndroidJavaClass(AnalyticsClassName); + +#endif + private static bool _isDebug = false; + public static bool UseWorker = true; + + #region 工具方法 + + + + /// + /// 调用静态方法 + /// + /// + /// + private static void CallStatic(string methodName, params object[] args) + { +#if UNITY_ANDROID + try + { + if (ClassAnalytics != null) + { + ClassAnalytics.CallStatic(methodName, args); + if(_isDebug) Debug.Log($"{GuruAnalytics.Tag} Android call static :: {methodName}"); + } + } + catch (Exception e) + { + Debug.LogError(e.Message); + } +#endif + } + + /// + /// 调用静态方法 + /// + /// + /// + /// + private static T CallStatic(string methodName, params object[] args) + { +#if UNITY_ANDROID + try + { + if (ClassAnalytics != null) + { + if(_isDebug) Debug.Log($"{GuruAnalytics.Tag} Android call static <{typeof(T).ToString()}> :: {methodName}"); + return ClassAnalytics.CallStatic(methodName, args); + } + } + catch (Exception e) + { + Debug.LogError(e.Message); + } +#endif + return default(T); + } + + #endregion + + #region 接口实现 + + public void Init(string appId, string deviceInfo, bool isDebug = false) + { + _isDebug = isDebug; + string bundleId = Application.identifier; + CallStatic("init", appId, deviceInfo, bundleId, UseWorker, isDebug); // 调用接口 + } + public void SetScreen(string screenName) => CallStatic("setScreen", screenName); + public void SetAdId(string id) => CallStatic("setAdId", id); + public void SetUserProperty(string key, string value) => CallStatic("setUserProperty", key, value); + public void SetFirebaseId(string id) => CallStatic("setFirebaseId", id); + public void SetAdjustId(string id) => CallStatic("setAdjustId", id); + public void SetDeviceId(string deviceId) => CallStatic("setDeviceId", deviceId); + public void SetUid(string uid) => CallStatic("setUid", uid); + public bool IsDebug => CallStatic("isDebug"); + public void LogEvent(string eventName, string parameters) => CallStatic("logEvent", eventName, parameters); + public void ReportEventSuccessRate() => CallStatic("reportEventRate"); + public void SetTch02Value(double value) => CallStatic("setTch02Value", value); + #endregion + + } +} \ No newline at end of file diff --git a/Runtime/GuruAnalytics/Runtime/Script/Impl/AnalyticsAgentAndroid.cs.meta b/Runtime/GuruAnalytics/Runtime/Script/Impl/AnalyticsAgentAndroid.cs.meta new file mode 100644 index 0000000..df3d964 --- /dev/null +++ b/Runtime/GuruAnalytics/Runtime/Script/Impl/AnalyticsAgentAndroid.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 27181ab00e124c639b1f4468c535047c +timeCreated: 1672389052 \ No newline at end of file diff --git a/Runtime/GuruAnalytics/Runtime/Script/Impl/AnalyticsAgentIOS.cs b/Runtime/GuruAnalytics/Runtime/Script/Impl/AnalyticsAgentIOS.cs new file mode 100644 index 0000000..42d1af9 --- /dev/null +++ b/Runtime/GuruAnalytics/Runtime/Script/Impl/AnalyticsAgentIOS.cs @@ -0,0 +1,124 @@ +using System.Runtime.InteropServices; + +namespace Guru +{ + public class AnalyticsAgentIOS: IAnalyticsAgent + { + + #region 属性定义 + + private const string K_INTERNAL = "__Internal"; + +#if UNITY_IOS + // ------------- U3DAnalytics.mm Interface ----------------- + [DllImport(K_INTERNAL)] private static extern void unityInitAnalytics(string appId, string deviceInfo, bool isDebug); + [DllImport(K_INTERNAL)] private static extern void unitySetUserID(string uid); + [DllImport(K_INTERNAL)] private static extern void unitySetScreen(string screenName); + [DllImport(K_INTERNAL)] private static extern void unitySetAdId(string adId); + [DllImport(K_INTERNAL)] private static extern void unitySetAdjustID(string adjustId); + [DllImport(K_INTERNAL)] private static extern void unitySetFirebaseId(string fid); + [DllImport(K_INTERNAL)] private static extern void unitySetDeviceId(string did); + [DllImport(K_INTERNAL)] private static extern void unitySetUserProperty(string key, string value); + [DllImport(K_INTERNAL)] private static extern void unityLogEvent(string key, string data); + [DllImport(K_INTERNAL)] private static extern void unityReportEventRate(); + [DllImport(K_INTERNAL)] private static extern void unityInitException(); + [DllImport(K_INTERNAL)] private static extern void unityTestUnrecognizedSelectorCrash(); + [DllImport(K_INTERNAL)] private static extern void unitySetTch02Value(double value); +#endif + + private static bool _isDebug = false; + + #endregion + + #region 接口实现 + + public void Init(string appId, string deviceInfo, bool isDebug = false) + { + _isDebug = isDebug; +#if UNITY_IOS + unityInitAnalytics(appId, deviceInfo, isDebug); + unityInitException(); // 初始化报错守护进程 +#endif + } + + public void SetScreen(string screenName) + { +#if UNITY_IOS + unitySetScreen(screenName); +#endif + } + + public void SetAdId(string id) + { +#if UNITY_IOS + unitySetAdId(id); +#endif + } + + public void SetUserProperty(string key, string value) + { +#if UNITY_IOS + unitySetUserProperty(key, value); +#endif + } + + public void SetFirebaseId(string fid) + { +#if UNITY_IOS + unitySetFirebaseId(fid); +#endif + } + + public void SetAdjustId(string id) + { +#if UNITY_IOS + unitySetAdjustID(id); +#endif + } + + public void SetDeviceId(string deviceId) + { +#if UNITY_IOS + unitySetDeviceId(deviceId); +#endif + } + + public void SetUid(string uid) + { +#if UNITY_IOS + unitySetUserID(uid); +#endif + } + + public bool IsDebug => _isDebug; + + public void LogEvent(string eventName, string data) + { +#if UNITY_IOS + unityLogEvent(eventName, data); +#endif + } + + public void ReportEventSuccessRate() + { +#if UNITY_IOS + unityReportEventRate(); +#endif + } + + public void SetTch02Value(double value) + { +#if UNITY_IOS + unitySetTch02Value(value); +#endif + } + +#if UNITY_IOS + // iOS 测试用事件 + public static void TestCrashEvent()=> unityTestUnrecognizedSelectorCrash(); +#endif + + #endregion + + } +} \ No newline at end of file diff --git a/Runtime/GuruAnalytics/Runtime/Script/Impl/AnalyticsAgentIOS.cs.meta b/Runtime/GuruAnalytics/Runtime/Script/Impl/AnalyticsAgentIOS.cs.meta new file mode 100644 index 0000000..113084c --- /dev/null +++ b/Runtime/GuruAnalytics/Runtime/Script/Impl/AnalyticsAgentIOS.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 93f0211868c14c60aed081f928dc114e +timeCreated: 1672901309 \ No newline at end of file diff --git a/Runtime/GuruAnalytics/Runtime/Script/Impl/AnalyticsAgentStub.cs b/Runtime/GuruAnalytics/Runtime/Script/Impl/AnalyticsAgentStub.cs new file mode 100644 index 0000000..00deab0 --- /dev/null +++ b/Runtime/GuruAnalytics/Runtime/Script/Impl/AnalyticsAgentStub.cs @@ -0,0 +1,122 @@ +using System.Text; +using UnityEngine; + +namespace Guru +{ + public class AnalyticsAgentStub: IAnalyticsAgent + { + + public static readonly string TAG = "[EDT]"; + + private bool _isShowLog = false; + private bool _isDebug = false; + + + public void Init(string appId, string deviceInfo, bool isDebug = false) + { +#if UNITY_EDITOR + _isShowLog = true; +#endif + _isDebug = isDebug; + if(_isShowLog) + Debug.Log($"{TAG} init with Debug: {isDebug} appId:{appId} deviceInfo:{deviceInfo}"); + } + + public void SetScreen(string screenName) + { + if(_isShowLog) + Debug.Log($"{TAG} SetScreen: {screenName}"); + } + + public void SetAdId(string id) + { + if(_isShowLog) + Debug.Log($"{TAG} SetAdId: {id}"); + } + + public void SetUserProperty(string key, string value) + { + if(_isShowLog) + Debug.Log($"{TAG} SetUserProperty: {key} : {value}"); + } + + public void SetFirebaseId(string id) + { + if(_isShowLog) + Debug.Log($"{TAG} SetFirebaseId: {id}"); + } + + public void SetAdjustId(string id) + { + if(_isShowLog) + Debug.Log($"{TAG} SetAdjustId: {id}"); + } + + public void SetDeviceId(string deviceId) + { + if(_isShowLog) + Debug.Log($"{TAG} SetDeviceId: {deviceId}"); + } + + public void SetUid(string uid) + { + if(_isShowLog) + Debug.Log($"{TAG} SetUid: {uid}"); + } + + public bool IsDebug => _isDebug; + + + public void LogEvent(string eventName, string parameters) + { + if (_isShowLog) + { + var raw = parameters.Split(','); + if (raw.Length > 0) + { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < raw.Length; i++) + { + var ss = raw[i]; + if(string.IsNullOrEmpty(ss)) continue; + + var p = ss.Split(':'); + if (p.Length > 1) + { + + var key = p[0]; + var t = p[1].Substring(0, 1); + var v = p[1].Substring(1, p[1].Length - 1); + + + // 字符串解析 + switch(t){ + case "i": + sb.Append($"{key} : [int]{v}\n"); + break; + case "d": + sb.Append($"{key} : [double]{v}\n"); + break; + default: + sb.Append($"{key} : [string]{v}\n"); + break; + } + } + } + + Debug.Log($"{TAG} LogEvent: event:{eventName} Properties:\n{sb.ToString()}"); + } + } + } + + public void ReportEventSuccessRate() + { + Debug.Log($"{TAG} Log Event Success Rate"); + } + + public void SetTch02Value(double value) + { + Debug.Log($"{TAG} Tch02MaxValue: {value}"); + } + } +} \ No newline at end of file diff --git a/Runtime/GuruAnalytics/Runtime/Script/Impl/AnalyticsAgentStub.cs.meta b/Runtime/GuruAnalytics/Runtime/Script/Impl/AnalyticsAgentStub.cs.meta new file mode 100644 index 0000000..c1466ca --- /dev/null +++ b/Runtime/GuruAnalytics/Runtime/Script/Impl/AnalyticsAgentStub.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 8fd78c80a8d44637819c9803c1b71724 +timeCreated: 1672389623 \ No newline at end of file diff --git a/Runtime/GuruAnalytics/~Sample.meta b/Runtime/GuruAnalytics/~Sample.meta new file mode 100644 index 0000000..d5d69bb --- /dev/null +++ b/Runtime/GuruAnalytics/~Sample.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 82693d012b64748c8ac997389b0426a7 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/GuruAnalytics/~Sample/CuruAnalyticsDemo.cs b/Runtime/GuruAnalytics/~Sample/CuruAnalyticsDemo.cs new file mode 100644 index 0000000..0d4842c --- /dev/null +++ b/Runtime/GuruAnalytics/~Sample/CuruAnalyticsDemo.cs @@ -0,0 +1,138 @@ +using System; +using System.Collections.Generic; +using UnityEngine; +using UnityEngine.UI; + +namespace Guru +{ + public class CuruAnalyticsDemo: MonoBehaviour + { + [SerializeField] private bool _isDebug = true; + [SerializeField] private Button _btnInitSDK; + [SerializeField] private Button _btnStatus; + [SerializeField] private Button _btnUserProperties; + [SerializeField] private Button _btnEvents; + [SerializeField] private Button _btnEvents2; + [SerializeField] private Button _btnReport; + [SerializeField] private Button _btnTestCrash; + + // ----------- All Status IDs ----------- + private static readonly string AdjustID = "e35b41522140fa2db9089ef3c78eb8f9"; + private static readonly string FirebaseID = "b7ab5fc399a7bc8725c004943fa82837"; + private static readonly string UID = "BS-YYYYF"; + private static readonly string AdID = "dda3cc2b-5a5e-44cb-8a59-4a0b1b3780fd"; + private static readonly string DeviceID = "e2fb3c5a4c36473648c989bd86a41153"; + private static readonly string AppID = ""; + private static readonly string DeviceInfo = ""; + + + private static readonly string ScreenName = "MainMenu"; + + + + private void Awake() + { + _btnInitSDK.onClick.AddListener(OnClickInit); + _btnStatus.onClick.AddListener(OnClickStatus); + _btnUserProperties.onClick.AddListener(OnClickUserProperties); + _btnEvents.onClick.AddListener(OnClickEvents); + _btnEvents2.onClick.AddListener(OnClickEvents2); + _btnReport.onClick.AddListener(OnClickReport); + _btnTestCrash.onClick.AddListener(OnClickTestCrash); +#if !UNITY_IOS + _btnTestCrash.gameObject.SetActive(false); +#endif + + + } + + + + #region Button Callbacks + + + private void OnClickInit() + { + Debug.Log($"---- [DEMO] Call Analytics init"); + GuruAnalytics.Init(AppID, DeviceInfo, _isDebug); + } + + private void OnClickStatus() + { + Debug.Log($"---- [DEMO] Report Stats IDs: UID:{UID} DeviceID:{DeviceID} FirebaseID:{FirebaseID} AdID:{AdID} AdjustID:{AdjustID}"); + GuruAnalytics.SetUid(UID); + GuruAnalytics.SetDeviceId(DeviceID); + GuruAnalytics.SetFirebaseId(FirebaseID); + GuruAnalytics.SetAdId(AdID); + GuruAnalytics.SetAdjustId(AdjustID); + } + + private void OnClickUserProperties() + { + string item_category = "main"; + int level = 7; + Debug.Log($"---- [DEMO] Call SetUserProperty: item_category:{item_category} level:{level}"); + GuruAnalytics.SetUserProperty("item_category", item_category); + GuruAnalytics.SetUserProperty("level", level.ToString()); + } + + private void OnClickEvents() + { + Debug.Log($"---- [DEMO] Report Screen: {ScreenName}"); + GuruAnalytics.SetScreen(ScreenName); + + string eventName = "user_get_coin"; + Dictionary data = new Dictionary() + { + { "level", 7 }, + { "user_coin", 105L }, + { "win_rate", 21.25f }, + { "b_level", 7 }, + { "result", "retry" } + }; + + string s = "---- Data ----\n"; + foreach (var k in data.Keys) + { + s += $"-- K:{k} V:{data[k]}\n"; + } + Debug.Log(s); + + Debug.Log($"---- [DEMO] Call LogEvent"); + GuruAnalytics.LogEvent(eventName, data); + } + + private void OnClickEvents2() + { + string eventName = "user_data_loaded"; + GuruAnalytics.LogEvent(eventName); + } + + + private void OnClickReport() + { + GuruAnalytics.ReportEventSuccessRate(); + } + + + + private void OnClickTestCrash() + { +#if UNITY_IOS + Debug.Log($"--> OnClickTestCrash"); + GuruAnalytics.TestCrash(); +#endif + } + + + #endregion + + + + + + + + + } +} \ No newline at end of file diff --git a/Runtime/GuruAnalytics/~Sample/CuruAnalyticsDemo.cs.meta b/Runtime/GuruAnalytics/~Sample/CuruAnalyticsDemo.cs.meta new file mode 100644 index 0000000..6d59a46 --- /dev/null +++ b/Runtime/GuruAnalytics/~Sample/CuruAnalyticsDemo.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 8fde2a18f7d347408a6b869ee03e7de9 +timeCreated: 1672712830 \ No newline at end of file diff --git a/Runtime/GuruAnalytics/~Sample/GuruAnalyticsDemo.unity b/Runtime/GuruAnalytics/~Sample/GuruAnalyticsDemo.unity new file mode 100644 index 0000000..c3a87bd --- /dev/null +++ b/Runtime/GuruAnalytics/~Sample/GuruAnalyticsDemo.unity @@ -0,0 +1,2014 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!29 &1 +OcclusionCullingSettings: + m_ObjectHideFlags: 0 + serializedVersion: 2 + m_OcclusionBakeSettings: + smallestOccluder: 5 + smallestHole: 0.25 + backfaceThreshold: 100 + m_SceneGUID: 00000000000000000000000000000000 + m_OcclusionCullingData: {fileID: 0} +--- !u!104 &2 +RenderSettings: + m_ObjectHideFlags: 0 + serializedVersion: 9 + m_Fog: 0 + m_FogColor: {r: 0.5, g: 0.5, b: 0.5, a: 1} + m_FogMode: 3 + m_FogDensity: 0.01 + m_LinearFogStart: 0 + m_LinearFogEnd: 300 + m_AmbientSkyColor: {r: 0.212, g: 0.227, b: 0.259, a: 1} + m_AmbientEquatorColor: {r: 0.114, g: 0.125, b: 0.133, a: 1} + m_AmbientGroundColor: {r: 0.047, g: 0.043, b: 0.035, a: 1} + m_AmbientIntensity: 1 + m_AmbientMode: 0 + m_SubtractiveShadowColor: {r: 0.42, g: 0.478, b: 0.627, a: 1} + m_SkyboxMaterial: {fileID: 10304, guid: 0000000000000000f000000000000000, type: 0} + m_HaloStrength: 0.5 + m_FlareStrength: 1 + m_FlareFadeSpeed: 3 + m_HaloTexture: {fileID: 0} + m_SpotCookie: {fileID: 10001, guid: 0000000000000000e000000000000000, type: 0} + m_DefaultReflectionMode: 0 + m_DefaultReflectionResolution: 128 + m_ReflectionBounces: 1 + m_ReflectionIntensity: 1 + m_CustomReflection: {fileID: 0} + m_Sun: {fileID: 0} + m_IndirectSpecularColor: {r: 0.3731193, g: 0.38073996, b: 0.35872698, a: 1} + m_UseRadianceAmbientProbe: 0 +--- !u!157 &3 +LightmapSettings: + m_ObjectHideFlags: 0 + serializedVersion: 12 + m_GIWorkflowMode: 1 + m_GISettings: + serializedVersion: 2 + m_BounceScale: 1 + m_IndirectOutputScale: 1 + m_AlbedoBoost: 1 + m_EnvironmentLightingMode: 0 + m_EnableBakedLightmaps: 1 + m_EnableRealtimeLightmaps: 0 + m_LightmapEditorSettings: + serializedVersion: 12 + m_Resolution: 2 + m_BakeResolution: 40 + m_AtlasSize: 1024 + m_AO: 0 + m_AOMaxDistance: 1 + m_CompAOExponent: 1 + m_CompAOExponentDirect: 0 + m_ExtractAmbientOcclusion: 0 + m_Padding: 2 + m_LightmapParameters: {fileID: 0} + m_LightmapsBakeMode: 1 + m_TextureCompression: 1 + m_FinalGather: 0 + m_FinalGatherFiltering: 1 + m_FinalGatherRayCount: 256 + m_ReflectionCompression: 2 + m_MixedBakeMode: 2 + m_BakeBackend: 1 + m_PVRSampling: 1 + m_PVRDirectSampleCount: 32 + m_PVRSampleCount: 512 + m_PVRBounces: 2 + m_PVREnvironmentSampleCount: 256 + m_PVREnvironmentReferencePointCount: 2048 + m_PVRFilteringMode: 1 + m_PVRDenoiserTypeDirect: 1 + m_PVRDenoiserTypeIndirect: 1 + m_PVRDenoiserTypeAO: 1 + m_PVRFilterTypeDirect: 0 + m_PVRFilterTypeIndirect: 0 + m_PVRFilterTypeAO: 0 + m_PVREnvironmentMIS: 1 + m_PVRCulling: 1 + m_PVRFilteringGaussRadiusDirect: 1 + m_PVRFilteringGaussRadiusIndirect: 5 + m_PVRFilteringGaussRadiusAO: 2 + m_PVRFilteringAtrousPositionSigmaDirect: 0.5 + m_PVRFilteringAtrousPositionSigmaIndirect: 2 + m_PVRFilteringAtrousPositionSigmaAO: 1 + m_ExportTrainingData: 0 + m_TrainingDataDestination: TrainingData + m_LightProbeSampleCountMultiplier: 4 + m_LightingDataAsset: {fileID: 0} + m_LightingSettings: {fileID: 0} +--- !u!196 &4 +NavMeshSettings: + serializedVersion: 2 + m_ObjectHideFlags: 0 + m_BuildSettings: + serializedVersion: 2 + agentTypeID: 0 + agentRadius: 0.5 + agentHeight: 2 + agentSlope: 45 + agentClimb: 0.4 + ledgeDropHeight: 0 + maxJumpAcrossDistance: 0 + minRegionArea: 2 + manualCellSize: 0 + cellSize: 0.16666667 + manualTileSize: 0 + tileSize: 256 + accuratePlacement: 0 + maxJobWorkers: 0 + preserveTilesOutsideBounds: 0 + debug: + m_Flags: 0 + m_NavMeshData: {fileID: 0} +--- !u!1 &66634519 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 66634520} + - component: {fileID: 66634523} + - component: {fileID: 66634522} + - component: {fileID: 66634521} + m_Layer: 5 + m_Name: btn_testcrash + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &66634520 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 66634519} + m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_Children: + - {fileID: 728747533} + m_Father: {fileID: 1270885176} + m_RootOrder: 6 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 0, y: 0} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 800, y: 120} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!114 &66634521 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 66634519} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 4e29b1a8efbd4b44bb3f3716e73f07ff, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Navigation: + m_Mode: 3 + m_WrapAround: 0 + m_SelectOnUp: {fileID: 0} + m_SelectOnDown: {fileID: 0} + m_SelectOnLeft: {fileID: 0} + m_SelectOnRight: {fileID: 0} + m_Transition: 1 + m_Colors: + m_NormalColor: {r: 1, g: 1, b: 1, a: 1} + m_HighlightedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1} + m_PressedColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 1} + m_SelectedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1} + m_DisabledColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 0.5019608} + m_ColorMultiplier: 1 + m_FadeDuration: 0.1 + m_SpriteState: + m_HighlightedSprite: {fileID: 0} + m_PressedSprite: {fileID: 0} + m_SelectedSprite: {fileID: 0} + m_DisabledSprite: {fileID: 0} + m_AnimationTriggers: + m_NormalTrigger: Normal + m_HighlightedTrigger: Highlighted + m_PressedTrigger: Pressed + m_SelectedTrigger: Selected + m_DisabledTrigger: Disabled + m_Interactable: 1 + m_TargetGraphic: {fileID: 66634522} + m_OnClick: + m_PersistentCalls: + m_Calls: [] +--- !u!114 &66634522 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 66634519} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 0.990566, g: 0.6337243, b: 0.43454072, a: 1} + m_RaycastTarget: 1 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_Sprite: {fileID: 10905, guid: 0000000000000000f000000000000000, type: 0} + m_Type: 1 + m_PreserveAspect: 0 + m_FillCenter: 1 + m_FillMethod: 4 + m_FillAmount: 1 + m_FillClockwise: 1 + m_FillOrigin: 0 + m_UseSpriteMesh: 0 + m_PixelsPerUnitMultiplier: 1 +--- !u!222 &66634523 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 66634519} + m_CullTransparentMesh: 1 +--- !u!1 &97229769 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 97229770} + - component: {fileID: 97229772} + - component: {fileID: 97229771} + - component: {fileID: 97229773} + m_Layer: 5 + m_Name: btn_init + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &97229770 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 97229769} + m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_Children: + - {fileID: 1657584965} + m_Father: {fileID: 1270885176} + m_RootOrder: 0 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 0, y: 0} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 800, y: 120} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!114 &97229771 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 97229769} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 1} + m_RaycastTarget: 1 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_Sprite: {fileID: 10905, guid: 0000000000000000f000000000000000, type: 0} + m_Type: 1 + m_PreserveAspect: 0 + m_FillCenter: 1 + m_FillMethod: 4 + m_FillAmount: 1 + m_FillClockwise: 1 + m_FillOrigin: 0 + m_UseSpriteMesh: 0 + m_PixelsPerUnitMultiplier: 1 +--- !u!222 &97229772 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 97229769} + m_CullTransparentMesh: 1 +--- !u!114 &97229773 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 97229769} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 4e29b1a8efbd4b44bb3f3716e73f07ff, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Navigation: + m_Mode: 3 + m_WrapAround: 0 + m_SelectOnUp: {fileID: 0} + m_SelectOnDown: {fileID: 0} + m_SelectOnLeft: {fileID: 0} + m_SelectOnRight: {fileID: 0} + m_Transition: 1 + m_Colors: + m_NormalColor: {r: 1, g: 1, b: 1, a: 1} + m_HighlightedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1} + m_PressedColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 1} + m_SelectedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1} + m_DisabledColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 0.5019608} + m_ColorMultiplier: 1 + m_FadeDuration: 0.1 + m_SpriteState: + m_HighlightedSprite: {fileID: 0} + m_PressedSprite: {fileID: 0} + m_SelectedSprite: {fileID: 0} + m_DisabledSprite: {fileID: 0} + m_AnimationTriggers: + m_NormalTrigger: Normal + m_HighlightedTrigger: Highlighted + m_PressedTrigger: Pressed + m_SelectedTrigger: Selected + m_DisabledTrigger: Disabled + m_Interactable: 1 + m_TargetGraphic: {fileID: 97229771} + m_OnClick: + m_PersistentCalls: + m_Calls: [] +--- !u!1 &170095335 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 170095336} + m_Layer: 5 + m_Name: root + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &170095336 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 170095335} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_Children: + - {fileID: 483802708} + m_Father: {fileID: 295971387} + m_RootOrder: 2 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 1, y: 1} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 0} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!1 &295971383 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 295971387} + - component: {fileID: 295971386} + - component: {fileID: 295971385} + - component: {fileID: 295971384} + m_Layer: 5 + m_Name: Canvas + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!114 &295971384 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 295971383} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: dc42784cf147c0c48a680349fa168899, type: 3} + m_Name: + m_EditorClassIdentifier: + m_IgnoreReversedGraphics: 1 + m_BlockingObjects: 0 + m_BlockingMask: + serializedVersion: 2 + m_Bits: 4294967295 +--- !u!114 &295971385 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 295971383} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 0cd44c1031e13a943bb63640046fad76, type: 3} + m_Name: + m_EditorClassIdentifier: + m_UiScaleMode: 1 + m_ReferencePixelsPerUnit: 100 + m_ScaleFactor: 1 + m_ReferenceResolution: {x: 1080, y: 1920} + m_ScreenMatchMode: 0 + m_MatchWidthOrHeight: 0 + m_PhysicalUnit: 3 + m_FallbackScreenDPI: 96 + m_DefaultSpriteDPI: 96 + m_DynamicPixelsPerUnit: 1 + m_PresetInfoIsWorld: 0 +--- !u!223 &295971386 +Canvas: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 295971383} + m_Enabled: 1 + serializedVersion: 3 + m_RenderMode: 1 + m_Camera: {fileID: 531764817} + m_PlaneDistance: 100 + m_PixelPerfect: 0 + m_ReceivesEvents: 1 + m_OverrideSorting: 0 + m_OverridePixelPerfect: 0 + m_SortingBucketNormalizedSize: 0 + m_AdditionalShaderChannelsFlag: 0 + m_SortingLayerID: 0 + m_SortingOrder: 0 + m_TargetDisplay: 0 +--- !u!224 &295971387 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 295971383} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 0, y: 0, z: 0} + m_Children: + - {fileID: 1486183099} + - {fileID: 531764818} + - {fileID: 170095336} + m_Father: {fileID: 0} + m_RootOrder: 0 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 0, y: 0} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 0} + m_Pivot: {x: 0, y: 0} +--- !u!1 &483802707 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 483802708} + - component: {fileID: 483802709} + m_Layer: 5 + m_Name: analytics_demo + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &483802708 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 483802707} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_Children: + - {fileID: 1996017830} + m_Father: {fileID: 170095336} + m_RootOrder: 0 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 1, y: 1} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 0} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!114 &483802709 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 483802707} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 8fde2a18f7d347408a6b869ee03e7de9, type: 3} + m_Name: + m_EditorClassIdentifier: + _isDebug: 1 + _btnInitSDK: {fileID: 97229773} + _btnStatus: {fileID: 2106733042} + _btnUserProperties: {fileID: 1299751324} + _btnEvents: {fileID: 1773066580} + _btnEvents2: {fileID: 2038787617} + _btnReport: {fileID: 611641100} + _btnTestCrash: {fileID: 66634521} +--- !u!1 &531764815 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 531764818} + - component: {fileID: 531764817} + - component: {fileID: 531764816} + m_Layer: 0 + m_Name: UICamera + m_TagString: MainCamera + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!81 &531764816 +AudioListener: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 531764815} + m_Enabled: 1 +--- !u!20 &531764817 +Camera: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 531764815} + m_Enabled: 1 + serializedVersion: 2 + m_ClearFlags: 2 + m_BackGroundColor: {r: 0, g: 0, b: 0, a: 0} + m_projectionMatrixMode: 1 + m_GateFitMode: 2 + m_FOVAxisMode: 0 + m_SensorSize: {x: 36, y: 24} + m_LensShift: {x: 0, y: 0} + m_FocalLength: 50 + m_NormalizedViewPortRect: + serializedVersion: 2 + x: 0 + y: 0 + width: 1 + height: 1 + near clip plane: -10 + far clip plane: 500 + field of view: 60 + orthographic: 1 + orthographic size: 5 + m_Depth: -1 + m_CullingMask: + serializedVersion: 2 + m_Bits: 4294967295 + m_RenderingPath: -1 + m_TargetTexture: {fileID: 0} + m_TargetDisplay: 0 + m_TargetEye: 3 + m_HDR: 1 + m_AllowMSAA: 1 + m_AllowDynamicResolution: 0 + m_ForceIntoRT: 0 + m_OcclusionCulling: 1 + m_StereoConvergence: 10 + m_StereoSeparation: 0.022 +--- !u!4 &531764818 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 531764815} + m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: -10} + m_LocalScale: {x: 1, y: 1, z: 1} + m_Children: [] + m_Father: {fileID: 295971387} + m_RootOrder: 1 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!1 &611641098 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 611641099} + - component: {fileID: 611641102} + - component: {fileID: 611641101} + - component: {fileID: 611641100} + m_Layer: 5 + m_Name: btn_report + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &611641099 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 611641098} + m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_Children: + - {fileID: 832164394} + m_Father: {fileID: 1270885176} + m_RootOrder: 5 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 0, y: 0} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 800, y: 120} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!114 &611641100 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 611641098} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 4e29b1a8efbd4b44bb3f3716e73f07ff, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Navigation: + m_Mode: 3 + m_WrapAround: 0 + m_SelectOnUp: {fileID: 0} + m_SelectOnDown: {fileID: 0} + m_SelectOnLeft: {fileID: 0} + m_SelectOnRight: {fileID: 0} + m_Transition: 1 + m_Colors: + m_NormalColor: {r: 1, g: 1, b: 1, a: 1} + m_HighlightedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1} + m_PressedColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 1} + m_SelectedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1} + m_DisabledColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 0.5019608} + m_ColorMultiplier: 1 + m_FadeDuration: 0.1 + m_SpriteState: + m_HighlightedSprite: {fileID: 0} + m_PressedSprite: {fileID: 0} + m_SelectedSprite: {fileID: 0} + m_DisabledSprite: {fileID: 0} + m_AnimationTriggers: + m_NormalTrigger: Normal + m_HighlightedTrigger: Highlighted + m_PressedTrigger: Pressed + m_SelectedTrigger: Selected + m_DisabledTrigger: Disabled + m_Interactable: 1 + m_TargetGraphic: {fileID: 611641101} + m_OnClick: + m_PersistentCalls: + m_Calls: [] +--- !u!114 &611641101 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 611641098} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 1} + m_RaycastTarget: 1 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_Sprite: {fileID: 10905, guid: 0000000000000000f000000000000000, type: 0} + m_Type: 1 + m_PreserveAspect: 0 + m_FillCenter: 1 + m_FillMethod: 4 + m_FillAmount: 1 + m_FillClockwise: 1 + m_FillOrigin: 0 + m_UseSpriteMesh: 0 + m_PixelsPerUnitMultiplier: 1 +--- !u!222 &611641102 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 611641098} + m_CullTransparentMesh: 1 +--- !u!1 &728747532 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 728747533} + - component: {fileID: 728747535} + - component: {fileID: 728747534} + m_Layer: 5 + m_Name: label + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &728747533 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 728747532} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_Children: [] + m_Father: {fileID: 66634520} + m_RootOrder: 0 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 1, y: 1} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: -20} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!114 &728747534 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 728747532} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 5f7201a12d95ffc409449d95f23cf332, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 0.23584908, g: 0.23584908, b: 0.23584908, a: 1} + m_RaycastTarget: 1 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_FontData: + m_Font: {fileID: 10102, guid: 0000000000000000e000000000000000, type: 0} + m_FontSize: 30 + m_FontStyle: 0 + m_BestFit: 0 + m_MinSize: 2 + m_MaxSize: 40 + m_Alignment: 4 + m_AlignByGeometry: 0 + m_RichText: 1 + m_HorizontalOverflow: 0 + m_VerticalOverflow: 0 + m_LineSpacing: 1 + m_Text: "\u6D4B\u8BD5\u5D29\u6E83\u4E0A\u62A5" +--- !u!222 &728747535 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 728747532} + m_CullTransparentMesh: 1 +--- !u!1 &832164393 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 832164394} + - component: {fileID: 832164396} + - component: {fileID: 832164395} + m_Layer: 5 + m_Name: label + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &832164394 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 832164393} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_Children: [] + m_Father: {fileID: 611641099} + m_RootOrder: 0 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 1, y: 1} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: -20} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!114 &832164395 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 832164393} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 5f7201a12d95ffc409449d95f23cf332, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 0.23584908, g: 0.23584908, b: 0.23584908, a: 1} + m_RaycastTarget: 1 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_FontData: + m_Font: {fileID: 10102, guid: 0000000000000000e000000000000000, type: 0} + m_FontSize: 30 + m_FontStyle: 0 + m_BestFit: 0 + m_MinSize: 2 + m_MaxSize: 40 + m_Alignment: 4 + m_AlignByGeometry: 0 + m_RichText: 1 + m_HorizontalOverflow: 0 + m_VerticalOverflow: 0 + m_LineSpacing: 1 + m_Text: "\u4E0A\u62A5\u6253\u70B9\u6210\u529F\u7387" +--- !u!222 &832164396 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 832164393} + m_CullTransparentMesh: 1 +--- !u!1 &920755264 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 920755265} + - component: {fileID: 920755267} + - component: {fileID: 920755266} + m_Layer: 5 + m_Name: label + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &920755265 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 920755264} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_Children: [] + m_Father: {fileID: 1773066579} + m_RootOrder: 0 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 1, y: 1} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: -20} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!114 &920755266 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 920755264} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 5f7201a12d95ffc409449d95f23cf332, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 0.23584908, g: 0.23584908, b: 0.23584908, a: 1} + m_RaycastTarget: 1 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_FontData: + m_Font: {fileID: 10102, guid: 0000000000000000e000000000000000, type: 0} + m_FontSize: 30 + m_FontStyle: 0 + m_BestFit: 0 + m_MinSize: 2 + m_MaxSize: 40 + m_Alignment: 4 + m_AlignByGeometry: 0 + m_RichText: 1 + m_HorizontalOverflow: 0 + m_VerticalOverflow: 0 + m_LineSpacing: 1 + m_Text: "\u4E0A\u62A5\u6253\u70B9\u4E8B\u4EF6" +--- !u!222 &920755267 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 920755264} + m_CullTransparentMesh: 1 +--- !u!1 &999622155 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 999622156} + - component: {fileID: 999622158} + - component: {fileID: 999622157} + m_Layer: 5 + m_Name: label + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &999622156 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 999622155} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_Children: [] + m_Father: {fileID: 2106733041} + m_RootOrder: 0 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 1, y: 1} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: -20} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!114 &999622157 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 999622155} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 5f7201a12d95ffc409449d95f23cf332, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 0.23584908, g: 0.23584908, b: 0.23584908, a: 1} + m_RaycastTarget: 1 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_FontData: + m_Font: {fileID: 10102, guid: 0000000000000000e000000000000000, type: 0} + m_FontSize: 30 + m_FontStyle: 0 + m_BestFit: 0 + m_MinSize: 2 + m_MaxSize: 40 + m_Alignment: 4 + m_AlignByGeometry: 0 + m_RichText: 1 + m_HorizontalOverflow: 0 + m_VerticalOverflow: 0 + m_LineSpacing: 1 + m_Text: "\u4E0A\u62A5\u5404\u5C5E\u6027ID" +--- !u!222 &999622158 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 999622155} + m_CullTransparentMesh: 1 +--- !u!1 &1270885175 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1270885176} + - component: {fileID: 1270885177} + m_Layer: 5 + m_Name: content + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &1270885176 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1270885175} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_Children: + - {fileID: 97229770} + - {fileID: 2106733041} + - {fileID: 1299751323} + - {fileID: 1773066579} + - {fileID: 2038787616} + - {fileID: 611641099} + - {fileID: 66634520} + m_Father: {fileID: 1996017830} + m_RootOrder: 0 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0.5, y: 0.5} + m_AnchorMax: {x: 0.5, y: 0.5} + m_AnchoredPosition: {x: 0, y: 405.2} + m_SizeDelta: {x: 100, y: 100} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!114 &1270885177 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1270885175} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 59f8146938fff824cb5fd77236b75775, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Padding: + m_Left: 0 + m_Right: 0 + m_Top: 0 + m_Bottom: 0 + m_ChildAlignment: 4 + m_Spacing: 20 + m_ChildForceExpandWidth: 1 + m_ChildForceExpandHeight: 1 + m_ChildControlWidth: 0 + m_ChildControlHeight: 0 + m_ChildScaleWidth: 0 + m_ChildScaleHeight: 0 + m_ReverseArrangement: 0 +--- !u!1 &1299751322 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1299751323} + - component: {fileID: 1299751326} + - component: {fileID: 1299751325} + - component: {fileID: 1299751324} + m_Layer: 5 + m_Name: btn_userproperties + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &1299751323 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1299751322} + m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_Children: + - {fileID: 1727689995} + m_Father: {fileID: 1270885176} + m_RootOrder: 2 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 0, y: 0} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 800, y: 120} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!114 &1299751324 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1299751322} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 4e29b1a8efbd4b44bb3f3716e73f07ff, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Navigation: + m_Mode: 3 + m_WrapAround: 0 + m_SelectOnUp: {fileID: 0} + m_SelectOnDown: {fileID: 0} + m_SelectOnLeft: {fileID: 0} + m_SelectOnRight: {fileID: 0} + m_Transition: 1 + m_Colors: + m_NormalColor: {r: 1, g: 1, b: 1, a: 1} + m_HighlightedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1} + m_PressedColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 1} + m_SelectedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1} + m_DisabledColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 0.5019608} + m_ColorMultiplier: 1 + m_FadeDuration: 0.1 + m_SpriteState: + m_HighlightedSprite: {fileID: 0} + m_PressedSprite: {fileID: 0} + m_SelectedSprite: {fileID: 0} + m_DisabledSprite: {fileID: 0} + m_AnimationTriggers: + m_NormalTrigger: Normal + m_HighlightedTrigger: Highlighted + m_PressedTrigger: Pressed + m_SelectedTrigger: Selected + m_DisabledTrigger: Disabled + m_Interactable: 1 + m_TargetGraphic: {fileID: 1299751325} + m_OnClick: + m_PersistentCalls: + m_Calls: [] +--- !u!114 &1299751325 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1299751322} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 1} + m_RaycastTarget: 1 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_Sprite: {fileID: 10905, guid: 0000000000000000f000000000000000, type: 0} + m_Type: 1 + m_PreserveAspect: 0 + m_FillCenter: 1 + m_FillMethod: 4 + m_FillAmount: 1 + m_FillClockwise: 1 + m_FillOrigin: 0 + m_UseSpriteMesh: 0 + m_PixelsPerUnitMultiplier: 1 +--- !u!222 &1299751326 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1299751322} + m_CullTransparentMesh: 1 +--- !u!1 &1486183098 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1486183099} + - component: {fileID: 1486183101} + - component: {fileID: 1486183100} + m_Layer: 0 + m_Name: EventSystem + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!4 &1486183099 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1486183098} + m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} + m_LocalPosition: {x: -540, y: -960, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_Children: [] + m_Father: {fileID: 295971387} + m_RootOrder: 0 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!114 &1486183100 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1486183098} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 4f231c4fb786f3946a6b90b886c48677, type: 3} + m_Name: + m_EditorClassIdentifier: + m_HorizontalAxis: Horizontal + m_VerticalAxis: Vertical + m_SubmitButton: Submit + m_CancelButton: Cancel + m_InputActionsPerSecond: 10 + m_RepeatDelay: 0.5 + m_ForceModuleActive: 0 +--- !u!114 &1486183101 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1486183098} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 76c392e42b5098c458856cdf6ecaaaa1, type: 3} + m_Name: + m_EditorClassIdentifier: + m_FirstSelected: {fileID: 0} + m_sendNavigationEvents: 1 + m_DragThreshold: 10 +--- !u!1 &1657584964 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1657584965} + - component: {fileID: 1657584967} + - component: {fileID: 1657584966} + m_Layer: 5 + m_Name: label + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &1657584965 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1657584964} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_Children: [] + m_Father: {fileID: 97229770} + m_RootOrder: 0 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 1, y: 1} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: -20} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!114 &1657584966 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1657584964} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 5f7201a12d95ffc409449d95f23cf332, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 0.23584908, g: 0.23584908, b: 0.23584908, a: 1} + m_RaycastTarget: 1 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_FontData: + m_Font: {fileID: 10102, guid: 0000000000000000e000000000000000, type: 0} + m_FontSize: 30 + m_FontStyle: 0 + m_BestFit: 0 + m_MinSize: 2 + m_MaxSize: 40 + m_Alignment: 4 + m_AlignByGeometry: 0 + m_RichText: 1 + m_HorizontalOverflow: 0 + m_VerticalOverflow: 0 + m_LineSpacing: 1 + m_Text: "\u521D\u59CB\u5316SDK" +--- !u!222 &1657584967 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1657584964} + m_CullTransparentMesh: 1 +--- !u!1 &1727689994 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1727689995} + - component: {fileID: 1727689997} + - component: {fileID: 1727689996} + m_Layer: 5 + m_Name: label + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &1727689995 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1727689994} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_Children: [] + m_Father: {fileID: 1299751323} + m_RootOrder: 0 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 1, y: 1} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: -20} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!114 &1727689996 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1727689994} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 5f7201a12d95ffc409449d95f23cf332, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 0.23584908, g: 0.23584908, b: 0.23584908, a: 1} + m_RaycastTarget: 1 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_FontData: + m_Font: {fileID: 10102, guid: 0000000000000000e000000000000000, type: 0} + m_FontSize: 30 + m_FontStyle: 0 + m_BestFit: 0 + m_MinSize: 2 + m_MaxSize: 40 + m_Alignment: 4 + m_AlignByGeometry: 0 + m_RichText: 1 + m_HorizontalOverflow: 0 + m_VerticalOverflow: 0 + m_LineSpacing: 1 + m_Text: "\u4E0A\u62A5\u7528\u6237\u5C5E\u6027" +--- !u!222 &1727689997 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1727689994} + m_CullTransparentMesh: 1 +--- !u!1 &1730981159 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1730981160} + - component: {fileID: 1730981162} + - component: {fileID: 1730981161} + m_Layer: 5 + m_Name: label + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &1730981160 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1730981159} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_Children: [] + m_Father: {fileID: 2038787616} + m_RootOrder: 0 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 1, y: 1} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: -20} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!114 &1730981161 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1730981159} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 5f7201a12d95ffc409449d95f23cf332, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 0.23584908, g: 0.23584908, b: 0.23584908, a: 1} + m_RaycastTarget: 1 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_FontData: + m_Font: {fileID: 10102, guid: 0000000000000000e000000000000000, type: 0} + m_FontSize: 30 + m_FontStyle: 0 + m_BestFit: 0 + m_MinSize: 2 + m_MaxSize: 40 + m_Alignment: 4 + m_AlignByGeometry: 0 + m_RichText: 1 + m_HorizontalOverflow: 0 + m_VerticalOverflow: 0 + m_LineSpacing: 1 + m_Text: "\u4E0A\u62A5\u6253\u70B9\u4E8B\u4EF6(\u4EC5\u4E8B\u4EF6\u540D)" +--- !u!222 &1730981162 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1730981159} + m_CullTransparentMesh: 1 +--- !u!1 &1773066578 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1773066579} + - component: {fileID: 1773066582} + - component: {fileID: 1773066581} + - component: {fileID: 1773066580} + m_Layer: 5 + m_Name: btn_events + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &1773066579 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1773066578} + m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_Children: + - {fileID: 920755265} + m_Father: {fileID: 1270885176} + m_RootOrder: 3 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 0, y: 0} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 800, y: 120} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!114 &1773066580 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1773066578} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 4e29b1a8efbd4b44bb3f3716e73f07ff, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Navigation: + m_Mode: 3 + m_WrapAround: 0 + m_SelectOnUp: {fileID: 0} + m_SelectOnDown: {fileID: 0} + m_SelectOnLeft: {fileID: 0} + m_SelectOnRight: {fileID: 0} + m_Transition: 1 + m_Colors: + m_NormalColor: {r: 1, g: 1, b: 1, a: 1} + m_HighlightedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1} + m_PressedColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 1} + m_SelectedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1} + m_DisabledColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 0.5019608} + m_ColorMultiplier: 1 + m_FadeDuration: 0.1 + m_SpriteState: + m_HighlightedSprite: {fileID: 0} + m_PressedSprite: {fileID: 0} + m_SelectedSprite: {fileID: 0} + m_DisabledSprite: {fileID: 0} + m_AnimationTriggers: + m_NormalTrigger: Normal + m_HighlightedTrigger: Highlighted + m_PressedTrigger: Pressed + m_SelectedTrigger: Selected + m_DisabledTrigger: Disabled + m_Interactable: 1 + m_TargetGraphic: {fileID: 1773066581} + m_OnClick: + m_PersistentCalls: + m_Calls: [] +--- !u!114 &1773066581 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1773066578} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 1} + m_RaycastTarget: 1 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_Sprite: {fileID: 10905, guid: 0000000000000000f000000000000000, type: 0} + m_Type: 1 + m_PreserveAspect: 0 + m_FillCenter: 1 + m_FillMethod: 4 + m_FillAmount: 1 + m_FillClockwise: 1 + m_FillOrigin: 0 + m_UseSpriteMesh: 0 + m_PixelsPerUnitMultiplier: 1 +--- !u!222 &1773066582 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1773066578} + m_CullTransparentMesh: 1 +--- !u!1 &1996017829 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1996017830} + - component: {fileID: 1996017832} + - component: {fileID: 1996017831} + m_Layer: 5 + m_Name: root + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &1996017830 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1996017829} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_Children: + - {fileID: 1270885176} + m_Father: {fileID: 483802708} + m_RootOrder: 0 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 1, y: 1} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 0} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!114 &1996017831 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1996017829} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 0.8} + m_RaycastTarget: 1 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_Sprite: {fileID: 10905, guid: 0000000000000000f000000000000000, type: 0} + m_Type: 1 + m_PreserveAspect: 0 + m_FillCenter: 1 + m_FillMethod: 4 + m_FillAmount: 1 + m_FillClockwise: 1 + m_FillOrigin: 0 + m_UseSpriteMesh: 0 + m_PixelsPerUnitMultiplier: 1 +--- !u!222 &1996017832 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1996017829} + m_CullTransparentMesh: 1 +--- !u!1 &2038787615 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 2038787616} + - component: {fileID: 2038787619} + - component: {fileID: 2038787618} + - component: {fileID: 2038787617} + m_Layer: 5 + m_Name: btn_event2 + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &2038787616 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 2038787615} + m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_Children: + - {fileID: 1730981160} + m_Father: {fileID: 1270885176} + m_RootOrder: 4 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 0, y: 0} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 800, y: 120} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!114 &2038787617 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 2038787615} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 4e29b1a8efbd4b44bb3f3716e73f07ff, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Navigation: + m_Mode: 3 + m_WrapAround: 0 + m_SelectOnUp: {fileID: 0} + m_SelectOnDown: {fileID: 0} + m_SelectOnLeft: {fileID: 0} + m_SelectOnRight: {fileID: 0} + m_Transition: 1 + m_Colors: + m_NormalColor: {r: 1, g: 1, b: 1, a: 1} + m_HighlightedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1} + m_PressedColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 1} + m_SelectedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1} + m_DisabledColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 0.5019608} + m_ColorMultiplier: 1 + m_FadeDuration: 0.1 + m_SpriteState: + m_HighlightedSprite: {fileID: 0} + m_PressedSprite: {fileID: 0} + m_SelectedSprite: {fileID: 0} + m_DisabledSprite: {fileID: 0} + m_AnimationTriggers: + m_NormalTrigger: Normal + m_HighlightedTrigger: Highlighted + m_PressedTrigger: Pressed + m_SelectedTrigger: Selected + m_DisabledTrigger: Disabled + m_Interactable: 1 + m_TargetGraphic: {fileID: 2038787618} + m_OnClick: + m_PersistentCalls: + m_Calls: [] +--- !u!114 &2038787618 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 2038787615} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 1} + m_RaycastTarget: 1 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_Sprite: {fileID: 10905, guid: 0000000000000000f000000000000000, type: 0} + m_Type: 1 + m_PreserveAspect: 0 + m_FillCenter: 1 + m_FillMethod: 4 + m_FillAmount: 1 + m_FillClockwise: 1 + m_FillOrigin: 0 + m_UseSpriteMesh: 0 + m_PixelsPerUnitMultiplier: 1 +--- !u!222 &2038787619 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 2038787615} + m_CullTransparentMesh: 1 +--- !u!1 &2106733040 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 2106733041} + - component: {fileID: 2106733044} + - component: {fileID: 2106733043} + - component: {fileID: 2106733042} + m_Layer: 5 + m_Name: btn_status + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &2106733041 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 2106733040} + m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_Children: + - {fileID: 999622156} + m_Father: {fileID: 1270885176} + m_RootOrder: 1 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 0, y: 0} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 800, y: 120} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!114 &2106733042 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 2106733040} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 4e29b1a8efbd4b44bb3f3716e73f07ff, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Navigation: + m_Mode: 3 + m_WrapAround: 0 + m_SelectOnUp: {fileID: 0} + m_SelectOnDown: {fileID: 0} + m_SelectOnLeft: {fileID: 0} + m_SelectOnRight: {fileID: 0} + m_Transition: 1 + m_Colors: + m_NormalColor: {r: 1, g: 1, b: 1, a: 1} + m_HighlightedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1} + m_PressedColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 1} + m_SelectedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1} + m_DisabledColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 0.5019608} + m_ColorMultiplier: 1 + m_FadeDuration: 0.1 + m_SpriteState: + m_HighlightedSprite: {fileID: 0} + m_PressedSprite: {fileID: 0} + m_SelectedSprite: {fileID: 0} + m_DisabledSprite: {fileID: 0} + m_AnimationTriggers: + m_NormalTrigger: Normal + m_HighlightedTrigger: Highlighted + m_PressedTrigger: Pressed + m_SelectedTrigger: Selected + m_DisabledTrigger: Disabled + m_Interactable: 1 + m_TargetGraphic: {fileID: 2106733043} + m_OnClick: + m_PersistentCalls: + m_Calls: [] +--- !u!114 &2106733043 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 2106733040} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 1} + m_RaycastTarget: 1 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_Sprite: {fileID: 10905, guid: 0000000000000000f000000000000000, type: 0} + m_Type: 1 + m_PreserveAspect: 0 + m_FillCenter: 1 + m_FillMethod: 4 + m_FillAmount: 1 + m_FillClockwise: 1 + m_FillOrigin: 0 + m_UseSpriteMesh: 0 + m_PixelsPerUnitMultiplier: 1 +--- !u!222 &2106733044 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 2106733040} + m_CullTransparentMesh: 1 diff --git a/Runtime/GuruAnalytics/~Sample/GuruAnalyticsDemo.unity.meta b/Runtime/GuruAnalytics/~Sample/GuruAnalyticsDemo.unity.meta new file mode 100644 index 0000000..f586962 --- /dev/null +++ b/Runtime/GuruAnalytics/~Sample/GuruAnalyticsDemo.unity.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: ea095a004daab4fc096aa297c21fca99 +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/GuruConsent.meta b/Runtime/GuruConsent.meta new file mode 100644 index 0000000..3e56665 --- /dev/null +++ b/Runtime/GuruConsent.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 950580d964e7240bfb4e937b11f896e2 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/GuruConsent/Docs.meta b/Runtime/GuruConsent/Docs.meta new file mode 100644 index 0000000..aac2b67 --- /dev/null +++ b/Runtime/GuruConsent/Docs.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 26dc756e74ffe47d69afc0835264b589 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/GuruConsent/Docs/screen_01.png b/Runtime/GuruConsent/Docs/screen_01.png new file mode 100644 index 0000000..860d3d4 Binary files /dev/null and b/Runtime/GuruConsent/Docs/screen_01.png differ diff --git a/Runtime/GuruConsent/Docs/screen_01.png.meta b/Runtime/GuruConsent/Docs/screen_01.png.meta new file mode 100644 index 0000000..84363f1 --- /dev/null +++ b/Runtime/GuruConsent/Docs/screen_01.png.meta @@ -0,0 +1,133 @@ +fileFormatVersion: 2 +guid: 3a12d9305667344adb40816832046845 +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 12 + mipmaps: + mipMapMode: 0 + enableMipMap: 1 + sRGBTexture: 1 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 1 + aniso: 1 + mipBias: 0 + wrapU: 0 + wrapV: 0 + wrapW: 0 + nPOTScale: 1 + lightmap: 0 + compressionQuality: 50 + spriteMode: 0 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 0 + spriteTessellationDetail: -1 + textureType: 0 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 0 + cookieLightType: 0 + platformSettings: + - serializedVersion: 3 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 3 + buildTarget: Standalone + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 3 + buildTarget: iPhone + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 3 + buildTarget: Android + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + physicsShape: [] + bones: [] + spriteID: + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + spritePackingTag: + pSDRemoveMatte: 0 + pSDShowRemoveMatteOption: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/GuruConsent/Docs/screen_02.png b/Runtime/GuruConsent/Docs/screen_02.png new file mode 100644 index 0000000..19be5a2 Binary files /dev/null and b/Runtime/GuruConsent/Docs/screen_02.png differ diff --git a/Runtime/GuruConsent/Docs/screen_02.png.meta b/Runtime/GuruConsent/Docs/screen_02.png.meta new file mode 100644 index 0000000..7880901 --- /dev/null +++ b/Runtime/GuruConsent/Docs/screen_02.png.meta @@ -0,0 +1,133 @@ +fileFormatVersion: 2 +guid: 78e4b9274dd524746aa4dcbaccb2f088 +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 12 + mipmaps: + mipMapMode: 0 + enableMipMap: 1 + sRGBTexture: 1 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 1 + aniso: 1 + mipBias: 0 + wrapU: 0 + wrapV: 0 + wrapW: 0 + nPOTScale: 1 + lightmap: 0 + compressionQuality: 50 + spriteMode: 0 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 0 + spriteTessellationDetail: -1 + textureType: 0 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 0 + cookieLightType: 0 + platformSettings: + - serializedVersion: 3 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 3 + buildTarget: Standalone + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 3 + buildTarget: iPhone + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 3 + buildTarget: Android + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + physicsShape: [] + bones: [] + spriteID: + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + spritePackingTag: + pSDRemoveMatte: 0 + pSDShowRemoveMatteOption: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/GuruConsent/Docs/screen_03.png b/Runtime/GuruConsent/Docs/screen_03.png new file mode 100644 index 0000000..f8c4dd9 Binary files /dev/null and b/Runtime/GuruConsent/Docs/screen_03.png differ diff --git a/Runtime/GuruConsent/Docs/screen_03.png.meta b/Runtime/GuruConsent/Docs/screen_03.png.meta new file mode 100644 index 0000000..d8128cb --- /dev/null +++ b/Runtime/GuruConsent/Docs/screen_03.png.meta @@ -0,0 +1,133 @@ +fileFormatVersion: 2 +guid: 84ca9ad80c4454abebc931ca2754d337 +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 12 + mipmaps: + mipMapMode: 0 + enableMipMap: 1 + sRGBTexture: 1 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 1 + aniso: 1 + mipBias: 0 + wrapU: 0 + wrapV: 0 + wrapW: 0 + nPOTScale: 1 + lightmap: 0 + compressionQuality: 50 + spriteMode: 0 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 0 + spriteTessellationDetail: -1 + textureType: 0 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 0 + cookieLightType: 0 + platformSettings: + - serializedVersion: 3 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 3 + buildTarget: Standalone + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 3 + buildTarget: iPhone + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 3 + buildTarget: Android + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + physicsShape: [] + bones: [] + spriteID: + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + spritePackingTag: + pSDRemoveMatte: 0 + pSDShowRemoveMatteOption: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/GuruConsent/Editor.meta b/Runtime/GuruConsent/Editor.meta new file mode 100644 index 0000000..77f4f64 --- /dev/null +++ b/Runtime/GuruConsent/Editor.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 3db4a8ec59fec4e6db495eb526c75e24 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/GuruConsent/Editor/Dependencies.xml b/Runtime/GuruConsent/Editor/Dependencies.xml new file mode 100644 index 0000000..b00d894 --- /dev/null +++ b/Runtime/GuruConsent/Editor/Dependencies.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + + git@github.com:castbox/GuruSpecs.git + + > + + diff --git a/Runtime/GuruConsent/Editor/Dependencies.xml.meta b/Runtime/GuruConsent/Editor/Dependencies.xml.meta new file mode 100644 index 0000000..3585504 --- /dev/null +++ b/Runtime/GuruConsent/Editor/Dependencies.xml.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: a0fef219c4ad0481daf0727fd2317811 +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/GuruConsent/Plugins.meta b/Runtime/GuruConsent/Plugins.meta new file mode 100644 index 0000000..5019595 --- /dev/null +++ b/Runtime/GuruConsent/Plugins.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: a3335276818f940c3bff4918da9f215a +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/GuruConsent/Plugins/Android.meta b/Runtime/GuruConsent/Plugins/Android.meta new file mode 100644 index 0000000..0009a4e --- /dev/null +++ b/Runtime/GuruConsent/Plugins/Android.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 8b97beda3dd1948fabf9cd9ae0955ebc +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/GuruConsent/Plugins/Android/GuruConsent-1.0.0.aar b/Runtime/GuruConsent/Plugins/Android/GuruConsent-1.0.0.aar new file mode 100644 index 0000000..d24ab95 Binary files /dev/null and b/Runtime/GuruConsent/Plugins/Android/GuruConsent-1.0.0.aar differ diff --git a/Runtime/GuruConsent/Plugins/Android/GuruConsent-1.0.0.aar.meta b/Runtime/GuruConsent/Plugins/Android/GuruConsent-1.0.0.aar.meta new file mode 100644 index 0000000..b87d020 --- /dev/null +++ b/Runtime/GuruConsent/Plugins/Android/GuruConsent-1.0.0.aar.meta @@ -0,0 +1,32 @@ +fileFormatVersion: 2 +guid: 08990e1d7b56347988297f28b027d646 +PluginImporter: + externalObjects: {} + serializedVersion: 2 + iconMap: {} + executionOrder: {} + defineConstraints: [] + isPreloaded: 0 + isOverridable: 0 + isExplicitlyReferenced: 0 + validateReferences: 1 + platformData: + - first: + Android: Android + second: + enabled: 1 + settings: {} + - first: + Any: + second: + enabled: 0 + settings: {} + - first: + Editor: Editor + second: + enabled: 0 + settings: + DefaultValueInitialized: true + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/GuruConsent/Plugins/Android/consent-release.aar b/Runtime/GuruConsent/Plugins/Android/consent-release.aar new file mode 100644 index 0000000..6479a70 Binary files /dev/null and b/Runtime/GuruConsent/Plugins/Android/consent-release.aar differ diff --git a/Runtime/GuruConsent/Plugins/Android/consent-release.aar.meta b/Runtime/GuruConsent/Plugins/Android/consent-release.aar.meta new file mode 100644 index 0000000..c35ddda --- /dev/null +++ b/Runtime/GuruConsent/Plugins/Android/consent-release.aar.meta @@ -0,0 +1,32 @@ +fileFormatVersion: 2 +guid: 7b835e3950e934b5eaa8d02c24aa5a18 +PluginImporter: + externalObjects: {} + serializedVersion: 2 + iconMap: {} + executionOrder: {} + defineConstraints: [] + isPreloaded: 0 + isOverridable: 0 + isExplicitlyReferenced: 0 + validateReferences: 1 + platformData: + - first: + Android: Android + second: + enabled: 1 + settings: {} + - first: + Any: + second: + enabled: 0 + settings: {} + - first: + Editor: Editor + second: + enabled: 0 + settings: + DefaultValueInitialized: true + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/GuruConsent/Plugins/iOS.meta b/Runtime/GuruConsent/Plugins/iOS.meta new file mode 100644 index 0000000..bcf8332 --- /dev/null +++ b/Runtime/GuruConsent/Plugins/iOS.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: cd3df7c40f00544738c8f711f11265e0 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/GuruConsent/Plugins/iOS/U3DConsent.mm b/Runtime/GuruConsent/Plugins/iOS/U3DConsent.mm new file mode 100644 index 0000000..760567d --- /dev/null +++ b/Runtime/GuruConsent/Plugins/iOS/U3DConsent.mm @@ -0,0 +1,164 @@ +// ============================================== +// U3DConsent.mm +// UnityFramework +// +// Created by EricHu on 2022/11/17. +// Copyright © 2022 guru. All rights reserved. +// ============================================== + +#import +#import +#import "UnityAppController+UnityInterface.h" + +@interface U3DConsent : NSObject + +//+ (void)requestGDPR: (NSString *)deviceId :(int) debugGeo; +//+ (UIViewController *) getUnityViewController; + +@end + +static NSString *gameobjectName; +static NSString *callbackName; + + +@implementation U3DConsent + + ++(UnityAppController *)GetAppController +{ + return (UnityAppController*)[UIApplication sharedApplication].delegate; +} + + ++(UIViewController *) getUnityViewController { + return UnityGetGLViewController(); +} + + +// 请求GDPR的接口 ++(void) requestGDPR: (NSString *)deviceId :(int)debugGeography +{ +// NSLog(@"--- deviceId: %@", deviceId); + if(deviceId.length){ + NSLog(@"--- set debug device Id: %@", deviceId); + GuruConsentDebugSettings *debug = [[GuruConsentDebugSettings alloc] init]; + debug.testDeviceIdentifiers = @[deviceId]; + debug.geography = (GuruConsentDebugSettingsGeography)debugGeography; + GuruConsent.debug = debug; + } + + + // 开始请求 + [GuruConsent startFrom: [U3DConsent getUnityViewController] + success:^(enum GuruConsentGDPRStatus status) { + + if (@available(iOS 14, *)) { +// NSLog(@"ATT 结果: %lu", +// (unsigned long)ATTrackingManager.trackingAuthorizationStatus); + } + + NSString *msg = @""; + + switch (status) { + case GuruConsentGDPRStatusUnknown: + NSLog(@"--- GuruConsentGDPRStatusUnknown"); + msg = @"GuruConsentGDPRStatusUnknown"; + break; + + case GuruConsentGDPRStatusRequired: + NSLog(@"--- GuruConsentGDPRStatusRequired"); + msg = @"GuruConsentGDPRStatusRequired"; + break; + + case GuruConsentGDPRStatusNotRequired: + NSLog(@"--- GuruConsentGDPRStatusNotRequired"); + msg = @"GuruConsentGDPRStatusNotRequired"; + break; + + case GuruConsentGDPRStatusObtained: + NSLog(@"--- GuruConsentGDPRStatusObtained"); + msg = @"GuruConsentGDPRStatusObtained"; + break; + + default: + break; + } + NSLog(@"GDPR 结果: %ld", (long)status); + + // 发送数据 + [U3DConsent sendMessage: [U3DConsent buildDataString: (int)status andMessage:msg]]; + + } failure:^(NSError * _Nonnull error) { + NSLog(@"失败: %@", error); + [U3DConsent sendMessage: [U3DConsent buildDataString: -100 andMessage:@"request failed"]]; + }]; +} + +// 字符串转换 ++(const char*) stringToChar: (NSString *) str{ + return [str cStringUsingEncoding:NSASCIIStringEncoding]; +} + + ++(char*) finalChar: (NSString *) string +{ + const char *tmpChar = [string cStringUsingEncoding:NSASCIIStringEncoding]; + + if (string == NULL) + return NULL; + + char* res = (char*)malloc(strlen(tmpChar) + 1); + strcpy(res, tmpChar); + + return res; +} + + +// 构建数据 ++(NSString *) buildDataString: (int)status andMessage: (NSString *)msg{ + + NSString *jsonString = [NSString stringWithFormat: @"{\"action\":\"gdpr\",\"data\":{\"status\":%d,\"msg\":\"%@\"}}", status, msg]; + +// NSDictionary *dict = @{@"action" : @"gdpr"}; +// [dict setValue:@{@"status" : [NSString stringWithFormat:@"%d",status], @"msg": msg} forKey:@"data"]; +// +// NSData *data = [NSJSONSerialization dataWithJSONObject:dict options:NSJSONWritingPrettyPrinted error:nil]; +// NSString *jsonString = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]; + + return jsonString; +} + + +// 向Unity发送数据 ++(void) sendMessage: (NSString *)msg +{ +// NSLog(@"--- unityInitSDK222: %@:%@", gameobjectName, callbackName); + if(gameobjectName != nil && callbackName != nil){ + char *t1 = [U3DConsent finalChar: gameobjectName]; + char *t2 = [U3DConsent finalChar: callbackName]; + char *t3 = [U3DConsent finalChar: msg]; + + UnitySendMessage(t1, t2, t3); + } +} +@end + + +extern "C" { + + // 请求GDPR + void unityRequestGDPR(const char * value, int debugGeo){ + [U3DConsent requestGDPR:[NSString stringWithUTF8String:value] :debugGeo]; + } + + // 初始化SDK + void unityInitSDK(const char *gameobject, const char *method){ +// NSLog(@"--- unityInitSDK111: %s:%s", gameobject, method); + gameobjectName = [NSString stringWithUTF8String:gameobject]; + callbackName = [NSString stringWithUTF8String:method]; + } + +} + + + diff --git a/Runtime/GuruConsent/Plugins/iOS/U3DConsent.mm.meta b/Runtime/GuruConsent/Plugins/iOS/U3DConsent.mm.meta new file mode 100644 index 0000000..94f845b --- /dev/null +++ b/Runtime/GuruConsent/Plugins/iOS/U3DConsent.mm.meta @@ -0,0 +1,85 @@ +fileFormatVersion: 2 +guid: 7cbac7394e9d14cb7a976b19670c4dd1 +PluginImporter: + externalObjects: {} + serializedVersion: 2 + iconMap: {} + executionOrder: {} + defineConstraints: [] + isPreloaded: 0 + isOverridable: 0 + isExplicitlyReferenced: 0 + validateReferences: 1 + platformData: + - first: + : Any + second: + enabled: 0 + settings: + Exclude Android: 1 + Exclude Editor: 1 + Exclude Linux64: 1 + Exclude OSXUniversal: 1 + Exclude Win: 1 + Exclude Win64: 1 + Exclude iOS: 0 + - first: + Android: Android + second: + enabled: 0 + settings: + CPU: ARMv7 + - first: + Any: + second: + enabled: 0 + settings: {} + - first: + Editor: Editor + second: + enabled: 0 + settings: + CPU: AnyCPU + DefaultValueInitialized: true + OS: AnyOS + - first: + Standalone: Linux64 + second: + enabled: 0 + settings: + CPU: None + - first: + Standalone: OSXUniversal + second: + enabled: 0 + settings: + CPU: None + - first: + Standalone: Win + second: + enabled: 0 + settings: + CPU: x86 + - first: + Standalone: Win64 + second: + enabled: 0 + settings: + CPU: x86_64 + - first: + iPhone: iOS + second: + enabled: 1 + settings: + AddToEmbeddedBinaries: false + CPU: AnyCPU + CompileFlags: + FrameworkDependencies: AdSupport;CoreData;CoreFoundation; + - first: + tvOS: tvOS + second: + enabled: 1 + settings: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/GuruConsent/README.md b/Runtime/GuruConsent/README.md new file mode 100644 index 0000000..39076d2 --- /dev/null +++ b/Runtime/GuruConsent/README.md @@ -0,0 +1,162 @@ +# Guru Unity Consent + +## Version 1.0.3 +* 更新了SDKCallback对象的名称和逻辑, 同SDK本体进行区分 + + + +## Version 1.0.2 + +- 注意本插件库依赖 `LitJson` 用于解析Json格式数据. 请确保项目内引入此库. +- 使用了 *EDM* 插件实现自动依赖注入. +
+ + +--- + +## 插件接入 + +- 项目整合插件后, 只需在App启动的时候调用代码, 整个Consent流程会自动启动. + + ```C# + void Awake(){ + + // 无需回调的话可直接调用 + // GuruConsent.StartConsent(); + + GuruConsent.StartConsent(OnGetConsentStatus); + } + + /// + /// 获取到 ConsentStatus + /// + /// + private void OnGetConsentStatus(int status) + { + string msg = $"[Unity] Get Status: {status}"; + Debug.Log(msg); + //TODO 后继若有处理,可补全逻辑 + } + ``` +- 测试功能时请确保开启了**欧洲的VPN**, 比如开启英国或者德国的VPN来直接唤起GDPR + ![](Docs/screen_01.png) +- iOS设备在点击 `Consent` 后会立即拉起 'ATT' 弹窗 + ![](Docs/screen_02.png) + +--- + +
+ +## 插件注入: + +本项目已开始使用 `ExternalDependencyManager` 简称 `EDM` 来解决各种库的依赖问题 + +详细配置可见: [Dependencies.xml](Editor/Dependencies.xml) + +IOS 项目注意配置如下图: + +--> 取消勾选 **Link frameworks statically** + +![](Docs/screen_03.png) + + + +## Android 平台配置说明 + +于主菜单 `BuildSettings/PlayerSettings/PubishSettings:` + +开启如下选项: + +- [x] Custom Main Manifest +- [x] Custom Main Gradle Template +- [x] Custom Properties Gradle Template + +之后会在项目的 `Plugins/Android`内生成对应的文件. + +(A) 修改 `AndroidManifest` + +确保于 `` 内添加本项目的 `GoogleClientID`, 例如: + +```xml +` + + ... + + + + +``` + +(B) 修改 `gradleTemplate.properties` + +添加一下内容支持 `AndroidX` + +```java +org.gradle.jvmargs=-Xmx**JVM_HEAP_SIZE**M +org.gradle.parallel=true +android.enableR8=false +unityStreamingAssets=.unity3d**STREAMING_ASSETS** +android.useAndroidX=true +android.enableJetifier=true +**ADDITIONAL_PROPERTIES** +``` + +(C) 修改 `mainTemplate.gradle` + +于 `dependency` 内添加如下依赖 + +```java +dependencies { + ... + implementation 'androidx.appcompat:appcompat:1.1.0' + implementation 'com.google.android.ump:user-messaging-platform:2.0.0' + compile 'com.mapzen:on-the-road:0.8.1' + ... +} + +``` + +最低 `minTarget` 设置为 **21** + +(D) 修改 `proguard-user.txt` 文件, 在最后追加此插件的相关代码 + +若项目使用了 ProGuard 压缩混淆, 需要修改此文件, 否则可能造成JAVA类无法被找到 + +```java +... +-keep class com.guru.** { *; } +-keep class guru.core.** { *; } +``` + + +--- + +
+ + +## iOS平台配置说明 + +- 先确 `Info.plist` 中经正确配置了 firebase 的应用ID + ```xml + GADApplicationIdentifier + YOUR-APP-ID + ``` +- 确保 ATT 的文本配置也正确设置了 + ```xml + NSUserTrackingUsageDescription + This identifier will be used to deliver personalized ads to you. + ``` +- GDPR的构建管线([IOSPostBuild_GDPR](Editor/IOS_POST_GDPR/IOSPostBuild_GDPR.cs))会自动添加`pod`源和target, 确保不会和项目的POD产生冲突. + + +## 示例项目 + +- 示例项目位于 [~Sample](~Sample) 目录内. +- 示例借用了 WaterSortPuzzle 的 `AppID` 和 `BundleID` +- 示例项目可输入测试设备ID, 可在设备接入LOG显示的情况下, 第一次使用空DeviceID来请求设备ID, 会在LOG中显示. 然后再次打开应用, 输入ID, 即可模拟欧洲IP展示流程. +- 挂载欧洲VPN可直接进行请求. 并显示正确的GDPR流程 + + + diff --git a/Runtime/GuruConsent/README.md.meta b/Runtime/GuruConsent/README.md.meta new file mode 100644 index 0000000..8977be4 --- /dev/null +++ b/Runtime/GuruConsent/README.md.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 49b1811407bbe4ee3ac38cc1c6b0ccfd +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/GuruConsent/Runtime.meta b/Runtime/GuruConsent/Runtime.meta new file mode 100644 index 0000000..afe646a --- /dev/null +++ b/Runtime/GuruConsent/Runtime.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 1ee30d8a0350147cca08edd3d14f2ef5 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/GuruConsent/Runtime/Script.meta b/Runtime/GuruConsent/Runtime/Script.meta new file mode 100644 index 0000000..edc1c72 --- /dev/null +++ b/Runtime/GuruConsent/Runtime/Script.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: af19a2aa76ada4ffdac4d0823c327075 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/GuruConsent/Runtime/Script/Consent.meta b/Runtime/GuruConsent/Runtime/Script/Consent.meta new file mode 100644 index 0000000..06872f5 --- /dev/null +++ b/Runtime/GuruConsent/Runtime/Script/Consent.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: b112ee07b3e149638d5130fc694d100a +timeCreated: 1669023933 \ No newline at end of file diff --git a/Runtime/GuruConsent/Runtime/Script/Consent/GuruConsent.cs b/Runtime/GuruConsent/Runtime/Script/Consent/GuruConsent.cs new file mode 100644 index 0000000..ee8e299 --- /dev/null +++ b/Runtime/GuruConsent/Runtime/Script/Consent/GuruConsent.cs @@ -0,0 +1,137 @@ +namespace Guru +{ + using System; + using System.Collections; + using Guru.LitJson; + using UnityEngine; + + /// + /// GuruConsent 流程封装 + /// + public class GuruConsent + { + // Guru Consent Version + public static string Version = "1.0.3"; + + + #region 公用接口 + + private static Action onCompleteHandler = null; + + private static IConsentAgent _agent; + private static IConsentAgent Agent + { + get + { + if (_agent == null) + { +#if UNITY_EDITOR + _agent = new ConsentAgentStub(); +#elif UNITY_ANDROID + _agent = new ConsentAgentAndroid(); +#elif UNITY_IOS + _agent = new ConsentAgentIOS(); +#endif + _agent?.Init(GuruSDKCallback.ObjectName, GuruSDKCallback.MethodName); + } + return _agent; + } + } + + /// + /// 对外公开接口 + /// + /// + /// + /// + public static void StartConsent(Action onComplete = null, string deviceId = "", int debugGeography = -1 ) + { + onCompleteHandler = onComplete; + // 初始化SDK对象 + GuruSDKCallback.AddCallback(OnSDKCallback); + if (debugGeography == -1) debugGeography = DebugGeography.DEBUG_GEOGRAPHY_EEA; + + Agent?.RequestGDPR(deviceId, debugGeography); + } + + + + /// + /// 获取SDK回调 + /// + /// + private static void OnSDKCallback(string msg) + { + Debug.Log($"[U3D] msg: \n{msg}"); + + var jo = JsonMapper.ToObject(msg); + if (jo != null && jo.ContainsKey("action") + && jo["action"].ToString() == "gdpr") + { + var json = jo["data"].ToJson(); + Debug.Log($"--- data json: {json}"); + var data = JsonMapper.ToObject(json); + if (data != null) + { + Debug.Log($"[GDPR] == status: {data.status} msg: {data.msg}"); + onCompleteHandler?.Invoke(data.status); + } + } + else + { + Debug.Log("[Unity] Parse callback Error"); + } + } + + #endregion + + #region 常量定义 + + + /// + /// GDPR 状态对象 + /// + [Serializable] + internal class ConsentStatus + { + public int status; + public string msg; + } + + + /// + /// Consent 状态 + /// + public static class StatusCode + { + public const int NOT_AVAILABLE = -100; + public const int NOT_REQUIRED = 1; + public const int OBTAINED = 3; + public const int REQUIRED = 2; + public const int UNKNOWN = 0; + } + + /// + /// DEBUG地理信息 + /// + public static class DebugGeography + { + public const int DEBUG_GEOGRAPHY_DISABLED = 0; + public const int DEBUG_GEOGRAPHY_EEA = 1; + public const int DEBUG_GEOGRAPHY_NOT_EEA = 2; + } + + #endregion + + + + } + + + + +} + + + + diff --git a/Runtime/GuruConsent/Runtime/Script/Consent/GuruConsent.cs.meta b/Runtime/GuruConsent/Runtime/Script/Consent/GuruConsent.cs.meta new file mode 100644 index 0000000..e3f2fda --- /dev/null +++ b/Runtime/GuruConsent/Runtime/Script/Consent/GuruConsent.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 453a591a559d44511bb58cbfd72785d9 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/GuruConsent/Runtime/Script/Consent/IConsentAgent.cs b/Runtime/GuruConsent/Runtime/Script/Consent/IConsentAgent.cs new file mode 100644 index 0000000..cceac3e --- /dev/null +++ b/Runtime/GuruConsent/Runtime/Script/Consent/IConsentAgent.cs @@ -0,0 +1,9 @@ +namespace Guru +{ + public interface IConsentAgent + { + void Init(string objectName, string callbackName); + + void RequestGDPR(string deviceId = "", int debugGeography = -1); + } +} \ No newline at end of file diff --git a/Runtime/GuruConsent/Runtime/Script/Consent/IConsentAgent.cs.meta b/Runtime/GuruConsent/Runtime/Script/Consent/IConsentAgent.cs.meta new file mode 100644 index 0000000..c1c1567 --- /dev/null +++ b/Runtime/GuruConsent/Runtime/Script/Consent/IConsentAgent.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: fc891ab92343476d9dc368d2b04cad6a +timeCreated: 1674894274 \ No newline at end of file diff --git a/Runtime/GuruConsent/Runtime/Script/Consent/Impl.meta b/Runtime/GuruConsent/Runtime/Script/Consent/Impl.meta new file mode 100644 index 0000000..1285215 --- /dev/null +++ b/Runtime/GuruConsent/Runtime/Script/Consent/Impl.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: a13c6ec7baee4ef493a0b38cc2f96f2e +timeCreated: 1674894257 \ No newline at end of file diff --git a/Runtime/GuruConsent/Runtime/Script/Consent/Impl/ConsentAgentAndroid.cs b/Runtime/GuruConsent/Runtime/Script/Consent/Impl/ConsentAgentAndroid.cs new file mode 100644 index 0000000..b211327 --- /dev/null +++ b/Runtime/GuruConsent/Runtime/Script/Consent/Impl/ConsentAgentAndroid.cs @@ -0,0 +1,40 @@ +namespace Guru +{ + + using UnityEngine; + + + public class ConsentAgentAndroid: IConsentAgent + { + + private static readonly string ConsentAndroidClassName = "com.guru.unity.consent.Consent"; + + + private string _objName; + private string _callbackName; + + public void Init(string objectName, string callbackName) + { + _objName = objectName; + _callbackName = callbackName; + } + + + public void RequestGDPR(string deviceId = "", int debugGeography = -1) + { +#if UNITY_ANDROID + + // 初始化Android SDK + using (var jc = new AndroidJavaClass("com.guru.unity.consent.Consent")) + { + // 绑定Unity回调物体 + jc.CallStatic("initSDK", _objName, _callbackName); + + // request gdpr + jc.CallStatic("requestGDPR", deviceId, debugGeography); + } +#endif + + } + } +} \ No newline at end of file diff --git a/Runtime/GuruConsent/Runtime/Script/Consent/Impl/ConsentAgentAndroid.cs.meta b/Runtime/GuruConsent/Runtime/Script/Consent/Impl/ConsentAgentAndroid.cs.meta new file mode 100644 index 0000000..47ed5f2 --- /dev/null +++ b/Runtime/GuruConsent/Runtime/Script/Consent/Impl/ConsentAgentAndroid.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 265406f7029e4fc8b10d79438cee3071 +timeCreated: 1674895574 \ No newline at end of file diff --git a/Runtime/GuruConsent/Runtime/Script/Consent/Impl/ConsentAgentIOS.cs b/Runtime/GuruConsent/Runtime/Script/Consent/Impl/ConsentAgentIOS.cs new file mode 100644 index 0000000..d64b057 --- /dev/null +++ b/Runtime/GuruConsent/Runtime/Script/Consent/Impl/ConsentAgentIOS.cs @@ -0,0 +1,45 @@ +namespace Guru +{ +#if UNITY_IOS + using System.Runtime.InteropServices; +#endif + + public class ConsentAgentIOS:IConsentAgent + { + private const string DLL_INTERNAL = "__Internal"; + +#if UNITY_IOS + [DllImport(DLL_INTERNAL)] + private static extern void unityRequestGDPR(string deviceId, int debugGeography); // IOS 调用接口 + + [DllImport(DLL_INTERNAL)] + private static extern void unityInitSDK(string gameobject, string callback); // IOS 调用接口 +#endif + + private string _objName; + private string _callbackName; + + public void Init(string objectName, string callbackName) + { + _objName = objectName; + _callbackName = callbackName; + } + + + /// + /// 调用请求 + /// + /// + /// + public void RequestGDPR(string deviceId = "", int debugGeography = -1) + { +#if UNITY_IOS + string goName = _objName; + string cbName = _callbackName; + // UnityEngine.Debug.Log($"[U3D] init SDK -> {goName}:{cbName}"); + unityInitSDK(goName, cbName); + unityRequestGDPR(deviceId, debugGeography); +#endif + } + } +} \ No newline at end of file diff --git a/Runtime/GuruConsent/Runtime/Script/Consent/Impl/ConsentAgentIOS.cs.meta b/Runtime/GuruConsent/Runtime/Script/Consent/Impl/ConsentAgentIOS.cs.meta new file mode 100644 index 0000000..33ea45d --- /dev/null +++ b/Runtime/GuruConsent/Runtime/Script/Consent/Impl/ConsentAgentIOS.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 31fa3d84baf8416297d574d172878a5f +timeCreated: 1674895870 \ No newline at end of file diff --git a/Runtime/GuruConsent/Runtime/Script/Consent/Impl/ConsentAgentStub.cs b/Runtime/GuruConsent/Runtime/Script/Consent/Impl/ConsentAgentStub.cs new file mode 100644 index 0000000..19b4289 --- /dev/null +++ b/Runtime/GuruConsent/Runtime/Script/Consent/Impl/ConsentAgentStub.cs @@ -0,0 +1,52 @@ + + +namespace Guru +{ + using UnityEngine; + + + public class ConsentAgentStub: IConsentAgent + { + + private static readonly string Tag = "[Stub]"; + public static int DebugStatusCode { get; set; } = GuruConsent.StatusCode.OBTAINED; + public static string DebugMessage { get; set; } = "You have already obtained the consent."; + + private static readonly string callbackMsgFmt = @"{""action"":""gdpr"", ""data"": {""status"":$0, ""msg"":""$1"" }}"; + + + + + #region 接口实现 + + private string _objName; + private string _callbackName; + public void Init(string objectName, string callbackName) + { + _objName = objectName; + _callbackName = callbackName; + } + + public void RequestGDPR(string deviceId = "", int debugGeography = -1) + { + Debug.Log($"{Tag} Consent Request -> deviceid: {deviceId} debugGeography: {debugGeography}"); + +#if UNITY_EDITOR + string msg = callbackMsgFmt.Replace("$0", $"{DebugStatusCode}").Replace("$1",DebugMessage); + var go = GameObject.Find(_objName); + if (go != null) + { + go.SendMessage(_callbackName, msg); + } + else + { + Debug.LogError($"{Tag} Can't find callback object"); + } +#endif + } + + #endregion + + + } +} \ No newline at end of file diff --git a/Runtime/GuruConsent/Runtime/Script/Consent/Impl/ConsentAgentStub.cs.meta b/Runtime/GuruConsent/Runtime/Script/Consent/Impl/ConsentAgentStub.cs.meta new file mode 100644 index 0000000..7797ee8 --- /dev/null +++ b/Runtime/GuruConsent/Runtime/Script/Consent/Impl/ConsentAgentStub.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: bca2a5de4dae4c6599160414d7808c75 +timeCreated: 1674894355 \ No newline at end of file diff --git a/Runtime/GuruConsent/~Sample.meta b/Runtime/GuruConsent/~Sample.meta new file mode 100644 index 0000000..a36bb8e --- /dev/null +++ b/Runtime/GuruConsent/~Sample.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: deec72831fee94496be7f47eda173e23 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/GuruConsent/~Sample/GuruConsetDemo.meta b/Runtime/GuruConsent/~Sample/GuruConsetDemo.meta new file mode 100644 index 0000000..8bdf248 --- /dev/null +++ b/Runtime/GuruConsent/~Sample/GuruConsetDemo.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: ee533990348c4615bb9ecc89f64fd194 +timeCreated: 1674188930 \ No newline at end of file diff --git a/Runtime/GuruConsent/~Sample/GuruConsetDemo/Editor.meta b/Runtime/GuruConsent/~Sample/GuruConsetDemo/Editor.meta new file mode 100644 index 0000000..97e00f9 --- /dev/null +++ b/Runtime/GuruConsent/~Sample/GuruConsetDemo/Editor.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 844e37009ce0414aa9e7144032f09167 +timeCreated: 1669195476 \ No newline at end of file diff --git a/Runtime/GuruConsent/~Sample/GuruConsetDemo/Editor/IOSPostBuild_Demo.cs b/Runtime/GuruConsent/~Sample/GuruConsetDemo/Editor/IOSPostBuild_Demo.cs new file mode 100644 index 0000000..1e2c2f1 --- /dev/null +++ b/Runtime/GuruConsent/~Sample/GuruConsetDemo/Editor/IOSPostBuild_Demo.cs @@ -0,0 +1,55 @@ +#if UNITY_IOS +using System.Diagnostics; +using System.IO; +using UnityEditor; +using UnityEditor.Callbacks; +using Debug = UnityEngine.Debug; +using UnityEditor.iOS.Xcode; + +public class IOSPostBuild_Demo +{ + // ----- 如果需要构建 DEMO, 可使用此管线注入预置的项目ID以测试数据 + // 正式项目内请注释掉该行 + // [PostProcessBuild(40)] + public static void OnPostProcessBuild(BuildTarget target, string buildPath) + { + if(target != BuildTarget.iOS) return; + + FixInfoPlist(buildPath); + } + + /// + /// Update info.plist + /// + /// + private static void FixInfoPlist(string buildPath) + { + string filePath = $"{buildPath}/Info.plist"; + if (File.Exists(filePath)) + { + PlistDocument doc = new PlistDocument(); + doc.ReadFromFile(filePath); + var dict = doc.root; + string key = "GADApplicationIdentifier"; + if (!dict.values.ContainsKey(key)) + { + dict.SetString(key, "ca-app-pub-2436733915645843~7788635385"); + } + + key = "NSUserTrackingUsageDescription"; + if (!dict.values.ContainsKey(key)) + { + dict.SetString(key, + "By allowing tracking, we'll be able to better tailor ads served to you on this game."); + } + + doc.WriteToFile(filePath); + Debug.Log($"--- Info.plist has updated ---"); + } + else + { + Debug.LogError($"Can't find Info.plist at {filePath}"); + } + } +} +#endif diff --git a/Runtime/GuruConsent/~Sample/GuruConsetDemo/Editor/IOSPostBuild_Demo.cs.meta b/Runtime/GuruConsent/~Sample/GuruConsetDemo/Editor/IOSPostBuild_Demo.cs.meta new file mode 100644 index 0000000..f663717 --- /dev/null +++ b/Runtime/GuruConsent/~Sample/GuruConsetDemo/Editor/IOSPostBuild_Demo.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 407921d5ba494e0a959f499b3a933d11 +timeCreated: 1669195496 \ No newline at end of file diff --git a/Runtime/GuruConsent/~Sample/GuruConsetDemo/GuruConsetDemo.cs b/Runtime/GuruConsent/~Sample/GuruConsetDemo/GuruConsetDemo.cs new file mode 100644 index 0000000..2ce9e86 --- /dev/null +++ b/Runtime/GuruConsent/~Sample/GuruConsetDemo/GuruConsetDemo.cs @@ -0,0 +1,46 @@ +using System.Collections; +using System.Collections.Generic; +using UnityEngine; +using UnityEngine.UI; +using Guru; + +/// +/// Consent流程演示 +/// +public class GuruConsetDemo : MonoBehaviour +{ + public Button _btnRequest; + public Text _txtInfo; + public InputField _inputBox; + + + // Start is called before the first frame update + void Start() + { + _btnRequest.onClick.AddListener(OnClickRequest); + } + + /// + /// 点击请求 + /// + void OnClickRequest() + { + // 无需回调的话可直接调用 + // GuruConsent.StartConsent(); + + var deviceId = _inputBox.text; + GuruConsent.StartConsent(OnGetConsentStatus, deviceId); + } + + + /// + /// 获取到 ConsentStatus + /// + /// + private void OnGetConsentStatus(int status) + { + string msg = $"--- [Unity] Get Status: {status}"; + Debug.Log(msg); + _txtInfo.text = msg; + } +} diff --git a/Runtime/GuruConsent/~Sample/GuruConsetDemo/GuruConsetDemo.cs.meta b/Runtime/GuruConsent/~Sample/GuruConsetDemo/GuruConsetDemo.cs.meta new file mode 100644 index 0000000..1b6bc5a --- /dev/null +++ b/Runtime/GuruConsent/~Sample/GuruConsetDemo/GuruConsetDemo.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 06a81548f6a3e4450b2b4edcc0889b9a +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/GuruConsent/~Sample/GuruConsetDemo/GuruConsetDemo.unity b/Runtime/GuruConsent/~Sample/GuruConsetDemo/GuruConsetDemo.unity new file mode 100644 index 0000000..d028887 --- /dev/null +++ b/Runtime/GuruConsent/~Sample/GuruConsetDemo/GuruConsetDemo.unity @@ -0,0 +1,1040 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!29 &1 +OcclusionCullingSettings: + m_ObjectHideFlags: 0 + serializedVersion: 2 + m_OcclusionBakeSettings: + smallestOccluder: 5 + smallestHole: 0.25 + backfaceThreshold: 100 + m_SceneGUID: 00000000000000000000000000000000 + m_OcclusionCullingData: {fileID: 0} +--- !u!104 &2 +RenderSettings: + m_ObjectHideFlags: 0 + serializedVersion: 9 + m_Fog: 0 + m_FogColor: {r: 0.5, g: 0.5, b: 0.5, a: 1} + m_FogMode: 3 + m_FogDensity: 0.01 + m_LinearFogStart: 0 + m_LinearFogEnd: 300 + m_AmbientSkyColor: {r: 0.212, g: 0.227, b: 0.259, a: 1} + m_AmbientEquatorColor: {r: 0.114, g: 0.125, b: 0.133, a: 1} + m_AmbientGroundColor: {r: 0.047, g: 0.043, b: 0.035, a: 1} + m_AmbientIntensity: 1 + m_AmbientMode: 0 + m_SubtractiveShadowColor: {r: 0.42, g: 0.478, b: 0.627, a: 1} + m_SkyboxMaterial: {fileID: 10304, guid: 0000000000000000f000000000000000, type: 0} + m_HaloStrength: 0.5 + m_FlareStrength: 1 + m_FlareFadeSpeed: 3 + m_HaloTexture: {fileID: 0} + m_SpotCookie: {fileID: 10001, guid: 0000000000000000e000000000000000, type: 0} + m_DefaultReflectionMode: 0 + m_DefaultReflectionResolution: 128 + m_ReflectionBounces: 1 + m_ReflectionIntensity: 1 + m_CustomReflection: {fileID: 0} + m_Sun: {fileID: 0} + m_IndirectSpecularColor: {r: 0.3731193, g: 0.38073996, b: 0.35872698, a: 1} + m_UseRadianceAmbientProbe: 0 +--- !u!157 &3 +LightmapSettings: + m_ObjectHideFlags: 0 + serializedVersion: 12 + m_GIWorkflowMode: 1 + m_GISettings: + serializedVersion: 2 + m_BounceScale: 1 + m_IndirectOutputScale: 1 + m_AlbedoBoost: 1 + m_EnvironmentLightingMode: 0 + m_EnableBakedLightmaps: 1 + m_EnableRealtimeLightmaps: 0 + m_LightmapEditorSettings: + serializedVersion: 12 + m_Resolution: 2 + m_BakeResolution: 40 + m_AtlasSize: 1024 + m_AO: 0 + m_AOMaxDistance: 1 + m_CompAOExponent: 1 + m_CompAOExponentDirect: 0 + m_ExtractAmbientOcclusion: 0 + m_Padding: 2 + m_LightmapParameters: {fileID: 0} + m_LightmapsBakeMode: 1 + m_TextureCompression: 1 + m_FinalGather: 0 + m_FinalGatherFiltering: 1 + m_FinalGatherRayCount: 256 + m_ReflectionCompression: 2 + m_MixedBakeMode: 2 + m_BakeBackend: 1 + m_PVRSampling: 1 + m_PVRDirectSampleCount: 32 + m_PVRSampleCount: 512 + m_PVRBounces: 2 + m_PVREnvironmentSampleCount: 256 + m_PVREnvironmentReferencePointCount: 2048 + m_PVRFilteringMode: 1 + m_PVRDenoiserTypeDirect: 1 + m_PVRDenoiserTypeIndirect: 1 + m_PVRDenoiserTypeAO: 1 + m_PVRFilterTypeDirect: 0 + m_PVRFilterTypeIndirect: 0 + m_PVRFilterTypeAO: 0 + m_PVREnvironmentMIS: 1 + m_PVRCulling: 1 + m_PVRFilteringGaussRadiusDirect: 1 + m_PVRFilteringGaussRadiusIndirect: 5 + m_PVRFilteringGaussRadiusAO: 2 + m_PVRFilteringAtrousPositionSigmaDirect: 0.5 + m_PVRFilteringAtrousPositionSigmaIndirect: 2 + m_PVRFilteringAtrousPositionSigmaAO: 1 + m_ExportTrainingData: 0 + m_TrainingDataDestination: TrainingData + m_LightProbeSampleCountMultiplier: 4 + m_LightingDataAsset: {fileID: 0} + m_LightingSettings: {fileID: 0} +--- !u!196 &4 +NavMeshSettings: + serializedVersion: 2 + m_ObjectHideFlags: 0 + m_BuildSettings: + serializedVersion: 2 + agentTypeID: 0 + agentRadius: 0.5 + agentHeight: 2 + agentSlope: 45 + agentClimb: 0.4 + ledgeDropHeight: 0 + maxJumpAcrossDistance: 0 + minRegionArea: 2 + manualCellSize: 0 + cellSize: 0.16666667 + manualTileSize: 0 + tileSize: 256 + accuratePlacement: 0 + maxJobWorkers: 0 + preserveTilesOutsideBounds: 0 + debug: + m_Flags: 0 + m_NavMeshData: {fileID: 0} +--- !u!1 &2240829 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 2240830} + - component: {fileID: 2240832} + - component: {fileID: 2240831} + - component: {fileID: 2240833} + m_Layer: 5 + m_Name: btn_request + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &2240830 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 2240829} + m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_Children: + - {fileID: 2060236570} + m_Father: {fileID: 1965763133} + m_RootOrder: 2 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0.5, y: 0.5} + m_AnchorMax: {x: 0.5, y: 0.5} + m_AnchoredPosition: {x: 0, y: 257} + m_SizeDelta: {x: 1000, y: 150} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!114 &2240831 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 2240829} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 1, g: 0.5509934, b: 0, a: 1} + m_RaycastTarget: 1 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_Sprite: {fileID: 10905, guid: 0000000000000000f000000000000000, type: 0} + m_Type: 1 + m_PreserveAspect: 0 + m_FillCenter: 1 + m_FillMethod: 4 + m_FillAmount: 1 + m_FillClockwise: 1 + m_FillOrigin: 0 + m_UseSpriteMesh: 0 + m_PixelsPerUnitMultiplier: 1 +--- !u!222 &2240832 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 2240829} + m_CullTransparentMesh: 1 +--- !u!114 &2240833 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 2240829} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 4e29b1a8efbd4b44bb3f3716e73f07ff, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Navigation: + m_Mode: 3 + m_WrapAround: 0 + m_SelectOnUp: {fileID: 0} + m_SelectOnDown: {fileID: 0} + m_SelectOnLeft: {fileID: 0} + m_SelectOnRight: {fileID: 0} + m_Transition: 1 + m_Colors: + m_NormalColor: {r: 1, g: 1, b: 1, a: 1} + m_HighlightedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1} + m_PressedColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 1} + m_SelectedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1} + m_DisabledColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 0.5019608} + m_ColorMultiplier: 1 + m_FadeDuration: 0.1 + m_SpriteState: + m_HighlightedSprite: {fileID: 0} + m_PressedSprite: {fileID: 0} + m_SelectedSprite: {fileID: 0} + m_DisabledSprite: {fileID: 0} + m_AnimationTriggers: + m_NormalTrigger: Normal + m_HighlightedTrigger: Highlighted + m_PressedTrigger: Pressed + m_SelectedTrigger: Selected + m_DisabledTrigger: Disabled + m_Interactable: 1 + m_TargetGraphic: {fileID: 2240831} + m_OnClick: + m_PersistentCalls: + m_Calls: [] +--- !u!1 &149289275 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 149289276} + - component: {fileID: 149289279} + - component: {fileID: 149289278} + - component: {fileID: 149289277} + m_Layer: 5 + m_Name: Input_id + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &149289276 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 149289275} + m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_Children: + - {fileID: 1701054717} + - {fileID: 1056772437} + m_Father: {fileID: 1965763133} + m_RootOrder: 1 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0.5, y: 0.5} + m_AnchorMax: {x: 0.5, y: 0.5} + m_AnchoredPosition: {x: 0, y: 439} + m_SizeDelta: {x: 1000, y: 140} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!114 &149289277 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 149289275} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: d199490a83bb2b844b9695cbf13b01ef, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Navigation: + m_Mode: 3 + m_WrapAround: 0 + m_SelectOnUp: {fileID: 0} + m_SelectOnDown: {fileID: 0} + m_SelectOnLeft: {fileID: 0} + m_SelectOnRight: {fileID: 0} + m_Transition: 1 + m_Colors: + m_NormalColor: {r: 1, g: 1, b: 1, a: 1} + m_HighlightedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1} + m_PressedColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 1} + m_SelectedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1} + m_DisabledColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 0.5019608} + m_ColorMultiplier: 1 + m_FadeDuration: 0.1 + m_SpriteState: + m_HighlightedSprite: {fileID: 0} + m_PressedSprite: {fileID: 0} + m_SelectedSprite: {fileID: 0} + m_DisabledSprite: {fileID: 0} + m_AnimationTriggers: + m_NormalTrigger: Normal + m_HighlightedTrigger: Highlighted + m_PressedTrigger: Pressed + m_SelectedTrigger: Selected + m_DisabledTrigger: Disabled + m_Interactable: 1 + m_TargetGraphic: {fileID: 149289278} + m_TextComponent: {fileID: 1056772438} + m_Placeholder: {fileID: 1701054718} + m_ContentType: 0 + m_InputType: 0 + m_AsteriskChar: 42 + m_KeyboardType: 0 + m_LineType: 0 + m_HideMobileInput: 0 + m_CharacterValidation: 0 + m_CharacterLimit: 0 + m_OnEndEdit: + m_PersistentCalls: + m_Calls: [] + m_OnValueChanged: + m_PersistentCalls: + m_Calls: [] + m_CaretColor: {r: 0.19607843, g: 0.19607843, b: 0.19607843, a: 1} + m_CustomCaretColor: 0 + m_SelectionColor: {r: 0.65882355, g: 0.80784315, b: 1, a: 0.7529412} + m_Text: + m_CaretBlinkRate: 0.85 + m_CaretWidth: 1 + m_ReadOnly: 0 + m_ShouldActivateOnSelect: 1 +--- !u!114 &149289278 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 149289275} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 1} + m_RaycastTarget: 1 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_Sprite: {fileID: 10911, guid: 0000000000000000f000000000000000, type: 0} + m_Type: 1 + m_PreserveAspect: 0 + m_FillCenter: 1 + m_FillMethod: 4 + m_FillAmount: 1 + m_FillClockwise: 1 + m_FillOrigin: 0 + m_UseSpriteMesh: 0 + m_PixelsPerUnitMultiplier: 1 +--- !u!222 &149289279 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 149289275} + m_CullTransparentMesh: 1 +--- !u!1 &532655501 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 532655505} + - component: {fileID: 532655504} + - component: {fileID: 532655503} + - component: {fileID: 532655502} + m_Layer: 5 + m_Name: UIRoot + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!114 &532655502 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 532655501} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: dc42784cf147c0c48a680349fa168899, type: 3} + m_Name: + m_EditorClassIdentifier: + m_IgnoreReversedGraphics: 1 + m_BlockingObjects: 0 + m_BlockingMask: + serializedVersion: 2 + m_Bits: 4294967295 +--- !u!114 &532655503 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 532655501} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 0cd44c1031e13a943bb63640046fad76, type: 3} + m_Name: + m_EditorClassIdentifier: + m_UiScaleMode: 0 + m_ReferencePixelsPerUnit: 100 + m_ScaleFactor: 1 + m_ReferenceResolution: {x: 800, y: 600} + m_ScreenMatchMode: 0 + m_MatchWidthOrHeight: 0 + m_PhysicalUnit: 3 + m_FallbackScreenDPI: 96 + m_DefaultSpriteDPI: 96 + m_DynamicPixelsPerUnit: 1 + m_PresetInfoIsWorld: 0 +--- !u!223 &532655504 +Canvas: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 532655501} + m_Enabled: 1 + serializedVersion: 3 + m_RenderMode: 1 + m_Camera: {fileID: 2029049860} + m_PlaneDistance: 100 + m_PixelPerfect: 0 + m_ReceivesEvents: 1 + m_OverrideSorting: 0 + m_OverridePixelPerfect: 0 + m_SortingBucketNormalizedSize: 0 + m_AdditionalShaderChannelsFlag: 0 + m_SortingLayerID: 0 + m_SortingOrder: 0 + m_TargetDisplay: 0 +--- !u!224 &532655505 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 532655501} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 0, y: 0, z: 0} + m_Children: + - {fileID: 913349416} + - {fileID: 2029049861} + - {fileID: 1965763133} + m_Father: {fileID: 0} + m_RootOrder: 0 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 0, y: 0} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 0} + m_Pivot: {x: 0, y: 0} +--- !u!1 &582936093 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 582936094} + - component: {fileID: 582936096} + - component: {fileID: 582936095} + m_Layer: 5 + m_Name: txt_info + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &582936094 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 582936093} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_Children: [] + m_Father: {fileID: 1965763133} + m_RootOrder: 0 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0.5} + m_AnchorMax: {x: 1, y: 0.5} + m_AnchoredPosition: {x: 0, y: 621} + m_SizeDelta: {x: -100, y: 200} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!114 &582936095 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 582936093} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 5f7201a12d95ffc409449d95f23cf332, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 1} + m_RaycastTarget: 1 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_FontData: + m_Font: {fileID: 10102, guid: 0000000000000000e000000000000000, type: 0} + m_FontSize: 40 + m_FontStyle: 0 + m_BestFit: 1 + m_MinSize: 0 + m_MaxSize: 50 + m_Alignment: 0 + m_AlignByGeometry: 0 + m_RichText: 1 + m_HorizontalOverflow: 0 + m_VerticalOverflow: 0 + m_LineSpacing: 1 + m_Text: device info +--- !u!222 &582936096 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 582936093} + m_CullTransparentMesh: 1 +--- !u!1 &913349415 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 913349416} + - component: {fileID: 913349418} + - component: {fileID: 913349417} + m_Layer: 0 + m_Name: EventSystem + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!4 &913349416 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 913349415} + m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} + m_LocalPosition: {x: -540, y: -1080, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_Children: [] + m_Father: {fileID: 532655505} + m_RootOrder: 0 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!114 &913349417 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 913349415} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 4f231c4fb786f3946a6b90b886c48677, type: 3} + m_Name: + m_EditorClassIdentifier: + m_HorizontalAxis: Horizontal + m_VerticalAxis: Vertical + m_SubmitButton: Submit + m_CancelButton: Cancel + m_InputActionsPerSecond: 10 + m_RepeatDelay: 0.5 + m_ForceModuleActive: 0 +--- !u!114 &913349418 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 913349415} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 76c392e42b5098c458856cdf6ecaaaa1, type: 3} + m_Name: + m_EditorClassIdentifier: + m_FirstSelected: {fileID: 0} + m_sendNavigationEvents: 1 + m_DragThreshold: 10 +--- !u!1 &1056772436 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1056772437} + - component: {fileID: 1056772439} + - component: {fileID: 1056772438} + m_Layer: 5 + m_Name: Text + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &1056772437 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1056772436} + m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_Children: [] + m_Father: {fileID: 149289276} + m_RootOrder: 1 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 1, y: 1} + m_AnchoredPosition: {x: 0, y: -0.5} + m_SizeDelta: {x: -20, y: -13} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!114 &1056772438 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1056772436} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 5f7201a12d95ffc409449d95f23cf332, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 0.19607843, g: 0.19607843, b: 0.19607843, a: 1} + m_RaycastTarget: 1 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_FontData: + m_Font: {fileID: 10102, guid: 0000000000000000e000000000000000, type: 0} + m_FontSize: 32 + m_FontStyle: 0 + m_BestFit: 0 + m_MinSize: 3 + m_MaxSize: 40 + m_Alignment: 0 + m_AlignByGeometry: 0 + m_RichText: 0 + m_HorizontalOverflow: 1 + m_VerticalOverflow: 0 + m_LineSpacing: 1 + m_Text: +--- !u!222 &1056772439 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1056772436} + m_CullTransparentMesh: 1 +--- !u!1 &1070080682 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1070080684} + - component: {fileID: 1070080683} + m_Layer: 0 + m_Name: DemoMain + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!114 &1070080683 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1070080682} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 06a81548f6a3e4450b2b4edcc0889b9a, type: 3} + m_Name: + m_EditorClassIdentifier: + _btnRequest: {fileID: 2240833} + _txtInfo: {fileID: 582936095} + _inputBox: {fileID: 149289277} +--- !u!4 &1070080684 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1070080682} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_Children: [] + m_Father: {fileID: 0} + m_RootOrder: 1 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!1 &1701054716 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1701054717} + - component: {fileID: 1701054719} + - component: {fileID: 1701054718} + m_Layer: 5 + m_Name: Placeholder + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &1701054717 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1701054716} + m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_Children: [] + m_Father: {fileID: 149289276} + m_RootOrder: 0 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 1, y: 1} + m_AnchoredPosition: {x: 0, y: -0.5} + m_SizeDelta: {x: -20, y: -13} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!114 &1701054718 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1701054716} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 5f7201a12d95ffc409449d95f23cf332, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 0.19607843, g: 0.19607843, b: 0.19607843, a: 0.5} + m_RaycastTarget: 1 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_FontData: + m_Font: {fileID: 10102, guid: 0000000000000000e000000000000000, type: 0} + m_FontSize: 32 + m_FontStyle: 2 + m_BestFit: 0 + m_MinSize: 3 + m_MaxSize: 40 + m_Alignment: 0 + m_AlignByGeometry: 0 + m_RichText: 1 + m_HorizontalOverflow: 0 + m_VerticalOverflow: 0 + m_LineSpacing: 1 + m_Text: Enter text... +--- !u!222 &1701054719 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1701054716} + m_CullTransparentMesh: 1 +--- !u!1 &1965763132 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1965763133} + m_Layer: 5 + m_Name: root + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &1965763133 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1965763132} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_Children: + - {fileID: 582936094} + - {fileID: 149289276} + - {fileID: 2240830} + m_Father: {fileID: 532655505} + m_RootOrder: 2 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 1, y: 1} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 0} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!1 &2029049858 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 2029049861} + - component: {fileID: 2029049860} + - component: {fileID: 2029049859} + m_Layer: 0 + m_Name: UICamera + m_TagString: MainCamera + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!81 &2029049859 +AudioListener: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 2029049858} + m_Enabled: 1 +--- !u!20 &2029049860 +Camera: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 2029049858} + m_Enabled: 1 + serializedVersion: 2 + m_ClearFlags: 2 + m_BackGroundColor: {r: 0.1682983, g: 0.46553603, b: 0.5849056, a: 0} + m_projectionMatrixMode: 1 + m_GateFitMode: 2 + m_FOVAxisMode: 0 + m_SensorSize: {x: 36, y: 24} + m_LensShift: {x: 0, y: 0} + m_FocalLength: 50 + m_NormalizedViewPortRect: + serializedVersion: 2 + x: 0 + y: 0 + width: 1 + height: 1 + near clip plane: -100 + far clip plane: 1000 + field of view: 60 + orthographic: 1 + orthographic size: 5 + m_Depth: -1 + m_CullingMask: + serializedVersion: 2 + m_Bits: 4294967295 + m_RenderingPath: -1 + m_TargetTexture: {fileID: 0} + m_TargetDisplay: 0 + m_TargetEye: 3 + m_HDR: 1 + m_AllowMSAA: 1 + m_AllowDynamicResolution: 0 + m_ForceIntoRT: 0 + m_OcclusionCulling: 1 + m_StereoConvergence: 10 + m_StereoSeparation: 0.022 +--- !u!4 &2029049861 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 2029049858} + m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_Children: [] + m_Father: {fileID: 532655505} + m_RootOrder: 1 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!1 &2060236569 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 2060236570} + - component: {fileID: 2060236572} + - component: {fileID: 2060236571} + m_Layer: 5 + m_Name: txt_label + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &2060236570 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 2060236569} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_Children: [] + m_Father: {fileID: 2240830} + m_RootOrder: 0 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 1, y: 1} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: -20} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!114 &2060236571 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 2060236569} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 5f7201a12d95ffc409449d95f23cf332, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 0.3584906, g: 0.17579864, b: 0.021982923, a: 1} + m_RaycastTarget: 1 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_FontData: + m_Font: {fileID: 10102, guid: 0000000000000000e000000000000000, type: 0} + m_FontSize: 50 + m_FontStyle: 0 + m_BestFit: 0 + m_MinSize: 5 + m_MaxSize: 50 + m_Alignment: 4 + m_AlignByGeometry: 0 + m_RichText: 1 + m_HorizontalOverflow: 0 + m_VerticalOverflow: 0 + m_LineSpacing: 1 + m_Text: Request Consent +--- !u!222 &2060236572 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 2060236569} + m_CullTransparentMesh: 1 diff --git a/Runtime/GuruConsent/~Sample/GuruConsetDemo/GuruConsetDemo.unity.meta b/Runtime/GuruConsent/~Sample/GuruConsetDemo/GuruConsetDemo.unity.meta new file mode 100644 index 0000000..4d2ef4e --- /dev/null +++ b/Runtime/GuruConsent/~Sample/GuruConsetDemo/GuruConsetDemo.unity.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 641a71344bde64589b985d6cc023992a +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/GuruCore.meta b/Runtime/GuruCore.meta new file mode 100644 index 0000000..fc52fe7 --- /dev/null +++ b/Runtime/GuruCore.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: ae40c504410d45429eafc962c7e3639f +timeCreated: 1671010714 \ No newline at end of file diff --git a/Runtime/GuruCore/Editor.meta b/Runtime/GuruCore/Editor.meta new file mode 100644 index 0000000..d010e15 --- /dev/null +++ b/Runtime/GuruCore/Editor.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: c52eab15b6939490f95643859f5d5a7a +timeCreated: 1671010724 \ No newline at end of file diff --git a/Runtime/GuruCore/Editor/Dependencies.xml b/Runtime/GuruCore/Editor/Dependencies.xml new file mode 100644 index 0000000..34cc1f4 --- /dev/null +++ b/Runtime/GuruCore/Editor/Dependencies.xml @@ -0,0 +1,13 @@ + + + + + + + + + + diff --git a/Runtime/GuruCore/Editor/Dependencies.xml.meta b/Runtime/GuruCore/Editor/Dependencies.xml.meta new file mode 100644 index 0000000..418e0fe --- /dev/null +++ b/Runtime/GuruCore/Editor/Dependencies.xml.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: e8740034beb344ae198e4aa3ab9e0f79 +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/GuruCore/Editor/Proguards.txt b/Runtime/GuruCore/Editor/Proguards.txt new file mode 100644 index 0000000..e277f50 --- /dev/null +++ b/Runtime/GuruCore/Editor/Proguards.txt @@ -0,0 +1,2 @@ +-keep class com.guru.** { *; } +-keep class guru.core.** { *; } \ No newline at end of file diff --git a/Runtime/GuruCore/Editor/Proguards.txt.meta b/Runtime/GuruCore/Editor/Proguards.txt.meta new file mode 100644 index 0000000..0150445 --- /dev/null +++ b/Runtime/GuruCore/Editor/Proguards.txt.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 27c796d0ec00441da10d01e1358194d3 +timeCreated: 1687241187 \ No newline at end of file diff --git a/Runtime/GuruCore/Runtime.meta b/Runtime/GuruCore/Runtime.meta new file mode 100644 index 0000000..754530e --- /dev/null +++ b/Runtime/GuruCore/Runtime.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 7b10b8f1ae1984b70a656b34b98768c6 +timeCreated: 1671499352 \ No newline at end of file diff --git a/Runtime/GuruCore/Runtime/Adjust.meta b/Runtime/GuruCore/Runtime/Adjust.meta new file mode 100644 index 0000000..578fd0d --- /dev/null +++ b/Runtime/GuruCore/Runtime/Adjust.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 54500140ff6e4555bda08d0b70948adf +timeCreated: 1632464325 \ No newline at end of file diff --git a/Runtime/GuruCore/Runtime/Adjust/AdjustService.cs b/Runtime/GuruCore/Runtime/Adjust/AdjustService.cs new file mode 100644 index 0000000..e480a4a --- /dev/null +++ b/Runtime/GuruCore/Runtime/Adjust/AdjustService.cs @@ -0,0 +1,368 @@ +namespace Guru +{ + using UnityEngine; + using com.adjust.sdk; + using System.Threading.Tasks; + + + public static class AdjustService + { + public const string Version = "1.5.0"; + public static readonly string LOG_TAG = "Adjust"; + public static readonly float DelayTime = 1f; // 延迟启动时间 + + #region 启动服务 + + public static void StartService() + { + string appToken = GuruSettings.Instance.AdjustSetting.GetAppToken(); + if (string.IsNullOrEmpty(appToken)) + { + Log.E(LOG_TAG, "Adjust没有设置token,无法进行初始化"); + return; + } + + InstallEvent(IPMConfig.FIREBASE_ID, IPMConfig.IPM_DEVICE_ID); // 注入启动参数 + + AdjustEnvironment environment = GetAdjustEnvironment(); + AdjustConfig config = new AdjustConfig(appToken, environment); + config.setLogLevel(GetAdjustLogLevel()); + config.setDelayStart(DelayTime); + +#if UNITY_EDITOR || DEBUG + config.setLogDelegate(log => Log.I(LOG_TAG, log)); + config.setEventSuccessDelegate(OnEventSuccessCallback); + config.setEventFailureDelegate(OnEventFailureCallback); + config.setSessionSuccessDelegate(OnSessionSuccessCallback); + config.setSessionFailureDelegate(OnSessionFailureCallback); + config.setAttributionChangedDelegate(OnAttributionChangedCallback); +#endif + // 检查场景实例 + SetupInstance(); + Adjust.start(config); + + // 缓存标准属性 + StandardProperties.AdjustId = Adjust.getAdid(); // 获取AdjustID + Adjust.getGoogleAdId(gid => + { + if (!string.IsNullOrEmpty(gid)) StandardProperties.GoogleAdId = gid; // 获取Google AD ID + }); + } + + /// + /// 确保 Adjust 实例在场景中 + /// + private static void SetupInstance() + { + var go = UnityEngine.GameObject.Find(nameof(Adjust)); + if (go == null) + { + go = new GameObject(nameof(Adjust)); + var ins = go.AddComponent(); + ins.startManually = true; + ins.launchDeferredDeeplink = true; + ins.sendInBackground = true; + } + } + + + #endregion + + #region 关键属性上报 + + /// + /// 安装事件上报 + /// 该事件只有第一次上报有效 + /// pseudoId 为空时不上报 + /// deviceId 为空时也不上报 (一般不会为空) + /// + /// + /// + public static void InstallEvent(string pseudoId, string deviceId) + { + if (string.IsNullOrEmpty(pseudoId)) + { + Debug.LogWarning($">> Pseudo ID is Empty, skip install event reporting"); + return; + } + + if (string.IsNullOrEmpty(deviceId)) + { + Debug.LogWarning($">> Device ID is Empty, skip install event reporting"); + return; + } + Adjust.addSessionCallbackParameter("user_pseudo_id", pseudoId); + Adjust.addSessionCallbackParameter("device_id", deviceId); + } + + + #endregion + + #region 事件回调函数 + + private static void OnAttributionChangedCallback(AdjustAttribution attributionData) + { + Log.I(LOG_TAG, "Attribution changed!"); + + if (attributionData.trackerName != null) + { + Log.I(LOG_TAG, "Tracker name: " + attributionData.trackerName); + } + + if (attributionData.trackerToken != null) + { + Log.I(LOG_TAG, "Tracker token: " + attributionData.trackerToken); + } + + if (attributionData.network != null) + { + Log.I(LOG_TAG, "Network: " + attributionData.network); + } + + if (attributionData.campaign != null) + { + Log.I(LOG_TAG, "Campaign: " + attributionData.campaign); + } + + if (attributionData.adgroup != null) + { + Log.I(LOG_TAG, "Adgroup: " + attributionData.adgroup); + } + + if (attributionData.creative != null) + { + Log.I(LOG_TAG, "Creative: " + attributionData.creative); + } + + if (attributionData.clickLabel != null) + { + Log.I(LOG_TAG , "Click label: " + attributionData.clickLabel); + } + + if (attributionData.adid != null) + { + Log.I(LOG_TAG, "ADID: " + attributionData.adid); + } + } + + private static void OnEventSuccessCallback(AdjustEventSuccess eventSuccessData) + { + Log.I(LOG_TAG, "Event tracked successfully!"); + + if (eventSuccessData.Message != null) + { + Log.I(LOG_TAG, "Message: " + eventSuccessData.Message); + } + + if (eventSuccessData.Timestamp != null) + { + Log.I(LOG_TAG, "Timestamp: " + eventSuccessData.Timestamp); + } + + if (eventSuccessData.Adid != null) + { + Log.I(LOG_TAG, "Adid: " + eventSuccessData.Adid); + } + + if (eventSuccessData.EventToken != null) + { + Log.I(LOG_TAG, "EventToken: " + eventSuccessData.EventToken); + } + + if (eventSuccessData.CallbackId != null) + { + Log.I(LOG_TAG, "CallbackId: " + eventSuccessData.CallbackId); + } + + if (eventSuccessData.JsonResponse != null) + { + Log.I(LOG_TAG, "JsonResponse: " + eventSuccessData.GetJsonResponse()); + } + } + + private static void OnEventFailureCallback(AdjustEventFailure eventFailureData) + { + Log.I(LOG_TAG, "Event tracking failed!"); + + if (eventFailureData.Message != null) + { + Log.I(LOG_TAG, "Message: " + eventFailureData.Message); + } + + if (eventFailureData.Timestamp != null) + { + Log.I(LOG_TAG, "Timestamp: " + eventFailureData.Timestamp); + } + + if (eventFailureData.Adid != null) + { + Log.I(LOG_TAG, "Adid: " + eventFailureData.Adid); + } + + if (eventFailureData.EventToken != null) + { + Log.I(LOG_TAG, "EventToken: " + eventFailureData.EventToken); + } + + if (eventFailureData.CallbackId != null) + { + Log.I(LOG_TAG, "CallbackId: " + eventFailureData.CallbackId); + } + + if (eventFailureData.JsonResponse != null) + { + Log.I(LOG_TAG, "JsonResponse: " + eventFailureData.GetJsonResponse()); + } + + Log.I(LOG_TAG, "WillRetry: " + eventFailureData.WillRetry.ToString()); + } + + private static void OnSessionSuccessCallback(AdjustSessionSuccess sessionSuccessData) + { + Log.I(LOG_TAG,"Session tracked successfully!"); + + if (sessionSuccessData.Message != null) + { + Log.I(LOG_TAG,"Message: " + sessionSuccessData.Message); + } + + if (sessionSuccessData.Timestamp != null) + { + Log.I(LOG_TAG,"Timestamp: " + sessionSuccessData.Timestamp); + } + + if (sessionSuccessData.Adid != null) + { + Log.I(LOG_TAG, "Adid: " + sessionSuccessData.Adid); + } + + if (sessionSuccessData.JsonResponse != null) + { + Log.I(LOG_TAG, "JsonResponse: " + sessionSuccessData.GetJsonResponse()); + } + } + + private static void OnSessionFailureCallback(AdjustSessionFailure sessionFailureData) + { + Log.I(LOG_TAG,"Session tracking failed!"); + + if (sessionFailureData.Message != null) + { + Log.I(LOG_TAG,"Message: " + sessionFailureData.Message); + } + + if (sessionFailureData.Timestamp != null) + { + Log.I(LOG_TAG,"Timestamp: " + sessionFailureData.Timestamp); + } + + if (sessionFailureData.Adid != null) + { + Log.I(LOG_TAG,"Adid: " + sessionFailureData.Adid); + } + + if (sessionFailureData.JsonResponse != null) + { + Log.I(LOG_TAG,"JsonResponse: " + sessionFailureData.GetJsonResponse()); + } + + Log.I(LOG_TAG,"WillRetry: " + sessionFailureData.WillRetry.ToString()); + } + + #endregion + + #region IAP收入上报 + + /// + /// IAP支付事件上报 + /// + /// + /// + public static void TrackIAPPurchase(double revenue, string productID) + { + string tokenID = Analytics.GetAdjustEventToken("iap_purchase"); + if (string.IsNullOrEmpty(tokenID)) + return; + +#if UNITY_IOS + string platform = "appstore"; +#else + string platform = "google_play"; +#endif + AdjustEvent adjustEvent = new AdjustEvent(tokenID); + adjustEvent.setRevenue(revenue,"USD"); + adjustEvent.AddEventParameter("platform", platform); + adjustEvent.AddEventParameter("product_id", productID); + adjustEvent.AddEventParameter("value", $"{revenue}"); + Adjust.trackEvent(adjustEvent); + } + + + + #endregion + + #region 关键属性上报 + + /// + /// 上报PseudoId + /// + /// + public static void ReportPseudoID(string firebaseId) + { + if (!string.IsNullOrEmpty(firebaseId)) + { + Adjust.addSessionCallbackParameter("user_pseudo_id", firebaseId); // 关联 user_pseudo_id + } + } + + /// + /// 上报DeviceId + /// + /// + public static void ReportDeviceId(string deviceId) + { + if (!string.IsNullOrEmpty(deviceId)) + { + Adjust.addSessionCallbackParameter("device_id", deviceId); // 关联 user_pseudo_id + } + } + + #endregion + + + private static AdjustEnvironment GetAdjustEnvironment() + { +#if UNITY_EDITOR || DEBUG + return AdjustEnvironment.Sandbox; +#else + return AdjustEnvironment.Production; +#endif + } + + private static AdjustLogLevel GetAdjustLogLevel() + { +#if UNITY_EDITOR || DEBUG + return AdjustLogLevel.Verbose; +#else + return AdjustLogLevel.Suppress; +#endif + } + + public static void TrackADRevenue(MaxSdkBase.AdInfo adInfo) + { + if (adInfo == null) + return; + + var adRevenue = new AdjustAdRevenue(AdjustConfig.AdjustAdRevenueSourceAppLovinMAX); + adRevenue.setRevenue(adInfo.Revenue, "USD"); + adRevenue.setAdRevenueNetwork(adInfo.NetworkName); + adRevenue.setAdRevenueUnit(adInfo.AdUnitIdentifier); + adRevenue.setAdRevenuePlacement(adInfo.Placement); + Adjust.trackAdRevenue(adRevenue); + } + + + + + } +} \ No newline at end of file diff --git a/Runtime/GuruCore/Runtime/Adjust/AdjustService.cs.meta b/Runtime/GuruCore/Runtime/Adjust/AdjustService.cs.meta new file mode 100644 index 0000000..7b6c18e --- /dev/null +++ b/Runtime/GuruCore/Runtime/Adjust/AdjustService.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: ef17e6069310408c8bf8ce7f127a7122 +timeCreated: 1632464325 \ No newline at end of file diff --git a/Runtime/GuruCore/Runtime/Ads.meta b/Runtime/GuruCore/Runtime/Ads.meta new file mode 100644 index 0000000..53bdd7d --- /dev/null +++ b/Runtime/GuruCore/Runtime/Ads.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 6e9ff833fa09649128c279284acf9c76 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/GuruCore/Runtime/Ads/ADService.cs b/Runtime/GuruCore/Runtime/Ads/ADService.cs new file mode 100644 index 0000000..b9a34c7 --- /dev/null +++ b/Runtime/GuruCore/Runtime/Ads/ADService.cs @@ -0,0 +1,188 @@ + +using UnityEngine; + +namespace Guru +{ + using System.Collections.Generic; + + /// + /// 通用的ADService + /// + public class ADService: ADServiceBase + { + /// + /// 版本号 + /// + public static readonly string Version = "1.4.0"; + + #region 初始化 + + /// + /// 初始化服务 + /// + protected override void InitService() + { + base.InitService(); + InitChannels(); // 启动各广告渠道代理 + } + + #endregion + + #region 渠道管理 + + private HashSet _adChannels; + private AdChanelMax _chanelMax; + private IAsyncRequestChannel _asyncLoader; + + /// + /// 各渠道初始化 + /// + private void InitChannels() + { + _adChannels = new HashSet(); + IAdChannel channel = null; + _asyncLoader = null; + + _chanelMax = new AdChanelMax(); // 默认持有MAXChannel + _chanelMax.Initialize(); + + //------------ 以下为扩展的广告渠道 ------------------ + // 请根据项目需求实现各渠道接入的逻辑 + // 开启渠道需要添加对应的宏 + +#if AD_AMAZON + channel = new AdChanelAmazon(); + channel.Initialize(); + _adChannels.Add(channel); // Amazon + _asyncLoader = channel as IAsyncRequestChannel; + if (_asyncLoader != null) + { + _asyncLoader.OnBannerRequestOver = (success, firstLoad) => + { + Debug.Log($"--- [Amazon] Async banner response, success:{success} -> OnLoadMaxBanner"); + // if (success && firstLoad) OnLoadMaxBanner(); // 广告频率足够快, 不需要再次调用 + }; + _asyncLoader.OnInterstitialRequestOver = (success, firstLoad) => + { + Debug.Log($"--- [Amazon] Async IV response, success:{success} -> OnLoadMaxIV"); + if (success && firstLoad) OnLoadMaxIV(); + }; + _asyncLoader.OnRewardRequestOver = (success,firstLoad) => + { + Debug.Log($"--- [Amazon] Async RV response, success:{success} > OnLoadMaxRV"); + if (success && firstLoad) OnLoadMaxRV(); + }; + + + } +#endif + +#if AD_PUBMATIC + channel = new AdChannelPubMatic(); + channel.Initialize(); + _adChannels.Add(channel); // PubMatic +#endif + + } + #endregion + + #region Banner 广告 + + + /// + /// 加载Banner广告 + /// + public override void RequestBannerAD() + { + if (!IsInitialized) return; + OnChannelLoadBannerAD(); // 调用各渠道请求Banner + } + + + /// + /// 请求Banner广告 + /// + private void OnChannelLoadBannerAD() + { + IAsyncRequestChannel loader = null; + foreach (var channel in _adChannels) + { + channel?.LoadBannerAD(); + } + + // if (null == _asyncLoader) + OnLoadMaxBanner(); + } + + private void OnLoadMaxBanner() + { + _badsloadStartTime = Time.realtimeSinceStartup; + _chanelMax.LoadBannerAD(); + } + + #endregion + + #region Interstitial 广告 + + + public override void RequestInterstitialAD() + { + if (!IsInitialized) return; + OnChannelLoadInterstitialAD(); + } + + + /// + /// 请求插屏广告 + /// + private void OnChannelLoadInterstitialAD() + { + foreach (var channel in _adChannels) + { + channel?.LoadInterstitialAD(); + } + // if (_asyncLoader == null) + OnLoadMaxIV(); + } + + private void OnLoadMaxIV() { + _iadsLoadStartTime = Time.realtimeSinceStartup; // 更新计时器 + _chanelMax.LoadInterstitialAD(); + } + + + #endregion + + #region Reward 广告 + + public override void RequestRewardedAD() + { + if (!IsInitialized) return; + OnChannelLoadRewardAD(); + } + + /// + /// 请求激励视频 + /// + private void OnChannelLoadRewardAD() + { + foreach (var channel in _adChannels) + { + channel?.LoadRewardAD(); + } + // if (_asyncLoader == null) + OnLoadMaxRV(); + } + + private void OnLoadMaxRV() + { + _radsLoadStartTime = Time.realtimeSinceStartup; // 更新计时器 + _chanelMax.LoadRewardAD(); + } + + + + #endregion + + } +} \ No newline at end of file diff --git a/Runtime/GuruCore/Runtime/Ads/ADService.cs.meta b/Runtime/GuruCore/Runtime/Ads/ADService.cs.meta new file mode 100644 index 0000000..6bb574d --- /dev/null +++ b/Runtime/GuruCore/Runtime/Ads/ADService.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 02403fd8dbdd464ab87cad4885cea820 +timeCreated: 1693385050 \ No newline at end of file diff --git a/Runtime/GuruCore/Runtime/Ads/ADServiceBase.cs b/Runtime/GuruCore/Runtime/Ads/ADServiceBase.cs new file mode 100644 index 0000000..b0331e5 --- /dev/null +++ b/Runtime/GuruCore/Runtime/Ads/ADServiceBase.cs @@ -0,0 +1,603 @@ +using System; +using Guru; +using UnityEngine; + +namespace Guru +{ + public abstract class ADServiceBase : IADService where T : new() + { + // 单利定义 + private static T _instance; + + public static T Instance + { + get + { + if (null == _instance) _instance = new T(); + return _instance; + } + } + + protected static readonly string Tag = "[Ads]"; + public bool IsInitialized => MaxSdk.IsInitialized() || _isServiceStarted; + protected bool IsNetworkEnabled => Application.internetReachability != NetworkReachability.NotReachable; + + private bool _isServiceStarted; + protected bool _isDebugMode; + protected bool _isAutoLoadAds; + + private Action _onSdkInitReady; + + public static Action OnInterstitialLoaded; + public static Action OnInterstitialFailed; + + public static Action OnRewardLoaded; + public static Action OnRewardFailed; + + /// + /// 启动广告服务 + /// + /// 广告初始化回调 + /// 自动启动广告加载 + /// debug模式 + public virtual void StartService(Action callback = null, bool autoLoadAds = true, bool isDebugMode = false) + { + if (IsInitialized) return; // 已经初始化后, 无需再次初始化 + + _isServiceStarted = true; + _isAutoLoadAds = autoLoadAds; + _isDebugMode = isDebugMode; + _onSdkInitReady = callback; + this.Log("AD SDK Start Init"); + + //-------------- 初始化回调 ------------------ + MaxSdkCallbacks.OnSdkInitializedEvent += OnMaxSdkInitializedCallBack; + MaxSdkCallbacks.Interstitial.OnAdRevenuePaidEvent += OnAdRevenuePaidEvent; + MaxSdkCallbacks.Rewarded.OnAdRevenuePaidEvent += OnAdRevenuePaidEvent; + MaxSdkCallbacks.Banner.OnAdRevenuePaidEvent += OnAdRevenuePaidEvent; + MaxSdkCallbacks.MRec.OnAdRevenuePaidEvent += OnAdRevenuePaidEvent; + //--------------- Banner 回调 ----------------- + MaxSdkCallbacks.Banner.OnAdLoadedEvent += OnBannerLoadedEvent; + MaxSdkCallbacks.Banner.OnAdLoadFailedEvent += OnBannerFailedEvent; + MaxSdkCallbacks.Banner.OnAdClickedEvent += OnBannerClickedEvent; + //--------------- IV 回调 ----------------- + MaxSdkCallbacks.Interstitial.OnAdLoadedEvent += OnInterstitialLoadedEvent; + MaxSdkCallbacks.Interstitial.OnAdLoadFailedEvent += OnInterstitialFailedEvent; + MaxSdkCallbacks.Interstitial.OnAdDisplayFailedEvent += InterstitialFailedToDisplayEvent; + MaxSdkCallbacks.Interstitial.OnAdClickedEvent += OnInterstitialClickEvent; + MaxSdkCallbacks.Interstitial.OnAdDisplayedEvent += OnInterstitialDisplayEvent; + MaxSdkCallbacks.Interstitial.OnAdHiddenEvent += OnInterstitialDismissedEvent; + //--------------- RV 回调 ----------------- + MaxSdkCallbacks.Rewarded.OnAdLoadedEvent += OnRewardedAdLoadedEvent; + MaxSdkCallbacks.Rewarded.OnAdLoadFailedEvent += OnRewardedAdFailedEvent; + MaxSdkCallbacks.Rewarded.OnAdDisplayFailedEvent += OnRewardedAdFailedToDisplayEvent; + MaxSdkCallbacks.Rewarded.OnAdDisplayedEvent += OnRewardedAdDisplayedEvent; + MaxSdkCallbacks.Rewarded.OnAdClickedEvent += OnRewardedAdClickedEvent; + MaxSdkCallbacks.Rewarded.OnAdHiddenEvent += OnRewardedAdDismissedEvent; + MaxSdkCallbacks.Rewarded.OnAdReceivedRewardEvent += OnRewardedAdReceivedRewardEvent; + + //-------------- SDK 初始化 ------------------- + MaxSdk.SetVerboseLogging(_isDebugMode); + InitService(); + } + + protected virtual void InitService() + { + } + + + private void OnMaxSdkInitializedCallBack(MaxSdkBase.SdkConfiguration sdkConfiguration) + { + this.Log("AD SDK Init Success"); + if (_isAutoLoadAds) OnMaxSdkReady(); + _onSdkInitReady?.Invoke(); + } + + protected virtual void OnMaxSdkReady() + { + //TODO:各个项目根据自己情况进行修改,模版默认做法是SDK初始化完成后就进行广告请求 + RequestBannerAD(); + RequestInterstitialAD(); + RequestRewardedAD(); + } + + /// + /// 可加载广告 + /// + /// + public virtual bool CanLoadAds() + { + return IsInitialized && IsNetworkEnabled; + } + + #region ILRD + + private double TchAD001RevValue + { + get => PlayerPrefs.GetFloat(nameof(TchAD001RevValue), 0f); + set => PlayerPrefs.SetFloat(nameof(TchAD001RevValue), (float)value); + } + + private double TchAD02RevValue + { + get => PlayerPrefs.GetFloat(nameof(TchAD02RevValue), 0f); + set => PlayerPrefs.SetFloat(nameof(TchAD02RevValue), (float)value); + } + + public void OnAdRevenuePaidEvent(string adUnitId, MaxSdkBase.AdInfo adInfo) + { + if (adInfo == null) + return; + + double revenue = adInfo.Revenue; + CalcTaichi001Value(revenue); + CalcTaichi02Value(revenue); + AdjustService.TrackADRevenue(adInfo); + //根据变现需求:是否打开ARO买量 + OnAdImpression(adInfo); + } + + /// + /// 广告ARO收益打点 + /// + /// + private void OnAdImpression(MaxSdkBase.AdInfo adInfo) + { + Analytics.ADImpression(adInfo); + } + + + /// + /// 计算太极001收益 + /// + /// + private void CalcTaichi001Value(double revenue) + { + TchAD001RevValue += revenue; + double revenueValue = TchAD001RevValue; + this.Log("[TaichConfig]", $"获取累计收入TchAD001RevValue:{revenueValue}"); + if (revenueValue >= Analytics.Tch001TargetValue) + { + this.Log("[TaichConfig]", "获取太极收入累计超过0.01打点"); + Analytics.Tch001ADRev(revenueValue); + TchAD001RevValue = 0.0; + } + } + + /// + /// 计算太极02收益 + /// + /// + private void CalcTaichi02Value(double revenue) + { + if (!Analytics.EnableTch02Event) return; + + TchAD02RevValue += revenue; + double revenueValue = TchAD02RevValue; + this.Log("[TchConfig]", $"获取累计收入TchAD02RevValue:{revenueValue}"); + if (revenueValue >= 0.2f) + { + this.Log("[TchConfig]", "获取太极收入累计超过0.2打点"); + Analytics.Tch02ADRev(revenueValue); + TchAD02RevValue = 0.0; + } + } + + #endregion + + #region Banner Ads + + private string _backColorStr = "#50A436"; + private Color _backColor = new Color(0, 0, 0, 0); + private string _badsCategory; + protected float _badsloadStartTime = 0; + + + private int GetAdsLoadDuration(ref float startTime) + { + int duration = (int)((Time.realtimeSinceStartup - startTime) * 1000); + startTime = 0; + return duration; + } + + public virtual void RequestBannerAD() + { + LoadMaxBannerAd(); + } + + /// + /// Banner MAX 加载方式 + /// + protected void LoadMaxBannerAd() + { + OnLoadBads(); + // Banners are automatically sized to 320x50 on phones and 728x90 on tablets + // You may use the utility method `MaxSdkUtils.isTablet()` to help with view sizing adjustments + MaxSdk.CreateBanner(GetBannerID(), MaxSdkBase.BannerPosition.BottomCenter); + MaxSdk.SetBannerExtraParameter(GetBannerID(), "adaptive_banner", "false"); + // Set background or background color for banners to be fully functional + MaxSdk.SetBannerBackgroundColor(GetBannerID(), _backColor); + // Analytics.ADBadsLoad(GetBannerID()); + Analytics.ADBadsLoad(AdParams.Build(GetBannerID())); + } + + public void OnLoadBads() + { + _badsloadStartTime = Time.realtimeSinceStartup; + } + + + public virtual void ShowBanner(string category = "") + { + _badsCategory = category; + string adUnitId = GetBannerID(); + MaxSdk.ShowBanner(adUnitId); + MaxSdk.SetBannerBackgroundColor(adUnitId, _backColor); + OnBannerImpEvent(adUnitId); + } + + public virtual void HideBanner() + { + MaxSdk.HideBanner(GetBannerID()); + } + + private void OnBannerLoadedEvent(string adUnitId, MaxSdkBase.AdInfo adInfo) + { + // Analytics.ADBadsLoaded(adUnitId, GetAdsLoadDuration(ref _badsloadStartTime), _badsCategory); + Analytics.ADBadsLoaded(AdParams.Build(adUnitId, adInfo, + duration: GetAdsLoadDuration(ref _badsloadStartTime), category: _badsCategory)); + } + + private void OnBannerFailedEvent(string adUnitId, MaxSdkBase.ErrorInfo errorInfo) + { + // Analytics.ADBadsFailed(adUnitId, (int)errorInfo.Code, GetAdsLoadDuration(ref _badsloadStartTime), _badsCategory); + Analytics.ADBadsFailed(AdParams.Build(adUnitId, + duration: GetAdsLoadDuration(ref _badsloadStartTime), category: _badsCategory, + errorCode: (int)errorInfo.Code, + waterfallName: errorInfo?.WaterfallInfo?.Name ?? "")); + } + + private void OnBannerClickedEvent(string adUnitId, MaxSdkBase.AdInfo adInfo) + { + // Analytics.ADBadsClick(adUnitId, _badsCategory); + Analytics.ADBadsClick(AdParams.Build(adUnitId, adInfo, _badsCategory)); + } + + private void OnBannerImpEvent(string adUnitId) + { + // Analytics.ADBadsClick(adUnitId, _badsCategory); + Analytics.ADBadsImp(AdParams.Build(adUnitId, category: _badsCategory)); + } + + #endregion + + #region Interstitial Ads + + private string _iadsCategory = "main"; + private int _interstitialRetryAttempt; + protected float _iadsLoadStartTime; + private Action _interstitialDismissAction; + + public virtual void RequestInterstitialAD() + { + if (!CanLoadAds()) + return; + + LoadMaxInterstitial(); + } + + + protected void LoadMaxInterstitial() + { + OnLoadIads(); + MaxSdk.LoadInterstitial(GetInterstitialID()); + } + + public void OnLoadIads() + { + _iadsLoadStartTime = Time.realtimeSinceStartup; + } + + + public bool IsInterstitialADReady() + { + if (!IsInitialized) + return false; + + return MaxSdk.IsInterstitialReady(GetInterstitialID()); + } + + /// + /// 显示插屏广告 + /// + /// 广告奖励回调 + /// 广告失败回调 + /// 广告界面关闭回调 + public virtual void ShowInterstitialAD(string category, Action dismissAction = null) + { + if (!IsInitialized) + { + this.LogWarning("广告未初始化完成,无法显示插屏广告"); + return; + } + + if (!IsInterstitialADReady()) + { + this.LogWarning("插屏没有加载准备好,无法显示插屏广告"); + return; + } + + _iadsCategory = category; + _interstitialDismissAction = dismissAction; + MaxSdk.ShowInterstitial(GetInterstitialID()); + + RequestInterstitialAD(); // 直接加载下一个广告 + } + + protected virtual void OnInterstitialLoadedEvent(string adUnitId, MaxSdkBase.AdInfo adInfo) + { + // Interstitial ad is ready to be shown. MaxSdk.IsInterstitialReady(interstitialAdUnitId) will now return 'true' + // Reset retry attempt + // Analytics.ADIadsLoaded(adUnitId, GetAdsLoadDuration(ref _iadsLoadStartTime), _iadsCategory); + Analytics.ADIadsLoaded(AdParams.Build(adUnitId, + duration: GetAdsLoadDuration(ref _iadsLoadStartTime), category: _iadsCategory)); + _interstitialRetryAttempt = 0; + + OnInterstitialLoaded?.Invoke(); + } + + protected virtual void OnInterstitialFailedEvent(string adUnitId, MaxSdkBase.ErrorInfo errorInfo) + { + // Interstitial ad failed to load + // We recommend retrying with exponentially higher delays up to a maximum delay (in this case 64 seconds) + this.LogError( + $"OnInterstitialFailedEvent AdLoadFailureInfo:{errorInfo.AdLoadFailureInfo}, Message: {errorInfo.Message}"); + _interstitialRetryAttempt++; + double retryDelay = Math.Pow(2, Math.Min(3, _interstitialRetryAttempt)); + DelayCall((float)retryDelay, RequestInterstitialAD); + // Analytics.ADIadsFailed(adUnitId, (int)errorInfo.Code, GetAdsLoadDuration(ref _iadsLoadStartTime), _iadsCategory); + Analytics.ADIadsFailed(AdParams.Build(adUnitId, + duration: GetAdsLoadDuration(ref _iadsLoadStartTime), category: _iadsCategory, + errorCode: (int)errorInfo.Code, + waterfallName: errorInfo?.WaterfallInfo?.Name ?? "")); + + OnInterstitialFailed?.Invoke(); + } + + protected virtual void InterstitialFailedToDisplayEvent(string adUnitId, MaxSdkBase.ErrorInfo errorInfo, + MaxSdkBase.AdInfo adInfo) + { + // Interstitial ad failed to display. We recommend loading the next ad + this.LogError( + $"InterstitialFailedToDisplayEvent AdLoadFailureInfo:{errorInfo.AdLoadFailureInfo}, Message: {errorInfo.Message}"); + // Analytics.ADIadsFailed(adUnitId, (int)errorInfo.Code, GetAdsLoadDuration(ref _iadsLoadStartTime), _iadsCategory); + Analytics.ADIadsFailed(AdParams.Build(adUnitId, + duration: GetAdsLoadDuration(ref _iadsLoadStartTime), category: _iadsCategory, + errorCode: (int)errorInfo.Code, + waterfallName: errorInfo?.WaterfallInfo?.Name ?? "")); + DelayCall(2.0f, RequestInterstitialAD); + } + + protected virtual void OnInterstitialDisplayEvent(string adUnitId, MaxSdkBase.AdInfo adInfo) + { + // Analytics.ADIadsImp(adUnitId, _iadsCategory); + Analytics.ADIadsImp(AdParams.Build(adUnitId, category: _iadsCategory)); + } + + protected virtual void OnInterstitialClickEvent(string adUnitId, MaxSdkBase.AdInfo adInfo) + { + // Analytics.ADIadsClick(adUnitId, _iadsCategory); + Analytics.ADIadsClick(AdParams.Build(adUnitId, category: _iadsCategory)); + } + + protected virtual void OnInterstitialDismissedEvent(string adUnitId, MaxSdkBase.AdInfo adInfo) + { + // Interstitial ad is hidden. Pre-load the next ad + _interstitialDismissAction?.Invoke(); + // Analytics.ADIadsClose(adUnitId, _iadsCategory); + Analytics.ADIadsClose(AdParams.Build(adUnitId, category: _iadsCategory)); + //延时加载下一个广告 + DelayCall(2.0f, RequestInterstitialAD); + } + + #endregion + + #region Rewarded Ads + + private string _rewardCategory = "main"; + private int _rewardRetryAttempt; + protected float _radsLoadStartTime; + private Action _rewardAction; + private Action _failAction; + private Action _dismissAction; + + public virtual void RequestRewardedAD() + { + if (!IsInitialized) + return; + + LoadMaxRewardAd(); + } + + + /// + /// 默认加载 MAX 广告逻辑 + /// + protected void LoadMaxRewardAd(string unitId = "") + { + OnLoadRads(); + var id = GetRewardedVideoID(); + // Analytics.ADRadsLoad(id); + Analytics.ADIadsClose(AdParams.Build(id)); + MaxSdk.LoadRewardedAd(id); + } + + public void OnLoadRads() + { + _radsLoadStartTime = Time.realtimeSinceStartup; + } + + + public virtual bool IsRewardedADReady() + { + if (!IsInitialized) + return false; + + return MaxSdk.IsRewardedAdReady(GetRewardedVideoID()); + } + + /// + /// 显示激励视频广告 + /// + /// 广告奖励回调 + /// 广告失败回调 + /// 广告界面关闭回调 + public virtual void ShowRewardedAD(string category, Action rewardAction = null, + Action failAction = null, Action dismissAction = null) + { + if (!IsInitialized) + { + this.LogWarning("广告未初始化完成,无法显示视频广告"); + return; + } + + if (!IsRewardedADReady()) + { + this.LogWarning("广告没有准备好,无法显示视频广告"); + return; + } + + _rewardCategory = category; + _rewardAction = rewardAction; + _failAction = failAction; + _dismissAction = dismissAction; + MaxSdk.ShowRewardedAd(GetRewardedVideoID()); + + RequestRewardedAD(); + } + + protected virtual void OnRewardedAdLoadedEvent(string adUnitId, MaxSdkBase.AdInfo adInfo) + { + // Rewarded ad is ready to be shown. MaxSdk.IsRewardedAdReady(rewardedAdUnitId) will now return 'true' + // Reset retry attempt + // this.Log("OnRewardedAdLoadedEvent"); + // Analytics.ADRadsLoaded(adUnitId, GetAdsLoadDuration(ref _radsLoadStartTime), _rewardCategory); + Analytics.ADRadsLoaded(AdParams.Build(adUnitId, + duration: GetAdsLoadDuration(ref _iadsLoadStartTime), category: _iadsCategory)); + _rewardRetryAttempt = 0; + + OnRewardLoaded?.Invoke(); + } + + protected virtual void OnRewardedAdFailedEvent(string adUnitId, MaxSdkBase.ErrorInfo errorInfo) + { + // Rewarded ad failed to load + // We recommend retrying with exponentially higher delays up to a maximum delay (in this case 64 seconds) + this.LogError( + $"OnRewardedAdFailedEvent AdLoadFailureInfo:{errorInfo.AdLoadFailureInfo}, Message: {errorInfo.Message}"); + // Analytics.ADRadsFailed(adUnitId, (int)errorInfo.Code, GetAdsLoadDuration(ref _radsLoadStartTime), _rewardCategory); + Analytics.ADRadsFailed(AdParams.Build(adUnitId, + duration: GetAdsLoadDuration(ref _radsLoadStartTime), category: _rewardCategory, + errorCode: (int)errorInfo.Code, + waterfallName: errorInfo?.WaterfallInfo?.Name ?? "")); + _rewardRetryAttempt++; + double retryDelay = Math.Pow(2, Math.Min(3, _rewardRetryAttempt)); + DelayCall((float)retryDelay, RequestRewardedAD); + + OnRewardFailed?.Invoke(); + } + + protected virtual void OnRewardedAdFailedToDisplayEvent(string adUnitId, MaxSdkBase.ErrorInfo errorInfo, + MaxSdkBase.AdInfo arg3) + { + // Rewarded ad failed to display. We recommend loading the next ad + this.LogError( + $"OnRewardedAdFailedToDisplayEvent AdLoadFailureInfo:{errorInfo.AdLoadFailureInfo}, Message: {errorInfo.Message}"); + // Analytics.ADRadsFailed(adUnitId, (int)errorInfo.Code, GetAdsLoadDuration(ref _radsLoadStartTime), _rewardCategory); + Analytics.ADRadsFailed(AdParams.Build(adUnitId, + duration: GetAdsLoadDuration(ref _radsLoadStartTime), category: _rewardCategory, + errorCode: (int)errorInfo.Code, + waterfallName: errorInfo?.WaterfallInfo?.Name ?? "")); + _failAction?.Invoke("OnRewardedAdFailedToDisplayEvent"); + DelayCall(2.0f, RequestRewardedAD); + + OnRewardFailed?.Invoke(); + } + + protected virtual void OnRewardedAdDisplayedEvent(string adUnitId, MaxSdkBase.AdInfo adInfo) + { + this.Log("OnRewardedAdDisplayedEvent"); + // Analytics.ADRadsImp(adUnitId, _rewardCategory); + Analytics.ADRadsImp(AdParams.Build(adUnitId, category: _rewardCategory)); + } + + protected virtual void OnRewardedAdClickedEvent(string adUnitId, MaxSdkBase.AdInfo adInfo) + { + this.Log("OnRewardedAdClickedEvent"); + // Analytics.ADRadsClick(adUnitId, _rewardCategory); + Analytics.ADRadsClick(AdParams.Build(adUnitId, category: _rewardCategory)); + } + + protected virtual void OnRewardedAdDismissedEvent(string adUnitId, MaxSdkBase.AdInfo adInfo) + { + this.Log("OnRewardedAdDismissedEvent"); + // Analytics.ADRadsClose(adUnitId, _rewardCategory); + Analytics.ADRadsClose(AdParams.Build(adUnitId, category: _rewardCategory)); + _dismissAction?.Invoke(); + //延时加载下一个广告 + DelayCall(2.0f, RequestRewardedAD); + } + + protected virtual void OnRewardedAdReceivedRewardEvent(string adUnitId, MaxSdk.Reward reward, + MaxSdkBase.AdInfo arg3) + { + this.Log("OnRewardedAdReceivedRewardEvent"); + // Analytics.ADRadsRewarded(adUnitId, _rewardCategory); + Analytics.ADRadsRewarded(AdParams.Build(adUnitId, category: _rewardCategory)); + // Rewarded ad was displayed and user should receive the reward + _rewardAction?.Invoke(); + } + + #endregion + + #region Ad Settings + + protected virtual string GetRewardedVideoID() + { + return GuruSettings.Instance.ADSetting.GetRewardedVideoID(); + } + + protected virtual string GetInterstitialID() + { + return GuruSettings.Instance.ADSetting.GetInterstitialID(); + } + + protected virtual string GetBannerID() + { + return GuruSettings.Instance.ADSetting.GetBannerID(); + } + + #endregion + + #region MaxDebugView + + public void ShowMaxDebugPanel() + { + if (!IsInitialized) return; +#if !UNITY_EDITOR + MaxSdk.ShowMediationDebugger(); +#endif + } + + #endregion + + #region DelayCall + + private void DelayCall(float time, Action callback) + { + CoroutineHelper.Instance.StartDelayed(time, callback); + } + + #endregion + } +} \ No newline at end of file diff --git a/Runtime/GuruCore/Runtime/Ads/ADServiceBase.cs.meta b/Runtime/GuruCore/Runtime/Ads/ADServiceBase.cs.meta new file mode 100644 index 0000000..a9f55f2 --- /dev/null +++ b/Runtime/GuruCore/Runtime/Ads/ADServiceBase.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 41785d9c994342959c83b96643576643 +timeCreated: 1693384669 \ No newline at end of file diff --git a/Runtime/GuruCore/Runtime/Ads/ARO.meta b/Runtime/GuruCore/Runtime/Ads/ARO.meta new file mode 100644 index 0000000..6b8643d --- /dev/null +++ b/Runtime/GuruCore/Runtime/Ads/ARO.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 9080cfebfd2d4cda9c0710d7128132b0 +timeCreated: 1679381317 \ No newline at end of file diff --git a/Runtime/GuruCore/Runtime/Ads/ARO/GuruAROConfig.cs b/Runtime/GuruCore/Runtime/Ads/ARO/GuruAROConfig.cs new file mode 100644 index 0000000..81c7396 --- /dev/null +++ b/Runtime/GuruCore/Runtime/Ads/ARO/GuruAROConfig.cs @@ -0,0 +1,44 @@ +namespace Guru +{ + using UnityEngine; + + /// + /// ARO云控参数 + /// + public class GuruAROConfig + { + public bool enable = false; + + public static readonly string Key = "aro_impression_config"; // 配置Key + public static readonly string DefaultValue = "{\"enable\":false}"; // 默认值 + + /// + /// 获取在线参数 + /// + /// + public static GuruAROConfig Get() + { + FirebaseUtil.AppendDefaultValue(Key, DefaultValue); + + var config = FirebaseUtil.GetRemoteConfig(Key); + if (config == null) + { + config = new GuruAROConfig(); + } + FirebaseUtil.OnFetchRemoteSuccess += config.OnFetchSuccess; + return config; + } + + /// + /// 拉取完成 + /// + private void OnFetchSuccess() + { + FirebaseUtil.OnFetchRemoteSuccess -= OnFetchSuccess; + + var config = FirebaseUtil.GetRemoteConfig(Key); + if (config != null) enable = config.enable; + } + + } +} \ No newline at end of file diff --git a/Runtime/GuruCore/Runtime/Ads/ARO/GuruAROConfig.cs.meta b/Runtime/GuruCore/Runtime/Ads/ARO/GuruAROConfig.cs.meta new file mode 100644 index 0000000..71ce3ef --- /dev/null +++ b/Runtime/GuruCore/Runtime/Ads/ARO/GuruAROConfig.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 37bdc74bb6334e7783854ade25d76450 +timeCreated: 1681906541 \ No newline at end of file diff --git a/Runtime/GuruCore/Runtime/Ads/Channels.meta b/Runtime/GuruCore/Runtime/Ads/Channels.meta new file mode 100644 index 0000000..d6cceef --- /dev/null +++ b/Runtime/GuruCore/Runtime/Ads/Channels.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 6142b22d4512e40a1b66d57ac90606c4 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/GuruCore/Runtime/Ads/Channels/Max.meta b/Runtime/GuruCore/Runtime/Ads/Channels/Max.meta new file mode 100644 index 0000000..3898717 --- /dev/null +++ b/Runtime/GuruCore/Runtime/Ads/Channels/Max.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 6b46d1a216224430aefdaa77ac6ba00d +timeCreated: 1681289505 \ No newline at end of file diff --git a/Runtime/GuruCore/Runtime/Ads/Channels/Max/AdChanelMax.cs b/Runtime/GuruCore/Runtime/Ads/Channels/Max/AdChanelMax.cs new file mode 100644 index 0000000..c3b8bf0 --- /dev/null +++ b/Runtime/GuruCore/Runtime/Ads/Channels/Max/AdChanelMax.cs @@ -0,0 +1,84 @@ + +namespace Guru +{ + using UnityEngine; + + /// + /// MAX 自有渠道初始化 + /// + public class AdChanelMax: IAdChannel + { + #region 属性 + + public static readonly string ChanelName = "AppLovinMax"; + public string Name => ChanelName; + + public bool IsEnabled => true; + + #endregion + + #region 广告位 + + // ---------------- Max 广告位ID -------------------- + private string MaxBannerSlotID => GuruSettings.Instance.ADSetting.GetBannerID(); + private string MaxIVSlotID => GuruSettings.Instance.ADSetting.GetInterstitialID(); + private string MaxRVSlotID => GuruSettings.Instance.ADSetting.GetRewardedVideoID(); + + #endregion + + #region 初始化 + + /// + /// MAX 渠道初始化, 启动服务 + /// + public void Initialize() + { + MaxSdk.SetMuted(false); + MaxSdk.SetSdkKey(GuruSettings.Instance.ADSetting.SDK_KEY); + MaxSdk.SetUserId(IPMConfig.IPM_UID); // 上报用户ID + MaxSdk.InitializeSdk(); + } + + #endregion + + #region Banner + + private Color _backColor = Color.clear; + public bool IsBannerRequestOver => true; + public void LoadBannerAD() + { + // Banners are automatically sized to 320x50 on phones and 728x90 on tablets + // You may use the utility method `MaxSdkUtils.isTablet()` to help with view sizing adjustments + MaxSdk.CreateBanner(MaxBannerSlotID, MaxSdkBase.BannerPosition.BottomCenter); + MaxSdk.SetBannerExtraParameter(MaxBannerSlotID, "adaptive_banner", "false"); + // Set background or background color for banners to be fully functional + MaxSdk.SetBannerBackgroundColor(MaxBannerSlotID, _backColor); + Analytics.ADBadsLoad(AdParams.Build(MaxBannerSlotID)); + } + + #endregion + + #region Interstitial + + public bool IsInterstitialRequestOver => true; + public void LoadInterstitialAD() + { + Analytics.ADIadsLoad(AdParams.Build(MaxIVSlotID)); // 上报打点 + MaxSdk.LoadInterstitial(MaxIVSlotID); + } + + #endregion + + #region Reward + + public bool IsRewardRequestOver => true; + public void LoadRewardAD() + { + Analytics.ADRadsLoad(AdParams.Build(MaxRVSlotID)); // 上报打点 + MaxSdk.LoadRewardedAd(MaxRVSlotID); + } + + #endregion + + } +} \ No newline at end of file diff --git a/Runtime/GuruCore/Runtime/Ads/Channels/Max/AdChanelMax.cs.meta b/Runtime/GuruCore/Runtime/Ads/Channels/Max/AdChanelMax.cs.meta new file mode 100644 index 0000000..5d18f5a --- /dev/null +++ b/Runtime/GuruCore/Runtime/Ads/Channels/Max/AdChanelMax.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: a257717503f0440fb42a309c299d774a +timeCreated: 1681288384 \ No newline at end of file diff --git a/Runtime/GuruCore/Runtime/Ads/IADService.cs b/Runtime/GuruCore/Runtime/Ads/IADService.cs new file mode 100644 index 0000000..9a0d07d --- /dev/null +++ b/Runtime/GuruCore/Runtime/Ads/IADService.cs @@ -0,0 +1,40 @@ +using System; + +namespace Guru +{ + public interface IADService + { + + #region Lifecycle + + void StartService(Action onSdkReady = null, bool autoLoad = true, bool isDebugMode = false); + + #endregion + + #region Banner + + void RequestBannerAD(); + void ShowBanner(string category = ""); + void HideBanner(); + + #endregion + + #region Interstitial + + void RequestInterstitialAD(); + bool IsInterstitialADReady(); + void ShowInterstitialAD(string category, Action dismissAction = null); + + #endregion + + #region Rewarded Ads + + void RequestRewardedAD(); + bool IsRewardedADReady(); + void ShowRewardedAD(string category, Action rewardAction = null, Action failAction = null, Action dismissAction = null); + + + #endregion + + } +} \ No newline at end of file diff --git a/Runtime/GuruCore/Runtime/Ads/IADService.cs.meta b/Runtime/GuruCore/Runtime/Ads/IADService.cs.meta new file mode 100644 index 0000000..6c19ce2 --- /dev/null +++ b/Runtime/GuruCore/Runtime/Ads/IADService.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: e92e7594425a4ee7910a7f529de159f8 +timeCreated: 1693385885 \ No newline at end of file diff --git a/Runtime/GuruCore/Runtime/Ads/IAdChannel.cs b/Runtime/GuruCore/Runtime/Ads/IAdChannel.cs new file mode 100644 index 0000000..fd6ec3b --- /dev/null +++ b/Runtime/GuruCore/Runtime/Ads/IAdChannel.cs @@ -0,0 +1,34 @@ +namespace Guru +{ + using System; + /// + /// 广告渠道 + /// + public interface IAdChannel + { + // Action OnRequestOver { get; set; } + + void Initialize(); + + string Name { get;} + + bool IsEnabled { get; } + + void LoadBannerAD(); + + void LoadInterstitialAD(); + + void LoadRewardAD(); + } + + /// + /// 异步请求逻辑 + /// + public interface IAsyncRequestChannel + { + Action OnBannerRequestOver { get; set; } + Action OnInterstitialRequestOver { get; set; } + Action OnRewardRequestOver { get; set; } + } + +} \ No newline at end of file diff --git a/Runtime/GuruCore/Runtime/Ads/IAdChannel.cs.meta b/Runtime/GuruCore/Runtime/Ads/IAdChannel.cs.meta new file mode 100644 index 0000000..03e413d --- /dev/null +++ b/Runtime/GuruCore/Runtime/Ads/IAdChannel.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 62b0796686ac4e9886abf267fa90356f +timeCreated: 1681280450 \ No newline at end of file diff --git a/Runtime/GuruCore/Runtime/Analytics.meta b/Runtime/GuruCore/Runtime/Analytics.meta new file mode 100644 index 0000000..0a630ed --- /dev/null +++ b/Runtime/GuruCore/Runtime/Analytics.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: c68e28b2c03c46749634cf5ac74d381a +timeCreated: 1632389219 \ No newline at end of file diff --git a/Runtime/GuruCore/Runtime/Analytics/Analytics.AdjustToken.cs b/Runtime/GuruCore/Runtime/Analytics/Analytics.AdjustToken.cs new file mode 100644 index 0000000..71ed81f --- /dev/null +++ b/Runtime/GuruCore/Runtime/Analytics/Analytics.AdjustToken.cs @@ -0,0 +1,61 @@ +using System.Collections.Generic; +using com.adjust.sdk; + +namespace Guru +{ + //Adjust上报事件封装 + public static partial class Analytics + { + private static Dictionary AdjustEventTokenDict; + + private static void InitAdjustEventTokenDict() + { + AdjustEventTokenDict = new Dictionary(); + + foreach (var adjustEvent in GuruSettings.Instance.AnalyticsSetting.AdjustEventList) + { +#if UNITY_ANDROID || UNITY_EDITOR + AdjustEventTokenDict[adjustEvent.EventName] = adjustEvent.AndroidToken; +#elif UNITY_IOS + AdjustEventTokenDict[adjustEvent.EventName] = adjustEvent.IOSToken; +#endif + } + } + + private static AdjustEvent CreateAdjustEvent(string eventName) + { + string tokenID = GetAdjustEventToken(eventName); + if (string.IsNullOrEmpty(tokenID)) + { + return null; + } + + return new AdjustEvent(tokenID); + } + + public static AdjustEvent AddEventParameter(this AdjustEvent adjustEvent, string key, string value) + { + adjustEvent.addCallbackParameter(key, value); + adjustEvent.addPartnerParameter(key, value); + return adjustEvent; + } + + public static string GetAdjustEventToken(string eventName) + { + if (AdjustEventTokenDict == null) + { + InitAdjustEventTokenDict(); + } + + if (AdjustEventTokenDict.ContainsKey(eventName)) + { + return AdjustEventTokenDict[eventName]; + } + else + { + Log.W(AdjustService.LOG_TAG,$"AdjustEventTokenDict 没有添加 event:{eventName}的Token值"); + return null; + } + } + } +} \ No newline at end of file diff --git a/Runtime/GuruCore/Runtime/Analytics/Analytics.AdjustToken.cs.meta b/Runtime/GuruCore/Runtime/Analytics/Analytics.AdjustToken.cs.meta new file mode 100644 index 0000000..dd5bd81 --- /dev/null +++ b/Runtime/GuruCore/Runtime/Analytics/Analytics.AdjustToken.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: a2f2bcb4610943c2ae361ddc8d1017ac +timeCreated: 1632475035 \ No newline at end of file diff --git a/Runtime/GuruCore/Runtime/Analytics/Analytics.Ads.cs b/Runtime/GuruCore/Runtime/Analytics/Analytics.Ads.cs new file mode 100644 index 0000000..ea17a59 --- /dev/null +++ b/Runtime/GuruCore/Runtime/Analytics/Analytics.Ads.cs @@ -0,0 +1,296 @@ +using System.Collections.Generic; +using UnityEngine; + +namespace Guru +{ + public static partial class Analytics + { + + #region 广告属性 + + //------ 广告额外的参数 -------- + private static readonly string ParameterNetworkName = "network_name"; //NetworkName + private static readonly string ParameterNetworkPlacement = "network_placement"; //NetworkPlacement + private static readonly string ParameterWaterfall = "waterfall"; //Waterfall + + //------ 默认值 ------- + public const string DefaultCategory = "not_set"; + public const string DefaultWaterfall = "unknown"; + + + #endregion + + + + #region Ads + + private static Dictionary BuildAdEventData(AdParams adParams) + { + if (string.IsNullOrEmpty(adParams.category)) adParams.category = DefaultCategory; + if (string.IsNullOrEmpty(adParams.waterfallName)) adParams.waterfallName = DefaultWaterfall; + + var data = new Dictionary() + { + { ParameterItemCategory, adParams.category }, + { ParameterItemName, adParams.adUnitId }, + { ParameterNetworkName, adParams.networkName }, + { ParameterNetworkPlacement, adParams.networkPlacement }, + { ParameterWaterfall, adParams.waterfallName }, + { ParameterDuration, adParams.duration }, + }; + + if (adParams.errorCode != 0) data[ParameterErrorCode] = adParams.errorCode; + + return data; + } + + //---------------------- BANNER ------------------------- + public static void ADBadsLoad(AdParams adParams) + { + LogEvent(EventBadsLoad, BuildAdEventData(adParams)); + } + public static void ADBadsLoaded(AdParams adParams) + { + LogEvent(EventBadsLoaded, BuildAdEventData(adParams)); + } + public static void ADBadsFailed(AdParams adParams) + { + LogEvent(EventBadsFailed, BuildAdEventData(adParams)); + } + /// + /// 广告点击 + /// + public static void ADBadsClick(AdParams adParams) + { + LogEvent(EventBadsClick, BuildAdEventData(adParams)); + } + public static void ADBadsImp(AdParams adParams) + { + LogEvent(EventBadsImp, BuildAdEventData(adParams)); + } + + //---------------------- INTERSTITIAL ------------------------- + /// + /// 广告拉取 + /// + public static void ADIadsLoad(AdParams adParams) + { + LogEvent(EventIadsLoad, BuildAdEventData(adParams)); + } + + /// + /// 广告拉取成功 + /// + public static void ADIadsLoaded(AdParams adParams) + { + LogEvent(EventIadsLoaded, BuildAdEventData(adParams)); + } + + /// + /// 广告拉取失败 + /// + public static void ADIadsFailed(AdParams adParams) + { + LogEvent(EventIadsFailed, BuildAdEventData(adParams)); + } + + /// + /// 广告展示 + /// + public static void ADIadsImp(AdParams adParams) + { + LogEvent(EventIadsImp, BuildAdEventData(adParams)); + } + + /// + /// 广告点击 + /// + public static void ADIadsClick(AdParams adParams) + { + LogEvent(EventIadsClick, BuildAdEventData(adParams)); + } + + /// + /// 用户关闭广告 + /// + public static void ADIadsClose(AdParams adParams) + { + LogEvent(EventIadsClose, BuildAdEventData(adParams)); + } + + //---------------------- REWARDS ------------------------- + /// + /// 广告开始加载 + /// + public static void ADRadsLoad(AdParams adParams) + { + LogEvent(EventRadsLoad, BuildAdEventData(adParams)); + } + /// + /// 广告拉取成功 + /// + public static void ADRadsLoaded(AdParams adParams) + { + LogEvent(EventRadsLoaded, BuildAdEventData(adParams)); + } + /// + /// 广告拉取失败 + /// + public static void ADRadsFailed(AdParams adParams) + { + LogEvent(EventRadsFailed, BuildAdEventData(adParams)); + } + /// + /// 广告展示 + /// + public static void ADRadsImp(AdParams adParams) + { + LogEvent(EventRadsImp, BuildAdEventData(adParams)); + } + /// + /// 广告完成观看发奖励 + /// + public static void ADRadsRewarded(AdParams adParams) + { + var data = BuildAdEventData(adParams); + LogEvent(EventRadsRewarded, data); + + if (RadsRewardCount == 0) + { + RadsRewardCount = 1; + ADRadsFirstRewarded(data); + } + } + + + private static int RadsRewardCount + { + get => PlayerPrefs.GetInt(nameof(RadsRewardCount), 0); + set => PlayerPrefs.SetInt(nameof(RadsRewardCount), value); + } + + /// + /// 用户首次完成观看 + /// + public static void ADRadsFirstRewarded(Dictionary data) + { + LogEvent(EventFirstRadsRewarded, data); + } + + /// + /// 广告点击 + /// + public static void ADRadsClick(AdParams adParams) + { + LogEvent(EventRadsClick, BuildAdEventData(adParams)); + } + + /// + /// 用户关闭广告 + /// + public static void ADRadsClose(AdParams adParams) + { + LogEvent(EventRadsClose, BuildAdEventData(adParams)); + } + #endregion + + + #region Ads-ATT + +#if UNITY_IOS + + /// + /// ATT 结果打点 + /// + /// + /// + /// + public static void AttResult(string status, string type = "custom", string others = "") + { + SetAttProperty(status); + Debug.Log($"{TAG} AttResult: {status} type:{type} others:{others}"); + LogEvent(EventATTResult, new Dictionary() + { + { ParameterItemCategory, status }, + { "type", type }, + { ParameterItemName, others }, + }); + } + + /// + /// 上报 ATT 当前的属性 + /// + /// + public static void SetAttProperty(string status) + { + Debug.Log($"{TAG} SetAttProperty: {status}"); + SetUserProperty(PropertyATTStatus, status); + } + +#endif + + #endregion + + + + + + } + + + + /// + /// 广告打点上报参数 + /// + public class AdParams + { + public string category; + public string networkName; + public string networkPlacement; + public string waterfallName; + public string adUnitId; + public int errorCode = 0; + public int duration = 0; + + + /// + /// 构造AD参数 + /// + /// + /// + /// + /// + /// + /// + /// + public static AdParams Build(string adUnitId, MaxSdkBase.AdInfo adInfo = null, string category = "", + int duration = 0, int errorCode = 0, string waterfallName = "") + { + if (string.IsNullOrEmpty(adUnitId) && adInfo != null) adUnitId = adInfo.AdUnitIdentifier; + var networkName = ""; + var networkPlacement = ""; + if (adInfo != null) + { + networkName = adInfo.NetworkName; + networkPlacement = adInfo.NetworkPlacement; + + if (string.IsNullOrEmpty(waterfallName)) + waterfallName = adInfo.WaterfallInfo?.Name ?? ""; + } + + var p = new AdParams() + { + adUnitId = adUnitId, + duration = duration, + networkName = networkName, + networkPlacement = networkPlacement, + waterfallName = waterfallName, + category = category, + errorCode = errorCode, + }; + return p; + } + } + + +} \ No newline at end of file diff --git a/Runtime/GuruCore/Runtime/Analytics/Analytics.Ads.cs.meta b/Runtime/GuruCore/Runtime/Analytics/Analytics.Ads.cs.meta new file mode 100644 index 0000000..3714930 --- /dev/null +++ b/Runtime/GuruCore/Runtime/Analytics/Analytics.Ads.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: ea0c173656ce444babcc3cf7472f73fb +timeCreated: 1699847349 \ No newline at end of file diff --git a/Runtime/GuruCore/Runtime/Analytics/Analytics.Const.cs b/Runtime/GuruCore/Runtime/Analytics/Analytics.Const.cs new file mode 100644 index 0000000..1530e73 --- /dev/null +++ b/Runtime/GuruCore/Runtime/Analytics/Analytics.Const.cs @@ -0,0 +1,112 @@ +namespace Guru +{ + public enum ELevelResult + { + success, + fail, + timeout, + exit, + } + + //打点常量定义 + public static partial class Analytics + { + public static readonly string TAG = "Analytics"; + // 美元符号 + public static readonly string USD = "USD"; + // 广告平台 + public static readonly string AdMAX = "MAX"; + + //IAP打点事件 + public static readonly string EventIAPFirst = "first_iap"; + public static readonly string EventIAPImp = "iap_imp"; + public static readonly string EventIAPClose = "iap_close"; + public static readonly string EventIAPClick = "iap_clk"; + public static readonly string EventIAPReturnTrue = "iap_ret_true"; + public static readonly string EventIAPReturnFalse = "iap_ret_false"; + + //横幅广告打点事件 + public static readonly string EventBadsLoad = "bads_load"; + public static readonly string EventBadsLoaded = "bads_loaded"; + public static readonly string EventBadsFailed = "bads_failed"; + public static readonly string EventBadsClick = "bads_clk"; + public static readonly string EventBadsImp = "bads_imp"; + + //插屏广告打点事件 + public static readonly string EventIadsLoad = "iads_load"; + public static readonly string EventIadsLoaded = "iads_loaded"; + public static readonly string EventIadsFailed = "iads_failed"; + public static readonly string EventIadsImp = "iads_imp"; + public static readonly string EventIadsClick = "iads_clk"; + public static readonly string EventIadsClose = "iads_close"; + + //激励视频广告打点事件 + public static readonly string EventRadsLoad = "rads_load"; + public static readonly string EventRadsLoaded = "rads_loaded"; + public static readonly string EventRadsFailed = "rads_failed"; + public static readonly string EventRadsImp = "rads_imp"; + public static readonly string EventRadsRewarded = "rads_rewarded"; + public static readonly string EventRadsClick = "rads_clk"; + public static readonly string EventRadsClose = "rads_close"; + public static readonly string EventFirstRadsRewarded = "first_rads_rewarded"; + + //广告收益打点事件 + public static readonly string EventTchAdRev001Impression = "tch_ad_rev_roas_001"; + public static readonly string EventTchAdRev02Impression = "tch_ad_rev_roas_02"; + public static readonly string EventTchAdRevAbnormal = "tch_ad_rev_value_abnormal"; + + //内购成功事件上报 + public static readonly string EventIAPPurchase = "iap_purchase"; + public static readonly string IAPStoreCategory = "Store"; + public static readonly string IAPTypeProduct = "product"; + public static readonly string IAPTypeSubscription = "subscription"; + + //打点参数名 + public static readonly string ParameterResult = "result"; + public static readonly string ParameterStep = "step"; + public static readonly string ParameterDuration = "duration"; + public static readonly string ParameterErrorCode = "error_code"; + public static readonly string ParameterProductId = "product_id"; + public static readonly string ParameterPlatform = "platform"; + public static readonly string ParameterStartType = "start_type"; // 游戏启动类型 + public static readonly string ParameterReplay = "replay"; // 游戏重玩 + public static readonly string ParameterContinue = "continue"; // 游戏继续 + + + // 评价参数 + public static readonly string EventRateImp = "rate_imp"; // 评价弹窗展示 + public static readonly string EventRateNow = "rate_now"; // 点击评分引导弹窗中的评分 + + //打点内部执行错误 + public static string ParameterEventError => "event_error"; + + //ios ATT打点 + public static readonly string ATTGuideShow = "att_guide_show"; + public static readonly string ATTGuideOK = "att_guide_ok"; + public static readonly string ATTWindowShow = "att_window_show"; + public static readonly string ATTOptIn = "att_opt_in"; + public static readonly string ATTOpOut = "att_opt_out"; + public static readonly string ParameterATTStatus = "att_status"; + public static readonly string EventATTResult = "att_result"; + + // 用户属性 + public static readonly string PropertyFirstOpenTime = "first_open_time"; //用户第一次first_open的时间 + public static readonly string PropertyDeviceID = "device_id"; //用户的设备ID + public static readonly string PropertyUserID = "user_id"; + public static readonly string PropertyLevel = "b_level"; //"每次完成通关上升一次,显示用户完成的最大关卡数。只针对主关卡和主玩法的局数做累加,初始值为0。" + public static readonly string PropertyPlay = "b_play"; //每完成一局或者游戏触发, + public static readonly string PropertyLastPlayedLevel = "last_played_level"; + public static readonly string PropertyGrade = "grade"; //当游戏玩家角色升级时触发 + public static readonly string PropertyIsIAPUser = "is_iap_user"; //付费成功后设置属性参数为true,如果没有发生付费可以不用设置该属性 + public static readonly string PropertyIAPCoin = "iap_coin"; //付费所得的总金币数(iap获取累计值)\ + public static readonly string PropertyNonIAPCoin = "noniap_coin"; //非付费iap获取累计值 + public static readonly string PropertyCoin = "coin"; //当前金币数 + public static readonly string PropertyExp = "exp"; // 经验值 + public static readonly string PropertyHp = "hp"; // 生命值/体力 + public static readonly string PropertyPicture = "picture"; // 玩家在主线的mapid + public static readonly string PropertyNoAds = "no_ads"; // 玩家是否去广告 + public static readonly string PropertyATTStatus = "att_status"; // ATT 状态 + public static readonly string PropertyGDPR = "gdpr"; // GDPR状态 + + } +} \ No newline at end of file diff --git a/Runtime/GuruCore/Runtime/Analytics/Analytics.Const.cs.meta b/Runtime/GuruCore/Runtime/Analytics/Analytics.Const.cs.meta new file mode 100644 index 0000000..012d194 --- /dev/null +++ b/Runtime/GuruCore/Runtime/Analytics/Analytics.Const.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 10134a3336004d79b813087eeda9d262 +timeCreated: 1632404706 \ No newline at end of file diff --git a/Runtime/GuruCore/Runtime/Analytics/Analytics.Custom.cs b/Runtime/GuruCore/Runtime/Analytics/Analytics.Custom.cs new file mode 100644 index 0000000..0bed2eb --- /dev/null +++ b/Runtime/GuruCore/Runtime/Analytics/Analytics.Custom.cs @@ -0,0 +1,297 @@ +using System; +using System.Collections.Generic; +using com.adjust.sdk; +using Firebase.Analytics; +using Firebase.Crashlytics; +using Firebase.Extensions; +using UnityEngine; + +namespace Guru +{ + /// + /// 自打点逻辑 + /// + public partial class Analytics + { + private static bool _hasGotFirebaseId; //已取得FirebaseId + private static bool _hasGotAdId; // 已取得AdId + private static bool _hasGotAdjustId; // 已取得AdjustId + private static bool _hasGotDeviceId; // 已取得DeviceId + private static bool _hasGotUid; // 已取得UID + private static DateTime _lastReportRateDate; //上次上报信息的日期 + private static double _reportSuccessInterval; // 上报频率 + + private const string VALUE_NOT_FOR_IOS = "not_support_for_ios"; + + public static bool IsDebug { get; set; } = false; + + /// + /// 初始化Guru自打点系统 (请优先于 Firebase 初始化调用) + /// + public static void InstallGuruAnalytics(bool isDebug = false) + { + try + { +#if UNITY_EDITOR + IsDebug = true; +#else + IsDebug = isDebug; +#endif + string appId = IPMConfig.IPM_X_APP_ID; + string deviceInfo = new DeviceInfoData().ToString(); + GuruAnalytics.Init(appId, deviceInfo, IsDebug); // 初始化(带Header) + + _hasGotFirebaseId = false; + _hasGotAdId = false; + _hasGotAdjustId = false; + _hasGotDeviceId = false; + _hasGotUid = false; + _lastReportRateDate = DateTime.Now; + _reportSuccessInterval = 120; // 2分钟上报一次 + + UpdateAllValues(); + } + catch (Exception e) + { + Crashlytics.LogException(e); + } + } + + #region 各ID上报信息 + + /// + /// 设置用户ID + /// + private static void SetUid() + { + if (_hasGotUid) return; + + if (!string.IsNullOrEmpty(IPMConfig.IPM_UID)) + { + Debug.Log($"---[ANA] UID: {IPMConfig.IPM_UID}"); + GuruAnalytics.SetUid(IPMConfig.IPM_UID); + _hasGotUid = true; + } + + } + + /// + /// 设置设备ID + /// + private static void SetDeviceId() + { + if (_hasGotDeviceId) return; + + if (!string.IsNullOrEmpty(IPMConfig.IPM_DEVICE_ID)) + { + GuruAnalytics.SetDeviceId(IPMConfig.IPM_DEVICE_ID); + _hasGotDeviceId = true; + } + } + + /// + /// 设置 AdjustId + /// + private static void SetAdjustId() + { + if (_hasGotAdjustId) return; + + string adjustId = Adjust.getAdid(); + if (!string.IsNullOrEmpty(adjustId) + && string.IsNullOrEmpty(IPMConfig.ADJUST_ID)) + { + IPMConfig.ADJUST_ID = adjustId; + } + + if (!string.IsNullOrEmpty(IPMConfig.ADJUST_ID)) + { + GuruAnalytics.SetAdjustId(IPMConfig.ADJUST_ID); + _hasGotAdjustId = true; + } + else + { + Debug.Log($"--- [ANA] AdjustId is Empty.."); + } + + } + + /// + /// 设置 AdId + /// + private static void SetAdId() + { + if (_hasGotAdId) return; + + if (!string.IsNullOrEmpty(IPMConfig.ADJUST_ADID)) + { + GuruAnalytics.SetAdId(IPMConfig.ADJUST_ADID); + _hasGotAdId = true; + } + else + { +#if UNITY_ANDROID + Adjust.getGoogleAdId(adId => + { + if (!string.IsNullOrEmpty(adId) + && string.IsNullOrEmpty(IPMConfig.ADJUST_ADID)) + { + // Debug.Log($"---[ANA] ADID: {adId}"); + IPMConfig.ADJUST_ADID = adId; + } + }); +#else + // ============= ADID is not supported on Adjust iOS API ============== + IPMConfig.ADJUST_ADID = VALUE_NOT_FOR_IOS; + GuruAnalytics.SetAdId(IPMConfig.ADJUST_ADID); +#endif + } + } + + /// + /// 设置FirebaseId + /// + private static void SetFirebaseId() + { + if (_hasGotFirebaseId) return; + + if (!string.IsNullOrEmpty(IPMConfig.FIREBASE_ID)) + { + GuruAnalytics.SetFirebaseId(IPMConfig.FIREBASE_ID); + _hasGotFirebaseId = true; + } + else + { + FetchFirebaseId(); + } + } + + /// + /// 获取FirebaseID + /// + private static void FetchFirebaseId() + { + FirebaseAnalytics.GetAnalyticsInstanceIdAsync() + .ContinueWithOnMainThread(task => + { + if (task != null && task.IsCompleted) + { + var fid = task.Result; + if (!string.IsNullOrEmpty(fid) + && string.IsNullOrEmpty(IPMConfig.FIREBASE_ID)) + { + IPMConfig.FIREBASE_ID = fid; + } + } + }); + } + + + +#if UNITY_IOS + /// + /// 更新ATT状态 (Only IOS 有效) + /// + private static void SetATTStatus() + { + string status = ATTManager.GetStatus(); + GuruAnalytics.SetUserProperty(ParameterATTStatus, status); + } +#endif + + + + + /// + /// 获取全部ID + /// + private static void UpdateAllValues() + { + SetUid(); + SetDeviceId(); + SetAdjustId(); + SetFirebaseId(); + SetAdId(); +#if UNITY_IOS + SetATTStatus(); +#endif + ReportEventSuccessRate(); + } + + /// + /// 上报事件成功率 + /// + private static void ReportEventSuccessRate() + { + var interval = (DateTime.Now - _lastReportRateDate).TotalSeconds; + if (interval > _reportSuccessInterval) + { + GuruAnalytics.ReportEventSuccessRate(); + _lastReportRateDate = DateTime.Now; + } + } + + #endregion + + #region 自定义打点 + + /// + /// 自定义设置用户属性 + /// + /// + /// + private static void CustomSetUserProperty(string key, string value) + { + try + { + GuruAnalytics.SetUserProperty(key, value); + UpdateAllValues(); // 同步所有的ID + } + catch (Exception e) + { + Crashlytics.LogException(e); + } + + } + + /// + /// 自定义事件打点 + /// + /// + /// + private static void CustomLogEvent(string key, Dictionary data = null) + { + try + { + if (data == null) data = new Dictionary(); + GuruAnalytics.LogEvent(key, data); + UpdateAllValues(); // 同步所有的ID + } + catch (Exception e) + { + Crashlytics.LogException(e); + } + } + + /// + /// 设置太极02阀值 + /// + /// + public static void SetTch02Value(double value) + { + try + { + if (Math.Abs(_tch02TargetValue - value) > 0.001d) + { + _tch02TargetValue = value; + GuruAnalytics.SetTch02Value(value); + } + } + catch (Exception e) + { + Crashlytics.LogException(e); + } + } + + #endregion + } +} \ No newline at end of file diff --git a/Runtime/GuruCore/Runtime/Analytics/Analytics.Custom.cs.meta b/Runtime/GuruCore/Runtime/Analytics/Analytics.Custom.cs.meta new file mode 100644 index 0000000..e055b0c --- /dev/null +++ b/Runtime/GuruCore/Runtime/Analytics/Analytics.Custom.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 263ad4d8b3194345b4d1004402bb9e93 +timeCreated: 1681889038 \ No newline at end of file diff --git a/Runtime/GuruCore/Runtime/Analytics/Analytics.FirebaseDefine.cs b/Runtime/GuruCore/Runtime/Analytics/Analytics.FirebaseDefine.cs new file mode 100644 index 0000000..643c877 --- /dev/null +++ b/Runtime/GuruCore/Runtime/Analytics/Analytics.FirebaseDefine.cs @@ -0,0 +1,210 @@ +using Firebase.Analytics; + +namespace Guru +{ + //https://firebase.google.com/docs/reference/cpp/group/parameter-names + //Firebase内置定义事件名称和参数名称 + public static partial class Analytics + { + private static string EventAdImpression => + FirebaseUtil.IsFirebaseInitialized ? FirebaseAnalytics.EventAdImpression : "ad_impression"; + private static string EventAddPaymentInfo => + FirebaseUtil.IsFirebaseInitialized ? FirebaseAnalytics.EventAddPaymentInfo : "add_payment_info"; + private static string EventAddShippingInfo => + FirebaseUtil.IsFirebaseInitialized ? FirebaseAnalytics.EventAddShippingInfo : "add_shipping_info"; + private static string EventAddToCart => + FirebaseUtil.IsFirebaseInitialized ? FirebaseAnalytics.EventAddToCart : "add_to_cart"; + private static string EventAddToWishlist => + FirebaseUtil.IsFirebaseInitialized ? FirebaseAnalytics.EventAddToWishlist : "add_to_wishlist"; + private static string EventAppOpen => + FirebaseUtil.IsFirebaseInitialized ? FirebaseAnalytics.EventAppOpen : "app_open"; + private static string EventBeginCheckout => + FirebaseUtil.IsFirebaseInitialized ? FirebaseAnalytics.EventBeginCheckout : "begin_checkout"; + private static string EventCampaignDetails => + FirebaseUtil.IsFirebaseInitialized ? FirebaseAnalytics.EventCampaignDetails : "campaign_details"; + private static string EventEarnVirtualCurrency => + FirebaseUtil.IsFirebaseInitialized ? FirebaseAnalytics.EventEarnVirtualCurrency : "earn_virtual_currency"; + private static string EventGenerateLead => + FirebaseUtil.IsFirebaseInitialized ? FirebaseAnalytics.EventGenerateLead : "generate_lead"; + private static string EventJoinGroup => + FirebaseUtil.IsFirebaseInitialized ? FirebaseAnalytics.EventJoinGroup : "join_group"; + private static string EventLevelEnd => + FirebaseUtil.IsFirebaseInitialized ? FirebaseAnalytics.EventLevelEnd : "level_end"; + private static string EventLevelStart => + FirebaseUtil.IsFirebaseInitialized ? FirebaseAnalytics.EventLevelStart : "level_start"; + private static string EventLevelUp => + FirebaseUtil.IsFirebaseInitialized ? FirebaseAnalytics.EventLevelUp : "level_up"; + private static string EventLogin => + FirebaseUtil.IsFirebaseInitialized ? FirebaseAnalytics.EventLogin : "login"; + private static string EventPostScore => + FirebaseUtil.IsFirebaseInitialized ? FirebaseAnalytics.EventPostScore : "post_score"; + private static string EventPurchase => + FirebaseUtil.IsFirebaseInitialized ? FirebaseAnalytics.EventPurchase : "purchase"; + private static string EventRefund => + FirebaseUtil.IsFirebaseInitialized ? FirebaseAnalytics.EventRefund : "refund"; + private static string EventRemoveFromCart => + FirebaseUtil.IsFirebaseInitialized ? FirebaseAnalytics.EventRemoveFromCart : "remove_from_cart"; + private static string EventScreenView => + FirebaseUtil.IsFirebaseInitialized ? FirebaseAnalytics.EventScreenView : "screen_view"; + private static string EventSearch => + FirebaseUtil.IsFirebaseInitialized ? FirebaseAnalytics.EventSearch : "search"; + private static string EventSelectContent => + FirebaseUtil.IsFirebaseInitialized ? FirebaseAnalytics.EventSelectContent : "select_content"; + private static string EventSelectItem => + FirebaseUtil.IsFirebaseInitialized ? FirebaseAnalytics.EventSelectItem : "select_item"; + private static string EventSelectPromotion => + FirebaseUtil.IsFirebaseInitialized ? FirebaseAnalytics.EventSelectPromotion : "select_promotion"; + private static string EventShare => + FirebaseUtil.IsFirebaseInitialized ? FirebaseAnalytics.EventShare : "share"; + private static string EventSignUp => + FirebaseUtil.IsFirebaseInitialized ? FirebaseAnalytics.EventSignUp : "sign_up"; + private static string EventSpendVirtualCurrency => + FirebaseUtil.IsFirebaseInitialized ? FirebaseAnalytics.EventSpendVirtualCurrency : "spend_virtual_currency"; + private static string EventTutorialBegin => + FirebaseUtil.IsFirebaseInitialized ? FirebaseAnalytics.EventTutorialBegin : "tutorial_begin"; + private static string EventTutorialComplete => + FirebaseUtil.IsFirebaseInitialized ? FirebaseAnalytics.EventTutorialComplete : "tutorial_complete"; + private static string EventUnlockAchievement => + FirebaseUtil.IsFirebaseInitialized ? FirebaseAnalytics.EventUnlockAchievement : "unlock_achievement"; + private static string EventViewCart => + FirebaseUtil.IsFirebaseInitialized ? FirebaseAnalytics.EventViewCart : "view_cart"; + private static string EventViewItem => + FirebaseUtil.IsFirebaseInitialized ? FirebaseAnalytics.EventViewItem : "view_item"; + private static string EventViewItemList => + FirebaseUtil.IsFirebaseInitialized ? FirebaseAnalytics.EventViewItemList : "view_item_list"; + private static string EventViewPromotion => + FirebaseUtil.IsFirebaseInitialized ? FirebaseAnalytics.EventViewPromotion : "view_promotion"; + private static string EventViewSearchResults => + FirebaseUtil.IsFirebaseInitialized ? FirebaseAnalytics.EventViewSearchResults : "view_search_results"; + private static string ParameterAchievementId => + FirebaseUtil.IsFirebaseInitialized ? FirebaseAnalytics.ParameterAchievementId : "achievement_id"; + private static string ParameterAdFormat => + FirebaseUtil.IsFirebaseInitialized ? FirebaseAnalytics.ParameterAdFormat : "ad_format"; + private static string ParameterAdNetworkClickID => + FirebaseUtil.IsFirebaseInitialized ? FirebaseAnalytics.ParameterAdNetworkClickID : "aclid"; + private static string ParameterAdPlatform => + FirebaseUtil.IsFirebaseInitialized ? FirebaseAnalytics.ParameterAdPlatform : "ad_platform"; + private static string ParameterAdSource => + FirebaseUtil.IsFirebaseInitialized ? FirebaseAnalytics.ParameterAdSource : "ad_source"; + private static string ParameterAdUnitName => + FirebaseUtil.IsFirebaseInitialized ? FirebaseAnalytics.ParameterAdUnitName : "ad_unit_name"; + private static string ParameterAffiliation => + FirebaseUtil.IsFirebaseInitialized ? FirebaseAnalytics.ParameterAffiliation : "affiliation"; + private static string ParameterCP1 => + FirebaseUtil.IsFirebaseInitialized ? FirebaseAnalytics.ParameterCP1 : "cp1"; + private static string ParameterCampaign => + FirebaseUtil.IsFirebaseInitialized ? FirebaseAnalytics.ParameterCampaign : "campaign"; + private static string ParameterCharacter => + FirebaseUtil.IsFirebaseInitialized ? FirebaseAnalytics.ParameterCharacter : "character"; + private static string ParameterContent => + FirebaseUtil.IsFirebaseInitialized ? FirebaseAnalytics.ParameterContent : "content"; + private static string ParameterContentType => + FirebaseUtil.IsFirebaseInitialized ? FirebaseAnalytics.ParameterContentType : "content_type"; + private static string ParameterCoupon => + FirebaseUtil.IsFirebaseInitialized ? FirebaseAnalytics.ParameterCoupon : "coupon"; + private static string ParameterCreativeName => + FirebaseUtil.IsFirebaseInitialized ? FirebaseAnalytics.ParameterCreativeName : "creative_name"; + private static string ParameterCreativeSlot => + FirebaseUtil.IsFirebaseInitialized ? FirebaseAnalytics.ParameterCreativeSlot : "creative_slot"; + private static string ParameterCurrency => + FirebaseUtil.IsFirebaseInitialized ? FirebaseAnalytics.ParameterCurrency : "currency"; + private static string ParameterDestination => + FirebaseUtil.IsFirebaseInitialized ? FirebaseAnalytics.ParameterDestination : "destination"; + private static string ParameterDiscount => + FirebaseUtil.IsFirebaseInitialized ? FirebaseAnalytics.ParameterDiscount : "discount"; + private static string ParameterEndDate => + FirebaseUtil.IsFirebaseInitialized ? FirebaseAnalytics.ParameterEndDate : "end_date"; + private static string ParameterExtendSession => + FirebaseUtil.IsFirebaseInitialized ? FirebaseAnalytics.ParameterExtendSession : "extend_session"; + private static string ParameterFlightNumber => + FirebaseUtil.IsFirebaseInitialized ? FirebaseAnalytics.ParameterFlightNumber : "flight_number"; + private static string ParameterGroupId => + FirebaseUtil.IsFirebaseInitialized ? FirebaseAnalytics.ParameterGroupId : "group_id"; + private static string ParameterIndex => + FirebaseUtil.IsFirebaseInitialized ? FirebaseAnalytics.ParameterIndex : "index"; + private static string ParameterItemBrand => + FirebaseUtil.IsFirebaseInitialized ? FirebaseAnalytics.ParameterItemBrand : "item_brand"; + private static string ParameterItemCategory => + FirebaseUtil.IsFirebaseInitialized ? FirebaseAnalytics.ParameterItemCategory : "item_category"; + private static string ParameterItemCategory2 => + FirebaseUtil.IsFirebaseInitialized ? FirebaseAnalytics.ParameterItemCategory2 : "item_category2"; + private static string ParameterItemCategory3 => + FirebaseUtil.IsFirebaseInitialized ? FirebaseAnalytics.ParameterItemCategory3 : "item_category3"; + private static string ParameterItemCategory4 => + FirebaseUtil.IsFirebaseInitialized ? FirebaseAnalytics.ParameterItemCategory4 : "item_category4"; + private static string ParameterItemCategory5 => + FirebaseUtil.IsFirebaseInitialized ? FirebaseAnalytics.ParameterItemCategory5 : "item_category5"; + private static string ParameterItemId => + FirebaseUtil.IsFirebaseInitialized ? FirebaseAnalytics.ParameterItemId : "item_id"; + private static string ParameterItemList => + FirebaseUtil.IsFirebaseInitialized ? FirebaseAnalytics.ParameterItemListName : "item_id"; + private static string ParameterItemListID => + FirebaseUtil.IsFirebaseInitialized ? FirebaseAnalytics.ParameterItemListID : "item_list_id"; + private static string ParameterItemListName => + FirebaseUtil.IsFirebaseInitialized ? FirebaseAnalytics.ParameterItemListName : "item_list_name"; + private static string ParameterItemName => + FirebaseUtil.IsFirebaseInitialized ? FirebaseAnalytics.ParameterItemName : "item_name"; + private static string ParameterLevel => + FirebaseUtil.IsFirebaseInitialized ? FirebaseAnalytics.ParameterLevel : "level"; + private static string ParameterLevelName => + FirebaseUtil.IsFirebaseInitialized ? FirebaseAnalytics.ParameterLevelName : "level_name"; + private static string ParameterLocation => + FirebaseUtil.IsFirebaseInitialized ? FirebaseAnalytics.ParameterLocation : "location"; + private static string ParameterLocationID => + FirebaseUtil.IsFirebaseInitialized ? FirebaseAnalytics.ParameterLocationID : "location_id"; + private static string ParameterMedium => + FirebaseUtil.IsFirebaseInitialized ? FirebaseAnalytics.ParameterMedium : "medium"; + private static string ParameterMethod => + FirebaseUtil.IsFirebaseInitialized ? FirebaseAnalytics.ParameterMethod : "method"; + private static string ParameterNumberOfNights => + FirebaseUtil.IsFirebaseInitialized ? FirebaseAnalytics.ParameterNumberOfNights : "number_of_nights"; + private static string ParameterNumberOfPassengers => + FirebaseUtil.IsFirebaseInitialized ? FirebaseAnalytics.ParameterNumberOfPassengers : "number_of_passengers"; + private static string ParameterNumberOfRooms => + FirebaseUtil.IsFirebaseInitialized ? FirebaseAnalytics.ParameterNumberOfRooms : "number_of_rooms"; + private static string ParameterOrigin => + FirebaseUtil.IsFirebaseInitialized ? FirebaseAnalytics.ParameterOrigin : "origin"; + private static string ParameterPaymentType => + FirebaseUtil.IsFirebaseInitialized ? FirebaseAnalytics.ParameterPaymentType : "payment_type"; + private static string ParameterPrice => + FirebaseUtil.IsFirebaseInitialized ? FirebaseAnalytics.ParameterPrice : "price"; + private static string ParameterPromotionID => + FirebaseUtil.IsFirebaseInitialized ? FirebaseAnalytics.ParameterPromotionID : "promotion_id"; + private static string ParameterPromotionName => + FirebaseUtil.IsFirebaseInitialized ? FirebaseAnalytics.ParameterPromotionName : "promotion_name"; + private static string ParameterQuantity => + FirebaseUtil.IsFirebaseInitialized ? FirebaseAnalytics.ParameterQuantity : "quantity"; + private static string ParameterScore => + FirebaseUtil.IsFirebaseInitialized ? FirebaseAnalytics.ParameterScore : "score"; + private static string ParameterScreenClass => + FirebaseUtil.IsFirebaseInitialized ? FirebaseAnalytics.ParameterScreenClass : "screen_class"; + private static string ParameterScreenName => + FirebaseUtil.IsFirebaseInitialized ? FirebaseAnalytics.ParameterScreenName : "screen_name"; + private static string ParameterSearchTerm => + FirebaseUtil.IsFirebaseInitialized ? FirebaseAnalytics.ParameterSearchTerm : "search_term"; + private static string ParameterShipping => + FirebaseUtil.IsFirebaseInitialized ? FirebaseAnalytics.ParameterShipping : "shipping"; + private static string ParameterShippingTier => + FirebaseUtil.IsFirebaseInitialized ? FirebaseAnalytics.ParameterShippingTier : "shipping_tier"; + private static string ParameterSignUpMethod => + FirebaseUtil.IsFirebaseInitialized ? FirebaseAnalytics.UserPropertySignUpMethod : "sign_up_method"; + private static string ParameterSource => + FirebaseUtil.IsFirebaseInitialized ? FirebaseAnalytics.ParameterSource : "source"; + private static string ParameterStartDate => + FirebaseUtil.IsFirebaseInitialized ? FirebaseAnalytics.ParameterStartDate : "start_date"; + private static string ParameterSuccess => + FirebaseUtil.IsFirebaseInitialized ? FirebaseAnalytics.ParameterSuccess : "success"; + private static string ParameterTax => + FirebaseUtil.IsFirebaseInitialized ? FirebaseAnalytics.ParameterTax : "tax"; + private static string ParameterTerm => + FirebaseUtil.IsFirebaseInitialized ? FirebaseAnalytics.ParameterTerm : "term"; + private static string ParameterTransactionId => + FirebaseUtil.IsFirebaseInitialized ? FirebaseAnalytics.ParameterTransactionId : "transaction_id"; + private static string ParameterTravelClass => + FirebaseUtil.IsFirebaseInitialized ? FirebaseAnalytics.ParameterTravelClass : "travel_class"; + private static string ParameterValue => + FirebaseUtil.IsFirebaseInitialized ? FirebaseAnalytics.ParameterValue : "value"; + private static string ParameterVirtualCurrencyName => + FirebaseUtil.IsFirebaseInitialized ? FirebaseAnalytics.ParameterVirtualCurrencyName : "virtual_currency_name"; + } +} \ No newline at end of file diff --git a/Runtime/GuruCore/Runtime/Analytics/Analytics.FirebaseDefine.cs.meta b/Runtime/GuruCore/Runtime/Analytics/Analytics.FirebaseDefine.cs.meta new file mode 100644 index 0000000..514c9ba --- /dev/null +++ b/Runtime/GuruCore/Runtime/Analytics/Analytics.FirebaseDefine.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: cd4885b58f7b4e9887ecfc82fbc26c19 +timeCreated: 1644303761 \ No newline at end of file diff --git a/Runtime/GuruCore/Runtime/Analytics/Analytics.Property.cs b/Runtime/GuruCore/Runtime/Analytics/Analytics.Property.cs new file mode 100644 index 0000000..1c63fcb --- /dev/null +++ b/Runtime/GuruCore/Runtime/Analytics/Analytics.Property.cs @@ -0,0 +1,40 @@ +using UnityEngine; + +namespace Guru +{ + public partial class Analytics + { + /// + /// B_Level 属性 + /// + public static int BLevel + { + get => PlayerPrefs.GetInt(nameof(BLevel), 0); + set => PlayerPrefs.SetInt(nameof(BLevel), value); + } + /// + /// B_Play 属性 + /// + public static int BPlay + { + get => PlayerPrefs.GetInt(nameof(BPlay), 0); + set => PlayerPrefs.SetInt(nameof(BPlay), value); + } + /// + /// 首次启动时间 + /// + public static string FirstOpenTime + { + get + { + var stamp = PlayerPrefs.GetString(nameof(FirstOpenTime), ""); + if (string.IsNullOrEmpty(stamp)) + { + stamp = TimeUtil.GetCurrentTimeStamp().ToString(); + PlayerPrefs.SetString(nameof(FirstOpenTime), stamp); + } + return stamp; + } + } + } +} \ No newline at end of file diff --git a/Runtime/GuruCore/Runtime/Analytics/Analytics.Property.cs.meta b/Runtime/GuruCore/Runtime/Analytics/Analytics.Property.cs.meta new file mode 100644 index 0000000..417e0e2 --- /dev/null +++ b/Runtime/GuruCore/Runtime/Analytics/Analytics.Property.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 014946ce01294795b37c33bcb844e90b +timeCreated: 1687234188 \ No newline at end of file diff --git a/Runtime/GuruCore/Runtime/Analytics/Analytics.TemplateDefine.cs b/Runtime/GuruCore/Runtime/Analytics/Analytics.TemplateDefine.cs new file mode 100644 index 0000000..585740c --- /dev/null +++ b/Runtime/GuruCore/Runtime/Analytics/Analytics.TemplateDefine.cs @@ -0,0 +1,587 @@ +using System; +using System.Collections.Generic; +using Facebook.Unity; +using UnityEngine; + +namespace Guru +{ + //游戏通用模版打点定义 + public static partial class Analytics + { + #region 游戏通用打点 + /// + /// 当玩家在游戏中升级时触发 + /// + /// level (等级)从1开始 标准点 + /// 升级的角色,如果没有可不选 + public static void LevelUp(int level, string character) + { + LogEvent(EventLevelUp, new Dictionary() + { + { ParameterLevel, level }, + { ParameterCharacter, character } + }); + } + + /// + /// 玩家已完成解锁成就时触发。 + /// + /// 这里的成就ID值项目方自行定义 + public static void UnlockAchievement(string achievementID) + { + LogEvent(EventUnlockAchievement, new Dictionary() + { + { ParameterAchievementId, achievementID }, + }); + } + + /// + /// 玩家已开始挑战某个关卡时触发。 + /// + /// 关卡名称 + /// 关卡数 + [Obsolete("Obsolete method, please use instead. will be discard in next version.")] + public static void LevelStart(int level) + { + LogEvent(EventLevelStart, new Dictionary() + { + { ParameterLevel, level }, + { ParameterItemCategory, "main" }, + }); + } + + /// + /// 玩家已开始挑战某个关卡时触发。 + /// + /// 关卡名称 + /// 关卡数 + /// + /// + /// + /// + public static void LogLevelStart(int level, string levelName, + string itemCategory = "main", string itemId = "", string startType = "play", bool isReplay = false) + { + LogEvent(EventLevelStart, new Dictionary() + { + { ParameterLevel, level }, + { ParameterLevelName, levelName }, + { ParameterItemCategory, itemCategory }, + { ParameterItemId, itemId }, + { ParameterStartType, startType }, + { ParameterReplay, isReplay }, + }); + } + + /// + /// 玩家已开始挑战某个关卡时触发。 + /// + /// 关卡类型(主关卡xx模式/每日挑战/活动1/活动2 等) + /// 棋局id /图片id + /// 关卡名称 + /// 关卡数 + [Obsolete("Obsolete method, please use instead. will be discard in next version.")] + public static void LevelStart(string itemCategory, string itemID, string levelName, int level, bool isReplay, bool isContinue) + { + + string startType = "play"; + if (isReplay) startType = "replay"; + if (isContinue) startType = "continue"; + + LogEvent(EventLevelStart, new Dictionary() + { + { ParameterItemCategory, itemCategory}, + { ParameterItemId, itemID}, + { ParameterLevelName, levelName }, + { ParameterLevel, level }, + { ParameterReplay, isReplay ? "true" : "false"}, + { ParameterContinue, isContinue ? "true" : "false"}, + { ParameterStartType, startType }, + }); + } + + + + /// + /// 关卡结束(Firebase标准事件) + /// + /// 关卡类型(主关卡xx模式/每日挑战/活动1/活动2 等) + /// 棋局id /图片id + /// 关卡名称 + /// 关卡数 + /// "此参数是对success参数的补充说明,除成功/失败/超时/退出这四种情况外,其余情况可自行定义值" + /// 游戏时长(单位:msec)【可选】 + /// 游戏步数【可选】 + /// 游戏分数【可选】 + /// 重玩标记【可选】 + public static void LevelEnd(string itemCategory, string itemID, string levelName, int level, + ELevelResult result, int? duration = null, int? step = null, int? score = null, bool isReplay = false) + { + var dict = new Dictionary() + { + { ParameterItemCategory, itemCategory }, + { ParameterItemId, itemID }, + { ParameterLevelName, levelName }, + { ParameterLevel, level }, + { ParameterSuccess, result == ELevelResult.success ? "true" : "false" }, + { ParameterResult, result.ToString() }, + { ParameterReplay, isReplay ? "true" : "false"}, + }; + + if (duration != null) + dict[ParameterDuration] = duration.Value; + if(step != null) + dict[ParameterStep] = step.Value; + if(score != null) + dict[ParameterScore] = score.Value; + + LogEvent(EventLevelEnd, dict); + + if (result == ELevelResult.success) + { + int lv = BPlay; + if (lv == 0) lv = level; + LevelEndSuccess(itemCategory, lv, itemID); + } + } + + + public static void LogLevelEnd(int level, string result, + string levelName = "", string itemCategory = "main", string itemId = "", + int? duration = null, int? step = null, int? score = null ) + { + bool isSuccess = result.Equals("success"); + + var dict = new Dictionary() + { + { ParameterItemCategory, itemCategory }, + { ParameterItemId, itemId }, + { ParameterLevelName, levelName }, + { ParameterLevel, level }, + { ParameterSuccess, isSuccess ? "true" : "false" }, + { ParameterResult, result }, + }; + + if (duration != null) + dict[ParameterDuration] = duration.Value; + if(step != null) + dict[ParameterStep] = step.Value; + if(score != null) + dict[ParameterScore] = score.Value; + + LogEvent(EventLevelEnd, dict); + + if (isSuccess) + { + int lv = BPlay; + if (lv == 0) lv = level; + LevelEndSuccess(itemCategory, lv, itemId); + } + } + + + + /// + /// 新用户通过第几关(仅记录前n关,根据项目自行确定,不区分关卡类型)[买量用] + /// + /// + /// 关卡(从1开始) + /// /// 棋局id /图片id + public static void LevelEndSuccess(string itemCategory, int level, string itemID) + { + if (level > GuruSettings.Instance.AnalyticsSetting.LevelEndSuccessNum) + return; + + string eventName = $"level_end_success_{level}"; + LogEvent(eventName,new Dictionary() + { + { ParameterItemId, itemID }, + { ParameterItemCategory, itemCategory} + }); + } + + /// + /// 第一次通关打点 + /// + private static void LevelFirstEnd(string itemCategory, string itemID, string levelName, int level, + ELevelResult result, int? duration = null, int? step = null, int? score = null) + { + var dict = new Dictionary() + { + { ParameterItemCategory, itemCategory }, + { ParameterItemId, itemID }, + { ParameterLevelName, levelName }, + { ParameterLevel, level }, + { ParameterSuccess, result == ELevelResult.success ? 1 : 0 }, + { ParameterResult, result.ToString() }, + }; + + if (duration != null) + dict[ParameterDuration] = duration.Value; + if(step != null) + dict[ParameterStep] = step.Value; + if(score != null) + dict[ParameterScore] = score.Value; + } + + #endregion + + #region Coins + /// + /// 当用户获取了虚拟货币(金币、宝石、代币等)时触发 + /// + /// 虚拟货币的名称 + /// 虚拟货币的数量 + /// 金币获取的方式,通过IAP购买的方式,固定使用参数值,其余场景自行定义。 + /// 玩家当前剩余的虚拟货币数量 + /// 购买商品的product_id(购买时传参) + public static void EarnVirtualCurrency(string virtual_currency_name, int value, string item_category, int balance, string sku) + { + var dict = new Dictionary() + { + [ParameterVirtualCurrencyName] = virtual_currency_name, + [ParameterValue] = value, + [ParameterItemCategory] = item_category, + ["balance"] = balance, + ["sku"] = sku, + }; + LogEvent(EventEarnVirtualCurrency, dict); + } + + /// + /// 当用户支出了虚拟货币(金币、宝石、代币等)时触发 + /// + /// 虚拟货币的名称 + /// 虚拟货币的数量 + /// 虚拟货币花费场景 + /// 玩家当前剩余的虚拟货币数量 + public static void SpendVirtualCurrency(string virtual_currency_name, int value, string item_category, int balance) + { + LogEvent(EventSpendVirtualCurrency, new Dictionary() + { + [ParameterVirtualCurrencyName] = virtual_currency_name, + [ParameterValue] = value, + [ParameterItemCategory] = item_category, + ["balance"] = balance, + }); + } + #endregion + + #region HP + /// + /// 体力变化时触发 + /// + /// 体力变化的场景 + /// 本次行为变化前体力 + /// 本次行为带来的体力 + /// 本次行为变化后体力 + public static void HitPoints(string item_category, int hp_before, int hp, int hp_after) + { + LogEvent("hit_points", new Dictionary() + { + [ParameterItemCategory] = item_category, + ["hp_before"] = hp_before, + ["hp"] = hp, + ["hp_after"] = hp_after, + }); + } + #endregion + + #region Tch 太极打点逻辑 + + private static double _tch001MaxValue = 5.0d; // 预设保护值, 如果大于这个值, 算作异常上报 + private static double _tch001TargetValue = 0.01d; + public static double Tch001TargetValue => _tch001TargetValue; // 太极 001 设定值 + + + private static double _tch02TargetValue = 0.20d; + public static double Tch02TargetValue => _tch02TargetValue; // 太极 02 设定值 + + private static string IAPPlatform => Application.platform == RuntimePlatform.IPhonePlayer ? "appstore" : "google_play"; + public static bool EnableTch02Event { get; set; } = false; // 是否使用太极02事件(请手动开启) + + /// + /// 太极001 IAP收入 + /// 每发生一次iap收入,触发一次该事件,value值为本次iap的收入值; + /// + /// 中台返回地收入值 + public static void Tch001IAPRev(double value) + { + TchRevEvent(EventTchAdRev001Impression, IAPPlatform, value); + + if(EnableTch02Event) return; // 如果使用了太极02 则不做FB上报 + FBPurchase(value, USD, "iap", IAPPlatform); + } + + /// + /// 太极02 IAP 收入打点 + /// 发生一次iap收入,触发一次该事件,value值为本次iap的收入值; + /// + /// + public static void Tch02IAPRev(double value) + { + if (!EnableTch02Event) return; + + TchRevEvent(EventTchAdRev02Impression, IAPPlatform, value); + + FBPurchase(value, USD, "iap", IAPPlatform); + } + + /// + /// "1.广告收入累计超过0.01美元,触发一次该事件,重新清零后,开始下一次累计计算; + /// + /// + public static void Tch001ADRev(double value) + { + if (value > _tch001MaxValue) + { + TchAdAbnormalEvent(value); // 上报异常值 + return; + } + + if (value < Tch001TargetValue) value = Tch001TargetValue; // TCH广告添加0值校验修复, 不得小于0.01 + TchRevEvent(EventTchAdRev001Impression, AdMAX, value); + + if(EnableTch02Event) return; // 如果使用了太极02 则不做FB上报 + + //FB标准购买事件 + FBPurchase(value, USD, "ads", AdMAX); + } + + /// + /// "1.5 广告收入累计超过0.2美元,触发一次该事件,重新清零后,开始下一次累计计算; + /// + /// + public static void Tch02ADRev(double value) + { + if (!EnableTch02Event) return; + + if (value < Tch02TargetValue) value = Tch02TargetValue; // TCH广告添加0值校验修复 + TchRevEvent(EventTchAdRev02Impression, AdMAX, value); + + //FB标准购买事件 + FBPurchase(value, USD, "ads", AdMAX); + } + + + /// + /// 太极事件点位上报 + /// + /// + /// + /// + private static void TchRevEvent(string evtName, string platform, double value) + { + LogEvent(evtName, new Dictionary() + { + { ParameterAdPlatform, platform }, + { ParameterCurrency, USD }, + { ParameterValue, value }, + }); + } + + /// + /// Facebook 支付上报 + /// + /// + /// + /// + /// + public static void FBPurchase(decimal revenue, string currency, string type, string platfrom) + { + FB.LogPurchase(revenue, currency, new Dictionary() + { + { AppEventParameterName.Currency, USD }, + { AppEventParameterName.ContentType, type }, + { ParameterAdPlatform, platfrom}, + }); + } + + /// + /// Facebook 支付上报 + /// + /// + /// + /// + /// + public static void FBPurchase(double value, string currency, string type, string platfrom) + => FBPurchase(Convert.ToDecimal(value), currency, type, platfrom); + + /// + /// Google ARO买量点 + /// + /// 广告收入数据 + public static void ADImpression(MaxSdkBase.AdInfo impressionData) + { + double revenue = impressionData.Revenue; + LogEvent(EventAdImpression, new Dictionary() + { + [ParameterAdPlatform] = AdMAX, + [ParameterAdSource] = impressionData.NetworkName, + [ParameterAdUnitName] = impressionData.AdUnitIdentifier, + [ParameterAdFormat] = impressionData.AdFormat, + [ParameterValue] = revenue, + [ParameterCurrency] = USD, + }); + } + + public static void TchAdAbnormalEvent(double value) + { + LogEvent(EventTchAdRevAbnormal, new Dictionary() + { + { ParameterAdPlatform, AdMAX }, + { ParameterCurrency, USD }, + { ParameterValue, value }, + }); + } + + #endregion + + #region Analytics Game IAP 游戏内购打点 + + /// + /// app 内弹出的付费引导 + /// + /// 界面跳转的来源 + /// product id,多个产品用逗号分隔,第一个商品id放主推商品id + public static void IAPImp(string itemCategory, string productID) + { + LogEvent(EventIAPImp, new Dictionary() + { + { ParameterItemCategory, itemCategory }, + { ParameterItemName, productID }, + }); + } + + /// + /// app 内弹出的付费引导 + /// + /// 界面跳转的来源 + /// product id,多个产品用逗号分隔,第一个商品id放主推商品id + public static void IAPClose(string itemCategory, string productID) + { + LogEvent(EventIAPClose, new Dictionary() + { + { ParameterItemCategory, itemCategory }, + { ParameterItemName, productID }, + }); + } + + /// + /// app 内弹出的付费引导 + /// + /// 界面跳转的来源 + /// product id,多个产品用逗号分隔,第一个商品id放主推商品id + public static void IAPClick(string itemCategory, string productID) + { + LogEvent(EventIAPClick, new Dictionary() + { + { ParameterItemCategory, itemCategory }, + { ParameterItemName, productID }, + }); + } + + /// + /// "app 内弹出的付费引导IAP付费或试用成功打点" + /// + /// 界面跳转的来源 + /// product id,多个产品用逗号分隔,第一个商品id放主推商品id + /// 产品的价格 + /// 用户的付费币种 + /// 付费类型订阅/产品(subscription/product) + /// 是否为试用(1:试用,0:付费) + public static void IAPRetTrue(string itemCategory, string productID, double value, string currency, string type, bool isfree) + { + LogEvent(EventIAPReturnTrue, new Dictionary() + { + { ParameterItemCategory, itemCategory }, + { ParameterItemName, productID }, + { ParameterValue, value }, + { ParameterCurrency, currency }, + { "type", type}, + { "isfree", isfree ? 1 : 0 }, + }); + } + + /// + /// "app 内弹出的付费引导IAP付费或试用失败打点" + /// + /// 界面跳转的来源 + /// product id,多个产品用逗号分隔,第一个商品id放主推商品id + public static void IAPRetFalse(string itemCategory, string productID, string failReason) + { + LogEvent(EventIAPReturnFalse, new Dictionary() + { + { ParameterItemCategory, itemCategory }, + { ParameterItemName, productID }, + }); + } + + /// + /// 新用户首次 IAP 付费成功上报 (仅限应用内付费商品,不包含订阅等其它情况)【买量打点】 + /// + /// product_id 商品ID + /// 付费总金额 + /// 币种 + public static void FirstIAP(string itemName, double value, string currency) + { + LogEvent(EventIAPFirst, new Dictionary() + { + { ParameterItemName, itemName }, + { ParameterValue, value }, + { ParameterCurrency, currency }, + }); + } + + /// + /// 商品购买成功上报【买量打点】 + /// + /// 商品名称(商品ID一样) + /// product_id 商品ID + /// 付费总金额 + /// 币种 + public static void ProductIAP(string productName, string itemName, double value, string currency) + { + // 替换SKU中的 "." -> "_", 比如: "do.a.iapc.coin.100" 转换为 "do_a_iapc_coin_100" + if (productName.Contains(".")) productName = productName.Replace(".", "_"); + + string eventName = $"iap_{productName}"; + LogEvent(eventName, new Dictionary() + { + { ParameterItemName, itemName }, + { ParameterValue, value }, + { ParameterCurrency, currency }, + }); + } + + #endregion + + #region IAP_PURCHASE + + /// + /// IAP 内购上报 + /// + /// + /// + public static void IAPPurchase(float value, string productId) + { + IAPPurchaseReport(EventIAPPurchase, value, productId); + } + + private static void IAPPurchaseReport(string eventName, float value, string productId) + { + LogEvent(eventName, new Dictionary() + { + [ParameterAdPlatform] = IAPPlatform, + [ParameterPlatform] = IAPPlatform, + [ParameterCurrency] = USD, + [ParameterValue] = value, + [ParameterProductId] = productId, + }, new EventSetting() { EnableFirebaseAnalytics = true }); + } + + #endregion + + } +} + diff --git a/Runtime/GuruCore/Runtime/Analytics/Analytics.TemplateDefine.cs.meta b/Runtime/GuruCore/Runtime/Analytics/Analytics.TemplateDefine.cs.meta new file mode 100644 index 0000000..cf1b0de --- /dev/null +++ b/Runtime/GuruCore/Runtime/Analytics/Analytics.TemplateDefine.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 521ec82a2e754e25b580b4e9da9306df +timeCreated: 1693384660 \ No newline at end of file diff --git a/Runtime/GuruCore/Runtime/Analytics/Analytics.cs b/Runtime/GuruCore/Runtime/Analytics/Analytics.cs new file mode 100644 index 0000000..3654715 --- /dev/null +++ b/Runtime/GuruCore/Runtime/Analytics/Analytics.cs @@ -0,0 +1,238 @@ +using System; +using System.Collections.Generic; +using com.adjust.sdk; +using Facebook.Unity; +using Firebase.Analytics; + +namespace Guru +{ + //打点模块初始化和基础接口封装 + public static partial class Analytics + { + public class EventSetting + { + public bool EnableFirebaseAnalytics; + public bool EnableFacebookAnalytics; + public bool EnableAdjustAnalytics; + } + + private static EventSetting _defaultEventSetting; + + private static bool _isInited; //Analytics是否初始化完成 + public static bool EnableDebugAnalytics; //允许Debug包上报打点 + + private static bool IsEnable + { + get + { + //Firebase服务没有初始化完成不上报打点 + if (!FirebaseUtil.IsFirebaseInitialized) + return false; + + //Analytics没有初始化不上报打点 + if (!_isInited) + return false; + + //开发环境打点不上报 + if (PlatformUtil.IsDebug() && !EnableDebugAnalytics) + return false; + + return true; + } + } + + + + #region 初始化 + + public static void InitAnalytics() + { + if (_isInited) + return; + + _isInited = true; + if (_defaultEventSetting == null) + { + var analyticsSetting = GuruSettings.Instance.AnalyticsSetting; + _defaultEventSetting = new EventSetting + { + EnableFirebaseAnalytics = analyticsSetting.EnalbeFirebaseAnalytics, + EnableFacebookAnalytics = analyticsSetting.EnalbeFacebookAnalytics, + EnableAdjustAnalytics = analyticsSetting.EnalbeAdjustAnalytics + }; + } + + if (_defaultEventSetting.EnableFirebaseAnalytics) + { + FirebaseAnalytics.SetAnalyticsCollectionEnabled(true); + FirebaseAnalytics.SetSessionTimeoutDuration(new TimeSpan(0, 30, 0)); + SetUserProperty(FirebaseAnalytics.UserPropertySignUpMethod, "Google"); + SetUserProperty(PropertyDeviceID, IPMConfig.IPM_DEVICE_ID); + SetUserProperty(PropertyFirstOpenTime, FirstOpenTime); + } + } + + #endregion + + #region 屏幕(场景)名称 + + public static void SetCurrentScreen(string screenName, string className) + { + Log.I(TAG,$"SetCurrentScreen -> screenName:{screenName}, className:{className}"); + GuruAnalytics.SetScreen(screenName); + + if (!IsEnable) return; + FirebaseAnalytics.LogEvent(FirebaseAnalytics.EventScreenView, + new Parameter(FirebaseAnalytics.ParameterScreenClass, className), + new Parameter(FirebaseAnalytics.ParameterScreenName, screenName) + ); + + + } + + #endregion + + #region 用户属性上报 + + /// + /// Firebase上报用户ID + /// + /// 通过Auth认证地用户ID + public static void SetUserIDProperty(string userID) + { + Log.I(TAG,$"SetUserIDProperty -> userID:{userID}"); + if (!IsEnable) return; + + FirebaseAnalytics.SetUserId(userID); + } + + /// + /// Firebase上报用户属性 + /// + public static void SetUserProperty(string propertyName, string propertyValue) + { + Log.I(TAG,$"SetUserProperty -> propertyName:{propertyName}, propertyValue:{propertyValue}"); + + if (!IsEnable) + return; + + FirebaseAnalytics.SetUserProperty(propertyName, propertyValue); + CustomSetUserProperty(propertyName, propertyValue); + } + + #endregion + + #region 打点上报 + + /// + /// 打点上报 + /// + /// + /// + internal static void LogEvent(string eventName, EventSetting eventSetting = null) + { + Log.I(TAG, $"eventName:{eventName}"); + CustomLogEvent(eventName); // 自定义打点上报 + + if (!IsEnable) return; + + eventSetting ??= _defaultEventSetting; + + if (eventSetting.EnableFirebaseAnalytics) + { + FirebaseAnalytics.LogEvent(eventName); + } + + if (eventSetting.EnableFacebookAnalytics) + { + FB.LogAppEvent(eventName); + } + + if (eventSetting.EnableAdjustAnalytics) + { + Adjust.trackEvent(CreateAdjustEvent(eventName)); + } + } + + /// + /// 打点上报 (带参数) + /// + /// + /// + /// + internal static void LogEvent(string eventName, Dictionary extras, EventSetting eventSetting = null) + { + Log.I(TAG, $"eventName:{eventName}, params:{string.Join(",", extras)}"); + CustomLogEvent(eventName, extras); // 自定义打点上报 + + if (!IsEnable) return; + + eventSetting ??= _defaultEventSetting; + if (eventSetting.EnableFirebaseAnalytics) + { + List parameters = new List(); + foreach (var kv in extras) + { + if(kv.Value is string strValue) + parameters.Add(new Parameter(kv.Key, strValue)); + else if (kv.Value is int intValue) + parameters.Add(new Parameter(kv.Key, intValue)); + else if (kv.Value is long longValue) + parameters.Add(new Parameter(kv.Key, longValue)); + else if (kv.Value is float floatValue) + parameters.Add(new Parameter(kv.Key, floatValue)); + else if (kv.Value is double doubleValue) + parameters.Add(new Parameter(kv.Key, doubleValue)); + else if (kv.Value is decimal decimalValue) + parameters.Add(new Parameter(kv.Key, decimal.ToDouble(decimalValue))); + } + + FirebaseAnalytics.LogEvent(eventName, parameters.ToArray()); + } + + Dictionary dict = new Dictionary(extras); + if (eventSetting.EnableFacebookAnalytics) + { + FB.LogAppEvent(eventName, null, dict); + } + + if (eventSetting.EnableAdjustAnalytics) + { + AdjustEvent adjustEvent = Analytics.CreateAdjustEvent(eventName); + if (adjustEvent != null) + { + foreach (var kv in dict) + { + adjustEvent.AddEventParameter(kv.Key, kv.Value.ToString()); + } + Adjust.trackEvent(adjustEvent); + } + } + } + + #endregion + + #region 通用打点 + + /// + /// 一般的事件上报通用接口 + /// + /// + /// + public static void Track(string key, Dictionary data = null) + { + if (null != data) + { + LogEvent(key, data); + } + else + { + LogEvent(key); + } + } + + + #endregion + + } +} \ No newline at end of file diff --git a/Runtime/GuruCore/Runtime/Analytics/Analytics.cs.meta b/Runtime/GuruCore/Runtime/Analytics/Analytics.cs.meta new file mode 100644 index 0000000..9d77115 --- /dev/null +++ b/Runtime/GuruCore/Runtime/Analytics/Analytics.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 358671fab408440c93eb79e488772d00 +timeCreated: 1632389219 \ No newline at end of file diff --git a/Runtime/GuruCore/Runtime/Common.meta b/Runtime/GuruCore/Runtime/Common.meta new file mode 100644 index 0000000..8903ede --- /dev/null +++ b/Runtime/GuruCore/Runtime/Common.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 09e765ba42ec4250b846c4d4a9c880c5 +timeCreated: 1677717898 \ No newline at end of file diff --git a/Runtime/GuruCore/Runtime/Common/GuruSDKCallback.cs b/Runtime/GuruCore/Runtime/Common/GuruSDKCallback.cs new file mode 100644 index 0000000..dfc499c --- /dev/null +++ b/Runtime/GuruCore/Runtime/Common/GuruSDKCallback.cs @@ -0,0 +1,67 @@ +namespace Guru +{ + using UnityEngine; + using System; + + /// + /// SDK回调实体 + /// + public class GuruSDKCallback: MonoBehaviour + { + public const string ObjectName = "GuruCallback"; + public const string MethodName = nameof(OnCallback); + + + private event Action msgCallback; + + private static GuruSDKCallback _instance; + public static GuruSDKCallback Instance + { + get + { + if (_instance == null) + { + _instance = Create(); + } + return _instance; + } + } + + /// + /// 创建对象 + /// + /// + private static GuruSDKCallback Create() + { + var go = new GameObject(); + go.name = ObjectName; + DontDestroyOnLoad(go); + var ins = go.AddComponent(); + return ins; + } + + private void SetCallback(Action callback) + { + msgCallback += callback; + } + + /// + /// External 回调参数 + /// + /// + public void OnCallback(string message) + { + msgCallback?.Invoke(message); + } + + /// + /// 添加回调 + /// + /// + public static void AddCallback(Action callback) + { + Instance.SetCallback(callback); + } + + } +} \ No newline at end of file diff --git a/Runtime/GuruCore/Runtime/Common/GuruSDKCallback.cs.meta b/Runtime/GuruCore/Runtime/Common/GuruSDKCallback.cs.meta new file mode 100644 index 0000000..5ff2261 --- /dev/null +++ b/Runtime/GuruCore/Runtime/Common/GuruSDKCallback.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 529d399085b04f0d92c95109aaef4835 +timeCreated: 1703304107 \ No newline at end of file diff --git a/Runtime/GuruCore/Runtime/Common/JsonParser.cs b/Runtime/GuruCore/Runtime/Common/JsonParser.cs new file mode 100644 index 0000000..c044c7a --- /dev/null +++ b/Runtime/GuruCore/Runtime/Common/JsonParser.cs @@ -0,0 +1,96 @@ + + +namespace Guru +{ + using System; + using UnityEngine; + using Guru.LitJson; + + public static class JsonParser + { + /// + /// 判断是否是合法的JSON + /// + /// + /// + public static bool IsValidJson(string jsonStr) + { + try + { + if (!string.IsNullOrEmpty(jsonStr)) + { + if (jsonStr.TrimStart().StartsWith("{") && jsonStr.TrimEnd().EndsWith("}")) + { + return true; + } + + if (jsonStr.TrimStart().StartsWith("[") && jsonStr.TrimEnd().EndsWith("]")) + { + return true; + } + } + } + catch (Exception e) + { + Log.Exception(e); + } + return false; + } + + /// + /// + /// + /// + /// + /// + public static T ToObject(string jsonStr) + { + if (IsValidJson(jsonStr)) + { + try + { + return JsonMapper.ToObject(jsonStr); + } + catch (Exception e) + { + Log.Exception(e); + } + } + return default(T); + } + + /// + /// 转化为JSON字符串 + /// + /// + /// + /// + public static string ToJson(object obj, bool prettyFormat = false) + { + try + { + if (!prettyFormat) + { + return JsonMapper.ToJson(obj); + } + else + { + JsonWriter writer = new JsonWriter() + { + IndentValue = 2, + PrettyPrint = true, + }; + JsonMapper.ToJson(obj, writer); + return writer.ToString(); + } + + } + catch (Exception e) + { + Log.Exception(e); + } + + return ""; + } + } +} \ No newline at end of file diff --git a/Runtime/GuruCore/Runtime/Common/JsonParser.cs.meta b/Runtime/GuruCore/Runtime/Common/JsonParser.cs.meta new file mode 100644 index 0000000..df8fb8b --- /dev/null +++ b/Runtime/GuruCore/Runtime/Common/JsonParser.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: e49994deb03446c49d4dd918df4c6870 +timeCreated: 1703504523 \ No newline at end of file diff --git a/Runtime/GuruCore/Runtime/Data.meta b/Runtime/GuruCore/Runtime/Data.meta new file mode 100644 index 0000000..57145eb --- /dev/null +++ b/Runtime/GuruCore/Runtime/Data.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 005347f128bb460f9312778f93e39a9b +timeCreated: 1679986193 \ No newline at end of file diff --git a/Runtime/GuruCore/Runtime/Data/StandardProperties.cs b/Runtime/GuruCore/Runtime/Data/StandardProperties.cs new file mode 100644 index 0000000..e757754 --- /dev/null +++ b/Runtime/GuruCore/Runtime/Data/StandardProperties.cs @@ -0,0 +1,206 @@ + +namespace Guru +{ + using System; + using System.Globalization; + using UnityEngine; + using System.IO; + + /// + /// 应用的全局标准属性 + /// + public static class StandardProperties + { + + #region 初始化 + + /// + /// 全局属性初始化 需要在应用启动时调用一次 + /// + public static void Init() + { + // 首次安装初始化各种参数 + if (IsFirstInstall) + { + string key = ""; + FirstInstallDate = DateTime.Now; + + key = nameof(SoundEffectEnabled); + if (!HasKey(key)) SoundEffectEnabled = true; + + key = nameof(VibrationEnabled); + if (!HasKey(key)) VibrationEnabled = true; + + key = nameof(FirstInstallVersion); + if (!HasKey(key)) FirstInstallVersion = FullVersion; + + Save(); + } + } + + private static bool IsFirstInstall => HasKey(nameof(FirstInstallDate)); + + private static bool HasKey(string key) => PlayerPrefs.HasKey(key); + + private static void Save() => PlayerPrefs.Save(); + + #endregion + + #region 标准属性值 + + /// + /// FirebaseId + /// + public static string FirebaseId + { + get => PlayerPrefs.GetString(nameof(FirebaseId), ""); + set { + if (!string.IsNullOrEmpty(value)) + { + PlayerPrefs.SetString(nameof(FirebaseId), value); + GuruAnalytics.SetFirebaseId(value); + } + } + } + + /// + /// Adjust ADID + /// + public static string AdjustId + { + get => PlayerPrefs.GetString(nameof(AdjustId), ""); + set { + if (!string.IsNullOrEmpty(value)) + { + PlayerPrefs.SetString(nameof(AdjustId), value); + GuruAnalytics.SetAdjustId(value); + } + } + } + + /// + /// Google ADID + /// + public static string GoogleAdId + { + get => PlayerPrefs.GetString(nameof(GoogleAdId), ""); + set { + if (!string.IsNullOrEmpty(value)) + { + PlayerPrefs.SetString(nameof(GoogleAdId), value); + GuruAnalytics.SetAdId(value); + } + } + } + + /// + /// 免费金币资源 + /// + public static int Coin + { + get => PlayerPrefs.GetInt(nameof(Coin), 0); + set => PlayerPrefs.SetInt(nameof(Coin), value); + } + + /// + /// 付费金币资源 + /// + public static int IAPCoin + { + get => PlayerPrefs.GetInt(nameof(IAPCoin), 0); + set => PlayerPrefs.SetInt(nameof(IAPCoin), value); + } + + /// + /// 用户累计购买次数 + /// + public static int PurchaseCount + { + get => PlayerPrefs.GetInt(nameof(PurchaseCount), 0); + set => PlayerPrefs.SetInt(nameof(PurchaseCount), value); + } + + /// + /// 首次安装日期 + /// + public static DateTime FirstInstallDate + { + get + { + var key = nameof(FirstInstallDate); + if (PlayerPrefs.HasKey(key)) + { + var value = PlayerPrefs.GetString(key, ""); + if (!string.IsNullOrEmpty(value)) return DateTime.Parse(value); + } + var now = DateTime.Now.ToUniversalTime(); + FirstInstallDate = now; + return now; + } + + set => PlayerPrefs.SetString(nameof(FirstInstallDate), + value.ToUniversalTime().ToString(CultureInfo.InvariantCulture)); + } + + /// + /// 首次安装版本号 + /// + public static string FirstInstallVersion + { + get => PlayerPrefs.GetString(nameof(FirstInstallVersion), ""); + set => PlayerPrefs.SetString(nameof(FirstInstallVersion), value); + } + + /// + /// 当前应用的版本 + /// + public static string AppVersion => Application.version; + + /// + /// 当前应用的版本Code + /// + public static string VersionCode + { + get + { + var path = $"{Application.streamingAssetsPath}/{GuruCore.VERSION_CODE_FILE}"; + if (File.Exists(path)) return File.ReadAllText(path); + return ""; + } + } + + /// + /// 完整版本号 + /// + public static string FullVersion + { + get + { + var code = VersionCode; + if (string.IsNullOrEmpty(code)) code = "unknown"; + return $"{AppVersion}-{code}"; + } + } + + /// + /// 音效开关 + /// + public static bool SoundEffectEnabled + { + get => PlayerPrefs.GetInt(nameof(SoundEffectEnabled), 0) == 1; + set => PlayerPrefs.SetInt(nameof(SoundEffectEnabled), value? 1: 0); + } + + /// + /// 震动开关 + /// + public static bool VibrationEnabled + { + get => PlayerPrefs.GetInt(nameof(VibrationEnabled), 0) == 1; + set => PlayerPrefs.SetInt(nameof(VibrationEnabled), value? 1: 0); + } + + #endregion + + } +} \ No newline at end of file diff --git a/Runtime/GuruCore/Runtime/Data/StandardProperties.cs.meta b/Runtime/GuruCore/Runtime/Data/StandardProperties.cs.meta new file mode 100644 index 0000000..66dffaf --- /dev/null +++ b/Runtime/GuruCore/Runtime/Data/StandardProperties.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 2b2215a925974a3593ff2c6db2cf02fc +timeCreated: 1679986312 \ No newline at end of file diff --git a/Runtime/GuruCore/Runtime/ExtensionKit.meta b/Runtime/GuruCore/Runtime/ExtensionKit.meta new file mode 100644 index 0000000..89001f4 --- /dev/null +++ b/Runtime/GuruCore/Runtime/ExtensionKit.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: f62ad544013645f4a845aa50793e3a12 +timeCreated: 1660299324 \ No newline at end of file diff --git a/Runtime/GuruCore/Runtime/ExtensionKit/CDNLoader.cs b/Runtime/GuruCore/Runtime/ExtensionKit/CDNLoader.cs new file mode 100644 index 0000000..b2e086a --- /dev/null +++ b/Runtime/GuruCore/Runtime/ExtensionKit/CDNLoader.cs @@ -0,0 +1,445 @@ + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using Firebase.Crashlytics; +using Guru; +using UnityEngine; +using UnityEngine.Networking; + +#if UNITY_EDITOR +using UnityEditor; +#endif + + +/// +/// CDN云控配置 +/// +[Serializable] +public class CDNConfig +{ + public const string KEY_CDN_CONFIG = "cdn_config"; + + public string[] replace; + public string main; + public string fallback; + public int retry; + public int timeout; + + private static CDNConfig _config; + + public static void Init(string defaultValue) + { + FirebaseUtil.AppendDefaultValue(KEY_CDN_CONFIG, defaultValue); + } + + /// + /// 加载配置 + /// + /// + public static CDNConfig Load() + { + if (_config != null) + return _config; + + try + { + _config = FirebaseUtil.GetRemoteConfig(KEY_CDN_CONFIG); + } + catch (Exception e) + { + Log.E(e); + Crashlytics.LogException(e); + } + return _config; + } +} + +public class CDNLoader : MonoBehaviour +{ + + /// + /// 受保护的URL参数列表 + /// + private static string[] protectedParamKeys = new string[] + { + "generation" + }; + + /// + /// 加载配置 + /// + private CDNConfig _config; + public CDNConfig Config => _config; + + /// + /// 使用备用地址 + /// + private bool _useFallback; + private int _retryNum; + private UnityWebRequest _request; + + private Action onError; + private Action onComplete; + private LinkedList _loadList; // 加载列表 + private bool _isOnLoading; + + #region 初始化 + + /// + /// 创建Loader + /// + /// + public static CDNLoader Create() + { + var go = new GameObject(nameof(CDNLoader)); + var loader = go.AddComponent(); + return loader; + } + + private void Awake() + { + Init(); + } + + private void Init() + { + _config = CDNConfig.Load(); + _isOnLoading = false; + _useFallback = false; + _loadList = new LinkedList(); + } + + /// + /// 获取链接地址 + /// + /// + /// + public string GetUrl(string originUrl, bool useFallback = false) + { + var prefix = useFallback ? _config.fallback : _config.main; + + if (_config.replace != null && _config.replace.Length > 0) + { + foreach (var rp in _config.replace) + { + if (originUrl.Contains(rp)) + { + originUrl = originUrl.Replace(rp, ""); + break; + } + } + } + + // 带有参数的URL地址过滤和拼接 + originUrl = FixUrlParameter(originUrl); + + return $"{prefix}{originUrl}"; + } + + /// + /// 过滤整理URL参数 + /// + /// + /// + public static string FixUrlParameter(string originUrl) + { + originUrl = originUrl.Replace("%2F", "/"); + + // 带有参数的URL地址过滤和拼接 + if (originUrl.Contains("?")) + { + + var raw = originUrl.Split('?'); + if (raw.Length > 1) + { + var args = ""; + var temp = raw[1].Split('&'); + if (temp.Length > 0) + { + for (int i = 0; i < temp.Length; i++) + { + if (temp[i].Contains("=")) + { + var kvp = temp[i].Split('='); + if (protectedParamKeys.Contains(kvp[0])) + { + args += $"{temp[i]}"; + if (i < temp.Length - 2) args += "&"; + } + } + } + } + + originUrl = raw[0]; + if (args.Length > 0) originUrl += $"?{args}"; + } + + } + + return originUrl; + } + + #endregion + + #region 队列管理 + + private int TaskCount + { + get + { + if (_loadList != null) + return _loadList.Count; + else + return -1; + } + } + + /// + /// 添加任务 + /// + /// + private void AddTask(LoadTask task, bool addFirst = false) + { + if (_loadList == null) + _loadList = new LinkedList(); + + if(addFirst) + _loadList.AddFirst(task); + else + _loadList.AddLast(task); + } + + /// + /// 获取任务 + /// + /// + private LoadTask GetTask() + { + if (TaskCount > 0) + { + var node = _loadList.First; + _loadList.RemoveFirst(); + return node.Value; + } + return null; + } + + #endregion + + #region 加载接口 + + /// + /// 加载文本 + /// + /// + /// + /// + /// + public void LoadText(string url, Action onComplete, Action onFail, Action onProgress = null) + { + AddTask(new LoadTask() + { + url = url, + onGetString = onComplete, + onFail = onFail, + onProgress = onProgress, + type = LoadType.Text + }); + } + + /// + /// 加载文本 + /// + /// + /// + /// + /// + public void LoadFile(string url, Action onComplete, Action onFail, Action onProgress = null) + { + AddTask(new LoadTask() + { + url = url, + onGetBytes = onComplete, + onFail = onFail, + onProgress = onProgress, + type = LoadType.Bytes + }); + } + + /// + /// 加载文本 + /// + /// + /// + /// + /// + public void LoadTexture2D(string url, Action onComplete, Action onFail, Action onProgress = null) + { + AddTask(new LoadTask() + { + url = url, + onGetTexture = onComplete, + onFail = onFail, + onProgress = onProgress, + type = LoadType.Bytes + }); + } + + #endregion + + #region 加载队列 + + /// + /// 请务必在主线程中实例化CDNLoader + /// + private void Update() + { + // 加载中则跳过 + if (_isOnLoading) return; + + // 无任务则跳过 + if (TaskCount <= 0) return; + + // 开始下载任务 + StartCoroutine(nameof(CRLoadTask)); + } + + /// + /// 协程加载任务 + /// + /// + private IEnumerator CRLoadTask() + { + _isOnLoading = true; + var task = GetTask(); + if (task != null) + { + task.fixedUrl = GetUrl(task.url, task.useFallback); + Debug.Log($"--- 下载地址: { task.fixedUrl }"); + // Debug.Log($"--- _useFallback: {_useFallback}"); + // Debug.Log($"--- _retryNum: {_retryNum}"); + + // 使用Using初始化加载器 + using (UnityWebRequest www = UnityWebRequest.Get(task.fixedUrl)) + { + if (task.type == LoadType.Texture2D) + www.downloadHandler = new DownloadHandlerTexture(); + else + www.downloadHandler = new DownloadHandlerBuffer(); + + www.timeout = _config.timeout; + yield return www.SendWebRequest(); + + if (www.result == UnityWebRequest.Result.Success) + { + switch (task.type) + { + case LoadType.Text: + task.onGetString?.Invoke(www.downloadHandler.text); + break; + case LoadType.Bytes: + task.onGetBytes?.Invoke(www.downloadHandler.data); + break; + case LoadType.Texture2D: + var handler2D = www.downloadHandler as DownloadHandlerTexture; + task.onGetTexture?.Invoke(handler2D?.texture ? handler2D.texture : null); + break; + default: + //TODO 请扩展更多的加载接口 + break; + } + } + else + { + // 打印(上报) 错误信息 + Debug.LogError( + $"Load asset fail, code:{www.responseCode} error:{www.error} url:{task.fixedUrl} type:{task.type.ToString()}"); + // 下载错误 + task.retryNum++; + if (task.retryNum > 2) + { + if (task.useFallback) + { + task.onFail(www.error); + task.Dispose(); + } + else + { + task.retryNum = 0; + task.useFallback = true; + AddTask(task); + } + } + else + { + // 增加次数后加到队尾重新下载 + AddTask(task); + } + } + + _isOnLoading = false; + } + } + } + + + #endregion + + #region 单元测试 + +#if UNITY_EDITOR + + [MenuItem("Test/Test CDNLoader...")] + public static void EditorTestGetURL() + { + string url = "https://cdn.dof.fungame.studio/PicAssets%2FAndroid%2F6_cx_0818_17?generation=1649423811796255&alt=media"; + string url1 = "https://cdn.dof.fungame.studio/PicAssets%2FAndroid%2F6_cx_0818_17?alt=media&generation=1649423811796255"; + string url2 = "https://cdn.dof.fungame.studio/PicAssets%2FAndroid%2F6_cx_0818_17?alt=media"; + string newUrl = FixUrlParameter(url1); + Debug.Log($"---> newUrl: {newUrl}"); + } +#endif + + + + + #endregion + +} + +public enum LoadType +{ + Text, + Bytes, + Texture2D, +} + +/// +/// 加载任务 +/// +[Serializable] +internal class LoadTask +{ + public string url; + public string fixedUrl; + public Action onGetString; + public Action onGetBytes; + public Action onGetTexture; + public Action onFail; + public Action onProgress; + public bool useFallback = false; + public int retryNum = 0; + public LoadType type; + public int piority = 100; + + public void Dispose() + { + onGetString = null; + onGetBytes = null; + onGetTexture = null; + onFail = null; + onProgress = null; + } +} \ No newline at end of file diff --git a/Runtime/GuruCore/Runtime/ExtensionKit/CDNLoader.cs.meta b/Runtime/GuruCore/Runtime/ExtensionKit/CDNLoader.cs.meta new file mode 100644 index 0000000..9762031 --- /dev/null +++ b/Runtime/GuruCore/Runtime/ExtensionKit/CDNLoader.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: fedd3f387a60445fbbb4db453c7f14f0 +timeCreated: 1661743198 \ No newline at end of file diff --git a/Runtime/GuruCore/Runtime/ExtensionKit/CoroutineHelper.cs b/Runtime/GuruCore/Runtime/ExtensionKit/CoroutineHelper.cs new file mode 100644 index 0000000..e2a2265 --- /dev/null +++ b/Runtime/GuruCore/Runtime/ExtensionKit/CoroutineHelper.cs @@ -0,0 +1,36 @@ +namespace Guru +{ + using System; + using System.Collections; + using UnityEngine; + + [MonoSingleton(EMonoSingletonType.CreateOnNewGameObject, false)] + public sealed class CoroutineHelper : MonoSingleton + { + public Coroutine Begin(IEnumerator enumerator) + { + return StartCoroutine(enumerator); + } + + public void Stop(Coroutine coroutine) + { + StopCoroutine(coroutine); + } + + public Coroutine StartDelayed(WaitForSeconds delay, Action callback) + { + return ((MonoBehaviour)this).StartDelayed(delay, callback); + } + + public Coroutine StartDelayed(float delay, Action callback) + { + return ((MonoBehaviour)this).StartDelayed(delay, callback); + } + + public Coroutine StartDelayed(int framesOfDelay, Action callback) + { + return ((MonoBehaviour)this).StartDelayed(framesOfDelay, callback); + } + } + +} \ No newline at end of file diff --git a/Runtime/GuruCore/Runtime/ExtensionKit/CoroutineHelper.cs.meta b/Runtime/GuruCore/Runtime/ExtensionKit/CoroutineHelper.cs.meta new file mode 100644 index 0000000..6f314bf --- /dev/null +++ b/Runtime/GuruCore/Runtime/ExtensionKit/CoroutineHelper.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: ff7431723cce4cc2b01cbeefdd1c1efa +timeCreated: 1632404299 \ No newline at end of file diff --git a/Runtime/GuruCore/Runtime/ExtensionKit/Extension.cs b/Runtime/GuruCore/Runtime/ExtensionKit/Extension.cs new file mode 100644 index 0000000..0561903 --- /dev/null +++ b/Runtime/GuruCore/Runtime/ExtensionKit/Extension.cs @@ -0,0 +1,2571 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Text.RegularExpressions; +using UnityEngine; +using UnityEngine.Events; +using UnityEngine.UI; + +//参考QFramework拓展方法 +namespace Guru +{ + public static class DelegateExtension + { + #region Func Extension + + /// + /// 功能:不为空则调用 Func + /// + /// 示例: + /// + /// Func func = ()=> 1; + /// var number = func.InvokeGracefully(); // 等价于 if (func != null) number = func(); + /// + /// + /// + /// + /// + public static T InvokeGracefully(this Func selfFunc) + { + return null != selfFunc ? selfFunc() : default(T); + } + + #endregion + + #region Action + + /// + /// 功能:不为空则调用 Action + /// + /// 示例: + /// + /// System.Action action = () => Log.I("action called"); + /// action.InvokeGracefully(); // if (action != null) action(); + /// + /// + /// action 对象 + /// 是否调用成功 + public static bool InvokeGracefully(this Action selfAction) + { + if (null != selfAction) + { + selfAction(); + return true; + } + + return false; + } + + /// + /// 不为空则调用 Action + /// + /// 示例: + /// + /// System.Action action = (number) => Log.I("action called" + number); + /// action.InvokeGracefully(10); // if (action != null) action(10); + /// + /// + /// action 对象 + /// 参数 + /// 是否调用成功 + public static bool InvokeGracefully(this Action selfAction, T t) + { + if (null != selfAction) + { + selfAction(t); + return true; + } + + return false; + } + + /// + /// 不为空则调用 Action + /// + /// 示例 + /// + /// System.Action action = (number,name) => Log.I("action called" + number + name); + /// action.InvokeGracefully(10,"qframework"); // if (action != null) action(10,"qframework"); + /// + /// + /// + /// call succeed + public static bool InvokeGracefully(this Action selfAction, T t, K k) + { + if (null != selfAction) + { + selfAction(t, k); + return true; + } + + return false; + } + + /// + /// 不为空则调用委托 + /// + /// 示例: + /// + /// // delegate + /// TestDelegate testDelegate = () => { }; + /// testDelegate.InvokeGracefully(); + /// + /// + /// + /// call suceed + public static bool InvokeGracefully(this Delegate selfAction, params object[] args) + { + if (null != selfAction) + { + selfAction.DynamicInvoke(args); + return true; + } + + return false; + } + + #endregion + } + + public static class CSharpObjectExtension + { + /// + /// 是否相等 + /// + /// 示例: + /// + /// if (this.Is(player)) + /// { + /// ... + /// } + /// + /// + /// + /// + /// + public static bool Is(this object selfObj, object value) + { + return selfObj == value; + } + + public static bool Is(this T selfObj, Func condition) + { + return condition(selfObj); + } + + /// + /// 表达式成立 则执行 Action + /// + /// 示例: + /// + /// (1 == 1).Do(()=>Debug.Log("1 == 1"); + /// + /// + /// + /// + /// + public static bool Do(this bool selfCondition, Action action) + { + if (selfCondition) + { + action(); + } + + return selfCondition; + } + + /// + /// 不管表达成不成立 都执行 Action,并把结果返回 + /// + /// 示例: + /// + /// (1 == 1).Do((result)=>Debug.Log("1 == 1:" + result); + /// + /// + /// + /// + /// + public static bool Do(this bool selfCondition, Action action) + { + action(selfCondition); + + return selfCondition; + } + + /// + /// 功能:判断是否为空 + /// + /// 示例: + /// + /// var simpleObject = new object(); + /// + /// if (simpleObject.IsNull()) // 等价于 simpleObject == null + /// { + /// // do sth + /// } + /// + /// + /// 判断对象(this) + /// 对象的类型(可不填) + /// 是否为空 + public static bool IsNull(this T selfObj) where T : class + { + return null == selfObj; + } + + /// + /// 功能:判断不是为空 + /// 示例: + /// + /// var simpleObject = new object(); + /// + /// if (simpleObject.IsNotNull()) // 等价于 simpleObject != null + /// { + /// // do sth + /// } + /// + /// + /// 判断对象(this) + /// 对象的类型(可不填) + /// 是否不为空 + public static bool IsNotNull(this T selfObj) where T : class + { + return null != selfObj; + } + + public static void DoIfNotNull(this T selfObj, Action action) where T : class + { + if (selfObj != null) + { + action(selfObj); + } + } + } + + /// + /// 泛型工具 + /// + /// 实例: + /// + /// 示例: + /// var typeName = GenericExtention.GetTypeName(); + /// typeName.LogInfo(); // string + /// + /// + public static class GenericUtil + { + /// + /// 获取泛型名字 + /// + /// var typeName = GenericExtention.GetTypeName(); + /// typeName.LogInfo(); // string + /// + /// + /// + /// + public static string GetTypeName() + { + return typeof(T).ToString(); + } + } + + /// + /// 可枚举的集合扩展(Array、List、Dictionary) + /// + public static class IEnumerableExtension + { + #region Array Extension + + /// + /// 遍历数组 + /// + /// var testArray = new[] { 1, 2, 3 }; + /// testArray.ForEach(number => number.LogInfo()); + /// + /// + /// The each. + /// Self array. + /// Action. + /// The 1st type parameter. + /// 返回自己 + public static T[] ForEach(this T[] selfArray, Action action) + { + Array.ForEach(selfArray, action); + return selfArray; + } + + /// + /// 遍历 IEnumerable + /// + /// // IEnumerable + /// IEnumerable testIenumerable = new List { 1, 2, 3 }; + /// testIenumerable.ForEach(number => number.LogInfo()); + /// // 支持字典的遍历 + /// new Dictionary() + /// .ForEach(keyValue => Log.I("key:{0},value:{1}", keyValue.Key, keyValue.Value)); + /// + /// + /// The each. + /// Self array. + /// Action. + /// The 1st type parameter. + public static IEnumerable ForEach(this IEnumerable selfArray, Action action) + { + if (action == null) + throw new ArgumentException(); + + foreach (var item in selfArray) + { + action(item); + } + + return selfArray; + } + + #endregion + + #region List Extension + + /// + /// 倒序遍历 + /// + /// var testList = new List { 1, 2, 3 }; + /// testList.ForEachReverse(number => number.LogInfo()); // 3, 2, 1 + /// + /// + /// 返回自己 + /// Self list. + /// Action. + /// The 1st type parameter. + public static List ForEachReverse(this List selfList, Action action) + { + if (action == null) + throw new ArgumentException(); + + for (var i = selfList.Count - 1; i >= 0; --i) + action(selfList[i]); + + return selfList; + } + + /// + /// 倒序遍历(可获得索引) + /// + /// var testList = new List { 1, 2, 3 }; + /// testList.ForEachReverse((number,index)=> number.LogInfo()); // 3, 2, 1 + /// + /// + /// The each reverse. + /// Self list. + /// Action. + /// The 1st type parameter. + public static List ForEachReverse(this List selfList, Action action) + { + if (action == null) + throw new ArgumentException(); + + for (var i = selfList.Count - 1; i >= 0; --i) + action(selfList[i], i); + + return selfList; + } + + /// + /// 遍历列表(可获得索引) + /// + /// var testList = new List {1, 2, 3 }; + /// testList.Foreach((number,index)=>number.LogInfo()); // 1, 2, 3, + /// + /// + /// 列表类型 + /// 目标表 + /// 行为 + public static void ForEach(this List list, Action action) + { + for (var i = 0; i < list.Count; i++) + { + action(i, list[i]); + } + } + + #endregion + + #region Dictionary Extension + + /// + /// 合并字典 + /// + /// // 示例 + /// var dictionary1 = new Dictionary { { "1", "2" } }; + /// var dictionary2 = new Dictionary { { "3", "4" } }; + /// var dictionary3 = dictionary1.Merge(dictionary2); + /// dictionary3.ForEach(pair => Log.I("{0}:{1}", pair.Key, pair.Value)); + /// + /// + /// The merge. + /// Dictionary. + /// Dictionaries. + /// The 1st type parameter. + /// The 2nd type parameter. + public static Dictionary Merge(this Dictionary dictionary, + params Dictionary[] dictionaries) + { + return dictionaries.Aggregate(dictionary, + (current, dict) => current.Union(dict).ToDictionary(kv => kv.Key, kv => kv.Value)); + } + + /// + /// 遍历字典 + /// + /// var dict = new Dictionary {{"name","liangxie},{"age","18"}}; + /// dict.ForEach((key,value)=> Log.I("{0}:{1}",key,value);// name:liangxie age:18 + /// + /// + /// + /// + /// + /// + public static void ForEach(this Dictionary dict, Action action) + { + var dictE = dict.GetEnumerator(); + + while (dictE.MoveNext()) + { + var current = dictE.Current; + action(current.Key, current.Value); + } + + dictE.Dispose(); + } + + /// + /// 字典添加新的词典 + /// + /// + /// + /// + /// + /// + public static void AddRange(this Dictionary dict, Dictionary addInDict, + bool isOverride = false) + { + var dictE = addInDict.GetEnumerator(); + + while (dictE.MoveNext()) + { + var current = dictE.Current; + if (dict.ContainsKey(current.Key)) + { + if (isOverride) + dict[current.Key] = current.Value; + continue; + } + + dict.Add(current.Key, current.Value); + } + + dictE.Dispose(); + } + + #endregion + } + + /// + /// 对 System.IO 的一些扩展 + /// + public static class IOExtension + { + /// + /// 检测路径是否存在,如果不存在则创建 + /// + /// + public static string CreateDirIfNotExists4FilePath(this string path) + { + var direct = Path.GetDirectoryName(path); + + if (!Directory.Exists(direct)) + { + Directory.CreateDirectory(direct); + } + + return path; + } + + + /// + /// 创建新的文件夹,如果存在则不创建 + /// + /// var testDir = "Assets/TestFolder"; + /// testDir.CreateDirIfNotExists(); + /// // 结果为,在 Assets 目录下创建 TestFolder + /// + /// + public static string CreateDirIfNotExists(this string dirFullPath) + { + if (!Directory.Exists(dirFullPath)) + { + Directory.CreateDirectory(dirFullPath); + } + + return dirFullPath; + } + + /// + /// 删除文件夹,如果存在 + /// + /// var testDir = "Assets/TestFolder"; + /// testDir.DeleteDirIfExists(); + /// // 结果为,在 Assets 目录下删除了 TestFolder + /// + /// + public static void DeleteDirIfExists(this string dirFullPath) + { + if (Directory.Exists(dirFullPath)) + { + Directory.Delete(dirFullPath, true); + } + } + + /// + /// 清空 Dir(保留目录),如果存在。 + /// + /// var testDir = "Assets/TestFolder"; + /// testDir.EmptyDirIfExists(); + /// // 结果为,清空了 TestFolder 里的内容 + /// + /// + public static void EmptyDirIfExists(this string dirFullPath) + { + if (Directory.Exists(dirFullPath)) + { + Directory.Delete(dirFullPath, true); + } + + Directory.CreateDirectory(dirFullPath); + } + + /// + /// 删除文件 如果存在 + /// + /// // 示例 + /// var filePath = "Assets/Test.txt"; + /// File.Create("Assets/Test); + /// filePath.DeleteFileIfExists(); + /// // 结果为,删除了 Test.txt + /// + /// + /// + /// 是否进行了删除操作 + public static bool DeleteFileIfExists(this string fileFullPath) + { + if (File.Exists(fileFullPath)) + { + File.Delete(fileFullPath); + return true; + } + + return false; + } + + /// + /// 合并路径 + /// + /// // 示例: + /// Application.dataPath.CombinePath("Resources").LogInfo(); // /projectPath/Assets/Resources + /// + /// + /// + /// + /// 合并后的路径 + public static string CombinePath(this string selfPath, string toCombinePath) + { + return Path.Combine(selfPath, toCombinePath); + } + + /// + /// 打开文件夹 + /// + /// + public static void OpenFolder(string path) + { +#if UNITY_STANDALONE_OSX + System.Diagnostics.Process.Start("open", path); +#elif UNITY_STANDALONE_WIN + System.Diagnostics.Process.Start("explorer.exe", path); +#endif + } + + /// + /// 获取文件夹名 + /// + /// + /// + public static string GetDirectoryName(string fileName) + { + fileName = MakePathStandard(fileName); + return fileName.Substring(0, fileName.LastIndexOf('/')); + } + + /// + /// 获取文件名 + /// + /// + /// + /// + public static string GetFileName(string path, char separator = '/') + { + path = MakePathStandard(path); + return path.Substring(path.LastIndexOf(separator) + 1); + } + + /// + /// 获取不带后缀的文件名 + /// + /// + /// + /// + public static string GetFileNameWithoutExtention(string fileName, char separator = '/') + { + return GetFilePathWithoutExtention(GetFileName(fileName, separator)); + } + + /// + /// 获取不带后缀的文件路径 + /// + /// + /// + public static string GetFilePathWithoutExtention(string fileName) + { + if (fileName.Contains(".")) + return fileName.Substring(0, fileName.LastIndexOf('.')); + return fileName; + } + + /// + /// 使目录存在,Path可以是目录名必须是文件名 + /// + /// + public static void MakeFileDirectoryExist(string path) + { + string root = Path.GetDirectoryName(path); + if (!Directory.Exists(root)) + { + Directory.CreateDirectory(root); + } + } + + /// + /// 使目录存在 + /// + /// + public static void MakeDirectoryExist(string path) + { + if (!Directory.Exists(path)) + { + Directory.CreateDirectory(path); + } + } + + /// + /// 获取父文件夹 + /// + /// + /// + public static string GetPathParentFolder(this string path) + { + if (string.IsNullOrEmpty(path)) + { + return string.Empty; + } + + return Path.GetDirectoryName(path); + } + + /// + /// 使路径标准化,去除空格并将所有'\'转换为'/' + /// + /// + /// + public static string MakePathStandard(string path) + { + return path.Trim().Replace("\\", "/"); + } + + public static List GetDirSubFilePathList(this string dirABSPath, bool isRecursive = true, + string suffix = "") + { + var pathList = new List(); + var di = new DirectoryInfo(dirABSPath); + + if (!di.Exists) + { + return pathList; + } + + var files = di.GetFiles(); + foreach (var fi in files) + { + if (!string.IsNullOrEmpty(suffix)) + { + if (!fi.FullName.EndsWith(suffix, System.StringComparison.CurrentCultureIgnoreCase)) + { + continue; + } + } + + pathList.Add(fi.FullName); + } + + if (isRecursive) + { + var dirs = di.GetDirectories(); + foreach (var d in dirs) + { + pathList.AddRange(GetDirSubFilePathList(d.FullName, isRecursive, suffix)); + } + } + + return pathList; + } + + public static List GetDirSubDirNameList(this string dirABSPath) + { + var di = new DirectoryInfo(dirABSPath); + + var dirs = di.GetDirectories(); + + return dirs.Select(d => d.Name).ToList(); + } + + public static string GetFileName(this string absOrAssetsPath) + { + var name = absOrAssetsPath.Replace("\\", "/"); + var lastIndex = name.LastIndexOf("/"); + + return lastIndex >= 0 ? name.Substring(lastIndex + 1) : name; + } + + public static string GetFileNameWithoutExtend(this string absOrAssetsPath) + { + var fileName = GetFileName(absOrAssetsPath); + var lastIndex = fileName.LastIndexOf("."); + + return lastIndex >= 0 ? fileName.Substring(0, lastIndex) : fileName; + } + + public static string GetFileExtendName(this string absOrAssetsPath) + { + var lastIndex = absOrAssetsPath.LastIndexOf("."); + + if (lastIndex >= 0) + { + return absOrAssetsPath.Substring(lastIndex); + } + + return string.Empty; + } + + public static string GetDirPath(this string absOrAssetsPath) + { + var name = absOrAssetsPath.Replace("\\", "/"); + var lastIndex = name.LastIndexOf("/"); + return name.Substring(0, lastIndex + 1); + } + + public static string GetLastDirName(this string absOrAssetsPath) + { + var name = absOrAssetsPath.Replace("\\", "/"); + var dirs = name.Split('/'); + + return absOrAssetsPath.EndsWith("/") ? dirs[dirs.Length - 2] : dirs[dirs.Length - 1]; + } + } + + /// + /// 简单的概率计算 + /// + public static class ProbilityHelper + { + public static T RandomValueFrom(params T[] values) + { + return values[UnityEngine.Random.Range(0, values.Length)]; + } + + /// + /// percent probability + /// + /// 0 ~ 100 + /// + public static bool PercentProbability(int percent) + { + return UnityEngine.Random.Range(0, 1000) * 0.001f < 50 * 0.01f; + } + } + + /// + /// 面向对象扩展(继承、封装、多态) + /// + public static class OOPExtension + { + /// + /// Determines whether the type implements the specified interface + /// and is not an interface itself. + /// + /// true, if interface was implementsed, false otherwise. + /// Type. + /// The 1st type parameter. + public static bool ImplementsInterface(this Type type) + { + return !type.IsInterface && type.GetInterfaces().Contains(typeof(T)); + } + + /// + /// Determines whether the type implements the specified interface + /// and is not an interface itself. + /// + /// true, if interface was implementsed, false otherwise. + /// Type. + /// The 1st type parameter. + public static bool ImplementsInterface(this object obj) + { + var type = obj.GetType(); + return !type.IsInterface && type.GetInterfaces().Contains(typeof(T)); + } + } + + /// + /// 程序集工具 + /// + public class AssemblyUtil + { + /// + /// 获取 Assembly-CSharp 程序集 + /// + public static Assembly DefaultCSharpAssembly + { + get + { + return AppDomain.CurrentDomain.GetAssemblies() + .SingleOrDefault(a => a.GetName().Name == "Assembly-CSharp"); + } + } + + /// + /// 获取默认的程序集中的类型 + /// + /// + /// + public static Type GetDefaultAssemblyType(string typeName) + { + return DefaultCSharpAssembly.GetType(typeName); + } + } + + /// + /// 反射扩展 + /// + public static class ReflectionExtension + { + public static Assembly GetAssemblyCSharp() + { + var assemblies = AppDomain.CurrentDomain.GetAssemblies(); + foreach (var a in assemblies) + { + if (a.FullName.StartsWith("Assembly-CSharp,")) + return a; + } + + return null; + } + + public static Assembly GetAssemblyCSharpEditor() + { + var assemblies = AppDomain.CurrentDomain.GetAssemblies(); + foreach (var a in assemblies) + { + if (a.FullName.StartsWith("Assembly-CSharp-Editor,")) + return a; + } + + return null; + } + + /// + /// 通过反射方式调用函数 + /// + /// + /// 方法名 + /// 参数 + /// + public static object InvokeByReflect(this object obj, string methodName, params object[] args) + { + var methodInfo = obj.GetType().GetMethod(methodName); + return methodInfo == null ? null : methodInfo.Invoke(obj, args); + } + + /// + /// 通过反射方式获取域值 + /// + /// + /// 域名 + /// + public static object GetFieldByReflect(this object obj, string fieldName) + { + var fieldInfo = obj.GetType().GetField(fieldName); + return fieldInfo == null ? null : fieldInfo.GetValue(obj); + } + + /// + /// 通过反射方式获取属性 + /// + /// + /// 属性名 + /// + public static object GetPropertyByReflect(this object obj, string propertyName, object[] index = null) + { + var propertyInfo = obj.GetType().GetProperty(propertyName); + return propertyInfo == null ? null : propertyInfo.GetValue(obj, index); + } + + /// + /// 拥有特性 + /// + /// + public static bool HasAttribute(this PropertyInfo prop, Type attributeType, bool inherit) + { + return prop.GetCustomAttributes(attributeType, inherit).Length > 0; + } + + /// + /// 拥有特性 + /// + /// + public static bool HasAttribute(this FieldInfo field, Type attributeType, bool inherit) + { + return field.GetCustomAttributes(attributeType, inherit).Length > 0; + } + + /// + /// 拥有特性 + /// + /// + public static bool HasAttribute(this Type type, Type attributeType, bool inherit) + { + return type.GetCustomAttributes(attributeType, inherit).Length > 0; + } + + /// + /// 拥有特性 + /// + /// + public static bool HasAttribute(this MethodInfo method, Type attributeType, bool inherit) + { + return method.GetCustomAttributes(attributeType, inherit).Length > 0; + } + + /// + /// 获取第一个特性 + /// + public static T GetFirstAttribute(this MethodInfo method, bool inherit) where T : Attribute + { + var attrs = (T[])method.GetCustomAttributes(typeof(T), inherit); + if (attrs != null && attrs.Length > 0) + return attrs[0]; + return null; + } + + /// + /// 获取第一个特性 + /// + public static T GetFirstAttribute(this FieldInfo field, bool inherit) where T : Attribute + { + var attrs = (T[])field.GetCustomAttributes(typeof(T), inherit); + if (attrs != null && attrs.Length > 0) + return attrs[0]; + return null; + } + + /// + /// 获取第一个特性 + /// + public static T GetFirstAttribute(this PropertyInfo prop, bool inherit) where T : Attribute + { + var attrs = (T[])prop.GetCustomAttributes(typeof(T), inherit); + if (attrs != null && attrs.Length > 0) + return attrs[0]; + return null; + } + + /// + /// 获取第一个特性 + /// + public static T GetFirstAttribute(this Type type, bool inherit) where T : Attribute + { + var attrs = (T[])type.GetCustomAttributes(typeof(T), inherit); + if (attrs != null && attrs.Length > 0) + return attrs[0]; + return null; + } + } + + /// + /// 类型扩展 + /// + public static class TypeEx + { + /// + /// 获取默认值 + /// + /// + /// + public static object DefaultForType(this Type targetType) + { + return targetType.IsValueType ? Activator.CreateInstance(targetType) : null; + } + } + + /// + /// 字符串扩展 + /// + public static class StringExtention + { + /// + /// Check Whether string is null or empty + /// + /// + /// + public static bool IsNullOrEmpty(this string selfStr) + { + return string.IsNullOrEmpty(selfStr); + } + + /// + /// Check Whether string is null or empty + /// + /// + /// + public static bool IsNotNullAndEmpty(this string selfStr) + { + return !string.IsNullOrEmpty(selfStr); + } + + /// + /// Check Whether string trim is null or empty + /// + /// + /// + public static bool IsTrimNotNullAndEmpty(this string selfStr) + { + return selfStr != null && !string.IsNullOrEmpty(selfStr.Trim()); + } + + public static bool IsTrimNullOrEmpty(this string selfStr) + { + return selfStr == null || string.IsNullOrEmpty(selfStr.Trim()); + } + + /// + /// 缓存 + /// + private static readonly char[] mCachedSplitCharArray = { '.' }; + + /// + /// Split + /// + /// + /// + /// + public static string[] Split(this string selfStr, char splitSymbol) + { + mCachedSplitCharArray[0] = splitSymbol; + return selfStr.Split(mCachedSplitCharArray); + } + + /// + /// 首字母大写 + /// + /// + /// + public static string UppercaseFirst(this string str) + { + return char.ToUpper(str[0]) + str.Substring(1); + } + + /// + /// 首字母小写 + /// + /// + /// + public static string LowercaseFirst(this string str) + { + return char.ToLower(str[0]) + str.Substring(1); + } + + /// + /// + /// + /// + /// + public static string ToUnixLineEndings(this string str) + { + return str.Replace("\r\n", "\n").Replace("\r", "\n"); + } + + public static string ToSpacedCamelCase(this string text) + { + var sb = new StringBuilder(text.Length * 2); + sb.Append(char.ToUpper(text[0])); + for (var i = 1; i < text.Length; i++) + { + if (char.IsUpper(text[i]) && text[i - 1] != ' ') + { + sb.Append(' '); + } + + sb.Append(text[i]); + } + + return sb.ToString(); + } + + /// + /// 有点不安全,编译器不会帮你排查错误。 + /// + /// + /// + /// + public static string FillFormat(this string selfStr, params object[] args) + { + return string.Format(selfStr, args); + } + + /// + /// 添加前缀 + /// + /// + /// + /// + public static StringBuilder Append(this string selfStr, string toAppend) + { + return new StringBuilder(selfStr).Append(toAppend); + } + + /// + /// 添加后缀 + /// + /// + /// + /// + public static string AddPrefix(this string selfStr, string toPrefix) + { + return new StringBuilder(toPrefix).Append(selfStr).ToString(); + } + + /// + /// 格式化 + /// + /// + /// + /// + /// + public static StringBuilder AppendFormat(this string selfStr, string toAppend, params object[] args) + { + return new StringBuilder(selfStr).AppendFormat(toAppend, args); + } + + /// + /// 最后一个单词 + /// + /// + /// + public static string LastWord(this string selfUrl) + { + return selfUrl.Split('/').Last(); + } + + /// + /// 解析成数字类型 + /// + /// + /// + /// + public static int ToInt(this string selfStr, int defaulValue = 0) + { + var retValue = defaulValue; + return int.TryParse(selfStr, out retValue) ? retValue : defaulValue; + } + + /// + /// 解析到时间类型 + /// + /// + /// + /// + public static DateTime ToDateTime(this string selfStr, DateTime defaultValue = default(DateTime)) + { + var retValue = defaultValue; + return DateTime.TryParse(selfStr, out retValue) ? retValue : defaultValue; + } + + /// + /// 解析 Float 类型 + /// + /// + /// + /// + public static float ToFloat(this string selfStr, float defaulValue = 0) + { + var retValue = defaulValue; + return float.TryParse(selfStr, out retValue) ? retValue : defaulValue; + } + + /// + /// 是否存在中文字符 + /// + /// + /// + public static bool HasChinese(this string input) + { + return Regex.IsMatch(input, @"[\u4e00-\u9fa5]"); + } + + /// + /// 是否存在空格 + /// + /// + /// + public static bool HasSpace(this string input) + { + return input.Contains(" "); + } + + /// + /// 删除特定字符 + /// + /// + /// + /// + public static string RemoveString(this string str, params string[] targets) + { + return targets.Aggregate(str, (current, t) => current.Replace(t, string.Empty)); + } + } + + public static class BehaviourExtension + { + public static T Enable(this T selfBehaviour) where T : Behaviour + { + selfBehaviour.enabled = true; + return selfBehaviour; + } + + public static T Disable(this T selfBehaviour) where T : Behaviour + { + selfBehaviour.enabled = false; + return selfBehaviour; + } + } + + public static class CameraExtension + { + public static Texture2D CaptureCamera(this Camera camera, Rect rect) + { + var renderTexture = new RenderTexture(Screen.width, Screen.height, 0); + camera.targetTexture = renderTexture; + camera.Render(); + + RenderTexture.active = renderTexture; + + var screenShot = new Texture2D((int)rect.width, (int)rect.height, TextureFormat.RGB24, false); + screenShot.ReadPixels(rect, 0, 0); + screenShot.Apply(); + + camera.targetTexture = null; + RenderTexture.active = null; + UnityEngine.Object.Destroy(renderTexture); + + return screenShot; + } + } + + public static class ColorExtension + { + /// + /// #C5563CFF -> 197.0f / 255,86.0f / 255,60.0f / 255 + /// + /// + /// + public static Color HtmlStringToColor(this string htmlString) + { + Color retColor; + var parseSucceed = ColorUtility.TryParseHtmlString(htmlString, out retColor); + return parseSucceed ? retColor : Color.black; + } + + /// + /// unity's color always new a color + /// + public static Color White = Color.white; + } + + public static class GraphicExtension + { + public static T ColorAlpha(this T selfGraphic, float alpha) where T : Graphic + { + var color = selfGraphic.color; + color.a = alpha; + selfGraphic.color = color; + return selfGraphic; + } + } + + public static class ImageExtension + { + public static Image FillAmount(this Image selfImage, float fillamount) + { + selfImage.fillAmount = fillamount; + return selfImage; + } + } + + public static class LightmapExtension + { + public static void SetAmbientLightHTMLStringColor(string htmlStringColor) + { + RenderSettings.ambientLight = htmlStringColor.HtmlStringToColor(); + } + } + + public static class ObjectExtension + { + #region Instantiate + + public static T Instantiate(this T selfObj) where T : UnityEngine.Object + { + return UnityEngine.Object.Instantiate(selfObj); + } + + public static T Instantiate(this T selfObj, Vector3 position, Quaternion rotation) + where T : UnityEngine.Object + { + return UnityEngine.Object.Instantiate(selfObj, position, rotation); + } + + public static T Instantiate(this T selfObj, Vector3 position, Quaternion rotation, + Transform parent) where T : UnityEngine.Object + { + return UnityEngine.Object.Instantiate(selfObj, position, rotation, parent); + } + + public static T InstantiateWithParent(this T selfObj, Transform parent, bool worldPositionStays) + where T : UnityEngine.Object + { + return (T)UnityEngine.Object.Instantiate((UnityEngine.Object)selfObj, parent, worldPositionStays); + } + + public static T InstantiateWithParent(this T selfObj, Transform parent) where T : UnityEngine.Object + { + return UnityEngine.Object.Instantiate(selfObj, parent, false); + } + + #endregion + + #region Name + + public static T Name(this T selfObj, string name) where T : UnityEngine.Object + { + selfObj.name = name; + return selfObj; + } + + #endregion + + #region Destroy Self + + public static void DestroySelf(this T selfObj) where T : UnityEngine.Object + { + UnityEngine.Object.Destroy(selfObj); + } + + public static T DestroySelfGracefully(this T selfObj) where T : UnityEngine.Object + { + if (selfObj) + { + UnityEngine.Object.Destroy(selfObj); + } + + return selfObj; + } + + #endregion + + #region Destroy Self AfterDelay + + public static T DestroySelfAfterDelay(this T selfObj, float afterDelay) where T : UnityEngine.Object + { + UnityEngine.Object.Destroy(selfObj, afterDelay); + return selfObj; + } + + public static T DestroySelfAfterDelayGracefully(this T selfObj, float delay) where T : UnityEngine.Object + { + if (selfObj) + { + UnityEngine.Object.Destroy(selfObj, delay); + } + + return selfObj; + } + + #endregion + + #region Apply Self To + + public static T ApplySelfTo(this T selfObj, System.Action toFunction) where T : UnityEngine.Object + { + toFunction.InvokeGracefully(selfObj); + return selfObj; + } + + #endregion + + #region DontDestroyOnLoad + + public static T DontDestroyOnLoad(this T selfObj) where T : UnityEngine.Object + { + UnityEngine.Object.DontDestroyOnLoad(selfObj); + return selfObj; + } + + #endregion + + public static T As(this object selfObj) where T : class + { + return selfObj as T; + } + } + + public static class RectTransformExtension + { + public static Vector2 GetPosInRootTrans(this RectTransform selfRectTransform, Transform rootTrans) + { + return RectTransformUtility.CalculateRelativeRectTransformBounds(rootTrans, selfRectTransform).center; + } + + public static RectTransform AnchorPosX(this RectTransform selfRectTrans, float anchorPosX) + { + var anchorPos = selfRectTrans.anchoredPosition; + anchorPos.x = anchorPosX; + selfRectTrans.anchoredPosition = anchorPos; + return selfRectTrans; + } + + public static RectTransform AnchorPosY(this RectTransform selfRectTrans, float anchorPosY) + { + var anchorPos = selfRectTrans.anchoredPosition; + anchorPos.y = anchorPosY; + selfRectTrans.anchoredPosition = anchorPos; + return selfRectTrans; + } + + public static RectTransform SetSizeWidth(this RectTransform selfRectTrans, float sizeWidth) + { + var sizeDelta = selfRectTrans.sizeDelta; + sizeDelta.x = sizeWidth; + selfRectTrans.sizeDelta = sizeDelta; + return selfRectTrans; + } + + public static RectTransform SetSizeHeight(this RectTransform selfRectTrans, float sizeHeight) + { + var sizeDelta = selfRectTrans.sizeDelta; + sizeDelta.y = sizeHeight; + selfRectTrans.sizeDelta = sizeDelta; + return selfRectTrans; + } + + public static Vector2 GetWorldSize(this RectTransform selfRectTrans) + { + return RectTransformUtility.CalculateRelativeRectTransformBounds(selfRectTrans).size; + } + } + + public static class SelectableExtension + { + public static T EnableInteract(this T selfSelectable) where T : Selectable + { + selfSelectable.interactable = true; + return selfSelectable; + } + + public static T DisableInteract(this T selfSelectable) where T : Selectable + { + selfSelectable.interactable = false; + return selfSelectable; + } + + // public static T CancalAllTransitions(this T selfSelectable) where T : Selectable + // { + // selfSelectable.transition = Selectable.Transition.None; + // return selfSelectable; + // } + } + + public static class ToggleExtension + { + public static void RegOnValueChangedEvent(this Toggle selfToggle, UnityAction onValueChangedEvent) + { + selfToggle.onValueChanged.AddListener(onValueChangedEvent); + } + } + + /// + /// Transform's Extension + /// + public static class TransformExtension + { + /// + /// 缓存的一些变量,免得每次声明 + /// + private static Vector3 mLocalPos; + private static Vector3 mScale; + private static Vector3 mPos; + + #region Parent + + public static T Parent(this T selfComponent, Component parentComponent) where T : Component + { + selfComponent.transform.SetParent(parentComponent == null ? null : parentComponent.transform); + return selfComponent; + } + + /// + /// 设置成为顶端 Transform + /// + /// The root transform. + /// Self component. + /// The 1st type parameter. + public static T AsRootTransform(this T selfComponent) where T : Component + { + selfComponent.transform.SetParent(null); + return selfComponent; + } + + #endregion + + #region LocalIdentity + + public static T LocalIdentity(this T selfComponent) where T : Component + { + selfComponent.transform.localPosition = Vector3.zero; + selfComponent.transform.localRotation = Quaternion.identity; + selfComponent.transform.localScale = Vector3.one; + return selfComponent; + } + + #endregion + + #region LocalPosition + + public static T LocalPosition(this T selfComponent, Vector3 localPos) where T : Component + { + selfComponent.transform.localPosition = localPos; + return selfComponent; + } + + public static Vector3 GetLocalPosition(this T selfComponent) where T : Component + { + return selfComponent.transform.localPosition; + } + + public static T LocalPosition(this T selfComponent, float x, float y, float z) where T : Component + { + selfComponent.transform.localPosition = new Vector3(x, y, z); + return selfComponent; + } + + public static T LocalPosition(this T selfComponent, float x, float y) where T : Component + { + mLocalPos = selfComponent.transform.localPosition; + mLocalPos.x = x; + mLocalPos.y = y; + selfComponent.transform.localPosition = mLocalPos; + return selfComponent; + } + + public static T LocalPositionX(this T selfComponent, float x) where T : Component + { + mLocalPos = selfComponent.transform.localPosition; + mLocalPos.x = x; + selfComponent.transform.localPosition = mLocalPos; + return selfComponent; + } + + public static T LocalPositionY(this T selfComponent, float y) where T : Component + { + mLocalPos = selfComponent.transform.localPosition; + mLocalPos.y = y; + selfComponent.transform.localPosition = mLocalPos; + return selfComponent; + } + + public static T LocalPositionZ(this T selfComponent, float z) where T : Component + { + mLocalPos = selfComponent.transform.localPosition; + mLocalPos.z = z; + selfComponent.transform.localPosition = mLocalPos; + return selfComponent; + } + + public static T LocalPositionIdentity(this T selfComponent) where T : Component + { + selfComponent.transform.localPosition = Vector3.zero; + return selfComponent; + } + + #endregion + + #region LocalRotation + + public static Quaternion GetLocalRotation(this T selfComponent) where T : Component + { + return selfComponent.transform.localRotation; + } + + public static T LocalRotation(this T selfComponent, Quaternion localRotation) where T : Component + { + selfComponent.transform.localRotation = localRotation; + return selfComponent; + } + + public static T LocalRotationIdentity(this T selfComponent) where T : Component + { + selfComponent.transform.localRotation = Quaternion.identity; + return selfComponent; + } + + #endregion + + #region LocalScale + + public static T LocalScale(this T selfComponent, Vector3 scale) where T : Component + { + selfComponent.transform.localScale = scale; + return selfComponent; + } + + public static Vector3 GetLocalScale(this T selfComponent) where T : Component + { + return selfComponent.transform.localScale; + } + + public static T LocalScale(this T selfComponent, float xyz) where T : Component + { + selfComponent.transform.localScale = Vector3.one * xyz; + return selfComponent; + } + + public static T LocalScale(this T selfComponent, float x, float y, float z) where T : Component + { + mScale = selfComponent.transform.localScale; + mScale.x = x; + mScale.y = y; + mScale.z = z; + selfComponent.transform.localScale = mScale; + return selfComponent; + } + + public static T LocalScale(this T selfComponent, float x, float y) where T : Component + { + mScale = selfComponent.transform.localScale; + mScale.x = x; + mScale.y = y; + selfComponent.transform.localScale = mScale; + return selfComponent; + } + + public static T LocalScaleX(this T selfComponent, float x) where T : Component + { + mScale = selfComponent.transform.localScale; + mScale.x = x; + selfComponent.transform.localScale = mScale; + return selfComponent; + } + + public static T LocalScaleY(this T selfComponent, float y) where T : Component + { + mScale = selfComponent.transform.localScale; + mScale.y = y; + selfComponent.transform.localScale = mScale; + return selfComponent; + } + + public static T LocalScaleZ(this T selfComponent, float z) where T : Component + { + mScale = selfComponent.transform.localScale; + mScale.z = z; + selfComponent.transform.localScale = mScale; + return selfComponent; + } + + public static T LocalScaleIdentity(this T selfComponent) where T : Component + { + selfComponent.transform.localScale = Vector3.one; + return selfComponent; + } + + #endregion + + #region Identity + + public static T Identity(this T selfComponent) where T : Component + { + selfComponent.transform.position = Vector3.zero; + selfComponent.transform.rotation = Quaternion.identity; + selfComponent.transform.localScale = Vector3.one; + return selfComponent; + } + + #endregion + + #region Position + + public static T Position(this T selfComponent, Vector3 position) where T : Component + { + selfComponent.transform.position = position; + return selfComponent; + } + + public static Vector3 GetPosition(this T selfComponent) where T : Component + { + return selfComponent.transform.position; + } + + public static T Position(this T selfComponent, float x, float y, float z) where T : Component + { + selfComponent.transform.position = new Vector3(x, y, z); + return selfComponent; + } + + public static T Position(this T selfComponent, float x, float y) where T : Component + { + mPos = selfComponent.transform.position; + mPos.x = x; + mPos.y = y; + selfComponent.transform.position = mPos; + return selfComponent; + } + + public static T PositionIdentity(this T selfComponent) where T : Component + { + selfComponent.transform.position = Vector3.zero; + return selfComponent; + } + + public static T PositionX(this T selfComponent, float x) where T : Component + { + mPos = selfComponent.transform.position; + mPos.x = x; + selfComponent.transform.position = mPos; + return selfComponent; + } + + public static T PositionX(this T selfComponent, Func xSetter) where T : Component + { + mPos = selfComponent.transform.position; + mPos.x = xSetter(mPos.x); + selfComponent.transform.position = mPos; + return selfComponent; + } + + public static T PositionY(this T selfComponent, float y) where T : Component + { + mPos = selfComponent.transform.position; + mPos.y = y; + selfComponent.transform.position = mPos; + return selfComponent; + } + + public static T PositionY(this T selfComponent, Func ySetter) where T : Component + { + mPos = selfComponent.transform.position; + mPos.y = ySetter(mPos.y); + selfComponent.transform.position = mPos; + return selfComponent; + } + + public static T PositionZ(this T selfComponent, float z) where T : Component + { + mPos = selfComponent.transform.position; + mPos.z = z; + selfComponent.transform.position = mPos; + return selfComponent; + } + + public static T PositionZ(this T selfComponent, Func zSetter) where T : Component + { + mPos = selfComponent.transform.position; + mPos.z = zSetter(mPos.z); + selfComponent.transform.position = mPos; + return selfComponent; + } + + #endregion + + #region Rotation + + public static T RotationIdentity(this T selfComponent) where T : Component + { + selfComponent.transform.rotation = Quaternion.identity; + return selfComponent; + } + + public static T Rotation(this T selfComponent, Quaternion rotation) where T : Component + { + selfComponent.transform.rotation = rotation; + return selfComponent; + } + + public static Quaternion GetRotation(this T selfComponent) where T : Component + { + return selfComponent.transform.rotation; + } + + #endregion + + #region WorldScale/LossyScale/GlobalScale/Scale + + public static Vector3 GetGlobalScale(this T selfComponent) where T : Component + { + return selfComponent.transform.lossyScale; + } + + public static Vector3 GetScale(this T selfComponent) where T : Component + { + return selfComponent.transform.lossyScale; + } + + public static Vector3 GetWorldScale(this T selfComponent) where T : Component + { + return selfComponent.transform.lossyScale; + } + + public static Vector3 GetLossyScale(this T selfComponent) where T : Component + { + return selfComponent.transform.lossyScale; + } + + #endregion + + #region Destroy All Child + + [Obsolete("弃用啦 请使用 DestroyChildren")] + public static T DestroyAllChild(this T selfComponent) where T : Component + { + return selfComponent.DestroyChildren(); + } + + [Obsolete("弃用啦 请使用 DestroyChildren")] + public static GameObject DestroyAllChild(this GameObject selfGameObj) + { + return selfGameObj.DestroyChildren(); + } + + public static T DestroyChildren(this T selfComponent) where T : Component + { + var childCount = selfComponent.transform.childCount; + + for (var i = 0; i < childCount; i++) + { + selfComponent.transform.GetChild(i).DestroyGameObjGracefully(); + } + + return selfComponent; + } + + public static GameObject DestroyChildren(this GameObject selfGameObj) + { + var childCount = selfGameObj.transform.childCount; + + for (var i = 0; i < childCount; i++) + { + selfGameObj.transform.GetChild(i).DestroyGameObjGracefully(); + } + + return selfGameObj; + } + + #endregion + + #region Sibling Index + + public static T AsLastSibling(this T selfComponent) where T : Component + { + selfComponent.transform.SetAsLastSibling(); + return selfComponent; + } + + public static T AsFirstSibling(this T selfComponent) where T : Component + { + selfComponent.transform.SetAsFirstSibling(); + return selfComponent; + } + + public static T SiblingIndex(this T selfComponent, int index) where T : Component + { + selfComponent.transform.SetSiblingIndex(index); + return selfComponent; + } + + #endregion + + public static Transform FindByPath(this Transform selfTrans, string path) + { + return selfTrans.Find(path.Replace(".", "/")); + } + + public static Transform SeekTrans(this Transform selfTransform, string uniqueName) + { + var childTrans = selfTransform.Find(uniqueName); + + if (null != childTrans) + return childTrans; + + foreach (Transform trans in selfTransform) + { + childTrans = trans.SeekTrans(uniqueName); + + if (null != childTrans) + return childTrans; + } + + return null; + } + + public static T ShowChildTransByPath(this T selfComponent, string tranformPath) where T : Component + { + selfComponent.transform.Find(tranformPath).gameObject.Show(); + return selfComponent; + } + + public static T HideChildTransByPath(this T selfComponent, string tranformPath) where T : Component + { + selfComponent.transform.Find(tranformPath).Hide(); + return selfComponent; + } + + public static void CopyDataFromTrans(this Transform selfTrans, Transform fromTrans) + { + selfTrans.SetParent(fromTrans.parent); + selfTrans.localPosition = fromTrans.localPosition; + selfTrans.localRotation = fromTrans.localRotation; + selfTrans.localScale = fromTrans.localScale; + } + + /// + /// 递归遍历子物体,并调用函数 + /// + /// + /// + public static void ActionRecursion(this Transform tfParent, Action action) + { + action(tfParent); + foreach (Transform tfChild in tfParent) + { + tfChild.ActionRecursion(action); + } + } + + /// + /// 递归遍历查找指定的名字的子物体 + /// + /// 当前Transform + /// 目标名 + /// 字符串比较规则 + /// + public static Transform FindChildRecursion(this Transform tfParent, string name, + StringComparison stringComparison = StringComparison.Ordinal) + { + if (tfParent.name.Equals(name, stringComparison)) + { + //Debug.Log("Hit " + tfParent.name); + return tfParent; + } + + foreach (Transform tfChild in tfParent) + { + Transform tfFinal = null; + tfFinal = tfChild.FindChildRecursion(name, stringComparison); + if (tfFinal) + { + return tfFinal; + } + } + + return null; + } + + /// + /// 递归遍历查找相应条件的子物体 + /// + /// 当前Transform + /// 条件 + /// + public static Transform FindChildRecursion(this Transform tfParent, Func predicate) + { + if (predicate(tfParent)) + { + Log.I("Hit " + tfParent.name); + return tfParent; + } + + foreach (Transform tfChild in tfParent) + { + Transform tfFinal = null; + tfFinal = tfChild.FindChildRecursion(predicate); + if (tfFinal) + { + return tfFinal; + } + } + + return null; + } + + public static string GetPath(this Transform transform) + { + var sb = new System.Text.StringBuilder(); + var t = transform; + while (true) + { + sb.Insert(0, t.name); + t = t.parent; + if (t) + { + sb.Insert(0, "/"); + } + else + { + return sb.ToString(); + } + } + } + } + + public static class UnityActionExtension + { + /// + /// Call action + /// + /// + /// call succeed + public static bool InvokeGracefully(this UnityAction selfAction) + { + if (null != selfAction) + { + selfAction(); + return true; + } + + return false; + } + + /// + /// Call action + /// + /// + /// + /// + public static bool InvokeGracefully(this UnityAction selfAction, T t) + { + if (null != selfAction) + { + selfAction(t); + return true; + } + + return false; + } + + /// + /// Call action + /// + /// + /// call succeed + public static bool InvokeGracefully(this UnityAction selfAction, T t, K k) + { + if (null != selfAction) + { + selfAction(t, k); + return true; + } + + return false; + } + + /// + /// 获得随机列表中元素 + /// + /// 元素类型 + /// 列表 + /// + public static T GetRandomItem(this List list) + { + return list[UnityEngine.Random.Range(0, list.Count)]; + } + + /// + /// 根据权值来获取索引 + /// + /// + /// + public static int GetRandomWithPower(this List powers) + { + var sum = 0; + foreach (var power in powers) + { + sum += power; + } + + var randomNum = UnityEngine.Random.Range(0, sum); + var currentSum = 0; + for (var i = 0; i < powers.Count; i++) + { + var nextSum = currentSum + powers[i]; + if (randomNum >= currentSum && randomNum <= nextSum) + { + return i; + } + + currentSum = nextSum; + } + + Log.E("权值范围计算错误!"); + return -1; + } + + /// + /// 根据权值获取值,Key为值,Value为权值 + /// + /// + /// + /// + public static T GetRandomWithPower(this Dictionary powersDict) + { + var keys = new List(); + var values = new List(); + + foreach (var key in powersDict.Keys) + { + keys.Add(key); + values.Add(powersDict[key]); + } + + var finalKeyIndex = values.GetRandomWithPower(); + return keys[finalKeyIndex]; + } + } + + public static class AnimatorExtension + { + public static void AddAnimatorParameterIfExists(this Animator animator, string parameterName, + AnimatorControllerParameterType type, List parameterList) + { + if (animator.HasParameterOfType(parameterName, type)) + { + parameterList.Add(parameterName); + } + } + + // + /// Updates the animator bool. + /// + /// Animator. + /// Parameter name. + /// If set to true value. + public static void UpdateAnimatorBool(this Animator self, string parameterName, bool value, + List parameterList) + { + if (parameterList.Contains(parameterName)) + { + self.SetBool(parameterName, value); + } + } + + public static void UpdateAnimatorTrigger(this Animator self, string parameterName, List parameterList) + { + if (parameterList.Contains(parameterName)) + { + self.SetTrigger(parameterName); + } + } + + /// + /// Triggers an animator trigger. + /// + /// Animator. + /// Parameter name. + /// If set to true value. + public static void SetAnimatorTrigger(this Animator self, string parameterName, List parameterList) + { + if (parameterList.Contains(parameterName)) + { + self.SetTrigger(parameterName); + } + } + + /// + /// Updates the animator float. + /// + /// Animator. + /// Parameter name. + /// Value. + public static void UpdateAnimatorFloat(this Animator self, string parameterName, float value, + List parameterList) + { + if (parameterList.Contains(parameterName)) + { + self.SetFloat(parameterName, value); + } + } + + /// + /// Updates the animator integer. + /// + /// self. + /// Parameter name. + /// Value. + public static void UpdateAnimatorInteger(this Animator self, string parameterName, int value, + List parameterList) + { + if (parameterList.Contains(parameterName)) + { + self.SetInteger(parameterName, value); + } + } + + + // + /// Updates the animator bool without checking the parameter's existence. + /// + /// self. + /// Parameter name. + /// If set to true value. + public static void UpdateAnimatorBool(this Animator self, string parameterName, bool value) + { + self.SetBool(parameterName, value); + } + + /// + /// Updates the animator trigger without checking the parameter's existence + /// + /// self. + /// Parameter name. + public static void UpdateAnimatorTrigger(this Animator self, string parameterName) + { + self.SetTrigger(parameterName); + } + + /// + /// Triggers an animator trigger without checking for the parameter's existence. + /// + /// self. + /// Parameter name. + /// If set to true value. + public static void SetAnimatorTrigger(this Animator self, string parameterName) + { + self.SetTrigger(parameterName); + } + + /// + /// Updates the animator float without checking for the parameter's existence. + /// + /// self. + /// Parameter name. + /// Value. + public static void UpdateAnimatorFloat(this Animator self, string parameterName, float value) + { + self.SetFloat(parameterName, value); + } + + /// + /// Updates the animator integer without checking for the parameter's existence. + /// + /// self. + /// Parameter name. + /// Value. + public static void UpdateAnimatorInteger(this Animator self, string parameterName, int value) + { + self.SetInteger(parameterName, value); + } + + + // + /// Updates the animator bool after checking the parameter's existence. + /// + /// Animator. + /// Parameter name. + /// If set to true value. + public static void UpdateAnimatorBoolIfExists(this Animator self, string parameterName, bool value) + { + if (self.HasParameterOfType(parameterName, AnimatorControllerParameterType.Bool)) + { + self.SetBool(parameterName, value); + } + } + + public static void UpdateAnimatorTriggerIfExists(this Animator self, string parameterName) + { + if (self.HasParameterOfType(parameterName, AnimatorControllerParameterType.Trigger)) + { + self.SetTrigger(parameterName); + } + } + + /// + /// Triggers an animator trigger after checking for the parameter's existence. + /// + /// Animator. + /// Parameter name. + /// If set to true value. + public static void SetAnimatorTriggerIfExists(this Animator self, string parameterName) + { + if (self.HasParameterOfType(parameterName, AnimatorControllerParameterType.Trigger)) + { + self.SetTrigger(parameterName); + } + } + + /// + /// Updates the animator float after checking for the parameter's existence. + /// + /// Animator. + /// Parameter name. + /// Value. + public static void UpdateAnimatorFloatIfExists(this Animator self, string parameterName, float value) + { + if (self.HasParameterOfType(parameterName, AnimatorControllerParameterType.Float)) + { + self.SetFloat(parameterName, value); + } + } + + /// + /// Updates the animator integer after checking for the parameter's existence. + /// + /// Animator. + /// Parameter name. + /// Value. + public static void UpdateAnimatorIntegerIfExists(this Animator self, string parameterName, int value) + { + if (self.HasParameterOfType(parameterName, AnimatorControllerParameterType.Int)) + { + self.SetInteger(parameterName, value); + } + } + + /// + /// Determines if an animator contains a certain parameter, based on a type and a name + /// + /// true if has parameter of type the specified self name type; otherwise, false. + /// Self. + /// Name. + /// Type. + public static bool HasParameterOfType(this Animator self, string name, AnimatorControllerParameterType type) + { + if (string.IsNullOrEmpty(name)) + { + return false; + } + + var parameters = self.parameters; + return parameters.Any(currParam => currParam.type == type && currParam.name == name); + } + } + + /// + /// GameObject's Util/Static This Extension + /// + public static class GameObjectExtension + { + #region Show + + public static GameObject Show(this GameObject selfObj) + { + selfObj.SetActive(true); + return selfObj; + } + + public static T Show(this T selfComponent) where T : Component + { + selfComponent.gameObject.Show(); + return selfComponent; + } + + #endregion + + #region Hide + + public static GameObject Hide(this GameObject selfObj) + { + selfObj.SetActive(false); + return selfObj; + } + + public static T Hide(this T selfComponent) where T : Component + { + selfComponent.gameObject.Hide(); + return selfComponent; + } + + #endregion + + #region DestroyGameObj + + public static void DestroyGameObj(this T selfBehaviour) where T : Component + { + selfBehaviour.gameObject.DestroySelf(); + } + + #endregion + + #region DestroyGameObjGracefully + + public static void DestroyGameObjGracefully(this T selfBehaviour) where T : Component + { + if (selfBehaviour && selfBehaviour.gameObject) + { + selfBehaviour.gameObject.DestroySelfGracefully(); + } + } + + #endregion + + #region DestroyGameObjGracefully + + public static T DestroyGameObjAfterDelay(this T selfBehaviour, float delay) where T : Component + { + selfBehaviour.gameObject.DestroySelfAfterDelay(delay); + return selfBehaviour; + } + + public static T DestroyGameObjAfterDelayGracefully(this T selfBehaviour, float delay) where T : Component + { + if (selfBehaviour && selfBehaviour.gameObject) + { + selfBehaviour.gameObject.DestroySelfAfterDelay(delay); + } + + return selfBehaviour; + } + + #endregion + + #region Layer + + public static GameObject Layer(this GameObject selfObj, int layer) + { + selfObj.layer = layer; + return selfObj; + } + + public static T Layer(this T selfComponent, int layer) where T : Component + { + selfComponent.gameObject.layer = layer; + return selfComponent; + } + + public static GameObject Layer(this GameObject selfObj, string layerName) + { + selfObj.layer = LayerMask.NameToLayer(layerName); + return selfObj; + } + + public static T Layer(this T selfComponent, string layerName) where T : Component + { + selfComponent.gameObject.layer = LayerMask.NameToLayer(layerName); + return selfComponent; + } + + public static bool IsInLayerMask(this GameObject selfObj, LayerMask layerMask) + { + return LayerMaskUtility.IsInLayerMask(selfObj, layerMask); + } + + public static bool IsInLayerMask(this T selfComponent, LayerMask layerMask) where T : Component + { + return LayerMaskUtility.IsInLayerMask(selfComponent.gameObject, layerMask); + } + + #endregion + + #region Component + + public static T GetOrAddComponent(this GameObject selfComponent) where T : Component + { + var comp = selfComponent.gameObject.GetComponent(); + return comp ? comp : selfComponent.gameObject.AddComponent(); + } + + public static T GetOrAddComponent(this Component component) where T : Component + { + return component.gameObject.GetOrAddComponent(); + } + + public static Component GetOrAddComponent(this GameObject selfComponent, Type type) + { + var comp = selfComponent.gameObject.GetComponent(type); + return comp ? comp : selfComponent.gameObject.AddComponent(type); + } + + #endregion + } + + public static class LayerMaskExtension + { + public static bool ContainsGameObject(this LayerMask selfLayerMask, GameObject gameObject) + { + return LayerMaskUtility.IsInLayerMask(gameObject, selfLayerMask); + } + } + + public static class LayerMaskUtility + { + public static bool IsInLayerMask(GameObject gameObj, LayerMask layerMask) + { + // 根据Layer数值进行移位获得用于运算的Mask值 + var objLayerMask = 1 << gameObj.layer; + return (layerMask.value & objLayerMask) == objLayerMask; + } + } + + public static class MaterialExtension + { + /// + /// 参考资料: https://blog.csdn.net/qiminixi/article/details/78402505 + /// + /// + public static void SetStandardMaterialToTransparentMode(this Material self) + { + self.SetFloat("_Mode", 3); + self.SetInt("_SrcBlend", (int)UnityEngine.Rendering.BlendMode.SrcAlpha); + self.SetInt("_DstBlend", (int)UnityEngine.Rendering.BlendMode.OneMinusSrcAlpha); + self.SetInt("_ZWrite", 0); + self.DisableKeyword("_ALPHATEST_ON"); + self.EnableKeyword("_ALPHABLEND_ON"); + self.DisableKeyword("_ALPHAPREMULTIPLY_ON"); + self.renderQueue = 3000; + } + } + + public static class TextureExtensions + { + public static Sprite CreateSprite(this Texture2D self) + { + return Sprite.Create(self, new Rect(0, 0, self.width, self.height), Vector2.one * 0.5f); + } + } +} \ No newline at end of file diff --git a/Runtime/GuruCore/Runtime/ExtensionKit/Extension.cs.meta b/Runtime/GuruCore/Runtime/ExtensionKit/Extension.cs.meta new file mode 100644 index 0000000..a194981 --- /dev/null +++ b/Runtime/GuruCore/Runtime/ExtensionKit/Extension.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: a9b0964de1bf4c719f8d6bed9c8ae1fb +timeCreated: 1660299347 \ No newline at end of file diff --git a/Runtime/GuruCore/Runtime/ExtensionKit/FileUtil.cs b/Runtime/GuruCore/Runtime/ExtensionKit/FileUtil.cs new file mode 100644 index 0000000..473da57 --- /dev/null +++ b/Runtime/GuruCore/Runtime/ExtensionKit/FileUtil.cs @@ -0,0 +1,190 @@ +using System; +using System.IO; +using System.Text; +using System.Threading.Tasks; +using Guru; + +public class FileUtil +{ + /// + /// 递归删除目录 + /// + /// String. + public static void DeleteDir(string str) + { + DirectoryInfo dir = new DirectoryInfo (str); + if (!dir.Exists) + return; + + foreach (var file in dir.GetFiles()) + { + if (file.FullName.EndsWith(".meta", StringComparison.Ordinal)) + continue; + file.Delete (); + } + + foreach (var d in dir.GetDirectories()) + DeleteDir (d.FullName); + + dir.Delete (true); + } + + /// + /// 递归创建目录 + /// + /// String. + public static void CreateDir(string str, bool clean = false) + { + if (clean) + DeleteDir(str); + + DirectoryInfo dir = new DirectoryInfo (str); + if (!dir.Parent.Exists) + CreateDir (dir.Parent.FullName); + + if(!dir.Exists) + dir.Create (); + } + + /// + /// 拷贝目录 + /// + /// From. + /// To. + public static void CopyDir(string from, string to) + { + var dir = new DirectoryInfo (from); + if (!dir.Exists) + return; + + CreateDir (to); + foreach (var f in dir.GetFiles()) + { + var name = f.Name; + if (name.StartsWith (".", StringComparison.Ordinal) || name.StartsWith ("~", StringComparison.Ordinal)) + continue; + + var to_file = Path.Combine (to, name); + if (File.Exists (to_file)) + File.Delete (to_file); + File.Copy (f.FullName, to_file); + } + + foreach (var d in dir.GetDirectories()) + { + string sub = Path.Combine (to, d.Name); + CreateDir (sub); + CopyDir(d.FullName, sub); + } + } + + /// + /// 读取文件内容 + /// + /// The file. + /// File. + public static byte[] ReadFile(string file) + { + if (!File.Exists(file)) + return null; + + return File.ReadAllBytes(file); + } + + public static async Task ReadFileAsync(string file) + { + if (!File.Exists(file)) + return null; + FileStream fileStream = new FileStream(file,FileMode.Open); + byte[] bytes = new byte[fileStream.Length]; + await fileStream.ReadAsync(bytes,0,(int)fileStream.Length); + fileStream.Dispose(); + return bytes; + } + + /// + /// 写文件, 如果目录不存在自动创建 + /// + /// File. + /// Data. + public static void WriteFile(string file, byte[] data) + { + try + { + var parent = Directory.GetParent(file); + CreateDir(parent.FullName); + File.WriteAllBytes(file, data); + } + catch(Exception e) + { + Log.E("FileUtil", e); + } + } + + /// + /// 写文件, 如果目录不存在自动创建 + /// + /// File. + /// Data. + public static void WriteFile(string file, string data) + { + try + { + var parent = Directory.GetParent(file); + CreateDir(parent.FullName); + File.WriteAllText(file, data); + } + catch(Exception e) + { + Log.E("FileUtil", e); + } + } + + /// + /// 写文件, 如果目录不存在自动创建 + /// data 必须是utf-8的格式 + /// + /// 文件路径 + /// 数据 + public static async Task WriteFileAsync(string file,string data) + { + try + { + var parent = Directory.GetParent(file); + CreateDir(parent.FullName); + FileStream fileStream = new FileStream(file ,FileMode.OpenOrCreate); + byte[] bytes = Encoding.UTF8.GetBytes(data); + await fileStream.WriteAsync(bytes, 0, bytes.Length); + fileStream.Dispose(); + } + catch(Exception e) + { + Log.E("FileUtil", e); + } + } + + public static async Task WriteFileAsync(string file,byte[] data) + { + try + { + var parent = Directory.GetParent(file); + CreateDir(parent.FullName); + FileStream fileStream = new FileStream(file ,FileMode.OpenOrCreate); + await fileStream.WriteAsync(data, 0, data.Length); + fileStream.Dispose(); + } + catch(Exception e) + { + Log.E("FileUtil", e); + } + } + + /// + /// 判断文件是否存在 + /// + /// File. + public static bool Exists(string file) + { + return File.Exists(file); + } +} diff --git a/Runtime/GuruCore/Runtime/ExtensionKit/FileUtil.cs.meta b/Runtime/GuruCore/Runtime/ExtensionKit/FileUtil.cs.meta new file mode 100644 index 0000000..1aa6c9f --- /dev/null +++ b/Runtime/GuruCore/Runtime/ExtensionKit/FileUtil.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: ada637c6622d4da2bcb7c4917871c911 +timeCreated: 1632448747 \ No newline at end of file diff --git a/Runtime/GuruCore/Runtime/ExtensionKit/ListUtil.cs b/Runtime/GuruCore/Runtime/ExtensionKit/ListUtil.cs new file mode 100644 index 0000000..332cd8c --- /dev/null +++ b/Runtime/GuruCore/Runtime/ExtensionKit/ListUtil.cs @@ -0,0 +1,444 @@ +using System; +using System.Collections.Generic; +using UnityEngine; +using Random = System.Random; + +public static class ListUtil +{ + public static bool IsNullOrEmpty(this IList list) + { + return list == null || list.Count == 0; + } + + public static T GetElement(this IList list, int index) + { + if (list == null) + return default(T); + + if (index < 0 || index >= list.Count) + return default(T); + + return list[index]; + } + + #region Functional OP + + //Map + public static List MapList(this IList list, Func handler) + { + List result = new List(list.Count); + for (int i = 0; i < list.Count; i++) + { + U e = handler(list[i]); + result.Add(e); + } + + return result; + } + + public static void MapList(this IList list, Action handler) + { + for (int i = 0; i < list.Count; i++) + { + handler(list[i]); + } + } + + //Filter + public static List FilterList(this IList list, Predicate pred) + { + List result = new List(); + for (int i = 0; i < list.Count; i++) + { + T e = list[i]; + if (pred(e)) + result.Add(e); + } + + return result; + } + + //Fold 1 + public static T FoldList(this IList list, Func func) + { + if (list.Count == 0) + return default(T); + + T acc = list[0]; + for (int i = 1; i < list.Count; i++) + acc = func(acc, list[i]); + return acc; + } + + //Fold 2 + public static S FoldList(this IList list, S acc, Func func) + { + for (int i = 0; i < list.Count; i++) + acc = func(acc, list[i]); + return acc; + } + + public static void ForEach(this IList list, Action handler) + { + for (int i = 0; i < list.Count; i++) + { + handler(list[i]); + } + } + + #endregion + + #region Create OP + + //Generate list contains [start, end) + public static List CreateIntList(int start, int end) + { + List result = new List(end - start); + for (int n = start; n < end; n++) + result.Add(n); + return result; + } + + public static List CreateIntList(int value, int start, int end) + { + List result = new List(end - start); + for (int i = 0; i < end - start; ++i) + { + result.Add(value); + } + + return result; + } + + #endregion + + #region Collection OP + + //intersection: l1 intersect l2 + public static List IntersectList(IList l1, IList l2) + { + Predicate pred = (T e) => { return l2.Contains(e); }; + List result = FilterList(l1, pred); + return result; + } + + //subtraction: l1 - l2 + public static List SubtractList(List l1, IList l2) + { + Predicate pred = (T e) => { return !l2.Contains(e); }; + List result = FilterList(l1, pred); + return result; + } + + //Remove the elements which another list doesn't contain + public static void RemoveDifferenceSet(this IList superList, IList subList) + { + for (int i = 0; i < superList.Count; i++) + { + if (!IsContainElement(subList, superList[i])) + { + superList.RemoveAt(i); + i--; + } + } + } + + #endregion + + #region Find OP + + //Find all indexes for a particular element + public static List FindAllIndexes(this IList list, T element) where T : IComparable + { + List result = FindAllIndexes(list, (T e) => element.CompareTo(e) == 0); + return result; + } + + //Find all indexes for a predicate + public static List FindAllIndexes(this IList list, Predicate pred) + { + List result = new List(); + for (int i = 0; i < list.Count; i++) + { + if (pred(list[i])) + result.Add(i); + } + + return result; + } + + //Find the index for a predicate 有扩展了 + public static int Find(this IList list, Predicate pred) + { + int result = -1; + for (int i = 0; i < list.Count; i++) + { + if (pred(list[i])) + { + result = i; + break; + } + } + + return result; + } + + //Find + public static T FindFirstOrDefault(this IList list, Predicate pred) + { + for (int i = 0; i < list.Count; i++) + { + if (pred(list[i])) + { + return list[i]; + } + } + + return default(T); + } + + //Find last + public static T FindLastOrDefault(this IList list, Predicate pred) + { + for (int i = list.Count - 1; i >= 0; i--) + { + if (pred(list[i])) + { + return list[i]; + } + } + + return default(T); + } + + #endregion + + //If all elements are the same + public static bool IsAllElementsSame(this IList list) where T : IComparable + { + bool result = false; + if (list.Count > 0) + { + Predicate pred = (T t) => { return t.CompareTo(list[0]) == 0; }; + result = IsAllElementsSatisfied(list, pred); + } + + return result; + } + + //If all elements satisfy a predicate + public static bool IsAllElementsSatisfied(this IList list, Predicate pred) + { + bool result = true; + for (int i = 0; i < list.Count; i++) + { + if (!pred(list[i])) + { + result = false; + break; + } + } + + return result; + } + + //If any element satisfy a predicate + public static bool IsAnyElementSatisfied(this IList list, Predicate pred) + { + bool result = false; + for (int i = 0; i < list.Count; i++) + { + if (pred(list[i])) + { + result = true; + break; + } + } + + return result; + } + + //Fill the list with an single element + public static void FillElements(this IList list, T element) + { + for (int i = 0; i < list.Count; i++) + list[i] = element; + } + + //Count the number of one element + public static int GetElementCount(this IList list, T element) where T : IComparable + { + int count = GetElementCount(list, (T e) => { return element.CompareTo(e) == 0; }); + return count; + } + + //Count the number for predicate + public static int GetElementCount(this IList list, Predicate pred) + { + int count = 0; + for (int i = 0; i < list.Count; i++) + { + if (pred(list[i])) + ++count; + } + + return count; + } + + //If a list contains a particular element + public static bool IsContainElement(this IList list, T element) + { + bool result = false; + for (int i = 0; i < list.Count; i++) + { + if (list[i].Equals(element)) + { + result = true; + break; + } + } + + return result; + } + + //If a list contains all elements in another list + public static bool IsContainAllElements(this IList superList, IList subList) + { + bool result = IsAllElementsSatisfied(subList, (T e) => { return superList.Contains(e); }); + return result; + } + + //If two lists are the same + public static bool IsEqualLists(this IList l1, IList l2) where T : IComparable + { + bool result = false; + if (l1.Count == l2.Count) + { + result = true; + for (int i = 0; i < l1.Count; i++) + { + if (l1[i].CompareTo(l2[i]) != 0) + { + result = false; + break; + } + } + } + + return result; + } + + //Count element number for a predicate + public static int CountElements(this IList list, Predicate pred) + { + int result = 0; + for (int i = 0; i < list.Count; i++) + { + if (pred(list[i])) + ++result; + } + + return result; + } + + public static void AddElementsToList(IList listDst, IList listSrc) + { + if (listSrc.IsNullOrEmpty()) + return; + + for (int i = 0; i < listSrc.Count; i++) + { + listDst.Add(listSrc[i]); + } + } + + public static void RemoveAllNull(this IList list) + { + for (int i = list.Count - 1; i >= 0; i--) + { + if (list[i] == null) + list.RemoveAt(i); + } + } + + public static T RandomOne(this IList list) + { + if (list == null || list.Count == 0) + { + Debug.LogError("list is null or empty"); + return default(T); + } + + return list[new Random().Next(0, list.Count)]; + } + + public static T Random(this IList list) + { + return list[UnityEngine.Random.Range(0, list.Count)]; + } + + public static T RandomOne(this IList list, int seed) + { + if (list == null || list.Count == 0) + { + Debug.LogError("list is null or empty"); + return default(T); + } + + return list[new Random(seed).Next(0, list.Count)]; + } + + /// + /// 遇到规避项选后一个,时间固定,避免无对象抽或者超时 + /// 弊端是非均匀随机,看情况使用 + /// + public static T RandomOneAvoid(this IList list, T avoid, T fallback) + { + if (list == null || list.Count == 0) + return fallback; + + var rIndex = new Random().Next(0, list.Count); + if (Equals(list[rIndex], avoid)) + rIndex = (rIndex + 1) % list.Count; + + return list[rIndex]; + } + + public static T FirstOneOrDef(this IList list) + { + return list != null && list.Count > 0 ? list[0] : default; + } + + public static T LastOneOrDef(this IList list) + { + return list != null && list.Count > 0 ? list[list.Count - 1] : default; + } + + public static T FindMaxItem(this IList list, Comparison comparison) + { + if (list == null || list.Count == 0) + return default; + var ret = list[0]; + for (var i = 1; i < list.Count; i++) + { + if (comparison(ret, list[i]) < 0) + ret = list[i]; + } + + return ret; + } + + public static T FindMinItem(this IList list, Comparison comparison) + { + if (list == null || list.Count == 0) + return default; + var ret = list[0]; + for (var i = 1; i < list.Count; i++) + { + if (comparison(ret, list[i]) > 0) + ret = list[i]; + } + + return ret; + } +} \ No newline at end of file diff --git a/Runtime/GuruCore/Runtime/ExtensionKit/ListUtil.cs.meta b/Runtime/GuruCore/Runtime/ExtensionKit/ListUtil.cs.meta new file mode 100644 index 0000000..5fe88ef --- /dev/null +++ b/Runtime/GuruCore/Runtime/ExtensionKit/ListUtil.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: c2639f3a4f304ee7b9a32cc9cca72741 +timeCreated: 1632448821 \ No newline at end of file diff --git a/Runtime/GuruCore/Runtime/ExtensionKit/NetworkUtil.cs b/Runtime/GuruCore/Runtime/ExtensionKit/NetworkUtil.cs new file mode 100644 index 0000000..051441d --- /dev/null +++ b/Runtime/GuruCore/Runtime/ExtensionKit/NetworkUtil.cs @@ -0,0 +1,6 @@ +using UnityEngine; + +public static class NetworkUtil +{ + public static bool IsNetAvaliable => Application.internetReachability != NetworkReachability.NotReachable; +} \ No newline at end of file diff --git a/Runtime/GuruCore/Runtime/ExtensionKit/NetworkUtil.cs.meta b/Runtime/GuruCore/Runtime/ExtensionKit/NetworkUtil.cs.meta new file mode 100644 index 0000000..d8821d5 --- /dev/null +++ b/Runtime/GuruCore/Runtime/ExtensionKit/NetworkUtil.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: baee4ab4a4314b9db8d58bb48b5f4b23 +timeCreated: 1650362140 \ No newline at end of file diff --git a/Runtime/GuruCore/Runtime/ExtensionKit/PlatformUtil.cs b/Runtime/GuruCore/Runtime/ExtensionKit/PlatformUtil.cs new file mode 100644 index 0000000..92fd57d --- /dev/null +++ b/Runtime/GuruCore/Runtime/ExtensionKit/PlatformUtil.cs @@ -0,0 +1,32 @@ +using System.IO; +using UnityEngine; + +public static class PlatformUtil +{ + public static string GetPlatformName() + { +#if UNITY_IOS + return "iOS"; +#else + return "Android"; +#endif + } + + public static bool IsEnableLog() + { +#if ENABLE_LOG + return true; +#else + return false; +#endif + } + + public static bool IsDebug() + { +#if DEBUG || UNITY_EDITOR + return true; +#else + return false; +#endif + } +} diff --git a/Runtime/GuruCore/Runtime/ExtensionKit/PlatformUtil.cs.meta b/Runtime/GuruCore/Runtime/ExtensionKit/PlatformUtil.cs.meta new file mode 100644 index 0000000..19571c9 --- /dev/null +++ b/Runtime/GuruCore/Runtime/ExtensionKit/PlatformUtil.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 150d90a09e0b4e2288ca1f73105d3deb +timeCreated: 1632449124 \ No newline at end of file diff --git a/Runtime/GuruCore/Runtime/ExtensionKit/Singleton.meta b/Runtime/GuruCore/Runtime/ExtensionKit/Singleton.meta new file mode 100644 index 0000000..0257fe6 --- /dev/null +++ b/Runtime/GuruCore/Runtime/ExtensionKit/Singleton.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 20cc4a0f722624020b518bbd32b558fe +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/GuruCore/Runtime/ExtensionKit/Singleton/MonoSingleton.meta b/Runtime/GuruCore/Runtime/ExtensionKit/Singleton/MonoSingleton.meta new file mode 100644 index 0000000..e36aca6 --- /dev/null +++ b/Runtime/GuruCore/Runtime/ExtensionKit/Singleton/MonoSingleton.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: a4e63eaf196f6471ebc4a8080d78fad4 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/GuruCore/Runtime/ExtensionKit/Singleton/MonoSingleton/EMonoSingletonType.cs b/Runtime/GuruCore/Runtime/ExtensionKit/Singleton/MonoSingleton/EMonoSingletonType.cs new file mode 100644 index 0000000..43f91a9 --- /dev/null +++ b/Runtime/GuruCore/Runtime/ExtensionKit/Singleton/MonoSingleton/EMonoSingletonType.cs @@ -0,0 +1,23 @@ +namespace Guru +{ + /// + /// Mono单例类型 + /// + public enum EMonoSingletonType + { + /// + /// 已经存在在场景中 + /// + ExitsInScene, + + /// + /// 从Resources下加载 + /// + LoadedFromResources, + + /// + /// 创建一个新GameObject挂载脚本实现单例 + /// + CreateOnNewGameObject, + } +} \ No newline at end of file diff --git a/Runtime/GuruCore/Runtime/ExtensionKit/Singleton/MonoSingleton/EMonoSingletonType.cs.meta b/Runtime/GuruCore/Runtime/ExtensionKit/Singleton/MonoSingleton/EMonoSingletonType.cs.meta new file mode 100644 index 0000000..196d4f2 --- /dev/null +++ b/Runtime/GuruCore/Runtime/ExtensionKit/Singleton/MonoSingleton/EMonoSingletonType.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 9094c271e5b944801a9eaf8707a3e3ad +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/GuruCore/Runtime/ExtensionKit/Singleton/MonoSingleton/MonoSingleton.cs b/Runtime/GuruCore/Runtime/ExtensionKit/Singleton/MonoSingleton/MonoSingleton.cs new file mode 100644 index 0000000..6070555 --- /dev/null +++ b/Runtime/GuruCore/Runtime/ExtensionKit/Singleton/MonoSingleton/MonoSingleton.cs @@ -0,0 +1,213 @@ +using System; +using UnityEngine; + +namespace Guru +{ + /// + /// Unity MonoBehaviour单例基类 + /// use eg: + /// [UnitySingleton(UnitySingleton.SingletonType.LoadedFromResources, false, "test")] + /// class A : MonoSingleton { } + /// 注意:必须注明Unity单例的属性 + /// + public abstract class MonoSingleton : MonoBehaviour where T : MonoBehaviour + { + private static T _instance; + public static bool InstanceExists => _instance != null; + + public static T Instance + { + get + { + TouchInstance(); + return _instance; + } + set + { + var attribute = Attribute.GetCustomAttribute(typeof(T), typeof(MonoSingletonAttribute)) as MonoSingletonAttribute; + if (attribute == null) + { + Debug.LogError("Cannot find MonoSingleton attribute on " + typeof(T).Name); + return; + } + + if (attribute.AllowSetInstance) + { + _instance = value; + } + else + { + Debug.LogError(typeof(T).Name + + " is not allowed to set instances. Please set the allowSetInstace flag to true to enable this feature."); + } + } + } + + public static void DestroyInstance(bool destroyGameObject = true) + { + if (InstanceExists) + { + if (destroyGameObject) + { + Destroy(_instance.gameObject); + } + else + { + Destroy(_instance); + } + + _instance = null; + } + } + + /// + /// Called when this object is created. + /// Children should call this base method when overriding. + /// + protected virtual void Awake() + { + if (InstanceExists && _instance != this) + { + Destroy(gameObject); + } + } + + /// + /// 单例提供的一个空接口,用来初始化单例 + /// + public void Touch() + { + } + + /// + /// Ensures that an instance of this singleton is generated + /// + private static void TouchInstance() + { + if (!InstanceExists) + Generate(); + } + + /// + /// Generates this singleton + /// + private static void Generate() + { + var attribute = Attribute.GetCustomAttribute(typeof(T), typeof(MonoSingletonAttribute)) as MonoSingletonAttribute; + if (attribute == null) + { + Debug.LogError("Cannot find MonoSingleton attribute on " + typeof(T).Name); + return; + } + + for (int i = 0; i < attribute.SingletonCreateTypes.Length; i++) + { + if (TryGenerateInstance(attribute.SingletonCreateTypes[i], attribute.DestroyOnLoad, + attribute.ResourcesLoadPath, i == attribute.SingletonCreateTypes.Length - 1)) + break; + } + } + + /// + /// Attempts to generate a singleton with the given parameters + /// + /// 创建单例方式 + /// 关卡切换是否销毁 + /// resource加载路径 + /// 最后一种创建方式,没有创建成功显示报错 + /// + private static bool TryGenerateInstance(EMonoSingletonType singletonType, bool destroyOnLoad, + string resourcesLoadPath, bool warn) + { + if (singletonType == EMonoSingletonType.ExitsInScene) + { + _instance = FindObjectOfType(); + if (_instance == null) + { + if (warn) + { + Debug.LogError("Cannot find an object with a " + typeof(T).Name + + " . Please add one to the scene."); + } + + return false; + } + } + else if (singletonType == EMonoSingletonType.LoadedFromResources) + { + if (string.IsNullOrEmpty(resourcesLoadPath)) + { + if (warn) + { + Debug.LogError( + "MonoSingletonAttribute.resourcesLoadPath is not a valid Resources location in " + + typeof(T).Name); + } + + return false; + } + + T pref = Resources.Load(resourcesLoadPath); + if (pref == null) + { + if (warn) + { + Debug.LogError("Failed to load prefab with " + typeof(T).Name + + " component attached to it from folder Resources/" + resourcesLoadPath + + ". Please add a prefab with the component to that location, or update the location."); + } + + return false; + } + + _instance = Instantiate(pref); + if (_instance == null) + { + if (warn) + { + Debug.LogError("Failed to create instance of prefab " + pref + " with component " + + typeof(T).Name + ". Please check your memory constraints"); + } + + return false; + } + + _instance.name = "(Singleton)" + typeof(T).Name; + } + else if (singletonType == EMonoSingletonType.CreateOnNewGameObject) + { + GameObject go = new GameObject("(Singleton)" + typeof(T).Name); + if (go == null) + { + if (warn) + { + Debug.LogError("Failed to create gameobject for instance of " + typeof(T).Name + + ". Please check your memory constraints."); + } + + return false; + } + + _instance = go.AddComponent(); + if (_instance == null) + { + if (warn) + { + Debug.LogError("Failed to add component of " + typeof(T).Name + + " to new gameobject. Please check your memory constraints."); + } + + Destroy(go); + return false; + } + } + + if (!destroyOnLoad) + { + DontDestroyOnLoad(_instance.gameObject); + } + + return true; + } + } +} \ No newline at end of file diff --git a/Runtime/GuruCore/Runtime/ExtensionKit/Singleton/MonoSingleton/MonoSingleton.cs.meta b/Runtime/GuruCore/Runtime/ExtensionKit/Singleton/MonoSingleton/MonoSingleton.cs.meta new file mode 100644 index 0000000..ea75e3a --- /dev/null +++ b/Runtime/GuruCore/Runtime/ExtensionKit/Singleton/MonoSingleton/MonoSingleton.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 1392ab1e7f5e411bbdf270e262389031 +timeCreated: 1626241864 \ No newline at end of file diff --git a/Runtime/GuruCore/Runtime/ExtensionKit/Singleton/MonoSingleton/MonoSingletonAttribute.cs b/Runtime/GuruCore/Runtime/ExtensionKit/Singleton/MonoSingleton/MonoSingletonAttribute.cs new file mode 100644 index 0000000..6bfee22 --- /dev/null +++ b/Runtime/GuruCore/Runtime/ExtensionKit/Singleton/MonoSingleton/MonoSingletonAttribute.cs @@ -0,0 +1,34 @@ +using System; + +namespace Guru +{ + /// + /// MonoBehaviour单例属性 + /// + [AttributeUsage(AttributeTargets.Class, Inherited = true)] + public class MonoSingletonAttribute : Attribute + { + public readonly EMonoSingletonType[] SingletonCreateTypes; + public readonly bool DestroyOnLoad; + public readonly string ResourcesLoadPath; + public readonly bool AllowSetInstance; + + public MonoSingletonAttribute(EMonoSingletonType singletonCreateType, bool destroyInstanceOnSceneLoad = true, + string resourcesPath = "", bool allowSetInstance = false) + { + SingletonCreateTypes = new[] {singletonCreateType}; + DestroyOnLoad = destroyInstanceOnSceneLoad; + ResourcesLoadPath = resourcesPath; + AllowSetInstance = allowSetInstance; + } + + public MonoSingletonAttribute(EMonoSingletonType[] singletonCreateTypes, bool destroyInstanceOnSceneLoad = true, + string resourcesPath = "", bool allowSetInstance = false) + { + SingletonCreateTypes = singletonCreateTypes; + DestroyOnLoad = destroyInstanceOnSceneLoad; + ResourcesLoadPath = resourcesPath; + AllowSetInstance = allowSetInstance; + } + } +} \ No newline at end of file diff --git a/Runtime/GuruCore/Runtime/ExtensionKit/Singleton/MonoSingleton/MonoSingletonAttribute.cs.meta b/Runtime/GuruCore/Runtime/ExtensionKit/Singleton/MonoSingleton/MonoSingletonAttribute.cs.meta new file mode 100644 index 0000000..0c42eb3 --- /dev/null +++ b/Runtime/GuruCore/Runtime/ExtensionKit/Singleton/MonoSingleton/MonoSingletonAttribute.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: aace493104564534aba5cb3fa232ab9e +timeCreated: 1626241893 \ No newline at end of file diff --git a/Runtime/GuruCore/Runtime/ExtensionKit/Singleton/Singleton.meta b/Runtime/GuruCore/Runtime/ExtensionKit/Singleton/Singleton.meta new file mode 100644 index 0000000..6569a67 --- /dev/null +++ b/Runtime/GuruCore/Runtime/ExtensionKit/Singleton/Singleton.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 48e6553edb0304ad9b2d9b73d2b1f60d +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/GuruCore/Runtime/ExtensionKit/Singleton/Singleton/Singleton.cs b/Runtime/GuruCore/Runtime/ExtensionKit/Singleton/Singleton/Singleton.cs new file mode 100644 index 0000000..8a6339b --- /dev/null +++ b/Runtime/GuruCore/Runtime/ExtensionKit/Singleton/Singleton/Singleton.cs @@ -0,0 +1,30 @@ +using System; + +namespace Guru +{ + public class Singleton where T : class, new() + { + private static readonly Lazy _lazy = new Lazy(() => new T()); + public static T Instance => _lazy.Value; + + protected Singleton() + { + Init(); + } + + /// + /// 仅仅用来初始化单例成员变量,不要用来做初始化操作【多次在Init里写其他操作又调回本单例,导致循环调用引发程序崩溃】 + /// 建议每个单例初始化操作另写函数 + /// + protected virtual void Init() + { + } + + /// + /// 提供一个创建单例接口 + /// + public void Touch() + { + } + } +} \ No newline at end of file diff --git a/Runtime/GuruCore/Runtime/ExtensionKit/Singleton/Singleton/Singleton.cs.meta b/Runtime/GuruCore/Runtime/ExtensionKit/Singleton/Singleton/Singleton.cs.meta new file mode 100644 index 0000000..08c8c41 --- /dev/null +++ b/Runtime/GuruCore/Runtime/ExtensionKit/Singleton/Singleton/Singleton.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 8d11f80f0e034a539717a4b36cca146a +timeCreated: 1626241918 \ No newline at end of file diff --git a/Runtime/GuruCore/Runtime/ExtensionKit/TimeUtil.cs b/Runtime/GuruCore/Runtime/ExtensionKit/TimeUtil.cs new file mode 100644 index 0000000..d6de967 --- /dev/null +++ b/Runtime/GuruCore/Runtime/ExtensionKit/TimeUtil.cs @@ -0,0 +1,55 @@ +using System; +using System.Globalization; + +namespace Guru +{ + public static class TimeUtil + { + public static readonly int MIN_TO_SECOND = 60; + public static readonly int HOUR_TO_MIN = 60; + public static readonly int HOUR_TO_SECOND = 3600; //60 * 60; + public static readonly int DAY_TO_SECOND = 86400; //24 * 60 * 60; + + public static readonly DateTime START_TIME = TimeZone.CurrentTimeZone.ToLocalTime(new DateTime(1970, 1, 1)); + public static readonly DateTime UTC_START_TIME = new DateTime(1970, 1, 1); + + //UnbiasedTime.tick unit : 100 nanosecond + private static int TICK_TO_MILLISECOND = 10000; + + //from AD1_1_1 to 1970_1_1 timeStamp + private static long STAMP_1970_1_1 = 62135596800000; + + /// + /// 返回时间戳 + /// + public static long GetCurrentTimeStamp() => GetTimeStamp(DateTime.Now.ToUniversalTime()); + + public static int GetCurrentTimeStampSecond() => (int)(GetCurrentTimeStamp() * 0.001f); + + /// + /// 获取固定日期的时间戳 + /// + public static long GetTimeStamp(DateTime dateTime) + { + return dateTime.Ticks / TICK_TO_MILLISECOND - STAMP_1970_1_1; + } + + /// + /// 时间戳转日期 + /// + /// + /// + public static DateTime ConvertTimeSpanToDateTime(long unixTimeStamp) + { + return UTC_START_TIME.AddMilliseconds(unixTimeStamp); + } + + /// + /// 获取标准日期格式字符串202210826T170026 + /// + public static string GetFormatCurrentTime() + { + return DateTime.Now.ToString("yyyyMMddTHHmmss", CultureInfo.InvariantCulture); + } + } +} \ No newline at end of file diff --git a/Runtime/GuruCore/Runtime/ExtensionKit/TimeUtil.cs.meta b/Runtime/GuruCore/Runtime/ExtensionKit/TimeUtil.cs.meta new file mode 100644 index 0000000..a5f10f4 --- /dev/null +++ b/Runtime/GuruCore/Runtime/ExtensionKit/TimeUtil.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: c77ff01507744a54a7b33790b1ef663d +timeCreated: 1632404451 \ No newline at end of file diff --git a/Runtime/GuruCore/Runtime/ExtensionKit/UnityExtensions.cs b/Runtime/GuruCore/Runtime/ExtensionKit/UnityExtensions.cs new file mode 100644 index 0000000..ff053b7 --- /dev/null +++ b/Runtime/GuruCore/Runtime/ExtensionKit/UnityExtensions.cs @@ -0,0 +1,51 @@ +using System; +using System.Collections; +using UnityEngine; + +public static class UnityExtensions +{ + #region Monobehaviour Extension + public static Coroutine StartDelayed(this MonoBehaviour monoBehaviour, WaitForSeconds delay, Action callback) + { + return monoBehaviour.StartCoroutine(DelayedFire(delay, callback)); + } + + public static Coroutine StartDelayed(this MonoBehaviour monoBehaviour, float delay, Action callback) + { + return monoBehaviour.StartCoroutine(DelayedFire(delay, callback)); + } + + public static Coroutine StartDelayed(this MonoBehaviour monoBehaviour, int frames, Action callback) + { + return monoBehaviour.StartCoroutine(DelayedFire(frames, callback)); + } + + private static IEnumerator DelayedFire(WaitForSeconds delay, Action callback) + { + yield return delay; + callback?.Invoke(); + } + + private static IEnumerator DelayedFire(float delay, Action callback) + { + yield return new WaitForSeconds(delay); + callback?.Invoke(); + } + + private static IEnumerator DelayedFire(int frames, Action callback) + { + while (frames > 0) + { + yield return null; + frames--; + } + callback?.Invoke(); + } + #endregion + + #region string Extension + + public static bool IsNullOrEmpty(this string str) => string.IsNullOrEmpty(str); + + #endregion +} diff --git a/Runtime/GuruCore/Runtime/ExtensionKit/UnityExtensions.cs.meta b/Runtime/GuruCore/Runtime/ExtensionKit/UnityExtensions.cs.meta new file mode 100644 index 0000000..adc3973 --- /dev/null +++ b/Runtime/GuruCore/Runtime/ExtensionKit/UnityExtensions.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 50df1d1cc5d1445b81a686f82e75d264 +timeCreated: 1632404323 \ No newline at end of file diff --git a/Runtime/GuruCore/Runtime/FB.meta b/Runtime/GuruCore/Runtime/FB.meta new file mode 100644 index 0000000..4cfcedc --- /dev/null +++ b/Runtime/GuruCore/Runtime/FB.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: c8fbec695cb94941b7175fa67d6380e0 +timeCreated: 1632711407 \ No newline at end of file diff --git a/Runtime/GuruCore/Runtime/FB/FBService.cs b/Runtime/GuruCore/Runtime/FB/FBService.cs new file mode 100644 index 0000000..9648356 --- /dev/null +++ b/Runtime/GuruCore/Runtime/FB/FBService.cs @@ -0,0 +1,58 @@ +using System.Collections.Generic; +using Facebook.Unity; +using UnityEngine; + +namespace Guru +{ + [MonoSingleton(EMonoSingletonType.CreateOnNewGameObject, false)] + public class FBService : MonoSingleton + { + public static readonly string LOG_TAG = "FB"; + + public void StartService() + { + if (!FB.IsInitialized) + { + // Initialize the Facebook SDK + FB.Init(InitCallback, OnHideUnity); + } + else + { + // Already initialized, signal an app activation App Event + FB.ActivateApp(); + } + } + + private void InitCallback() + { + if (FB.IsInitialized) + { + // Signal an app activation App Event + FB.ActivateApp(); + FB.Mobile.SetAdvertiserIDCollectionEnabled(true); + FB.Mobile.SetAutoLogAppEventsEnabled(false); // 关闭自动打点上报 +#if UNITY_IOS + FB.Mobile.SetAdvertiserTrackingEnabled(true); +#endif + } + else + { + Log.E(LOG_TAG, "Failed to Initialize the Facebook SDK"); + } + } + + private void OnHideUnity(bool isGameShown) + { + if (!isGameShown) + { + // Pause the game - we will need to hide + Time.timeScale = 0; + } + else + { + // Resume the game - we're getting focus again + Time.timeScale = 1; + } + } + } +} \ No newline at end of file diff --git a/Runtime/GuruCore/Runtime/FB/FBService.cs.meta b/Runtime/GuruCore/Runtime/FB/FBService.cs.meta new file mode 100644 index 0000000..86e8112 --- /dev/null +++ b/Runtime/GuruCore/Runtime/FB/FBService.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 5e9d82fc63a54739a02c8e0f2e2820bf +timeCreated: 1632711407 \ No newline at end of file diff --git a/Runtime/GuruCore/Runtime/Firebase.meta b/Runtime/GuruCore/Runtime/Firebase.meta new file mode 100644 index 0000000..d23ec93 --- /dev/null +++ b/Runtime/GuruCore/Runtime/Firebase.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 8ee1b206c58b43688356507fd63ac566 +timeCreated: 1632453017 \ No newline at end of file diff --git a/Runtime/GuruCore/Runtime/Firebase/FirebaseUtil.Auth.cs b/Runtime/GuruCore/Runtime/Firebase/FirebaseUtil.Auth.cs new file mode 100644 index 0000000..6739c0e --- /dev/null +++ b/Runtime/GuruCore/Runtime/Firebase/FirebaseUtil.Auth.cs @@ -0,0 +1,45 @@ +using Firebase.Auth; +using Firebase.Extensions; +using UnityEngine; + +namespace Guru +{ + public static partial class FirebaseUtil + { + public static FirebaseUser CurrentUser => FirebaseAuth.DefaultInstance.CurrentUser; + + private static readonly WaitForSeconds _wait = new WaitForSeconds(10); + + public static void AuthUser() + { + //FirebaseAuth获取用户验证并同步用户数据 + if (CurrentUser != null) + { + Log.I(LOG_TAG, "[Auth]之前验证没过期,CurrentUser: " + CurrentUser.UserId); + return; + } + + string authToken = IPMConfig.IPM_FIREBASE_TOKEN; + Log.I(LOG_TAG, $"[Auth]AuthToken:{authToken}"); + if (string.IsNullOrEmpty(authToken) || !NetworkUtil.IsNetAvaliable) + { + CoroutineHelper.Instance.StartDelayed(_wait, AuthUser); + return; + } + + FirebaseAuth.DefaultInstance.SignInWithCustomTokenAsync(authToken) + .ContinueWithOnMainThread(task => + { + if (task.IsCanceled || task.IsFaulted) + { + Log.E(LOG_TAG,"[Auth] SignInWithCustomTokenAsync encountered an error: " + task.Exception); + CoroutineHelper.Instance.StartDelayed(_wait, AuthUser); + } + else if (CurrentUser == null) + { + CoroutineHelper.Instance.StartDelayed(_wait, AuthUser); + } + }); + } + } +} \ No newline at end of file diff --git a/Runtime/GuruCore/Runtime/Firebase/FirebaseUtil.Auth.cs.meta b/Runtime/GuruCore/Runtime/Firebase/FirebaseUtil.Auth.cs.meta new file mode 100644 index 0000000..6ca9240 --- /dev/null +++ b/Runtime/GuruCore/Runtime/Firebase/FirebaseUtil.Auth.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: a19fc6821e0347c885e5ef7d5c888a4b +timeCreated: 1635144380 \ No newline at end of file diff --git a/Runtime/GuruCore/Runtime/Firebase/FirebaseUtil.Crashlytics.cs b/Runtime/GuruCore/Runtime/Firebase/FirebaseUtil.Crashlytics.cs new file mode 100644 index 0000000..1fa140c --- /dev/null +++ b/Runtime/GuruCore/Runtime/Firebase/FirebaseUtil.Crashlytics.cs @@ -0,0 +1,48 @@ +using System; +using Firebase.Crashlytics; + +namespace Guru +{ + public static partial class FirebaseUtil + { + public static void InitCrashlytics() + { + if(!string.IsNullOrEmpty(IPMConfig.IPM_UID)) + SetUserID(IPMConfig.IPM_UID); + Crashlytics.IsCrashlyticsCollectionEnabled = true; + } + + /// + /// 设置用户ID + /// + public static void SetUserID(string userID) + { + Crashlytics.SetUserId(userID); + } + + /// + /// 设置自定义数据 + /// 崩溃时玩家进度和状态 + /// + public static void SetCustomData(string key, string value) + { + Crashlytics.SetCustomKey(key, value); + } + + /// + /// 上报自定义信息日志到Crashlytics + /// + public static void LogMessage(string message) + { + Crashlytics.Log(message); + } + + /// + /// 上报自定义崩溃 + /// + public static void LogException(Exception exception) + { + Crashlytics.LogException(exception); + } + } +} \ No newline at end of file diff --git a/Runtime/GuruCore/Runtime/Firebase/FirebaseUtil.Crashlytics.cs.meta b/Runtime/GuruCore/Runtime/Firebase/FirebaseUtil.Crashlytics.cs.meta new file mode 100644 index 0000000..a4b6128 --- /dev/null +++ b/Runtime/GuruCore/Runtime/Firebase/FirebaseUtil.Crashlytics.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 7814cd46f49c439194ab98b75b9ef333 +timeCreated: 1635146300 \ No newline at end of file diff --git a/Runtime/GuruCore/Runtime/Firebase/FirebaseUtil.FireStore.cs b/Runtime/GuruCore/Runtime/Firebase/FirebaseUtil.FireStore.cs new file mode 100644 index 0000000..6bb3f56 --- /dev/null +++ b/Runtime/GuruCore/Runtime/Firebase/FirebaseUtil.FireStore.cs @@ -0,0 +1,264 @@ +using System; +using System.Collections.Generic; +using Firebase.Extensions; +using Firebase.Firestore; + +namespace Guru +{ + public static partial class FirebaseUtil + { + private static FirebaseFirestore _firestore => FirebaseFirestore.DefaultInstance; + + #region 添加数据 + + public static void FireStore_SetDocument(string collection, object data) + { + _firestore.Collection(collection)?.AddAsync(data); + } + + public static void FireStore_SetData(string collection, string document, object data, SetOptions options) + { + _firestore.Collection(collection).Document(document).SetAsync(data, options); + } + + public static void FireStore_SetData(string collection, string document, Dictionary data, SetOptions options) + { + _firestore.Collection(collection).Document(document).SetAsync(data, options); + } + + public static void FireStore_UpdateData(string collection, string document, IDictionary data) + { + _firestore.Collection(collection).Document(document).UpdateAsync(data); + } + + public static void FireStore_UpdateData(string collection, string document, string field, object data) + { + _firestore.Collection(collection).Document(document).UpdateAsync(field, data); + } + + #endregion + + #region 自定义类形式获取数据 + + public static void FireStore_GetDocumentData(string collection, string document, + Action SuccessCallBack, Action FailCallBack) + { + try + { + _firestore.Collection(collection).Document(document).GetSnapshotAsync() + .ContinueWithOnMainThread(task => + { + if (task.IsCanceled || task.IsFaulted) + { + FailCallBack?.Invoke(task?.Exception?.ToString()); + Log.E(LOG_TAG, task.Exception); + } + else + { + DocumentSnapshot documentSnapshot = task.Result; + if (documentSnapshot != null && documentSnapshot.Exists) + { + T t = documentSnapshot.ConvertTo(); + if (t != null) + { + SuccessCallBack?.Invoke(t); + } + else + { + FailCallBack?.Invoke("conver to is null"); + } + } + else + { + FailCallBack?.Invoke("no document exist"); + } + } + }); + } + catch (Exception e) + { + FailCallBack.Invoke(e.ToString()); + Log.E(LOG_TAG, e.ToString()); + } + } + + public static void FireStore_GetDocumentDataAsync(string collection, string document, + Action SuccessCallBack, Action FailCallBack) + { + try + { + _firestore.Collection(collection).Document(document).GetSnapshotAsync() + .ContinueWithOnMainThread(task => + { + if (task.IsCanceled || task.IsFaulted) + { + FailCallBack?.Invoke(task?.Exception?.ToString()); + Log.E(LOG_TAG, task.Exception); + } + else + { + DocumentSnapshot documentSnapshot = task.Result; + if (documentSnapshot != null && documentSnapshot.Exists) + { + T t = documentSnapshot.ConvertTo(); + if (t != null) + { + SuccessCallBack?.Invoke(t); + } + else + { + FailCallBack?.Invoke("conver to is null"); + } + } + else + { + FailCallBack?.Invoke("no document exist"); + } + } + }); + } + catch (Exception e) + { + FailCallBack.Invoke(e.ToString()); + Log.E(LOG_TAG, e.ToString()); + } + } + + #endregion + + #region Dictionary形式获取数据 + + public static void FireStore_GetDocumentData(string collection, string document, + Action> SuccessCallBack, Action FailCallBack) + { + try + { + _firestore.Collection(collection).Document(document).GetSnapshotAsync() + .ContinueWithOnMainThread(task => + { + if (task.IsCanceled || task.IsFaulted) + { + FailCallBack?.Invoke(task?.Exception?.ToString()); + Log.E(LOG_TAG, task.Exception); + } + else + { + DocumentSnapshot documentSnapshot = task.Result; + if (documentSnapshot != null && documentSnapshot.Exists) + { + SuccessCallBack?.Invoke(documentSnapshot.ToDictionary()); + } + else + { + FailCallBack?.Invoke("no document exist"); + } + } + }); + } + catch (Exception e) + { + FailCallBack.Invoke(e.ToString()); + Log.E(LOG_TAG, e.ToString()); + } + } + + public static void FireStore_GetDocumentDataAsync(string collection, string document, Action> SuccessCallBack, Action FailCallBack) + { + try + { + _firestore.Collection(collection).Document(document).GetSnapshotAsync() + .ContinueWithOnMainThread(task => + { + if (task.IsCanceled || task.IsFaulted) + { + FailCallBack?.Invoke(task?.Exception?.ToString()); + Log.E(LOG_TAG, task.Exception); + } + else + { + DocumentSnapshot documentSnapshot = task.Result; + if (documentSnapshot != null && documentSnapshot.Exists) + { + SuccessCallBack?.Invoke(documentSnapshot.ToDictionary()); + } + else + { + FailCallBack?.Invoke("no document exist"); + } + } + }); + } + catch (Exception e) + { + FailCallBack.Invoke(e.ToString()); + Log.E(LOG_TAG, e.ToString()); + } + } + + #endregion + + #region 获取Collections下所有文档 + + public static void FireStore_GetCollections(string collection, Action> SuccessCallBack, Action FailCallBack) + { + try + { + _firestore.Collection(collection).GetSnapshotAsync() + .ContinueWithOnMainThread(task => + { + if (task.IsCanceled || task.IsFaulted) + { + FailCallBack?.Invoke(task?.Exception?.ToString()); + Log.E(LOG_TAG, task.Exception); + } + else + { + List results = new List(); + foreach (DocumentSnapshot documentSnapshot in task.Result.Documents) + { + results.Add(documentSnapshot.ConvertTo()); + } + SuccessCallBack?.Invoke(results); + } + }); + } + catch (Exception e) + { + FailCallBack.Invoke(e.ToString()); + Log.E(LOG_TAG, e.ToString()); + } + } + + public static void FireStore_GetCollectionsAsync(string collection, Action> SuccessCallBack, Action FailCallBack) + { + try + { + _firestore.Collection(collection).GetSnapshotAsync() + .ContinueWithOnMainThread(task => + { + if (task.IsCanceled || task.IsFaulted) + { + FailCallBack?.Invoke(task?.Exception?.ToString()); + Log.E(LOG_TAG, task.Exception); + } + else + { + List results = new List(); + foreach (DocumentSnapshot documentSnapshot in task.Result.Documents) + { + results.Add(documentSnapshot.ConvertTo()); + } + SuccessCallBack?.Invoke(results); + } + }); + } + catch (Exception e) + { + FailCallBack.Invoke(e.ToString()); + Log.E(LOG_TAG, e.ToString()); + } + } + + #endregion + } +} \ No newline at end of file diff --git a/Runtime/GuruCore/Runtime/Firebase/FirebaseUtil.FireStore.cs.meta b/Runtime/GuruCore/Runtime/Firebase/FirebaseUtil.FireStore.cs.meta new file mode 100644 index 0000000..e7f5319 --- /dev/null +++ b/Runtime/GuruCore/Runtime/Firebase/FirebaseUtil.FireStore.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 95aad5168ede4b238e7d8f3bdc091867 +timeCreated: 1635146723 \ No newline at end of file diff --git a/Runtime/GuruCore/Runtime/Firebase/FirebaseUtil.Message.cs b/Runtime/GuruCore/Runtime/Firebase/FirebaseUtil.Message.cs new file mode 100644 index 0000000..837a66c --- /dev/null +++ b/Runtime/GuruCore/Runtime/Firebase/FirebaseUtil.Message.cs @@ -0,0 +1,84 @@ +using System; +using System.Collections; +using Firebase.Messaging; +using UnityEngine; + +namespace Guru +{ + public static partial class FirebaseUtil + { + private static int _messageRetry = 5; + public static bool? IsInitMessage; + + public static void InitializeMessage() + { + FirebaseMessaging.TokenReceived += OnTokenReceived; + FirebaseMessaging.MessageReceived += OnMessageReceived; + GetFCMToken(); + IsInitMessage = true; + } + + private static void GetFCMToken() + { + CoroutineHelper.Instance.StartCoroutine(CoroutineGetFCMToken()); + } + + private static IEnumerator CoroutineGetFCMToken() + { + var task = FirebaseMessaging.GetTokenAsync(); + while (!task.IsCompleted) + yield return new WaitForEndOfFrame(); + + if (task.IsFaulted || task.IsCanceled) + { + CoroutineHelper.Instance.StartDelayed(_wait, GetFCMToken); + } + else + { + Log.I(LOG_TAG, "GetTokenAsync Token: " + task.Result); + if (IPMConfig.IPM_PUSH_TOKEN != task.Result || !IPMConfig.IS_UPLOAD_DEVICE_SUCCESS) + { + IPMConfig.IPM_PUSH_TOKEN = task.Result; + UploadDeviceInfo(); + } + } + } + + private static void UploadDeviceInfo() + { + if (!NetworkUtil.IsNetAvaliable) + { + double retryDelay = Math.Pow(2, _messageRetry); + _messageRetry++; + CoroutineHelper.Instance.StartDelayed((float) retryDelay, UploadDeviceInfo); + } + else + { + Log.I(LOG_TAG, "FirebaseMessage Send UploadDeviceInfo"); + //延时重试 + new DeviceInfoUploadRequest() + .SetRetryTimes(1) + .SetFailCallBack(() => + { + double retryDelay = Math.Pow(2, _messageRetry); + _messageRetry++; + CoroutineHelper.Instance.StartDelayed((float) retryDelay, UploadDeviceInfo); + }).Send(); + } + } + + private static void OnTokenReceived(object sender, Firebase.Messaging.TokenReceivedEventArgs token) + { +#if UNITY_IOS + DeviceUtil.SetiOSBadge(); +#endif + } + + public static void OnMessageReceived(object sender, Firebase.Messaging.MessageReceivedEventArgs e) + { +#if UNITY_IOS + DeviceUtil.SetiOSBadge(); +#endif + } + } +} \ No newline at end of file diff --git a/Runtime/GuruCore/Runtime/Firebase/FirebaseUtil.Message.cs.meta b/Runtime/GuruCore/Runtime/Firebase/FirebaseUtil.Message.cs.meta new file mode 100644 index 0000000..4ccf591 --- /dev/null +++ b/Runtime/GuruCore/Runtime/Firebase/FirebaseUtil.Message.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: fd921abbf9184ee98dddaf1530b24827 +timeCreated: 1635145458 \ No newline at end of file diff --git a/Runtime/GuruCore/Runtime/Firebase/FirebaseUtil.RemoteConfig.cs b/Runtime/GuruCore/Runtime/Firebase/FirebaseUtil.RemoteConfig.cs new file mode 100644 index 0000000..db682c5 --- /dev/null +++ b/Runtime/GuruCore/Runtime/Firebase/FirebaseUtil.RemoteConfig.cs @@ -0,0 +1,127 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using Firebase.Extensions; +using Firebase.RemoteConfig; +using UnityEngine; + +namespace Guru +{ + public static partial class FirebaseUtil + { + private static FirebaseRemoteConfig _remoteConfigInstance => FirebaseRemoteConfig.DefaultInstance; + private static Dictionary _defaults = new Dictionary(); + private static bool _isFetchSuccess; + public static event Action> OnSetDefaultParams; + public static event Action OnFetchRemoteSuccess; + + public static bool GetRemoteBooleanValue(string key) => FirebaseRemoteConfig.DefaultInstance.GetValue(key).BooleanValue; + public static long GetRemoteLongValue(string key) => FirebaseRemoteConfig.DefaultInstance.GetValue(key).LongValue; + public static string GetRemoteStringValue(string key) => FirebaseRemoteConfig.DefaultInstance.GetValue(key).StringValue; + public static double GetRemoteDoubleValue(string key) => FirebaseRemoteConfig.DefaultInstance.GetValue(key).DoubleValue; + public static int GetRemoteIntValue(string key) + { + try + { + if (int.TryParse(FirebaseRemoteConfig.DefaultInstance.GetValue(key).StringValue, out int value)) + return value; + else + return FallBack(); + } + catch (Exception e) + { + return FallBack(); + } + + int FallBack() + { + if (_defaults.ContainsKey(key)) + return (int) _defaults[key]; + else + return -1; + } + } + public static float GetRemoteFloatValue(string key) + { + try + { + if (float.TryParse(FirebaseRemoteConfig.DefaultInstance.GetValue(key).StringValue, out float value)) + return value; + else + return FallBack(); + } + catch (Exception e) + { + return FallBack(); + } + + float FallBack() + { + if (_defaults.ContainsKey(key)) + return (float) _defaults[key]; + else + return -1f; + } + } + public static T GetRemoteConfig(string key) where T : class + { + string value = GetRemoteStringValue(key); + if (value.IsNotNullAndEmpty()) + return JsonUtility.FromJson(value); + else if (_defaults.ContainsKey(key)) + return JsonUtility.FromJson((string) _defaults[key]); + else + return null; + } + + public static void InitRemoteConfig() + { + InitSetDefaultParams(); + FetchRemoteValue(); + } + + private static void InitSetDefaultParams() + { + OnSetDefaultParams?.Invoke(_defaults); + _remoteConfigInstance.SetDefaultsAsync(_defaults); + } + + private static void FetchRemoteValue() + { + TimeSpan timeSpan = PlatformUtil.IsDebug() ? TimeSpan.Zero : TimeSpan.FromHours(12); + _remoteConfigInstance.FetchAsync(timeSpan).ContinueWithOnMainThread(task => + { + if (task.IsCanceled || task.IsFaulted + || _remoteConfigInstance.Info.LastFetchStatus != LastFetchStatus.Success) + { + Log.E(LOG_TAG, "Firebase RemoteConfig Fetch Failure"); + CoroutineHelper.Instance.StartDelayed(_wait, FetchRemoteValue); + return; + } + + _isFetchSuccess = true; + _remoteConfigInstance.ActivateAsync(); + OnFetchRemoteSuccess?.Invoke(); + }); + } + + public static void AppendDefaultValue(string key, object value) + { + _defaults[key] = value; + _remoteConfigInstance.SetDefaultsAsync(_defaults); + } + + + public static bool IsFetchSuccess => _isFetchSuccess; + + + /// + /// 是否包含远程Key + /// + /// + /// + public static bool HasRemoteKey(string key) => _remoteConfigInstance?.Keys.Contains(key) ?? false; + + } +} \ No newline at end of file diff --git a/Runtime/GuruCore/Runtime/Firebase/FirebaseUtil.RemoteConfig.cs.meta b/Runtime/GuruCore/Runtime/Firebase/FirebaseUtil.RemoteConfig.cs.meta new file mode 100644 index 0000000..039b5fc --- /dev/null +++ b/Runtime/GuruCore/Runtime/Firebase/FirebaseUtil.RemoteConfig.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: f786f50a88e94ef69ceb49566815fb5e +timeCreated: 1635145578 \ No newline at end of file diff --git a/Runtime/GuruCore/Runtime/Firebase/FirebaseUtil.cs b/Runtime/GuruCore/Runtime/Firebase/FirebaseUtil.cs new file mode 100644 index 0000000..caa2db3 --- /dev/null +++ b/Runtime/GuruCore/Runtime/Firebase/FirebaseUtil.cs @@ -0,0 +1,109 @@ +using System; +using Firebase; +using Firebase.Analytics; +using Firebase.Extensions; + +namespace Guru +{ + public static partial class FirebaseUtil + { + private static readonly string LOG_TAG = "Firebase"; + private static bool _isDebug = false; + + public static DependencyStatus DependencyStatus = DependencyStatus.UnavailableOther; + public static bool IsFirebaseInitialized => DependencyStatus == DependencyStatus.Available; + public static void InitFirebase(Action callback, bool isDebug = false) + { + _isDebug = isDebug; + FirebaseApp.CheckAndFixDependenciesAsync().ContinueWithOnMainThread(task => { + DependencyStatus = task.Result; + if (DependencyStatus == DependencyStatus.Available) + { + LinkFirebaseID2Adjust(); // 上报FirebaseID + InitializeFirebaseComp(); + callback?.Invoke(); + } + else + { + Log.E(LOG_TAG, "Could not resolve all Firebase dependencies: " + DependencyStatus); + } + }); + } + private static void InitializeFirebaseComp() + { + InitCrashlytics(); + InitRemoteConfig(); + Analytics.InstallGuruAnalytics(_isDebug); // 初始化Guru自打点 + Analytics.InitAnalytics(); + + if (IPMConfig.IPM_UID.IsNullOrEmpty()) + { + Log.I(LOG_TAG, "没有存储UID时,从中台获取匿名认证授权"); + //没有存储UID时,从中台获取匿名认证授权 + new AuthUserRequest() + .SetRetryTimes(-1) + .SetSuccessCallBack(() => + { + InitializeMessage(); + AuthUser(); + }) + .Send(); + } + else + { + InitializeMessage(); + int currentTimeStamp = TimeUtil.GetCurrentTimeStampSecond(); + if(currentTimeStamp - IPMConfig.IPM_TOKEN_TIME >= IPMConfig.TOKEN_VALID_TIME) + { + //中台Token失效,从中台重新获取Token + new RefreshTokenRequest().SetRetryTimes(-1).Send(); + } + + if(currentTimeStamp - IPMConfig.IPM_FIREBASE_TOKEN_TIME >= IPMConfig.FIREBASE_TOKEN_VALID_TIME) + { + //中台firebaseToken失效,从中台重新获取firebaseToken + new RefreshFirebaseTokenRequest() + .SetRetryTimes(-1) + .SetSuccessCallBack(AuthUser) + .Send(); + } + else + { + AuthUser(); + } + } + } + + #region 关联FirebaseID 到 Adjust + + /// + /// 关联FirebaseID到Adjust + /// + private static void LinkFirebaseID2Adjust() + { + FirebaseAnalytics.GetAnalyticsInstanceIdAsync() + .ContinueWithOnMainThread(task => + { + if (task.IsCompleted && !string.IsNullOrEmpty(task.Result)) + { + // 保存本地ID备份 + string fid = task.Result; + StandardProperties.FirebaseId = fid; // 保存本次非空ID + IPMConfig.FIREBASE_ID = fid; // 保存FirebaseID + } + else + { + UnityEngine.Debug.LogError($"Fetch FirebaseID failed on start!"); + } + + AdjustService.StartService(); // 关联启动AdjustService + }); + } + + + #endregion + + + + } +} \ No newline at end of file diff --git a/Runtime/GuruCore/Runtime/Firebase/FirebaseUtil.cs.meta b/Runtime/GuruCore/Runtime/Firebase/FirebaseUtil.cs.meta new file mode 100644 index 0000000..645abf0 --- /dev/null +++ b/Runtime/GuruCore/Runtime/Firebase/FirebaseUtil.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 723b6ad4642b41e3aa186d9037a759a9 +timeCreated: 1635147829 \ No newline at end of file diff --git a/Runtime/GuruCore/Runtime/GameDefine.cs b/Runtime/GuruCore/Runtime/GameDefine.cs new file mode 100644 index 0000000..bbcbc4f --- /dev/null +++ b/Runtime/GuruCore/Runtime/GameDefine.cs @@ -0,0 +1,11 @@ +namespace Guru +{ + /// + /// 游戏常量定义 + /// + public partial class GameDefine + { + public static string MACRO_DEBUG = "DEBUG"; + public static string MACRO_RELEASE = "RELEASE"; + } +} \ No newline at end of file diff --git a/Runtime/GuruCore/Runtime/GameDefine.cs.meta b/Runtime/GuruCore/Runtime/GameDefine.cs.meta new file mode 100644 index 0000000..d58fbc7 --- /dev/null +++ b/Runtime/GuruCore/Runtime/GameDefine.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: fe618364209e42aeb91fbb765932a081 +timeCreated: 1654834337 \ No newline at end of file diff --git a/Runtime/GuruCore/Runtime/GuruCore.cs b/Runtime/GuruCore/Runtime/GuruCore.cs new file mode 100644 index 0000000..5f2dddf --- /dev/null +++ b/Runtime/GuruCore/Runtime/GuruCore.cs @@ -0,0 +1,19 @@ +namespace Guru +{ + //=== DO NOT CHANGE THIS FILE, AUTO FIXED BY CODE ===// + public class GuruCore + { + //**** GURU CORE VERSION ****// + public static readonly string Version = "2.0.3"; + //**** GURU VERSION URL ****// + public static readonly string VersionListUrl = $"{CONTENT_URL_MAIN_PREFIX}version.json"; + // GuruCore Project Path + public const string UNITY_GURUCORE_URL = "https://github.com/castbox/unity_gurucore"; + // Template Project Path + public const string UNITY_TEMPLATE_URL = "https://github.com/castbox/unity_template"; + // Content Url Prefix for main branch + public const string CONTENT_URL_MAIN_PREFIX = "https://raw.githubusercontent.com/castbox/unity_gurucore/main/"; + // Version Code File Name + public const string VERSION_CODE_FILE = "verison_code"; + } +} diff --git a/Runtime/GuruCore/Runtime/GuruCore.cs.meta b/Runtime/GuruCore/Runtime/GuruCore.cs.meta new file mode 100644 index 0000000..92afd83 --- /dev/null +++ b/Runtime/GuruCore/Runtime/GuruCore.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: e589a914d4a14262b8bdf8d8bd063d7b +timeCreated: 1671010395 \ No newline at end of file diff --git a/Runtime/GuruCore/Runtime/IPM.meta b/Runtime/GuruCore/Runtime/IPM.meta new file mode 100644 index 0000000..7c932b2 --- /dev/null +++ b/Runtime/GuruCore/Runtime/IPM.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: a81cf44b157224297b8d9bee5bcd6105 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/GuruCore/Runtime/IPM/Scripts.meta b/Runtime/GuruCore/Runtime/IPM/Scripts.meta new file mode 100644 index 0000000..9489651 --- /dev/null +++ b/Runtime/GuruCore/Runtime/IPM/Scripts.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 02d255778e5142539009a4b473f8037a +timeCreated: 1632378201 \ No newline at end of file diff --git a/Runtime/GuruCore/Runtime/IPM/Scripts/DeviceUtil.cs b/Runtime/GuruCore/Runtime/IPM/Scripts/DeviceUtil.cs new file mode 100644 index 0000000..cf29501 --- /dev/null +++ b/Runtime/GuruCore/Runtime/IPM/Scripts/DeviceUtil.cs @@ -0,0 +1,143 @@ +using System.Runtime.InteropServices; +using UnityEngine; + +namespace Guru +{ + public static class DeviceUtil + { + public static bool IsGetDeviceInfoSuccess; +#if UNITY_IOS + [DllImport ("__Internal")] + private static extern string iOSDeviceInfo(); + [DllImport ("__Internal")] + private static extern void iOSSetBadge(); + [DllImport ("__Internal")] + private static extern void savePlayerPrefs2AppGroup(string appGroupName); + [DllImport ("__Internal")] + private static extern void iOSClearBadge(); +#endif + public static bool GetDeviceInfo() + { + if (!IsGetDeviceInfoSuccess) + { +#if UNITY_ANDROID + GetAndroidDeviceInfo(); +#elif UNITY_IOS + GetIOSDeviceInfo(); +#endif + } + return IsGetDeviceInfoSuccess; + } + + #region IOS + + private static void GetIOSDeviceInfo() + { +#if UNITY_IOS && !UNITY_EDITOR + string content = iOSDeviceInfo(); + Debug.Log($"GetDeviceInfo:{content}"); + if(!string.IsNullOrEmpty(content)) + { + string[] infos = content.Split('$'); + IPMConfig.IPM_DEVICE_ID = infos[0]; + IPMConfig.IPM_APP_VERSION = infos[1]; + IPMConfig.IPM_TIMEZONE = infos[2]; + IPMConfig.IPM_MODEL = infos[3]; + IPMConfig.IPM_LANGUAGE = infos[4]; + IPMConfig.IPM_LOCALE = infos[5]; + IPMConfig.IPM_COUNTRY_CODE = infos[6]; + IsGetDeviceInfoSuccess = true; + } +#endif + } + + public static void SetiOSBadge() + { +#if UNITY_IOS && !UNITY_EDITOR + iOSSetBadge(); +#endif + } + + public static void Save2AppGroup() + { +#if UNITY_IOS && !UNITY_EDITOR + savePlayerPrefs2AppGroup(IPMConfig.IPM_IOS_APP_GROUP); +#endif + } + + public static void ClerBadge() + { +#if UNITY_IOS && !UNITY_EDITOR + iOSClearBadge(); +#endif + } + + #endregion + + #region Android + + private static AndroidJavaObject _androidJavaObject; + private static void GetAndroidDeviceInfo() + { +#if UNITY_ANDROID + _androidJavaObject ??= new AndroidJavaObject("com.guru.u3d2android.u3d2android"); + if (_androidJavaObject != null) + { + string content = _androidJavaObject.Call("getDeviceInfo"); + Debug.Log($"GetDeviceInfo:{content}"); + if(!string.IsNullOrEmpty(content)) + { + string[] infos = content.Split('$'); + IPMConfig.IPM_DEVICE_ID = SystemInfo.deviceUniqueIdentifier; + IPMConfig.IPM_BRAND = infos[0]; + IPMConfig.IPM_LANGUAGE = infos[1]; + IPMConfig.IPM_MODEL = infos[2]; + IPMConfig.IPM_TIMEZONE = infos[4]; + IPMConfig.IPM_LOCALE = infos[5]; + IPMConfig.IPM_COUNTRY_CODE = infos[6]; + IsGetDeviceInfoSuccess = true; + } + } +#endif + } + + + #endregion + + + #region 系统弹框 + public static void ShowToast(string content) + { +#if UNITY_EDITOR + UnityEditor.EditorUtility.DisplayDialog("系统提示", content, "OK"); +#elif UNITY_ANDROID + if (_androidJavaObject == null) return; + _androidJavaObject.Call("showToast", content); +#endif + Debug.Log($"--------- INFORMATION --------\n{content}\n--------- INFORMATION --------"); + } + + + #endregion + + #region 系统版本 + + /// + /// 获取AndroidOS系统的版本号 + /// 如果获取失败则返回 0 + /// + /// + public static int GetAndroidOSVersionInt() + { +#if UNITY_ANDROID && !UNITY_EDITOR + return _androidJavaObject?.CallStatic("getSystemVersionSdkInt") ?? 0; +#endif + return 0; + } + + + + + #endregion + } +} \ No newline at end of file diff --git a/Runtime/GuruCore/Runtime/IPM/Scripts/DeviceUtil.cs.meta b/Runtime/GuruCore/Runtime/IPM/Scripts/DeviceUtil.cs.meta new file mode 100644 index 0000000..c9a89fb --- /dev/null +++ b/Runtime/GuruCore/Runtime/IPM/Scripts/DeviceUtil.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 6738dfb1570f4f3cb10107b9cda7109d +timeCreated: 1654832862 \ No newline at end of file diff --git a/Runtime/GuruCore/Runtime/IPM/Scripts/IPMConfig.cs b/Runtime/GuruCore/Runtime/IPM/Scripts/IPMConfig.cs new file mode 100644 index 0000000..ca47d8d --- /dev/null +++ b/Runtime/GuruCore/Runtime/IPM/Scripts/IPMConfig.cs @@ -0,0 +1,143 @@ +using System; +using System.Globalization; +using UnityEngine; + +namespace Guru +{ + /// + /// 中台配置信息参数 + /// + public static class IPMConfig + { + public static string IPM_X_APP_ID => GuruSettings.Instance.IPMSetting.AppId; + public static int TOKEN_VALID_TIME => GuruSettings.Instance.IPMSetting.TokenValidTime; + public static readonly int FIREBASE_TOKEN_VALID_TIME = TimeUtil.HOUR_TO_SECOND; + + + public static readonly string Header_Param_APPID = "X-APP-ID"; + public static readonly string Header_Param_DEVICE_INFO = "X-DEVICE-INFO"; + public static readonly string Header_Param_UID = "X-UID"; + public static readonly string Header_Param_ACCESS_TOKEN = "X-ACCESS-TOKEN"; + public static readonly string Header_Param_ContentType = "Content-Type"; + public static readonly string Header_Value_ContentType = "application/json"; + public static readonly string POST_Param_AccessToken = "accessToken"; + public static readonly string POST_Param_IDToken = "idToken"; + public static readonly string POST_Param_Token = "token"; + public static readonly string POST_Param_Provider = "provider"; + public static readonly string POST_Param_ClientType = "clientType"; + public static readonly string POST_Param_TokenSecret = "tokenSecret"; + + public static string IPM_APP_PACKAGE_NAME = Application.identifier; + public static string IPM_DEVICE_ID = SystemInfo.deviceUniqueIdentifier; + public static string IPM_APP_VERSION = Application.version; + public static string IPM_IOS_APP_GROUP = "group." + Application.identifier; + public static string IPM_BRAND = ""; + public static string IPM_LANGUAGE = ""; + public static string IPM_MODEL = ""; + public static string IPM_TIMEZONE = ""; + public static string IPM_LOCALE = ""; + public static string IPM_COUNTRY_CODE = RegionInfo.CurrentRegion.TwoLetterISORegionName; + public static bool IPM_NEWUSER = true; + +#if DEBUG + public static readonly string IPM_URL = "https://dev.saas.castbox.fm/"; +#else + public static readonly string IPM_URL = "https://saas.castbox.fm/"; +#endif + public static readonly string IPM_EVENT_URL = IPM_URL + "push/api/v1/push/app/event"; + + public static string IPM_UID + { + get => PlayerPrefs.GetString(nameof(IPM_UID), ""); + set => PlayerPrefs.SetString(nameof(IPM_UID), value); + } + + public static long IPM_UID_INT + { + get => Convert.ToInt64(PlayerPrefs.GetString(nameof(IPM_UID_INT), "0")); + set => PlayerPrefs.SetString(nameof(IPM_UID_INT), value.ToString()); + } + + public static string IPM_TOKEN + { + get => PlayerPrefs.GetString(nameof(IPM_TOKEN), ""); + set => PlayerPrefs.SetString(nameof(IPM_TOKEN), value); + } + + public static int IPM_TOKEN_TIME + { + get => PlayerPrefs.GetInt(nameof(IPM_TOKEN_TIME), 0); + set => PlayerPrefs.SetInt(nameof(IPM_TOKEN_TIME), value); + } + + public static string IPM_FIREBASE_TOKEN + { + get => PlayerPrefs.GetString(nameof(IPM_FIREBASE_TOKEN), ""); + set => PlayerPrefs.SetString(nameof(IPM_FIREBASE_TOKEN), value); + } + + public static string IPM_PUSH_TOKEN + { + get => PlayerPrefs.GetString(nameof(IPM_PUSH_TOKEN), ""); + set => PlayerPrefs.SetString(nameof(IPM_PUSH_TOKEN), value); + } + + //是否已经成功上报过设备信息 + public static bool IS_UPLOAD_DEVICE_SUCCESS + { + get => PlayerPrefs.GetInt(nameof(IS_UPLOAD_DEVICE_SUCCESS), 0) == 1; + set => PlayerPrefs.SetInt(nameof(IS_UPLOAD_DEVICE_SUCCESS), true ? 1 : 0); + } + + public static int IPM_FIREBASE_TOKEN_TIME + { + get => PlayerPrefs.GetInt(nameof(IPM_FIREBASE_TOKEN_TIME), 0); + set => PlayerPrefs.SetInt(nameof(IPM_FIREBASE_TOKEN_TIME), value); + } + + public static long IPM_CREATED_TIMESTAMP + { + get => Convert.ToInt64(PlayerPrefs.GetString(nameof(IPM_CREATED_TIMESTAMP), "")); + set => PlayerPrefs.SetString(nameof(IPM_CREATED_TIMESTAMP), value.ToString()); + } + + public static string GetCountryCode() + { +#if UNITY_EDITOR + return "US"; +#else + return IPM_COUNTRY_CODE.ToUpper(); +#endif + } + + public static string GetDeviceType() + { + #if UNITY_IOS + return "iOS"; + #else + return "android"; + #endif + } + + //------------ 2023-04 new IDs -------------- + public static string FIREBASE_ID + { + get => PlayerPrefs.GetString(nameof(FIREBASE_ID), ""); + set => PlayerPrefs.SetString(nameof(FIREBASE_ID), value); + } + + public static string ADJUST_ID + { + get => PlayerPrefs.GetString(nameof(ADJUST_ID), ""); + set => PlayerPrefs.SetString(nameof(ADJUST_ID), value); + } + + public static string ADJUST_ADID + { + get => PlayerPrefs.GetString(nameof(ADJUST_ADID), ""); + set => PlayerPrefs.SetString(nameof(ADJUST_ADID), value); + } + + + } +} \ No newline at end of file diff --git a/Runtime/GuruCore/Runtime/IPM/Scripts/IPMConfig.cs.meta b/Runtime/GuruCore/Runtime/IPM/Scripts/IPMConfig.cs.meta new file mode 100644 index 0000000..8c021bb --- /dev/null +++ b/Runtime/GuruCore/Runtime/IPM/Scripts/IPMConfig.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 7a8237756c384cedba6d621bb7ec8c2e +timeCreated: 1632365900 \ No newline at end of file diff --git a/Runtime/GuruCore/Runtime/IPM/Scripts/RequestData.meta b/Runtime/GuruCore/Runtime/IPM/Scripts/RequestData.meta new file mode 100644 index 0000000..a44caf5 --- /dev/null +++ b/Runtime/GuruCore/Runtime/IPM/Scripts/RequestData.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: f2cecf38b7bd4ea39ef121cf256a6ad3 +timeCreated: 1632554408 \ No newline at end of file diff --git a/Runtime/GuruCore/Runtime/IPM/Scripts/RequestData/AppleOrderData.cs b/Runtime/GuruCore/Runtime/IPM/Scripts/RequestData/AppleOrderData.cs new file mode 100644 index 0000000..b9a475c --- /dev/null +++ b/Runtime/GuruCore/Runtime/IPM/Scripts/RequestData/AppleOrderData.cs @@ -0,0 +1,30 @@ +using System; +using System.Collections.Generic; + +namespace Guru +{ + [Serializable] + public class AppleOrderData + { + public string bundleId; + public string receipt; + public string country; + public Dictionary userInfo; + + public AppleOrderData(string receipt, int level) + { + this.receipt = receipt; + bundleId = GuruSettings.Instance.GameIdentifier; + country = IPMConfig.IPM_COUNTRY_CODE; + userInfo = new Dictionary + { + ["level"] = level + }; + } + + public override string ToString() + { + return $"{nameof(bundleId)}: {bundleId}, {nameof(receipt)}: {receipt}, {nameof(country)}: {country}"; + } + } +} \ No newline at end of file diff --git a/Runtime/GuruCore/Runtime/IPM/Scripts/RequestData/AppleOrderData.cs.meta b/Runtime/GuruCore/Runtime/IPM/Scripts/RequestData/AppleOrderData.cs.meta new file mode 100644 index 0000000..affe469 --- /dev/null +++ b/Runtime/GuruCore/Runtime/IPM/Scripts/RequestData/AppleOrderData.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 7c2f5ef519e54c99ba9087f94d2bc066 +timeCreated: 1654833954 \ No newline at end of file diff --git a/Runtime/GuruCore/Runtime/IPM/Scripts/RequestData/DeviceData.cs b/Runtime/GuruCore/Runtime/IPM/Scripts/RequestData/DeviceData.cs new file mode 100644 index 0000000..c8c5ad4 --- /dev/null +++ b/Runtime/GuruCore/Runtime/IPM/Scripts/RequestData/DeviceData.cs @@ -0,0 +1,51 @@ +using System; + +namespace Guru +{ + [Serializable] + public class DeviceData + { + public string deviceId; //必填,设备唯一ID + public string uid; //必填,用户唯一ID,授权结果返回的uid + public string androidId; //android设备有效 + public string appCountry; //国家编码,大写,如果APP内可以设置国家信息则使用该属性,参考ISO 3166-1 alpha-2 + public string deviceCountry;//国家编码,大写,手机的系统国家,参考ISO 3166-1 alpha-2 + public string language; //语言编码,如en + public string locale; //locale编码,如en-US + public string deviceToken; //必填,设备发送消息的token,如google firebase的token + public string deviceType; //必填,具体内容:Android设备:android IOS设备:iOS/iPhone/iPad/iPod touch其中一种 + public string pushType; //必填,目前仅支持:FCM + public string appIdentifier;//APP包名,如android的packageName或者ios的bundleId + public string appVersion; //APP版本,如1.0.0 + public string brand; //手机品牌 + public string model; //手机品牌下的手机型号 + public string timezone; //必填,时区,用于按照当地时间发推送消息,如America/Chicago + public bool pushNotificationEnable; //必填,默认true,发送消息的总开关 + + public DeviceData() + { + DeviceUtil.GetDeviceInfo(); + deviceId = IPMConfig.IPM_DEVICE_ID; + uid = IPMConfig.IPM_UID; + androidId = IPMConfig.IPM_DEVICE_ID; + appCountry = IPMConfig.IPM_COUNTRY_CODE; + deviceCountry = IPMConfig.IPM_COUNTRY_CODE; + language = IPMConfig.IPM_LANGUAGE; + locale = IPMConfig.IPM_LOCALE; + deviceToken = IPMConfig.IPM_PUSH_TOKEN; + deviceType = IPMConfig.GetDeviceType(); + pushType = "FCM"; + appIdentifier = IPMConfig.IPM_APP_PACKAGE_NAME; + appVersion = IPMConfig.IPM_APP_VERSION; + brand = IPMConfig.IPM_BRAND; + model = IPMConfig.IPM_MODEL; + timezone = IPMConfig.IPM_TIMEZONE; + pushNotificationEnable = true; + } + + public override string ToString() + { + return $"{nameof(deviceId)}: {deviceId}, {nameof(uid)}: {uid}, {nameof(androidId)}: {androidId}, {nameof(appCountry)}: {appCountry}, {nameof(deviceCountry)}: {deviceCountry}, {nameof(language)}: {language}, {nameof(locale)}: {locale}, {nameof(deviceToken)}: {deviceToken}, {nameof(deviceType)}: {deviceType}, {nameof(pushType)}: {pushType}, {nameof(appIdentifier)}: {appIdentifier}, {nameof(appVersion)}: {appVersion}, {nameof(brand)}: {brand}, {nameof(model)}: {model}, {nameof(timezone)}: {timezone}, {nameof(pushNotificationEnable)}: {pushNotificationEnable}"; + } + } +} \ No newline at end of file diff --git a/Runtime/GuruCore/Runtime/IPM/Scripts/RequestData/DeviceData.cs.meta b/Runtime/GuruCore/Runtime/IPM/Scripts/RequestData/DeviceData.cs.meta new file mode 100644 index 0000000..c723e85 --- /dev/null +++ b/Runtime/GuruCore/Runtime/IPM/Scripts/RequestData/DeviceData.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 1f62cddbdb1a44a5a66e0e2be8923d18 +timeCreated: 1632554417 \ No newline at end of file diff --git a/Runtime/GuruCore/Runtime/IPM/Scripts/RequestData/DeviceInfoData.cs b/Runtime/GuruCore/Runtime/IPM/Scripts/RequestData/DeviceInfoData.cs new file mode 100644 index 0000000..a6e6c03 --- /dev/null +++ b/Runtime/GuruCore/Runtime/IPM/Scripts/RequestData/DeviceInfoData.cs @@ -0,0 +1,40 @@ +namespace Guru +{ + public class DeviceInfoData + { + public string appIdentifier;//APP包名,如android的packageName或者ios的bundleId + public string appVersion; //APP版本,如1.0.0 + public string deviceType; //必填,具体内容:Android设备:android IOS设备:iOS/iPhone/iPad/iPod touch其中一种 + public string deviceCountry;//国家编码,大写,手机的系统国家,参考ISO 3166-1 alpha-2 + public string appCountry; //国家编码,大写,如果APP内可以设置国家信息则使用该属性,参考ISO 3166-1 alpha-2 + public string locale; //locale编码,如en-US + public string language; //语言编码,如en + public string timezone; //必填,时区,用于按照当地时间发推送消息,如America/Chicago + public string brand; //手机品牌 + public string model; //手机品牌下的手机型号 + public string androidId; //android设备有效 + + public DeviceInfoData() + { + DeviceUtil.GetDeviceInfo(); + appIdentifier = IPMConfig.IPM_APP_PACKAGE_NAME; + appVersion = IPMConfig.IPM_APP_VERSION; + deviceType = IPMConfig.GetDeviceType(); + deviceCountry = IPMConfig.IPM_COUNTRY_CODE; + appCountry = IPMConfig.IPM_COUNTRY_CODE; + locale = IPMConfig.IPM_LOCALE; + language = IPMConfig.IPM_LANGUAGE; + timezone = IPMConfig.IPM_TIMEZONE; + brand = IPMConfig.IPM_BRAND; + model = IPMConfig.IPM_MODEL; + androidId = IPMConfig.IPM_DEVICE_ID; + } + + public override string ToString() + { + string value = $"{nameof(appIdentifier)}={appIdentifier};{nameof(appVersion)}={appVersion};{nameof(deviceType)}={deviceType};{nameof(deviceCountry)}={deviceCountry};{nameof(appCountry)}={appCountry};{nameof(locale)}={locale};{nameof(language)}={language};{nameof(timezone)}={timezone};{nameof(brand)}={brand};{nameof(model)}={model};{nameof(androidId)}={androidId}"; + this.Log($"IPM.Auth.User DeviceInfo Header:[{value}]"); + return value; + } + } +} \ No newline at end of file diff --git a/Runtime/GuruCore/Runtime/IPM/Scripts/RequestData/DeviceInfoData.cs.meta b/Runtime/GuruCore/Runtime/IPM/Scripts/RequestData/DeviceInfoData.cs.meta new file mode 100644 index 0000000..8e8678d --- /dev/null +++ b/Runtime/GuruCore/Runtime/IPM/Scripts/RequestData/DeviceInfoData.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 4d939308580041b18518ebd14b24e2d6 +timeCreated: 1650361350 \ No newline at end of file diff --git a/Runtime/GuruCore/Runtime/IPM/Scripts/RequestData/GoogleOrderData.cs b/Runtime/GuruCore/Runtime/IPM/Scripts/RequestData/GoogleOrderData.cs new file mode 100644 index 0000000..01833a5 --- /dev/null +++ b/Runtime/GuruCore/Runtime/IPM/Scripts/RequestData/GoogleOrderData.cs @@ -0,0 +1,32 @@ +using System; +using System.Collections.Generic; + +namespace Guru +{ + [Serializable] + public class GoogleOrderData + { + public int orderType; + public string packageName; + public string productId; + public string token; + public Dictionary userInfo; + + public GoogleOrderData(int orderType, string productId, string token, int level) + { + this.orderType = orderType; + this.packageName = GuruSettings.Instance.GameIdentifier; + this.productId = productId; + this.token = token; + userInfo = new Dictionary + { + ["level"] = level + }; + } + + public override string ToString() + { + return $"{nameof(orderType)}: {orderType}, {nameof(packageName)}: {packageName}, {nameof(productId)}: {productId}, {nameof(token)}: {token}"; + } + } +} \ No newline at end of file diff --git a/Runtime/GuruCore/Runtime/IPM/Scripts/RequestData/GoogleOrderData.cs.meta b/Runtime/GuruCore/Runtime/IPM/Scripts/RequestData/GoogleOrderData.cs.meta new file mode 100644 index 0000000..f99873d --- /dev/null +++ b/Runtime/GuruCore/Runtime/IPM/Scripts/RequestData/GoogleOrderData.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: d52c2e34bd754eaba2dadc88f791022c +timeCreated: 1654834013 \ No newline at end of file diff --git a/Runtime/GuruCore/Runtime/IPM/Scripts/Requests.meta b/Runtime/GuruCore/Runtime/IPM/Scripts/Requests.meta new file mode 100644 index 0000000..1f7c1b1 --- /dev/null +++ b/Runtime/GuruCore/Runtime/IPM/Scripts/Requests.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: d7b97a80688448c3a2e22fdbbe78a8a5 +timeCreated: 1632541488 \ No newline at end of file diff --git a/Runtime/GuruCore/Runtime/IPM/Scripts/Requests/AppleOrderRequest.cs b/Runtime/GuruCore/Runtime/IPM/Scripts/Requests/AppleOrderRequest.cs new file mode 100644 index 0000000..26c5cdb --- /dev/null +++ b/Runtime/GuruCore/Runtime/IPM/Scripts/Requests/AppleOrderRequest.cs @@ -0,0 +1,53 @@ +using System.Text; +using Guru.LitJson; +using UnityEngine; +using UnityEngine.Networking; + +namespace Guru +{ + public class AppleOrderRequest : RequestBase + { + public int orderType; + public string productId; + public string receipt; + public int level; + + public AppleOrderRequest(){} + public AppleOrderRequest(int orderType, string productId, string receipt, int level) + { + this.orderType = orderType; + this.productId = productId; + this.receipt = receipt; + this.level = level; + } + + protected override string RequestURL => IPMConfig.IPM_URL + "order/api/v1/orders/ios"; + protected override UnityWebRequest CreateRequest() + { + AppleOrderData appleOrderData = new AppleOrderData(receipt, level); + this.Log($"send orderData:{appleOrderData}"); + var request = new UnityWebRequest(RequestURL, "POST"); + request.uploadHandler = new UploadHandlerRaw(Encoding.UTF8.GetBytes(JsonMapper.ToJson(appleOrderData))); + request.downloadHandler = new DownloadHandlerBuffer(); + request.SetRequestHeader(IPMConfig.Header_Param_APPID, IPMConfig.IPM_X_APP_ID); + request.SetRequestHeader(IPMConfig.Header_Param_UID, IPMConfig.IPM_UID); + request.SetRequestHeader(IPMConfig.Header_Param_ACCESS_TOKEN, IPMConfig.IPM_TOKEN); + request.SetRequestHeader(IPMConfig.Header_Param_ContentType, IPMConfig.Header_Value_ContentType); + return request; + } + + protected override void RequestSuccessCallBack(string response) + { + ResponseData responseData = JsonUtility.FromJson>(response); + if (responseData != null && responseData.data != null) + { + // Analytics.Tch001IAPRev(responseData.data.usdPrice); + float usdPrice = responseData.data.usdPrice; + Analytics.Tch001IAPRev(usdPrice); + Analytics.Tch02IAPRev(usdPrice); + AdjustService.TrackIAPPurchase(usdPrice, productId); + Analytics.IAPPurchase(usdPrice, productId); + } + } + } +} \ No newline at end of file diff --git a/Runtime/GuruCore/Runtime/IPM/Scripts/Requests/AppleOrderRequest.cs.meta b/Runtime/GuruCore/Runtime/IPM/Scripts/Requests/AppleOrderRequest.cs.meta new file mode 100644 index 0000000..230055e --- /dev/null +++ b/Runtime/GuruCore/Runtime/IPM/Scripts/Requests/AppleOrderRequest.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: c59d82cadb8745688c64557011986c42 +timeCreated: 1654834247 \ No newline at end of file diff --git a/Runtime/GuruCore/Runtime/IPM/Scripts/Requests/AuthUserRequest.cs b/Runtime/GuruCore/Runtime/IPM/Scripts/Requests/AuthUserRequest.cs new file mode 100644 index 0000000..e115477 --- /dev/null +++ b/Runtime/GuruCore/Runtime/IPM/Scripts/Requests/AuthUserRequest.cs @@ -0,0 +1,54 @@ +using System.Text; +using Firebase.Crashlytics; +using Guru.LitJson; +using UnityEngine; +using UnityEngine.Networking; + +namespace Guru +{ + /// + /// 中台匿名授权用户请求 + /// https://docs.google.com/document/d/1yRE0HQTwaDfBeH7Zd1li1Xr8JIdfqkz3ixqdjD-aH44/edit#heading=h.eispbvmfw5oo + /// + public class AuthUserRequest : RequestBase + { + protected override string RequestURL => IPMConfig.IPM_URL + "auth/api/v1/tokens/provider/secret"; + + protected override UnityWebRequest CreateRequest() + { + JsonData jsonData = new JsonData {["secret"] = IPMConfig.IPM_DEVICE_ID}; + var request = new UnityWebRequest(RequestURL, "POST"); + request.uploadHandler = new UploadHandlerRaw(Encoding.UTF8.GetBytes(jsonData.ToJson())); + request.downloadHandler = new DownloadHandlerBuffer(); + request.SetRequestHeader(IPMConfig.Header_Param_ContentType, IPMConfig.Header_Value_ContentType); + request.SetRequestHeader(IPMConfig.Header_Param_APPID, IPMConfig.IPM_X_APP_ID); + request.SetRequestHeader(IPMConfig.Header_Param_DEVICE_INFO, new DeviceInfoData().ToString()); + return request; + } + + protected override void RequestSuccessCallBack(string response) + { + ResponseData responseData = + JsonUtility.FromJson>(response); + + if (responseData == null || responseData.data == null) + return; + + this.Log(response); + this.Log(responseData.data.ToString()); + + Analytics.SetUserIDProperty(responseData.data.uid); + Crashlytics.SetUserId(responseData.data.uid); + + IPMConfig.IPM_UID = responseData.data.uid; + IPMConfig.IPM_UID_INT = responseData.data.uidInt; + IPMConfig.IPM_TOKEN = responseData.data.token; + IPMConfig.IPM_FIREBASE_TOKEN = responseData.data.firebaseToken; + IPMConfig.IPM_CREATED_TIMESTAMP = responseData.data.createdAtTimestamp; + IPMConfig.IPM_NEWUSER = responseData.data.newUser; + IPMConfig.IPM_TOKEN_TIME = TimeUtil.GetCurrentTimeStampSecond(); + IPMConfig.IPM_FIREBASE_TOKEN_TIME = TimeUtil.GetCurrentTimeStampSecond(); + DeviceUtil.Save2AppGroup(); + } + } +} \ No newline at end of file diff --git a/Runtime/GuruCore/Runtime/IPM/Scripts/Requests/AuthUserRequest.cs.meta b/Runtime/GuruCore/Runtime/IPM/Scripts/Requests/AuthUserRequest.cs.meta new file mode 100644 index 0000000..76736e5 --- /dev/null +++ b/Runtime/GuruCore/Runtime/IPM/Scripts/Requests/AuthUserRequest.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 7efb1598282d40edb65f88e50055db43 +timeCreated: 1632541544 \ No newline at end of file diff --git a/Runtime/GuruCore/Runtime/IPM/Scripts/Requests/DeviceInfoUploadRequest.cs b/Runtime/GuruCore/Runtime/IPM/Scripts/Requests/DeviceInfoUploadRequest.cs new file mode 100644 index 0000000..0180945 --- /dev/null +++ b/Runtime/GuruCore/Runtime/IPM/Scripts/Requests/DeviceInfoUploadRequest.cs @@ -0,0 +1,31 @@ +using System; +using System.Text; +using UnityEngine; +using UnityEngine.Networking; + +namespace Guru +{ + public class DeviceInfoUploadRequest : RequestBase + { + protected override string RequestURL => IPMConfig.IPM_URL + "device/api/v1/devices"; + + protected override UnityWebRequest CreateRequest() + { + DeviceData deviceData = new DeviceData(); + this.Log($"send deviceData:{deviceData}"); + var request = new UnityWebRequest(RequestURL, "POST"); + request.uploadHandler = new UploadHandlerRaw(Encoding.UTF8.GetBytes(JsonUtility.ToJson(deviceData))); + request.downloadHandler = new DownloadHandlerBuffer(); + request.SetRequestHeader(IPMConfig.Header_Param_ContentType, IPMConfig.Header_Value_ContentType); + request.SetRequestHeader(IPMConfig.Header_Param_APPID, IPMConfig.IPM_X_APP_ID); + request.SetRequestHeader(IPMConfig.Header_Param_ACCESS_TOKEN, IPMConfig.IPM_TOKEN); + return request; + } + + protected override void RequestSuccessCallBack(string response) + { + this.Log("@@@ Send OK!"); + IPMConfig.IS_UPLOAD_DEVICE_SUCCESS = true; + } + } +} \ No newline at end of file diff --git a/Runtime/GuruCore/Runtime/IPM/Scripts/Requests/DeviceInfoUploadRequest.cs.meta b/Runtime/GuruCore/Runtime/IPM/Scripts/Requests/DeviceInfoUploadRequest.cs.meta new file mode 100644 index 0000000..5072e87 --- /dev/null +++ b/Runtime/GuruCore/Runtime/IPM/Scripts/Requests/DeviceInfoUploadRequest.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 207581eea53a40398e03e2c874b8d1cf +timeCreated: 1632554303 \ No newline at end of file diff --git a/Runtime/GuruCore/Runtime/IPM/Scripts/Requests/GoogleOrderRequest.cs b/Runtime/GuruCore/Runtime/IPM/Scripts/Requests/GoogleOrderRequest.cs new file mode 100644 index 0000000..61d437f --- /dev/null +++ b/Runtime/GuruCore/Runtime/IPM/Scripts/Requests/GoogleOrderRequest.cs @@ -0,0 +1,59 @@ +using System.Text; +using Guru.LitJson; +using UnityEngine; +using UnityEngine.Networking; + +namespace Guru +{ + public class GoogleOrderRequest : RequestBase + { + public int orderType; + public string productId; + public string subscriptionId; + public string token; + public string packageName; + public int level; + + public GoogleOrderRequest(){} + + public GoogleOrderRequest(int orderType, string productId, string subscriptionId, + string token, int level) + { + this.orderType = orderType; + this.packageName = Application.identifier; + this.productId = productId; + this.subscriptionId = subscriptionId; + this.token = token; + this.level = level; + } + + protected override string RequestURL => IPMConfig.IPM_URL + "order/api/v1/orders/android"; + protected override UnityWebRequest CreateRequest() + { + GoogleOrderData googleOrderData = new GoogleOrderData(orderType, productId, token, level); + this.Log($"send orderData:{googleOrderData}"); + var request = new UnityWebRequest(RequestURL, "POST"); + request.uploadHandler = new UploadHandlerRaw(Encoding.UTF8.GetBytes(JsonMapper.ToJson(googleOrderData))); + request.downloadHandler = new DownloadHandlerBuffer(); + request.SetRequestHeader(IPMConfig.Header_Param_ContentType, IPMConfig.Header_Value_ContentType); + request.SetRequestHeader(IPMConfig.Header_Param_APPID, IPMConfig.IPM_X_APP_ID); + request.SetRequestHeader(IPMConfig.Header_Param_UID, IPMConfig.IPM_UID); + request.SetRequestHeader(IPMConfig.Header_Param_ACCESS_TOKEN, IPMConfig.IPM_TOKEN); + return request; + } + + protected override void RequestSuccessCallBack(string response) + { + ResponseData responseData = JsonUtility.FromJson>(response); + if (responseData != null && responseData.data != null) + { + // Analytics.Tch001IAPRev(responseData.data.usdPrice); + float usdPrice = responseData.data.usdPrice; + Analytics.Tch001IAPRev(usdPrice); + Analytics.Tch02IAPRev(usdPrice); + AdjustService.TrackIAPPurchase(usdPrice, productId); + Analytics.IAPPurchase(usdPrice, productId); + } + } + } +} \ No newline at end of file diff --git a/Runtime/GuruCore/Runtime/IPM/Scripts/Requests/GoogleOrderRequest.cs.meta b/Runtime/GuruCore/Runtime/IPM/Scripts/Requests/GoogleOrderRequest.cs.meta new file mode 100644 index 0000000..3f80c11 --- /dev/null +++ b/Runtime/GuruCore/Runtime/IPM/Scripts/Requests/GoogleOrderRequest.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: b35fd28197c94a87a9e43008f36d9c59 +timeCreated: 1654834271 \ No newline at end of file diff --git a/Runtime/GuruCore/Runtime/IPM/Scripts/Requests/RefreshFirebaseTokenRequest.cs b/Runtime/GuruCore/Runtime/IPM/Scripts/Requests/RefreshFirebaseTokenRequest.cs new file mode 100644 index 0000000..d750745 --- /dev/null +++ b/Runtime/GuruCore/Runtime/IPM/Scripts/Requests/RefreshFirebaseTokenRequest.cs @@ -0,0 +1,37 @@ +using System.Text; +using Guru.LitJson; +using UnityEngine; +using UnityEngine.Networking; + +namespace Guru +{ + public class RefreshFirebaseTokenRequest : RequestBase + { + protected override string RequestURL => IPMConfig.IPM_URL + "auth/api/v1/renewals/firebase"; + + protected override UnityWebRequest CreateRequest() + { + var request = new UnityWebRequest(RequestURL, "POST"); + //request.uploadHandler = new UploadHandlerRaw(); + request.downloadHandler = new DownloadHandlerBuffer(); + request.SetRequestHeader(IPMConfig.Header_Param_ContentType, IPMConfig.Header_Value_ContentType); + request.SetRequestHeader(IPMConfig.Header_Param_APPID, IPMConfig.IPM_X_APP_ID); + request.SetRequestHeader(IPMConfig.Header_Param_UID, IPMConfig.IPM_UID); + request.SetRequestHeader(IPMConfig.Header_Param_ACCESS_TOKEN, IPMConfig.IPM_TOKEN); + return request; + } + + protected override void RequestSuccessCallBack(string response) + { + ResponseData responseData = + JsonUtility.FromJson>(response); + + Debug.Log(response); + Debug.Log(responseData.data.ToString()); + IPMConfig.IPM_FIREBASE_TOKEN = responseData.data.firebaseToken; + IPMConfig.IPM_FIREBASE_TOKEN_TIME = TimeUtil.GetCurrentTimeStampSecond(); + + this.Log("@@@ Send OK!"); + } + } +} \ No newline at end of file diff --git a/Runtime/GuruCore/Runtime/IPM/Scripts/Requests/RefreshFirebaseTokenRequest.cs.meta b/Runtime/GuruCore/Runtime/IPM/Scripts/Requests/RefreshFirebaseTokenRequest.cs.meta new file mode 100644 index 0000000..2a594fd --- /dev/null +++ b/Runtime/GuruCore/Runtime/IPM/Scripts/Requests/RefreshFirebaseTokenRequest.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: c47f1735a97a484c9165c2bc5d5877e4 +timeCreated: 1632549645 \ No newline at end of file diff --git a/Runtime/GuruCore/Runtime/IPM/Scripts/Requests/RefreshTokenRequest.cs b/Runtime/GuruCore/Runtime/IPM/Scripts/Requests/RefreshTokenRequest.cs new file mode 100644 index 0000000..74e3409 --- /dev/null +++ b/Runtime/GuruCore/Runtime/IPM/Scripts/Requests/RefreshTokenRequest.cs @@ -0,0 +1,36 @@ +using System.Text; +using Guru.LitJson; +using UnityEngine; +using UnityEngine.Networking; + +namespace Guru +{ + public class RefreshTokenRequest : RequestBase + { + protected override string RequestURL => IPMConfig.IPM_URL + "auth/api/v1/renewals/token"; + protected override UnityWebRequest CreateRequest() + { + var request = new UnityWebRequest(RequestURL, "POST"); + //request.uploadHandler = new UploadHandlerRaw(); + request.downloadHandler = new DownloadHandlerBuffer(); + request.SetRequestHeader(IPMConfig.Header_Param_ContentType, IPMConfig.Header_Value_ContentType); + request.SetRequestHeader(IPMConfig.Header_Param_APPID, IPMConfig.IPM_X_APP_ID); + request.SetRequestHeader(IPMConfig.Header_Param_UID, IPMConfig.IPM_UID); + request.SetRequestHeader(IPMConfig.Header_Param_ACCESS_TOKEN, IPMConfig.IPM_TOKEN); + return request; + } + + protected override void RequestSuccessCallBack(string response) + { + ResponseData responseData = + JsonUtility.FromJson>(response); + + this.Log(response); + this.Log(responseData.data.ToString()); + IPMConfig.IPM_TOKEN = responseData.data.token; + IPMConfig.IPM_TOKEN_TIME = TimeUtil.GetCurrentTimeStampSecond(); + PlayerPrefs.SetString("IPM_TOKEN", responseData.data.token); + this.Log("@@@ Send OK!"); + } + } +} \ No newline at end of file diff --git a/Runtime/GuruCore/Runtime/IPM/Scripts/Requests/RefreshTokenRequest.cs.meta b/Runtime/GuruCore/Runtime/IPM/Scripts/Requests/RefreshTokenRequest.cs.meta new file mode 100644 index 0000000..0c59190 --- /dev/null +++ b/Runtime/GuruCore/Runtime/IPM/Scripts/Requests/RefreshTokenRequest.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: fcbfd31d9b2d4cbf9e6a5a38510eea9d +timeCreated: 1632554002 \ No newline at end of file diff --git a/Runtime/GuruCore/Runtime/IPM/Scripts/Requests/RequestBase.cs b/Runtime/GuruCore/Runtime/IPM/Scripts/Requests/RequestBase.cs new file mode 100644 index 0000000..d6f6904 --- /dev/null +++ b/Runtime/GuruCore/Runtime/IPM/Scripts/Requests/RequestBase.cs @@ -0,0 +1,94 @@ + + +namespace Guru +{ + using System; + using System.Collections; + using UnityEngine; + using UnityEngine.Networking; + + public abstract class RequestBase + { + protected abstract string RequestURL { get; } + protected abstract UnityWebRequest CreateRequest(); + protected abstract void RequestSuccessCallBack(string response); + + private readonly WaitForSeconds _waitTime = new WaitForSeconds(5); + + private int _retryTimes = 3; + private int _timeOut = 90; + private int _currentRetryTimes; + private Action _onSuccessCallBack; + private Action _onFailCallBack; + private readonly string TAG = "IPM"; + + public RequestBase SetRetryTimes(int retryTimes) + { + _retryTimes = retryTimes; + return this; + } + + public RequestBase SetTimeOut(int timeOut) + { + _timeOut = timeOut; + return this; + } + + public RequestBase SetSuccessCallBack(Action successCallBack) + { + _onSuccessCallBack = successCallBack; + return this; + } + + public RequestBase SetFailCallBack(Action failCallBack) + { + _onFailCallBack = failCallBack; + return this; + } + + public virtual void Send() + { + var request = CreateRequest(); + if (request == null) + return; + + request.timeout = _timeOut; + CoroutineHelper.Instance.StartCoroutine(SendRequest(request)); + } + + private IEnumerator SendRequest(UnityWebRequest request) + { + yield return request.SendWebRequest(); + + if (request.result != UnityWebRequest.Result.Success) + { + _currentRetryTimes++; + this.LogError($"{request.url} reqeust fail. /n [responseCode:{request.responseCode}, result:{request.result}, error:{request.error}]"); + if (_retryTimes > 0 && _currentRetryTimes >= _retryTimes) + { + this.LogError(TAG, $"{request.url} 请求超出重试次数限制,请求失败"); + _onFailCallBack?.Invoke(); + ClearRetry(); + } + else + { + yield return _waitTime; + Send(); + } + } + else + { + string response = request.downloadHandler.text; + this.Log(TAG, $"{RequestURL} response : {response}"); + RequestSuccessCallBack(response); + _onSuccessCallBack?.Invoke(); + ClearRetry(); + } + } + + private void ClearRetry() + { + _currentRetryTimes = 0; + } + } +} \ No newline at end of file diff --git a/Runtime/GuruCore/Runtime/IPM/Scripts/Requests/RequestBase.cs.meta b/Runtime/GuruCore/Runtime/IPM/Scripts/Requests/RequestBase.cs.meta new file mode 100644 index 0000000..cd6d6d4 --- /dev/null +++ b/Runtime/GuruCore/Runtime/IPM/Scripts/Requests/RequestBase.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 707570bb8ecd44f985ee47b0d16e3b77 +timeCreated: 1632547680 \ No newline at end of file diff --git a/Runtime/GuruCore/Runtime/IPM/Scripts/ResponseData.meta b/Runtime/GuruCore/Runtime/IPM/Scripts/ResponseData.meta new file mode 100644 index 0000000..72663da --- /dev/null +++ b/Runtime/GuruCore/Runtime/IPM/Scripts/ResponseData.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 7a1a77d48ab344b5af3c0ed422639196 +timeCreated: 1632542554 \ No newline at end of file diff --git a/Runtime/GuruCore/Runtime/IPM/Scripts/ResponseData/OrderResponse.cs b/Runtime/GuruCore/Runtime/IPM/Scripts/ResponseData/OrderResponse.cs new file mode 100644 index 0000000..06aea14 --- /dev/null +++ b/Runtime/GuruCore/Runtime/IPM/Scripts/ResponseData/OrderResponse.cs @@ -0,0 +1,15 @@ +using System; + +namespace Guru +{ + [Serializable] + public class OrderResponse + { + public float usdPrice; + + public override string ToString() + { + return $"{nameof(usdPrice)}: {usdPrice}"; + } + } +} \ No newline at end of file diff --git a/Runtime/GuruCore/Runtime/IPM/Scripts/ResponseData/OrderResponse.cs.meta b/Runtime/GuruCore/Runtime/IPM/Scripts/ResponseData/OrderResponse.cs.meta new file mode 100644 index 0000000..35170d9 --- /dev/null +++ b/Runtime/GuruCore/Runtime/IPM/Scripts/ResponseData/OrderResponse.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 6492bea8b41f4f57886031ec45783cff +timeCreated: 1654834160 \ No newline at end of file diff --git a/Runtime/GuruCore/Runtime/IPM/Scripts/ResponseData/ResponseData.cs b/Runtime/GuruCore/Runtime/IPM/Scripts/ResponseData/ResponseData.cs new file mode 100644 index 0000000..85716ee --- /dev/null +++ b/Runtime/GuruCore/Runtime/IPM/Scripts/ResponseData/ResponseData.cs @@ -0,0 +1,7 @@ +namespace Guru +{ + public class ResponseData + { + public T data; + } +} \ No newline at end of file diff --git a/Runtime/GuruCore/Runtime/IPM/Scripts/ResponseData/ResponseData.cs.meta b/Runtime/GuruCore/Runtime/IPM/Scripts/ResponseData/ResponseData.cs.meta new file mode 100644 index 0000000..0428b1c --- /dev/null +++ b/Runtime/GuruCore/Runtime/IPM/Scripts/ResponseData/ResponseData.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: d7328bfc2a0c424c9ff1effe43f89900 +timeCreated: 1632542655 \ No newline at end of file diff --git a/Runtime/GuruCore/Runtime/IPM/Scripts/ResponseData/TokenResponse.cs b/Runtime/GuruCore/Runtime/IPM/Scripts/ResponseData/TokenResponse.cs new file mode 100644 index 0000000..7f7a867 --- /dev/null +++ b/Runtime/GuruCore/Runtime/IPM/Scripts/ResponseData/TokenResponse.cs @@ -0,0 +1,47 @@ +using System; + +namespace Guru +{ + [Serializable] + public class TokenResponse + { + /// + /// 用户ID,APP内唯一 + /// + public string uid; + + /// + /// 权token,用于访问API + /// + public string token; + + /// + /// 代表是否是新用户,用户第一次访问会生成新用户 + /// + public bool newUser; + + /// + /// 用户ID(整型),与uid一一对应,APP内唯一 + /// + public long uidInt; + + /// + /// Firebase token,用于firebase的登录,可以使用firebase sdk的signInWithCustomToken函数登录。 + /// 为了保证firebase的uid和中台登录系统的用户统一。 + /// + public string firebaseToken; + + /// + /// 用户创建时间 + /// 请各个项目组把该属性在客户端打点时放到user_properties里, + /// 属性的打点的属性key是user_created_timestamp(单位是微秒,透传即可) + /// BI后续会用这个属性做用户方面的相关数据分析和统计。 + /// + public long createdAtTimestamp; + + public override string ToString() + { + return $"{nameof(uid)}: {uid}, {nameof(token)}: {token}, {nameof(newUser)}: {newUser}, {nameof(uidInt)}: {uidInt}, {nameof(firebaseToken)}: {firebaseToken}, {nameof(createdAtTimestamp)}: {createdAtTimestamp}"; + } + } +} \ No newline at end of file diff --git a/Runtime/GuruCore/Runtime/IPM/Scripts/ResponseData/TokenResponse.cs.meta b/Runtime/GuruCore/Runtime/IPM/Scripts/ResponseData/TokenResponse.cs.meta new file mode 100644 index 0000000..f707a34 --- /dev/null +++ b/Runtime/GuruCore/Runtime/IPM/Scripts/ResponseData/TokenResponse.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 73236050dfe640899a2c65f663feaeda +timeCreated: 1632542589 \ No newline at end of file diff --git a/Runtime/GuruCore/Runtime/LogKit.meta b/Runtime/GuruCore/Runtime/LogKit.meta new file mode 100644 index 0000000..32fb443 --- /dev/null +++ b/Runtime/GuruCore/Runtime/LogKit.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 216c48a9c21d456b9731d729d6b6ea7c +timeCreated: 1660294890 \ No newline at end of file diff --git a/Runtime/GuruCore/Runtime/LogKit/Log.cs b/Runtime/GuruCore/Runtime/LogKit/Log.cs new file mode 100644 index 0000000..a9c7c54 --- /dev/null +++ b/Runtime/GuruCore/Runtime/LogKit/Log.cs @@ -0,0 +1,175 @@ + + +namespace Guru +{ + using System; + using System.Globalization; + using System.IO; + using System.Threading.Tasks; + using UnityEngine; + + public enum LogLevel + { + None = 0, + Exception = 1, + Error = 2, + Warning = 3, + Info = 4, + Max = 5, + } + + public static class Log + { + private static LogLevel _logLevel; + private static bool _logFile = true; + private static int _logFilePersistentTime = 3; //日志文件保存3天 + private static string _logFileDir; + private static StreamWriter _logFileWriter; + + public static LogLevel LogLevel { get => _logLevel; set => _logLevel = value; } + public static bool LogFile { get => _logFile; set => _logFile = value; } + + static Log() + { + _logLevel = PlatformUtil.IsDebug() ? LogLevel.Max : LogLevel.Error; + _logFileDir = string.Format("{0}/Log/", Application.persistentDataPath); + + //删除超出保存日期日志文件 + DeleteOutOfPersistentLogFile(); + } + + public static void I(object msg, params object[] args) + { + I(null, msg, args); + } + + public static void I(string tag, object msg, params object[] args) + { + if (_logLevel < LogLevel.Info) + return; + + string message = GetLogMessage(tag, msg, args); + Debug.Log(message); + if (_logFile) + Log2File(message); + } + + public static void W(object msg, params object[] args) + { + W(null, msg, args); + } + + public static void W(string tag, object msg, params object[] args) + { + if (_logLevel < LogLevel.Warning) + return; + + string message = GetLogMessage(tag, msg, args); + Debug.LogWarning(message); + if (_logFile) + Log2File(message); + } + + public static void E(object msg, params object[] args) + { + E(null, msg, args); + } + + public static void E(string tag, object msg, params object[] args) + { + if (_logLevel < LogLevel.Error) + return; + + string message = GetLogMessage(tag, msg, args); + Debug.LogError(message); + if (_logFile) + Log2File(message); + } + + public static void Exception(Exception e) + { + if (_logLevel < LogLevel.Exception) + return; + + Debug.LogException(e); + if (_logFile) + Log2File(e.ToString()); + } + + private static string GetLogMessage(string tag, object msg, object[] args) + { + string message = null; + if (args == null || args.Length == 0) + message = msg.ToString(); + else + message = string.Format(msg.ToString(), args); + + if (tag.IsNullOrEmpty()) + return message; + else + return string.Format("[{0}]::{1}", tag, message); + } + + private static async Task Log2File(string message, bool error = false) + { + if (_logFileWriter == null) + { + string logFileName = DateTime.Now.ToString("yyyyMMddTHHmmss", CultureInfo.InvariantCulture); + logFileName += ".log"; + string fullpath = _logFileDir + logFileName; + try + { + if (!Directory.Exists(_logFileDir)) + Directory.CreateDirectory(_logFileDir); + _logFileWriter = File.AppendText(fullpath); + _logFileWriter.AutoFlush = true; + } + catch (Exception e) + { + _logFileWriter = null; + Debug.LogError("LogToCache() " + e.Message + e.StackTrace); + return; + } + } + + if (_logFileWriter != null) + { + try + { + await _logFileWriter.WriteLineAsync(message); + if (error) + await _logFileWriter.WriteLineAsync(StackTraceUtility.ExtractStackTrace()); + } + catch (Exception) + { + // ignored + } + } + } + + private static void DeleteOutOfPersistentLogFile() + { + try + { + if (!Directory.Exists(_logFileDir)) + return; + + DateTime now = DateTime.Now; + string[] files = Directory.GetFiles(_logFileDir); + for (int i = 0; i < files.Length; i++) + { + string fileName = Path.GetFileNameWithoutExtension(files[i]); + DateTime.TryParseExact(fileName, "yyyyMMddTHHmmss", CultureInfo.InvariantCulture, DateTimeStyles.None, out DateTime fileTime); + if ((now - fileTime).Days >= _logFilePersistentTime) + { + File.Delete(files[i]); + } + } + } + catch (Exception e) + { + Debug.LogError("删除过期日志文件失败"); + } + } + } +} \ No newline at end of file diff --git a/Runtime/GuruCore/Runtime/LogKit/Log.cs.meta b/Runtime/GuruCore/Runtime/LogKit/Log.cs.meta new file mode 100644 index 0000000..18ebcdf --- /dev/null +++ b/Runtime/GuruCore/Runtime/LogKit/Log.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 0be02221ee204a60b3b0a5b224607a87 +timeCreated: 1660294923 \ No newline at end of file diff --git a/Runtime/GuruCore/Runtime/LogKit/LogExtension.cs b/Runtime/GuruCore/Runtime/LogKit/LogExtension.cs new file mode 100644 index 0000000..6e97898 --- /dev/null +++ b/Runtime/GuruCore/Runtime/LogKit/LogExtension.cs @@ -0,0 +1,25 @@ +namespace Guru +{ + public static class LogExtension + { + public static void Log(this object obj, string format, params object[] args) + { + Guru.Log.I(GetLogTag(obj), format, args); + } + + public static void LogWarning(this object obj, string format, params object[] args) + { + Guru.Log.W(GetLogTag(obj), format, args); + } + + public static void LogError(this object obj, string format, params object[] args) + { + Guru.Log.E(GetLogTag(obj), format, args); + } + + private static string GetLogTag(object obj) + { + return obj.GetType().ToString(); + } + } +} diff --git a/Runtime/GuruCore/Runtime/LogKit/LogExtension.cs.meta b/Runtime/GuruCore/Runtime/LogKit/LogExtension.cs.meta new file mode 100644 index 0000000..3372bdc --- /dev/null +++ b/Runtime/GuruCore/Runtime/LogKit/LogExtension.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 06f9c9924e864c218e02c47ffb7f6f48 +timeCreated: 1660295186 \ No newline at end of file diff --git a/Runtime/GuruCore/Runtime/Plugins.meta b/Runtime/GuruCore/Runtime/Plugins.meta new file mode 100644 index 0000000..6a55d0f --- /dev/null +++ b/Runtime/GuruCore/Runtime/Plugins.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: cc66eb73d80c439e94e5a852db652b0e +timeCreated: 1690447328 \ No newline at end of file diff --git a/Runtime/GuruCore/Runtime/Plugins/Android.meta b/Runtime/GuruCore/Runtime/Plugins/Android.meta new file mode 100644 index 0000000..b585aaa --- /dev/null +++ b/Runtime/GuruCore/Runtime/Plugins/Android.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: ea8c9a3a1a9c463696967b15fdb4d079 +timeCreated: 1690447335 \ No newline at end of file diff --git a/Runtime/GuruCore/Runtime/Plugins/Android/U3D2Android.meta b/Runtime/GuruCore/Runtime/Plugins/Android/U3D2Android.meta new file mode 100644 index 0000000..1d0294f --- /dev/null +++ b/Runtime/GuruCore/Runtime/Plugins/Android/U3D2Android.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: a3f51cba7c6e14e3391444d3d683f151 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/GuruCore/Runtime/Plugins/Android/U3D2Android/com.guru.u3d2android-1.4-release.aar b/Runtime/GuruCore/Runtime/Plugins/Android/U3D2Android/com.guru.u3d2android-1.4-release.aar new file mode 100644 index 0000000..c95bc02 Binary files /dev/null and b/Runtime/GuruCore/Runtime/Plugins/Android/U3D2Android/com.guru.u3d2android-1.4-release.aar differ diff --git a/Runtime/GuruCore/Runtime/Plugins/Android/U3D2Android/com.guru.u3d2android-1.4-release.aar.meta b/Runtime/GuruCore/Runtime/Plugins/Android/U3D2Android/com.guru.u3d2android-1.4-release.aar.meta new file mode 100644 index 0000000..b64f020 --- /dev/null +++ b/Runtime/GuruCore/Runtime/Plugins/Android/U3D2Android/com.guru.u3d2android-1.4-release.aar.meta @@ -0,0 +1,32 @@ +fileFormatVersion: 2 +guid: c257f0294290d48be845d655a5834642 +PluginImporter: + externalObjects: {} + serializedVersion: 2 + iconMap: {} + executionOrder: {} + defineConstraints: [] + isPreloaded: 0 + isOverridable: 0 + isExplicitlyReferenced: 0 + validateReferences: 1 + platformData: + - first: + Android: Android + second: + enabled: 1 + settings: {} + - first: + Any: + second: + enabled: 0 + settings: {} + - first: + Editor: Editor + second: + enabled: 0 + settings: + DefaultValueInitialized: true + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/GuruCore/Runtime/Plugins/iOS.meta b/Runtime/GuruCore/Runtime/Plugins/iOS.meta new file mode 100644 index 0000000..6a9ae03 --- /dev/null +++ b/Runtime/GuruCore/Runtime/Plugins/iOS.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: aca90b6b7d0749dbb3522d06a91f5c8a +timeCreated: 1690447340 \ No newline at end of file diff --git a/Runtime/GuruCore/Runtime/Plugins/iOS/U3D2iOS.meta b/Runtime/GuruCore/Runtime/Plugins/iOS/U3D2iOS.meta new file mode 100644 index 0000000..0dfa916 --- /dev/null +++ b/Runtime/GuruCore/Runtime/Plugins/iOS/U3D2iOS.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 11040658110a74100b273079cfed325e +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/GuruCore/Runtime/Plugins/iOS/U3D2iOS/libU3D2iOS-1.6.a b/Runtime/GuruCore/Runtime/Plugins/iOS/U3D2iOS/libU3D2iOS-1.6.a new file mode 100644 index 0000000..9249427 Binary files /dev/null and b/Runtime/GuruCore/Runtime/Plugins/iOS/U3D2iOS/libU3D2iOS-1.6.a differ diff --git a/Runtime/GuruCore/Runtime/Plugins/iOS/U3D2iOS/libU3D2iOS-1.6.a.meta b/Runtime/GuruCore/Runtime/Plugins/iOS/U3D2iOS/libU3D2iOS-1.6.a.meta new file mode 100644 index 0000000..e6326e6 --- /dev/null +++ b/Runtime/GuruCore/Runtime/Plugins/iOS/U3D2iOS/libU3D2iOS-1.6.a.meta @@ -0,0 +1,80 @@ +fileFormatVersion: 2 +guid: 21bf0bacef7634b82b6c9942402d1d9d +PluginImporter: + externalObjects: {} + serializedVersion: 2 + iconMap: {} + executionOrder: {} + defineConstraints: [] + isPreloaded: 1 + isOverridable: 0 + isExplicitlyReferenced: 0 + validateReferences: 1 + platformData: + - first: + : Any + second: + enabled: 0 + settings: + Exclude Android: 1 + Exclude Editor: 1 + Exclude Linux64: 1 + Exclude OSXUniversal: 1 + Exclude Win: 1 + Exclude Win64: 1 + Exclude iOS: 0 + - first: + Android: Android + second: + enabled: 0 + settings: + CPU: ARMv7 + - first: + Any: + second: + enabled: 0 + settings: {} + - first: + Editor: Editor + second: + enabled: 0 + settings: + CPU: AnyCPU + DefaultValueInitialized: true + OS: AnyOS + - first: + Standalone: Linux64 + second: + enabled: 0 + settings: + CPU: None + - first: + Standalone: OSXUniversal + second: + enabled: 0 + settings: + CPU: None + - first: + Standalone: Win + second: + enabled: 0 + settings: + CPU: x86 + - first: + Standalone: Win64 + second: + enabled: 0 + settings: + CPU: x86_64 + - first: + iPhone: iOS + second: + enabled: 1 + settings: + AddToEmbeddedBinaries: false + CPU: AnyCPU + CompileFlags: + FrameworkDependencies: + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/GuruCore/Runtime/Settings.meta b/Runtime/GuruCore/Runtime/Settings.meta new file mode 100644 index 0000000..0310534 --- /dev/null +++ b/Runtime/GuruCore/Runtime/Settings.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: fdf3ff10a8df4a39b244f707296b9afe +timeCreated: 1677587038 \ No newline at end of file diff --git a/Runtime/GuruCore/Runtime/Settings/GuruSettings.cs b/Runtime/GuruCore/Runtime/Settings/GuruSettings.cs new file mode 100644 index 0000000..41cd4b0 --- /dev/null +++ b/Runtime/GuruCore/Runtime/Settings/GuruSettings.cs @@ -0,0 +1,152 @@ +using System; +using System.Collections.Generic; +using UnityEngine; + +namespace Guru +{ + [CreateAssetMenu(fileName = "GuruSettings", menuName = "GuruSettings", order = 0)] + public partial class GuruSettings : ScriptableObject + { + private static GuruSettings _instance; + public static GuruSettings Instance + { + get + { + if (_instance == null) + { + _instance = LoadSettingsAsset(); + } + + return _instance; + } + } + + [Header("公司名称")] + public string CompanyName = "Guru"; + [Header("产品名称")] + public string ProductName = "Default Product"; + [Header("产品包名")] + public string GameIdentifier = "com.guru.default.product"; + [Header("产品反馈邮箱(评分反馈邮箱)")] + public string SupportEmail = "xxxx@fungame.studio"; + [Header("隐私协议URL")] + public string PriacyUrl = ""; + [Header("服务条款URL")] + public string TermsUrl = ""; + [Header("Android商店URL")] + public string AndroidStoreUrl = ""; + [Header("iOS商店URL")] + public string IOSStoreUrl = ""; + + + [Header("中台配置")] + public IPMSetting IPMSetting; + [Header("打点配置")] + public AnalyticsSetting AnalyticsSetting; + [Header("广告配置")] + public ADSetting ADSetting; + [Header("Adjust配置")] + public AdjustSetting AdjustSetting; + + private static GuruSettings LoadSettingsAsset() + { + return Resources.Load("GuruSettings"); + } + } + + [Serializable] + public class IPMSetting + { + [Header("中台项目ID")] + [SerializeField] private string appID; + [Header("中台Token有效时间(s)")] + [SerializeField] private int tokenValidTime = 604800; + + public string AppId => appID; + public int TokenValidTime => tokenValidTime; + } + + [Serializable] + public class AnalyticsSetting + { + [SerializeField] private int levelEndSuccessNum = 50; + [SerializeField] private bool enalbeFirebaseAnalytics = true; + [SerializeField] private bool enalbeFacebookAnalytics = true; + [SerializeField] private bool enalbeAdjustAnalytics = true; + [SerializeField] private List adjustEventList; + + public int LevelEndSuccessNum => levelEndSuccessNum; + public bool EnalbeFirebaseAnalytics => enalbeFirebaseAnalytics; + public bool EnalbeFacebookAnalytics => enalbeFacebookAnalytics; + public bool EnalbeAdjustAnalytics => enalbeAdjustAnalytics; + public List AdjustEventList => adjustEventList; + + [Serializable] + public class AdjustEvent + { + public string EventName; + public string AndroidToken; + public string IOSToken; + } + } + + [Serializable] + public class ADSetting + { + public string SDK_KEY; + public string Android_Banner_ID; + public string Android_Interstitial_ID; + public string Android_Rewarded_ID; + public string IOS_Banner_ID; + public string IOS_Interstitial_ID; + public string IOS_Rewarded_ID; + + public string GetRewardedVideoID() + { +#if UNITY_IOS + return IOS_Rewarded_ID; +#else + return Android_Rewarded_ID; +#endif + } + + public string GetInterstitialID() + { +#if UNITY_IOS + return IOS_Interstitial_ID; +#else + return Android_Interstitial_ID; +#endif + } + + public string GetBannerID() + { +#if UNITY_IOS + return IOS_Banner_ID; +#else + return Android_Banner_ID; +#endif + } + } + + [Serializable] + public class AdjustSetting + { + [SerializeField] private string androidAppToken; + [SerializeField] private string iOSAppToken; + + public string AndroidAppToken => androidAppToken; + public string IOSAppToken => iOSAppToken; + + public string GetAppToken() + { + #if UNITY_ANDROID + return androidAppToken; + #elif UNITY_IOS + return iOSAppToken; + #else + return string.Empty; + #endif + } + } +} \ No newline at end of file diff --git a/Runtime/GuruCore/Runtime/Settings/GuruSettings.cs.meta b/Runtime/GuruCore/Runtime/Settings/GuruSettings.cs.meta new file mode 100644 index 0000000..5824f0e --- /dev/null +++ b/Runtime/GuruCore/Runtime/Settings/GuruSettings.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 69301f601182495d90c1ff7b552a3e5b +timeCreated: 1634612149 \ No newline at end of file diff --git a/Runtime/GuruCore/Runtime/Storage.meta b/Runtime/GuruCore/Runtime/Storage.meta new file mode 100644 index 0000000..aaec63d --- /dev/null +++ b/Runtime/GuruCore/Runtime/Storage.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 4d6c6d6bcfea4f90985ab99f69f37725 +timeCreated: 1679986740 \ No newline at end of file diff --git a/Runtime/GuruCore/Runtime/Storage/ISavableValue.cs b/Runtime/GuruCore/Runtime/Storage/ISavableValue.cs new file mode 100644 index 0000000..e56e174 --- /dev/null +++ b/Runtime/GuruCore/Runtime/Storage/ISavableValue.cs @@ -0,0 +1,33 @@ + +using System.Collections; +using UnityEngine; + +namespace Guru +{ + using System; + using System.Collections.Generic; + + public interface ISavableValue + { + void SaveInt(int value); + void SaveFloat(float value); + void SaveBool(bool value); + void SaveString(string value); + void SaveVector2(Vector2 value); + void SaveVector3(Vector3 value); + void SaveVector4(Vector4 value); + void SaveArray(string[] value); + + int LoadInt(int defaultValue = 0); + float LoadFloat(float defaultValue = 0); + bool LoadBool(bool defaultValue = false); + string LoadString(string defaultValue = ""); + Vector3 LoadVector3(Vector3 defaultValue = new Vector3()); + Vector2 LoadVector2(Vector2 defaultValue = new Vector2()); + Vector4 LoadVector4(Vector4 defaultValue = new Vector4()); + string[] LoadArray(string[] defaultValue = null); + + void ClearValue(string key); + bool HasKey(); + } +} \ No newline at end of file diff --git a/Runtime/GuruCore/Runtime/Storage/ISavableValue.cs.meta b/Runtime/GuruCore/Runtime/Storage/ISavableValue.cs.meta new file mode 100644 index 0000000..8a44370 --- /dev/null +++ b/Runtime/GuruCore/Runtime/Storage/ISavableValue.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: a2507d668eac4b58b93a496c647bd4f1 +timeCreated: 1679988925 \ No newline at end of file diff --git a/Runtime/GuruCore/Runtime/Storage/SavableValue.cs b/Runtime/GuruCore/Runtime/Storage/SavableValue.cs new file mode 100644 index 0000000..76ff9c1 --- /dev/null +++ b/Runtime/GuruCore/Runtime/Storage/SavableValue.cs @@ -0,0 +1,275 @@ + +using System; +using System.Collections; +using System.Text; + +namespace Guru +{ + using UnityEngine; + using System.Collections.Generic; + + + public class SavableValue: ISavableValue + { + private T _value; + public T Value + { + get + { + if(null == _value) LoadValue(); + return _value; + } + + set => SaveValue(value); + } + + public Action OnValueChanged; + + public string Key => nameof(T); + + + #region 数据存储 + + + protected virtual void SaveValue(T value) + { + if (!_value.Equals(value)) + { + _value = value; + OnValueChanged?.Invoke(_value); + } + + // ------------- 存储各种类型 --------------- + if (_value is int) + { + SaveInt((int)(object)_value); + } + else if (_value is float) + { + SaveFloat((float)(object)_value); + } + else if (_value is string) + { + SaveString((string)(object)_value); + } + else if (_value is bool) + { + SaveBool((bool)(object)_value); + } + else if (_value is Vector2) + { + SaveVector2((Vector2)(object)_value); + } + else if (_value is Vector3) + { + SaveVector3((Vector3)(object)_value); + } + else if (_value is Vector4) + { + SaveVector4((Vector4)(object)_value); + } + else if (_value is Array) + { + SaveArray((string[])(object)_value); + } + } + + + #endregion + + + #region 数据读取 + + protected virtual void LoadValue() + { + if (!HasKey()) _value = default; + + if (_value is int) + { + _value = (T)(object)LoadInt(); + } + else if (_value is float) + { + _value = (T)(object)LoadFloat(); + } + else if (_value is string) + { + _value = (T)(object)LoadString(); + } + else if (_value is bool) + { + _value = (T)(object)LoadBool(); + } + else if (_value is Vector2) + { + _value = (T)(object)LoadVector2(); + } + else if (_value is Vector3) + { + _value = (T)(object)LoadVector3(); + } + else if (_value is Vector4) + { + _value = (T)(object)LoadVector4(); + } + else if (_value is string[]) + { + _value = (T)(object)LoadArray(); + } + } + + + #endregion + + + #region 数据存储接口 + public void SaveInt(int value) => PlayerPrefs.SetInt(Key, value); + public void SaveFloat(float value) => PlayerPrefs.SetString(Key, FloatToString(value)); + public void SaveBool(bool value) => PlayerPrefs.SetInt(Key, BoolToInt(value)); + public void SaveString(string value) => PlayerPrefs.SetString(Key, value); + public void SaveVector2(Vector2 value) => PlayerPrefs.SetString(Key, Vector2ToString(value)); + public void SaveVector3(Vector3 value) => PlayerPrefs.SetString(Key, Vector3ToString(value)); + public void SaveVector4(Vector4 value) => PlayerPrefs.SetString(Key, Vector4ToString(value)); + public void SaveArray(string[] value) => PlayerPrefs.SetString(Key, ArrayToString(value)); + + #endregion + + #region 数据加载接口 + + + public int LoadInt(int defaultValue = 0) => PlayerPrefs.GetInt(Key, 0); + public float LoadFloat(float defaultValue = 0) + { + return StringToFloat(PlayerPrefs.GetString(Key, $"{defaultValue}")); + } + + public bool LoadBool(bool defaultValue = false) + { + return IntToBool(PlayerPrefs.GetInt(Key, defaultValue?1: 0)); + } + + public string LoadString(string defaultValue = "") + { + return PlayerPrefs.GetString(Key, defaultValue); + } + + public Vector2 LoadVector2(Vector2 defaultValue = new Vector2()) + { + return StringToVector2(PlayerPrefs.GetString(Key, Vector2ToString(defaultValue))); + } + + public Vector3 LoadVector3(Vector3 defaultValue = new Vector3()) + { + return StringToVector3(PlayerPrefs.GetString(Key, Vector3ToString(defaultValue))); + } + + public Vector4 LoadVector4(Vector4 defaultValue = new Vector4()) + { + return StringToVector4(PlayerPrefs.GetString(Key, Vector4ToString(defaultValue))); + } + + public string[] LoadArray(string[] defaultValue = null) + { + if (!HasKey()) return defaultValue; + return StringToArray(PlayerPrefs.GetString(Key,"")); + } + + #endregion + + #region 操作接口 + + public bool HasKey() => PlayerPrefs.HasKey(Key); + + + public void ClearValue(string key) + { + PlayerPrefs.DeleteKey(key); + } + + + + + + #endregion + + #region 值转换 + + private string FloatToString(float value) => value.ToString(); + private float StringToFloat(string value) + { + float val = (float)0.00; + if (!string.IsNullOrEmpty(value)) + { + float.TryParse(value, out val); + } + return val; + } + + private int BoolToInt(bool value) => value ? 1 : 0; + private bool IntToBool(int value) => value == 1; + + private string Vector2ToString(Vector2 value) => $"{value.x}_{value.y}"; + private Vector2 StringToVector2(string value) + { + if(value.IsNullOrEmpty()) return Vector2.zero; + float x = 0; + float y = 0; + var raw = value.Split('_'); + if (raw.Length > 0) x = StringToFloat(raw[0]); + if (raw.Length > 1) y = StringToFloat(raw[1]); + return new Vector2(x, y); + } + + private string Vector3ToString(Vector3 value) => $"{value.x}_{value.y}_{value.z}"; + private Vector3 StringToVector3(string value) + { + if(value.IsNullOrEmpty()) return Vector3.zero; + float x = 0; + float y = 0; + float z = 0; + var raw = value.Split('_'); + if (raw.Length > 0) x = StringToFloat(raw[0]); + if (raw.Length > 1) y = StringToFloat(raw[1]); + if (raw.Length > 2) z = StringToFloat(raw[2]); + return new Vector3(x, y, z); + } + + private string Vector4ToString(Vector4 value) => $"{value.x}_{value.y}_{value.z}_{value.w}"; + private Vector4 StringToVector4(string value) + { + if(value.IsNullOrEmpty()) return Vector2.zero; + float x = 0; + float y = 0; + float z = 0; + float w = 0; + var raw = value.Split('_'); + if (raw.Length > 0) x = StringToFloat(raw[0]); + if (raw.Length > 1) y = StringToFloat(raw[1]); + if (raw.Length > 2) z = StringToFloat(raw[2]); + if (raw.Length > 3) w = StringToFloat(raw[3]); + return new Vector4(x, y, z, w); + } + + private string ArrayToString(string[] value) + { + return null == value ? "" : string.Join(",", value); + } + + private string[] StringToArray(string value) + { + if (string.IsNullOrEmpty(value)) return null; + var arr = value.Split(','); + return arr; + } + + + + #endregion + + } + + + + + +} \ No newline at end of file diff --git a/Runtime/GuruCore/Runtime/Storage/SavableValue.cs.meta b/Runtime/GuruCore/Runtime/Storage/SavableValue.cs.meta new file mode 100644 index 0000000..9692746 --- /dev/null +++ b/Runtime/GuruCore/Runtime/Storage/SavableValue.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: e04ba5bb228a4203a24e80a5ee863c40 +timeCreated: 1679986752 \ No newline at end of file diff --git a/Runtime/GuruIAP.meta b/Runtime/GuruIAP.meta new file mode 100644 index 0000000..466360c --- /dev/null +++ b/Runtime/GuruIAP.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: badeb360c4734842b8cf0bfa246beed3 +timeCreated: 1680687367 \ No newline at end of file diff --git a/Runtime/GuruIAP/Docs.meta b/Runtime/GuruIAP/Docs.meta new file mode 100644 index 0000000..0eea126 --- /dev/null +++ b/Runtime/GuruIAP/Docs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 287ab0023b34b4b25ac1552350687bb4 +timeCreated: 1680697965 \ No newline at end of file diff --git a/Runtime/GuruIAP/Docs/imgs.meta b/Runtime/GuruIAP/Docs/imgs.meta new file mode 100644 index 0000000..7215254 --- /dev/null +++ b/Runtime/GuruIAP/Docs/imgs.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 40191602e19ca484388552c6b3814bf6 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/GuruIAP/Docs/imgs/001.png b/Runtime/GuruIAP/Docs/imgs/001.png new file mode 100644 index 0000000..18b635d Binary files /dev/null and b/Runtime/GuruIAP/Docs/imgs/001.png differ diff --git a/Runtime/GuruIAP/Docs/imgs/001.png.meta b/Runtime/GuruIAP/Docs/imgs/001.png.meta new file mode 100644 index 0000000..637d7f9 --- /dev/null +++ b/Runtime/GuruIAP/Docs/imgs/001.png.meta @@ -0,0 +1,133 @@ +fileFormatVersion: 2 +guid: b62258e7a32cd4c75a60f2ca6254254a +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 12 + mipmaps: + mipMapMode: 0 + enableMipMap: 0 + sRGBTexture: 1 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 1 + aniso: 1 + mipBias: 0 + wrapU: 1 + wrapV: 1 + wrapW: 1 + nPOTScale: 0 + lightmap: 0 + compressionQuality: 50 + spriteMode: 1 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 1 + spriteTessellationDetail: -1 + textureType: 8 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 0 + cookieLightType: 0 + platformSettings: + - serializedVersion: 3 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 3 + buildTarget: Standalone + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 3 + buildTarget: iPhone + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 3 + buildTarget: Android + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + physicsShape: [] + bones: [] + spriteID: 5e97eb03825dee720800000000000000 + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + spritePackingTag: + pSDRemoveMatte: 0 + pSDShowRemoveMatteOption: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/GuruIAP/Docs/imgs/002.png b/Runtime/GuruIAP/Docs/imgs/002.png new file mode 100644 index 0000000..6c41d48 Binary files /dev/null and b/Runtime/GuruIAP/Docs/imgs/002.png differ diff --git a/Runtime/GuruIAP/Docs/imgs/002.png.meta b/Runtime/GuruIAP/Docs/imgs/002.png.meta new file mode 100644 index 0000000..85be6cb --- /dev/null +++ b/Runtime/GuruIAP/Docs/imgs/002.png.meta @@ -0,0 +1,133 @@ +fileFormatVersion: 2 +guid: 38577f9e5d8b544058bd9584b0e957be +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 12 + mipmaps: + mipMapMode: 0 + enableMipMap: 0 + sRGBTexture: 1 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 1 + aniso: 1 + mipBias: 0 + wrapU: 1 + wrapV: 1 + wrapW: 1 + nPOTScale: 0 + lightmap: 0 + compressionQuality: 50 + spriteMode: 1 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 1 + spriteTessellationDetail: -1 + textureType: 8 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 0 + cookieLightType: 0 + platformSettings: + - serializedVersion: 3 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 3 + buildTarget: Standalone + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 3 + buildTarget: iPhone + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 3 + buildTarget: Android + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + physicsShape: [] + bones: [] + spriteID: 5e97eb03825dee720800000000000000 + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + spritePackingTag: + pSDRemoveMatte: 0 + pSDShowRemoveMatteOption: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/GuruIAP/Docs/imgs/003.png b/Runtime/GuruIAP/Docs/imgs/003.png new file mode 100644 index 0000000..c9d58a1 Binary files /dev/null and b/Runtime/GuruIAP/Docs/imgs/003.png differ diff --git a/Runtime/GuruIAP/Docs/imgs/003.png.meta b/Runtime/GuruIAP/Docs/imgs/003.png.meta new file mode 100644 index 0000000..168355e --- /dev/null +++ b/Runtime/GuruIAP/Docs/imgs/003.png.meta @@ -0,0 +1,133 @@ +fileFormatVersion: 2 +guid: 105fc3de6aed44d9d977065f049c4b84 +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 12 + mipmaps: + mipMapMode: 0 + enableMipMap: 0 + sRGBTexture: 1 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 1 + aniso: 1 + mipBias: 0 + wrapU: 1 + wrapV: 1 + wrapW: 1 + nPOTScale: 0 + lightmap: 0 + compressionQuality: 50 + spriteMode: 1 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 1 + spriteTessellationDetail: -1 + textureType: 8 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 0 + cookieLightType: 0 + platformSettings: + - serializedVersion: 3 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 3 + buildTarget: Standalone + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 3 + buildTarget: iPhone + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 3 + buildTarget: Android + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + physicsShape: [] + bones: [] + spriteID: 5e97eb03825dee720800000000000000 + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + spritePackingTag: + pSDRemoveMatte: 0 + pSDShowRemoveMatteOption: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/GuruIAP/Docs/imgs/005.png b/Runtime/GuruIAP/Docs/imgs/005.png new file mode 100644 index 0000000..ff85450 Binary files /dev/null and b/Runtime/GuruIAP/Docs/imgs/005.png differ diff --git a/Runtime/GuruIAP/Docs/imgs/005.png.meta b/Runtime/GuruIAP/Docs/imgs/005.png.meta new file mode 100644 index 0000000..c2a8c6a --- /dev/null +++ b/Runtime/GuruIAP/Docs/imgs/005.png.meta @@ -0,0 +1,133 @@ +fileFormatVersion: 2 +guid: 0adf9dd758a5245319c9e28277e65b6c +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 12 + mipmaps: + mipMapMode: 0 + enableMipMap: 0 + sRGBTexture: 1 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 1 + aniso: 1 + mipBias: 0 + wrapU: 1 + wrapV: 1 + wrapW: 1 + nPOTScale: 0 + lightmap: 0 + compressionQuality: 50 + spriteMode: 1 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 1 + spriteTessellationDetail: -1 + textureType: 8 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 0 + cookieLightType: 0 + platformSettings: + - serializedVersion: 3 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 3 + buildTarget: Standalone + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 3 + buildTarget: iPhone + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 3 + buildTarget: Android + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + physicsShape: [] + bones: [] + spriteID: 5e97eb03825dee720800000000000000 + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + spritePackingTag: + pSDRemoveMatte: 0 + pSDShowRemoveMatteOption: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/GuruIAP/Docs/imgs/006.png b/Runtime/GuruIAP/Docs/imgs/006.png new file mode 100644 index 0000000..333a59d Binary files /dev/null and b/Runtime/GuruIAP/Docs/imgs/006.png differ diff --git a/Runtime/GuruIAP/Docs/imgs/006.png.meta b/Runtime/GuruIAP/Docs/imgs/006.png.meta new file mode 100644 index 0000000..c078bb1 --- /dev/null +++ b/Runtime/GuruIAP/Docs/imgs/006.png.meta @@ -0,0 +1,133 @@ +fileFormatVersion: 2 +guid: 253723a94a1394f24a98deaef5e6b36e +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 12 + mipmaps: + mipMapMode: 0 + enableMipMap: 0 + sRGBTexture: 1 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 1 + aniso: 1 + mipBias: 0 + wrapU: 1 + wrapV: 1 + wrapW: 1 + nPOTScale: 0 + lightmap: 0 + compressionQuality: 50 + spriteMode: 1 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 1 + spriteTessellationDetail: -1 + textureType: 8 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 0 + cookieLightType: 0 + platformSettings: + - serializedVersion: 3 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 3 + buildTarget: Standalone + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 3 + buildTarget: iPhone + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 3 + buildTarget: Android + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + physicsShape: [] + bones: [] + spriteID: 5e97eb03825dee720800000000000000 + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + spritePackingTag: + pSDRemoveMatte: 0 + pSDShowRemoveMatteOption: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/GuruIAP/Docs/imgs/007.png b/Runtime/GuruIAP/Docs/imgs/007.png new file mode 100644 index 0000000..bf869dd Binary files /dev/null and b/Runtime/GuruIAP/Docs/imgs/007.png differ diff --git a/Runtime/GuruIAP/Docs/imgs/007.png.meta b/Runtime/GuruIAP/Docs/imgs/007.png.meta new file mode 100644 index 0000000..5f00b4a --- /dev/null +++ b/Runtime/GuruIAP/Docs/imgs/007.png.meta @@ -0,0 +1,133 @@ +fileFormatVersion: 2 +guid: 8ad318db92b6746a9a5eb567eedac722 +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 12 + mipmaps: + mipMapMode: 0 + enableMipMap: 0 + sRGBTexture: 1 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 1 + aniso: 1 + mipBias: 0 + wrapU: 1 + wrapV: 1 + wrapW: 1 + nPOTScale: 0 + lightmap: 0 + compressionQuality: 50 + spriteMode: 1 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 1 + spriteTessellationDetail: -1 + textureType: 8 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 0 + cookieLightType: 0 + platformSettings: + - serializedVersion: 3 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 3 + buildTarget: Standalone + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 3 + buildTarget: iPhone + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 3 + buildTarget: Android + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + physicsShape: [] + bones: [] + spriteID: 5e97eb03825dee720800000000000000 + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + spritePackingTag: + pSDRemoveMatte: 0 + pSDShowRemoveMatteOption: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/GuruIAP/Docs/imgs/008.png b/Runtime/GuruIAP/Docs/imgs/008.png new file mode 100644 index 0000000..435bfc0 Binary files /dev/null and b/Runtime/GuruIAP/Docs/imgs/008.png differ diff --git a/Runtime/GuruIAP/Docs/imgs/008.png.meta b/Runtime/GuruIAP/Docs/imgs/008.png.meta new file mode 100644 index 0000000..fe1e8b1 --- /dev/null +++ b/Runtime/GuruIAP/Docs/imgs/008.png.meta @@ -0,0 +1,133 @@ +fileFormatVersion: 2 +guid: a2904003fd0a542cf9f32559a6e4c7eb +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 12 + mipmaps: + mipMapMode: 0 + enableMipMap: 0 + sRGBTexture: 1 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 1 + aniso: 1 + mipBias: 0 + wrapU: 1 + wrapV: 1 + wrapW: 1 + nPOTScale: 0 + lightmap: 0 + compressionQuality: 50 + spriteMode: 1 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 1 + spriteTessellationDetail: -1 + textureType: 8 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 0 + cookieLightType: 0 + platformSettings: + - serializedVersion: 3 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 3 + buildTarget: Standalone + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 3 + buildTarget: iPhone + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 3 + buildTarget: Android + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + physicsShape: [] + bones: [] + spriteID: 5e97eb03825dee720800000000000000 + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + spritePackingTag: + pSDRemoveMatte: 0 + pSDShowRemoveMatteOption: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/GuruIAP/Docs/imgs/009.png b/Runtime/GuruIAP/Docs/imgs/009.png new file mode 100644 index 0000000..5b22ab2 Binary files /dev/null and b/Runtime/GuruIAP/Docs/imgs/009.png differ diff --git a/Runtime/GuruIAP/Docs/imgs/009.png.meta b/Runtime/GuruIAP/Docs/imgs/009.png.meta new file mode 100644 index 0000000..34803e5 --- /dev/null +++ b/Runtime/GuruIAP/Docs/imgs/009.png.meta @@ -0,0 +1,133 @@ +fileFormatVersion: 2 +guid: e2296cff607874463b3c4be191576ea9 +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 12 + mipmaps: + mipMapMode: 0 + enableMipMap: 0 + sRGBTexture: 1 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 1 + aniso: 1 + mipBias: 0 + wrapU: 1 + wrapV: 1 + wrapW: 1 + nPOTScale: 0 + lightmap: 0 + compressionQuality: 50 + spriteMode: 1 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 1 + spriteTessellationDetail: -1 + textureType: 8 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 0 + cookieLightType: 0 + platformSettings: + - serializedVersion: 3 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 3 + buildTarget: Standalone + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 3 + buildTarget: iPhone + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 3 + buildTarget: Android + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + physicsShape: [] + bones: [] + spriteID: 5e97eb03825dee720800000000000000 + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + spritePackingTag: + pSDRemoveMatte: 0 + pSDShowRemoveMatteOption: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/GuruIAP/README.md b/Runtime/GuruIAP/README.md new file mode 100644 index 0000000..1f98718 --- /dev/null +++ b/Runtime/GuruIAP/README.md @@ -0,0 +1,149 @@ +# Guru IAPService + +- Version 0.9.1 + +- Guru 支付服务标准化接口 + +## 接入说明 + +### 配置 Unity 的 IAP 功能 + +#### (1) 安装 In App Purchasing 插件 + +- 打开 PackageManager, 安装 `In App Purchasing` (4.5.2) 版本 + +#### (2) 开启 Unity 的 IAP 服务 + +- 进入 Edit/Project Settings/Services/In-App Purchasing 面板 + + ![](Docs/imgs/001.png) + +- 初始需要登录账号, 必须使用公司的 Unity 账号: + > audiofirst2020@gmail.com + > + > Castbox123 + > +- 该账号需要选择邮箱登录, 有两步验证, 如需要获取验证码, 请联系 **@朱凯莉** 获取手机验证码 +- 下拉框选择组织 `audiounity` +- 如果是新项目, 选择创建 `project ID` + + ![](Docs/imgs/003.png) + +- 选择若干创建项目的选项, 问到年龄时可以选择13岁以下, 然后项目创建完毕 +- 首先将该项目的 Settings 开关打开: **OFF** -> **ON** +- 然后点击 `Dashboard` 直接进入网页版后台 (后面的设置在网页端会比较方便) + + ![](Docs/imgs/002.png) + +- 进入后台后先选中刚才创建的项目, 准备输入Google License Key + + ![](Docs/imgs/005.png) + +- 进入该项目对应的GooglePlayConsole 后台, 找到 `Financial reports/Monetization setup` +- 复制 `Base 54-encoded RSA public key` 然后回到 Unity 控制台页面, 粘贴刚才的代码. + + ![](Docs/imgs/006.png) + +- 返回Unity内, 可关闭Service面板, 然后在菜单上打开 `Services/In-App Purchasing/Receipt Validation Obfuscator...` + + ![](Docs/imgs/007.png) + +- 在面板内, 再次黏贴刚才的代码, 之后点击 `Obfuscator Google Play License Key` 生成对应的文件 + + ![](Docs/imgs/008.png) + +- 最后在默认的路径 `Scripts/UnityPurchasing/generated` 下会生成对应的Tangle文件 + + ![](Docs/imgs/009.png) + +- 注意: 这些文件可以移动到任意的路径, 保存好在后面的支付验证中会有用. + +
+ +--- + +### 继承 IAPServiceBase + +- 完成支付插件配置后, 将生成的文件导入后, 请删除 `GuruIAP/__NeedBeReplaced__` 的这个文件夹. 由于这个文件夹内是占位的文件. 需要被真正生成的文件替换 +- 接下来需要编写对应游戏内容的 `IAPService`, 需要继承自 `IAPServiceBase` 基类 +- 其中必要的接口, 需要根据游戏逻辑, 返回 `blevel` 的值 +- 其他逻辑根据游戏可进行扩展 + +```C# +using UnityEngine.Purchasing; + +namespace Guru.Sample +{ + public class MyIAPService: IAPServiceBase + { + /// + /// 必须实现 BLevel 反回值, 用于支付打点上报 + /// + /// + protected override int GetBLevel() + { + return UserData.Instance.GetLastFinishedLevel(); // 游戏目前进度完成的最后一关 + } + + public override void Init(bool showLog = false) + { + base.Init(showLog); + InitGameProducts(); + } + + private void InitGameProducts() + { + // 插入项目专用的初始化逻辑 + foreach (var key in Products.Keys) + { + var info = Products[key]; + if (info.Setting.Type == ProductType.Subscription) + { + // TODO: 针对订阅行道具进行处理逻辑 + } + } + } + } +} + +``` + +- 在项目中正常调用对应的支付服务逻辑: + +```csharp +public class MyDemoApp: MonoBehaviour +{ + private void Awake() + { + // 初始化回调 + MyIAPService.Instance.OnInitResult = success => + { + if (success) + { + // UIManager.Instance.OpenStoreUI(); + } + }; + } + + + /// + /// 点击支付按钮 + /// + /// + private void OnClickBuyItem(string productId) + { + MyIAPService.Instance.Buy(productId, (success, id) => + { + if (success) + { + // UIManager.Instance.OpenPaySuccess(); + } + else + { + // UIManager.Instance.OpenPayFail(); + } + }); + } +} + +``` \ No newline at end of file diff --git a/Runtime/GuruIAP/README.md.meta b/Runtime/GuruIAP/README.md.meta new file mode 100644 index 0000000..1265814 --- /dev/null +++ b/Runtime/GuruIAP/README.md.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 4b1dfc04e4bd4b85a3db692f42b9444c +timeCreated: 1680696000 \ No newline at end of file diff --git a/Runtime/GuruIAP/Runtime.meta b/Runtime/GuruIAP/Runtime.meta new file mode 100644 index 0000000..2a3ba7c --- /dev/null +++ b/Runtime/GuruIAP/Runtime.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 8e444ccaaece44c5bcff7537c7c5afea +timeCreated: 1680687375 \ No newline at end of file diff --git a/Runtime/GuruIAP/Runtime/Code.meta b/Runtime/GuruIAP/Runtime/Code.meta new file mode 100644 index 0000000..7e71616 --- /dev/null +++ b/Runtime/GuruIAP/Runtime/Code.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: dc83d81b0a124857a940b23205da863e +timeCreated: 1680687389 \ No newline at end of file diff --git a/Runtime/GuruIAP/Runtime/Code/IAPServiceBase.cs b/Runtime/GuruIAP/Runtime/Code/IAPServiceBase.cs new file mode 100644 index 0000000..0d8097e --- /dev/null +++ b/Runtime/GuruIAP/Runtime/Code/IAPServiceBase.cs @@ -0,0 +1,687 @@ +namespace Guru +{ + using System; + using System.Linq; + using UnityEngine; + using UnityEngine.Purchasing; + using UnityEngine.Purchasing.Security; + using System.Collections.Generic; + using Firebase.Crashlytics; + using Guru.LitJson; + + public abstract class IAPServiceBase: IStoreListener where T: IAPServiceBase , new() + { + + #region 属性定义 + + private const string Tag = "[IAP]"; + private const string DefaultCategory = "Store"; + + private static bool _showLog; + + private ConfigurationBuilder _configBuilder; // 商店配置创建器 + + private IStoreController _storeController; + private IExtensionProvider _storeExtensionProvider; + private IAppleExtensions _appleExtensions; + private IGooglePlayStoreExtensions _googlePlayStoreExtensions; + + private CrossPlatformValidator _validator; + private Dictionary _products; + protected Dictionary Products => _products; + + public bool IsInitialized => _storeController != null && _storeExtensionProvider != null; + + /// + /// 是否是首次购买 + /// + public int PurchaseCount + { + get => PlayerPrefs.GetInt(nameof(PurchaseCount), 0); + set => PlayerPrefs.SetInt(nameof(PurchaseCount), value); + } + + /// + /// 是否是首个IAP + /// + public bool IsFirstIAP => PurchaseCount == 0; + + private byte[] _googlePublicKey; + private byte[] _appleRootCert; + + /// + /// 服务初始化回调 + /// + public event Action OnInitResult; + + /// + /// 恢复购买回调 + /// + public event Action OnRestored; + + public event Action OnBuyStart; + public event Action OnBuyEnd; + +#if UNITY_IOS + /// + /// AppStore 支付, 处理苹果支付延迟反应 + /// + /// + public Action OnAppStorePurchaseDeferred; +#endif + + #endregion + + #region 单利模式 + + protected static T _instance; + private static object _locker = new object(); + + public static T Instance + { + get + { + if (null == _instance) + { + lock (_locker) + { + _instance = Activator.CreateInstance(); + _instance.OnCreatedInit(); + } + } + return _instance; + } + } + + /// + /// 组件创建初始化 + /// + protected virtual void OnCreatedInit() + { + Debug.Log("--- IAPService Init"); + } + + + #endregion + + #region 初始化 + + /// + /// 初始化支付服务 + /// + public virtual void Initialize(bool showLog = false) + { + _showLog = showLog; + InitPurchasing(); + } + + /// + /// 带有校验器的初始化 + /// + /// + /// + /// + public virtual void InitWithKeys(byte[] googlePublicKey, byte[] appleRootCert, bool showLog = false) + { + _googlePublicKey = googlePublicKey; + _appleRootCert = appleRootCert; + Initialize(showLog); + } + + + + + /// + /// 初始化支付插件 + /// + protected virtual void InitPurchasing() + { + _configBuilder = ConfigurationBuilder.Instance(StandardPurchasingModule.Instance()); + // 注入初始商品产品列表 + var settings = GetProductSettings(); + if (null != settings) + { + int len = settings.Length; + + if(_products != null) _products.Clear(); + _products = new Dictionary(len); + + ProductSetting item; + IDs ids; + for (int i = 0; i < len; i++) + { + item = settings[i]; + ids = new IDs(); + if (!string.IsNullOrEmpty(item.GooglePlayProductId)) + { + ids.Add(item.GooglePlayProductId, GooglePlay.Name); + } + + if (!string.IsNullOrEmpty(item.AppStoreProductId)) + { + ids.Add(item.AppStoreProductId, AppleAppStore.Name); + } + + _configBuilder.AddProduct(item.ProductId, item.Type, ids); // 添加商品 + + // 建立本地的商品信息列表 + if (string.IsNullOrEmpty(item.Category)) item.Category = DefaultCategory; + _products[item.ProductId] = new ProductInfo() { Setting = item }; + } + } + // 调用插件初始化 + UnityPurchasing.Initialize(this, _configBuilder); + } + + /// + /// 初始化成功 + /// + /// + /// + /// + public void OnInitialized(IStoreController controller, IExtensionProvider extensions) + { + LogI($"--- IAP Initialized Success"); + _storeController = controller; + _storeExtensionProvider = extensions; + +#if UNITY_IOS + _appleExtensions = extensions.GetExtension(); + // On Apple platforms we need to handle deferred purchases caused by Apple's Ask to Buy feature. + // On non-Apple platforms this will have no effect; OnDeferred will never be called. + _appleExtensions.RegisterPurchaseDeferredListener(item => + { + LogI("Purchase deferred: " + item.definition.id); + OnAppStorePurchaseDeferred?.Invoke(item); + }); +#elif UNITY_ANDROID + _configBuilder.Configure().SetObfuscatedAccountId(IPMConfig.IPM_UID); + _googlePlayStoreExtensions = extensions.GetExtension(); + // _googlePlayStoreExtensions.SetObfuscatedAccountId(IPMConfig.IPM_UID); + //添加安装游戏后第一次初试化进行恢复购买的回调 只有安卓才有 + _googlePlayStoreExtensions.RestoreTransactions(OnRestoreHandle); +#endif + + foreach (var item in _storeController.products.all) + { + if (!item.availableToPurchase) + { + continue; + } + + if (_products.ContainsKey(item.definition.id)) + { + _products[item.definition.id].Product = item; + } + } + + InitValidator(); // 初始化订单验证器 + OnInitResult?.Invoke(true); + } + + /// + /// 初始化失败 + /// + /// + /// + public void OnInitializeFailed(InitializationFailureReason error) + { + LogE($"--- IAP Initialized Fail: {error}"); + OnInitResult?.Invoke(false); + } + + /// + /// 初始化失败 + /// + /// + /// + /// + public void OnInitializeFailed(InitializationFailureReason error, string message) + { + LogE($"--- IAP Initialized Fail: {error} msg: {message}"); + OnInitResult?.Invoke(false); + } + + #endregion + + #region 数据查询 + + // + /// 获取商品Info + /// + /// 商品名称 + /// + public ProductInfo GetInfo(string productName) + { + if(null == Products || Products.Count == 0 ) return null; + return Products.Values.FirstOrDefault(c => c.Name == productName); + } + + /// + /// 通过商品ID获取对应的信息 + /// + /// 商品ID + /// + public ProductInfo GetInfoById(string productId) + { + if(null == Products || Products.Count == 0 ) return null; + return Products.Values.FirstOrDefault(c => c.Id == productId); + } + + /// + /// 获取道具价格 + /// + /// + /// + public double GetProductPrice(string name) + { + if (_storeController == null || _storeController.products == null) + { + return Fallback(); + } + + ProductInfo info = GetInfo(name); + var product = _storeController.products.WithID(info.Id); + if (product == null) + return Fallback(); + + return (double)product.metadata.localizedPrice; + + double Fallback() + { + ProductInfo info = GetInfo(name); + return info?.Price ?? 0.0; + } + } + + + /// + /// 获取道具价格(带单位 $0.01) + /// + /// + /// + public string GetProductPriceString(string name) + { + if (_storeController == null || _storeController.products == null) + { + return Fallback(); + } + + ProductInfo info = GetInfo(name); + var product = _storeController.products.WithID(info.Id); + if (product == null) + return Fallback(); + + return product.metadata.localizedPriceString; + + string Fallback() + { + ProductInfo info = GetInfo(name); + var pr = info?.Price ?? 0.0; + return "$" + pr; + } + } + + #endregion + + #region 订单验证器 + + /// + /// 是否支持订单校验 + /// + /// + private bool IsCurrentStoreSupportedByValidator() + => IsGooglePlayStoreSelected() || IsAppleAppStoreSelected(); + + + /// + /// Google 商店支持 + /// + /// + private bool IsGooglePlayStoreSelected() + { + var currentAppStore = StandardPurchasingModule.Instance().appStore; + return currentAppStore == AppStore.GooglePlay; + } + + /// + /// Apple 商店支持 + /// + /// + private bool IsAppleAppStoreSelected() + { + var currentAppStore = StandardPurchasingModule.Instance().appStore; + return currentAppStore == AppStore.AppleAppStore || currentAppStore == AppStore.MacAppStore; + } + + /// + /// 初始化订单校验器 + /// + protected virtual void InitValidator() + { + if (IsCurrentStoreSupportedByValidator()) + { + try + { + if (_googlePublicKey != null && _appleRootCert != null) + { + _validator = new CrossPlatformValidator(_googlePublicKey, _appleRootCert, Application.identifier); + } + else + { + Crashlytics.LogException(new Exception($"[IAP] Init Validator failed -> googlePublicKey: {_googlePublicKey} appleRootCert: {_appleRootCert}")); + } + } + catch (NotImplementedException exception) + { + LogE("Cross Platform Validator Not Implemented: " + exception); + } + } + } + + + #endregion + + #region 恢复购买 + + /// + /// 恢复购买 + /// + /// + protected virtual void OnRestoreHandle(bool success) + { + LogI($"--- Restore complete: {success}" ); + OnRestored?.Invoke(success); + } + + /// + /// 恢复购买道具 + /// + public virtual void Restore() + { + if (!IsInitialized) return; + +#if UNITY_IOS + _appleExtensions.RestoreTransactions(OnRestoreHandle); +#elif UNITY_ANDROID + _googlePlayStoreExtensions.RestoreTransactions(OnRestoreHandle); +#endif + } + + #endregion + + #region 购买流程 + + /// + /// 购买商品 + /// + /// + public virtual T Buy(string productName) + { + if (!IsInitialized) + { + LogE("Buy FAIL. Not initialized."); + OnBuyEnd?.Invoke(productName, false); + return (T)this; + } + + ProductInfo info = GetInfo(productName); + if (info == null) + { + LogE($"Buy FAIL. No product with name: {productName}"); + OnBuyEnd?.Invoke(productName, false); + return (T)this; + } + + Product product = _storeController.products.WithID(info.Setting.ProductId); + if (product != null && product.availableToPurchase) + { +#if UNITY_ANDROID + _configBuilder + .Configure() + .SetObfuscatedAccountId(IPMConfig.IPM_UID); +#endif + _storeController.InitiatePurchase(product); + + Analytics.IAPClick(info?.Category??"none", product.definition.id); + Analytics.IAPImp(DefaultCategory, product.definition.id); + + OnBuyStart?.Invoke(productName); + return (T)this; + } + + // 找不到商品 + LogE($"Can't find product by name: {productName}, pay canceled."); + OnPurchaseOver(false, productName); + OnBuyEnd?.Invoke(productName, false); + + return (T)this; + } + + /// + /// 处理支付流程 + /// + /// + /// + /// + public PurchaseProcessingResult ProcessPurchase(PurchaseEventArgs purchaseEvent) + { + string productId = purchaseEvent.purchasedProduct.definition.id; + ProductInfo info = GetInfoById(productId); + + if (IsFirstIAP && null != info) + { + Analytics.FirstIAP(info.Id, info.Price, info.CurrencyCode); // 上报首次支付打点 + } + + Analytics.ProductIAP(info.Id,info.Id, info.Price, info.CurrencyCode); + Analytics.IAPRetTrue(info.Category, info.Id, info.Price, info.CurrencyCode, info.Type, info.IsFree); + + PurchaseCount++; // 记录支付次数 + ReportPurchaseResult(purchaseEvent); // 支付结果上报 + + OnPurchaseOver(true, info.Name); // 支付成功处理逻辑 + OnBuyEnd?.Invoke(info.Name, false); + + return PurchaseProcessingResult.Complete; + } + + /// + /// 支付失败 + /// + /// + /// + /// + public void OnPurchaseFailed(Product product, PurchaseFailureReason failureReason) + { + string productId = product.definition.id; + ProductInfo info = GetInfoById(productId); + + //上报点位,用户购买失败的原因 + if (failureReason == PurchaseFailureReason.UserCancelled) + { + Analytics.IAPClose(info.Category, product.definition.id); + } + else + { + Analytics.IAPRetFalse(info.Category, product.definition.id, failureReason.ToString()); + } + + LogI("failureReason = " + failureReason); + // 失败的处理逻辑 + OnPurchaseOver(false, info.Name); + OnBuyEnd?.Invoke(info.Name, false); + } + + #endregion + + #region Log 输出 + + /// + /// 日志输出 + /// + /// + private static void LogI(object msg) + { + if (_showLog) + Debug.Log($"{Tag} {msg}"); + } + + private static void LogE(object msg) + { + if (_showLog) + Debug.LogError($"{Tag} {msg}"); + } + + private static void LogW(object msg) + { + if (_showLog) + Debug.LogWarning($"{Tag} {msg}"); + } + + #endregion + + #region 实现接口 + + /// + /// 需要游戏侧继承并完成Blevel的取值上报 + /// + /// + protected abstract int GetBLevel(); + + /// + /// 获取商品品配置列表 + /// + /// + protected virtual ProductSetting[] GetProductSettings() + => GuruSettings.Instance.Products; + + /// + /// 支付回调 + /// + /// 是否成功 + /// 商品名称 + protected abstract void OnPurchaseOver(bool success, string productName); + + #endregion + + + #region 支付上报逻辑 + + /// + /// 支付结果上报 + /// + protected virtual void ReportPurchaseResult(PurchaseEventArgs args) + { + int blevel = GetBLevel(); + int orderType = args.purchasedProduct.definition.type == ProductType.Subscription ? 1 : 0; + + if (_validator == null) + { + LogE("[IAP] --- Validator is null. Report Order failed."); + Crashlytics.LogException(new Exception($"IAPService can not report order because Validator is null!")); + return; + } + + try + { + // ----- 支付后的b_level上报逻辑 + LogI($"--- Report level:[{blevel}] in id:{args.purchasedProduct.definition.id} "); +#if UNITY_EDITOR + // Editor 不做上报逻辑 +#elif UNITY_ANDROID + // Android 订单验证, 上报打点信息 + var result = _validator.Validate(args.purchasedProduct.receipt); + string productID = orderType == 0 ? args.purchasedProduct.definition.id : ""; + string subscriptionID = orderType == 1 ? args.purchasedProduct.definition.id : ""; + foreach (var productReceipt in result) + { + if (productReceipt is GooglePlayReceipt google) + { + new GoogleOrderRequest(orderType, productID, subscriptionID, google.purchaseToken, blevel) + .SetTimeOut(3).Send(); + } + } +#elif UNITY_IOS + // iOS 订单验证, 上报打点信息 + var jsonData = JsonMapper.ToObject(args.purchasedProduct.receipt); + string receipt = jsonData["Payload"].ToString(); + if (HasReceipt(receipt)) + { + Debug.Log($"[IAP] Receipt has already reported: {receipt}"); + return; + } + AddReceipt(receipt); + new AppleOrderRequest(orderType, args.purchasedProduct.definition.id, receipt,blevel).Send(); +#endif + } + catch (Exception e) + { + LogE($"【IAPManager.RevenueUpload】De Exception:{e.Message}"); + Crashlytics.LogException(new Exception($"[IAP] Unity report purchase data with b_level={blevel} got error: {e.Message}")); + } + } + + #endregion + + #region IOS Orders Collection + + private HashSet iOSReceipts; + public HashSet IOSReceiptCollection + { + get + { + // 读取订单信息 + if (iOSReceipts == null) + { + iOSReceipts = new HashSet(); + string raw = PlayerPrefs.GetString(nameof(IOSReceiptCollection), ""); + if (!string.IsNullOrEmpty(raw)) + { + var arr = raw.Split(','); + for (int i = 0; i < arr.Length; i++) + { + iOSReceipts.Add(arr[i]); + } + } + } + return iOSReceipts; + } + + set + { + // 保存订单信息 + iOSReceipts = value; + PlayerPrefs.SetString(nameof(IOSReceiptCollection), string.Join(",", iOSReceipts)); + PlayerPrefs.Save(); + } + } + + /// + /// 添加订单信息 + /// + /// + public void AddReceipt(string receipt) + { + if (!HasReceipt(receipt)) + { + IOSReceiptCollection.Add(receipt); + } + } + + /// + /// 是否包含订单 + /// + /// + /// + public bool HasReceipt(string receipt) + { + return IOSReceiptCollection.Contains(receipt); + } + + + #endregion + + } + +} \ No newline at end of file diff --git a/Runtime/GuruIAP/Runtime/Code/IAPServiceBase.cs.meta b/Runtime/GuruIAP/Runtime/Code/IAPServiceBase.cs.meta new file mode 100644 index 0000000..760405f --- /dev/null +++ b/Runtime/GuruIAP/Runtime/Code/IAPServiceBase.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 08cb9e6a1a4d4732b0814dd0edd12d1f +timeCreated: 1680687403 \ No newline at end of file diff --git a/Runtime/GuruIAP/Runtime/Code/Settings.meta b/Runtime/GuruIAP/Runtime/Code/Settings.meta new file mode 100644 index 0000000..e3bf4ce --- /dev/null +++ b/Runtime/GuruIAP/Runtime/Code/Settings.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 356c529394fb47fb904882b74a21f03a +timeCreated: 1680689590 \ No newline at end of file diff --git a/Runtime/GuruIAP/Runtime/Code/Settings/GuruSettings.IAP.cs b/Runtime/GuruIAP/Runtime/Code/Settings/GuruSettings.IAP.cs new file mode 100644 index 0000000..b2b65b1 --- /dev/null +++ b/Runtime/GuruIAP/Runtime/Code/Settings/GuruSettings.IAP.cs @@ -0,0 +1,16 @@ +namespace Guru +{ + using UnityEngine; + + /// + /// 支付配置 + /// + public partial class GuruSettings + { + + [Header("IAP 商品配置")] + public ProductSetting[] Products; + + } + +} \ No newline at end of file diff --git a/Runtime/GuruIAP/Runtime/Code/Settings/GuruSettings.IAP.cs.meta b/Runtime/GuruIAP/Runtime/Code/Settings/GuruSettings.IAP.cs.meta new file mode 100644 index 0000000..8b1478c --- /dev/null +++ b/Runtime/GuruIAP/Runtime/Code/Settings/GuruSettings.IAP.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: cc9f692a52e84bc18842b8a88cb7378e +timeCreated: 1680689817 \ No newline at end of file diff --git a/Runtime/GuruIAP/Runtime/Code/Settings/ProductInfo.cs b/Runtime/GuruIAP/Runtime/Code/Settings/ProductInfo.cs new file mode 100644 index 0000000..8ec4488 --- /dev/null +++ b/Runtime/GuruIAP/Runtime/Code/Settings/ProductInfo.cs @@ -0,0 +1,82 @@ + +using UnityEngine; +using UnityEngine.Serialization; + +namespace Guru +{ + using System; + using UnityEngine.Purchasing; + + /// + /// 商品配置类 + /// + [Serializable] + public partial class ProductSetting + { + /// + /// 商品名称 + /// + [Header("商品名称")] [Tooltip("[必填] 程序调用时, 统一使用 ProductName 来进行购买操作")] + public string ProductName; + /// + /// 商品类型 + /// + [Header("商品类型")] [Tooltip("[必填] 产品类型: 不可消耗, 可消耗, 订阅")] + public ProductType Type; + /// + /// GooglePlay 商品ID + /// + [Header("Google商品ID")] + public string GooglePlayProductId; + /// + /// AppleStore 商品ID + /// + [Header("Apple商品ID")] + public string AppStoreProductId; + + [Header("自定义商品分类")][Tooltip("自定义标签:用于产品自有逻辑进行分类查找")] + public string Category = "Store"; + + [Header("是否免费(非必须)")] + public bool IsFree = false; + + [Header("预设商品价格($)")] + public double Price; + + /// + /// 商品ID + /// + public string ProductId + { + get + { +#if UNITY_IOS + return AppStoreProductId; +#else + return GooglePlayProductId; +#endif + } + } + } + + + /// + /// 商品信息 + /// + [Serializable] + public partial class ProductInfo + { + public ProductSetting Setting; + public Product Product; + + public string Name => Setting.ProductName; + public string Id => Product.definition.id; + public double Price => (double?)Product?.metadata?.localizedPrice ?? Setting.Price; + public string CurrencyCode => Product?.metadata?.isoCurrencyCode ?? "USD"; + public string Category => Setting.Category; + public string Type => Setting.Type == ProductType.Subscription ? "subscription" : "product"; + public bool IsFree => Setting.IsFree; + } + + +} \ No newline at end of file diff --git a/Runtime/GuruIAP/Runtime/Code/Settings/ProductInfo.cs.meta b/Runtime/GuruIAP/Runtime/Code/Settings/ProductInfo.cs.meta new file mode 100644 index 0000000..415dd27 --- /dev/null +++ b/Runtime/GuruIAP/Runtime/Code/Settings/ProductInfo.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: fccfd44f49b543db95672c823888c0f3 +timeCreated: 1680690825 \ No newline at end of file diff --git a/Runtime/GuruIAP/~Sample.meta b/Runtime/GuruIAP/~Sample.meta new file mode 100644 index 0000000..458a607 --- /dev/null +++ b/Runtime/GuruIAP/~Sample.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 34e005095faa4584a766400e6d8122c6 +timeCreated: 1680696158 \ No newline at end of file diff --git a/Runtime/GuruIAP/~Sample/MyDemoApp.cs b/Runtime/GuruIAP/~Sample/MyDemoApp.cs new file mode 100644 index 0000000..54c0eb8 --- /dev/null +++ b/Runtime/GuruIAP/~Sample/MyDemoApp.cs @@ -0,0 +1,42 @@ +using System; +using UnityEngine; + +namespace Guru.Sample +{ + public class MyDemoApp: MonoBehaviour + { + private void Awake() + { + // 初始化回调 + MyIAPService.Instance.OnInitResult += success => + { + if (success) + { + // UIManager.Instance.OpenStoreUI(); + } + }; + } + + + /// + /// 点击支付按钮 + /// + /// + private void OnClickBuyItem(string productId) + { + MyIAPService.Instance + .Buy(productId) + .OnBuyEnd += (productName, success) => + { + if (success) + { + Debug.Log($"Product {productName} isSuccess!"); + } + else + { + Debug.Log($"Product {productName} isFail!"); + } + }; + } + } +} \ No newline at end of file diff --git a/Runtime/GuruIAP/~Sample/MyDemoApp.cs.meta b/Runtime/GuruIAP/~Sample/MyDemoApp.cs.meta new file mode 100644 index 0000000..7d5aa22 --- /dev/null +++ b/Runtime/GuruIAP/~Sample/MyDemoApp.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: ece130fdda2a4dd08d4b3cd371f893f3 +timeCreated: 1680701216 \ No newline at end of file diff --git a/Runtime/GuruIAP/~Sample/MyIAPService.cs b/Runtime/GuruIAP/~Sample/MyIAPService.cs new file mode 100644 index 0000000..04eecab --- /dev/null +++ b/Runtime/GuruIAP/~Sample/MyIAPService.cs @@ -0,0 +1,44 @@ +using UnityEngine.Purchasing; + +namespace Guru.Sample +{ + public class MyIAPService: IAPServiceBase + { + /// + /// 必须实现 BLevel 反回值, 用于支付打点上报 + /// + /// + protected override int GetBLevel() + { + // return UserData.Instance.GetLastFinishedLevel(); // 游戏目前进度完成的最后一关 + return 1; + } + + public override void Initialize(bool showLog = false) + { + base.Initialize(true); + InitGameProducts(); + } + + private void InitGameProducts() + { + // 插入项目专用的初始化逻辑 + foreach (var key in Products.Keys) + { + var info = Products[key]; + if (info.Setting.Type == ProductType.Subscription) + { + // TODO: 针对订阅行道具进行处理逻辑 + } + } + } + + protected override void OnPurchaseOver(bool success, string productName) + { + switch (productName) + { + // TODO: 请在此处处理购买 + } + } + } +} \ No newline at end of file diff --git a/Runtime/GuruIAP/~Sample/MyIAPService.cs.meta b/Runtime/GuruIAP/~Sample/MyIAPService.cs.meta new file mode 100644 index 0000000..f17ff03 --- /dev/null +++ b/Runtime/GuruIAP/~Sample/MyIAPService.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 75f93c21156943cb9d7563f48ca16086 +timeCreated: 1680696171 \ No newline at end of file diff --git a/Runtime/GuruRemote.meta b/Runtime/GuruRemote.meta new file mode 100644 index 0000000..74ca3ba --- /dev/null +++ b/Runtime/GuruRemote.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 2a373579d62ab46b3bbe62dcf350eda9 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/GuruRemote/Runtime.meta b/Runtime/GuruRemote/Runtime.meta new file mode 100644 index 0000000..269e756 --- /dev/null +++ b/Runtime/GuruRemote/Runtime.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 50acf9c02af2345bf9887dc207b12bff +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/GuruRemote/Runtime/IRemoteConfig.cs b/Runtime/GuruRemote/Runtime/IRemoteConfig.cs new file mode 100644 index 0000000..a20bf3d --- /dev/null +++ b/Runtime/GuruRemote/Runtime/IRemoteConfig.cs @@ -0,0 +1,27 @@ +using Google; + +namespace Guru +{ + using System; + + /// + /// 运控配置接口类 + /// + public interface IRemoteConfig + { + bool enable { get; set; } + string key { get; } + string value { get; } + + Action OnValueChanged { get; set; } + + void Init(); + string ToJson(); + string GetDefaultValue(); + string GetRemoteValue(); + + void GetRemoteJsonAsync(Action onValueLoaded); + + + } +} \ No newline at end of file diff --git a/Runtime/GuruRemote/Runtime/IRemoteConfig.cs.meta b/Runtime/GuruRemote/Runtime/IRemoteConfig.cs.meta new file mode 100644 index 0000000..778ebd0 --- /dev/null +++ b/Runtime/GuruRemote/Runtime/IRemoteConfig.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 121503ce9dd043e88cfff24e580e603a +timeCreated: 1703504411 \ No newline at end of file diff --git a/Runtime/GuruRemote/Runtime/RemoteConfigBase.cs b/Runtime/GuruRemote/Runtime/RemoteConfigBase.cs new file mode 100644 index 0000000..023f3e3 --- /dev/null +++ b/Runtime/GuruRemote/Runtime/RemoteConfigBase.cs @@ -0,0 +1,45 @@ +using System; + +namespace Guru +{ + public abstract class RemoteConfigBase: IRemoteConfig + { + + /// + /// 配置是否可用 + /// + public bool enable { get; set; } = true; + + public virtual string key { get; } = "remote-config-base"; + public virtual string value { get; } = "{ \"enable\": true }"; + public Action OnValueChanged { get; set; } + + + public void Init() + { + + } + + /// + /// 转为Json + /// + /// + public virtual string ToJson() + => JsonParser.ToJson(this); + + public string GetDefaultValue() + { + throw new NotImplementedException(); + } + + public string GetRemoteValue() + { + throw new NotImplementedException(); + } + + public void GetRemoteJsonAsync(Action onValueLoaded) + { + throw new NotImplementedException(); + } + } +} \ No newline at end of file diff --git a/Runtime/GuruRemote/Runtime/RemoteConfigBase.cs.meta b/Runtime/GuruRemote/Runtime/RemoteConfigBase.cs.meta new file mode 100644 index 0000000..19bde5b --- /dev/null +++ b/Runtime/GuruRemote/Runtime/RemoteConfigBase.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 68a16f91579948018d155215c04bccba +timeCreated: 1703505234 \ No newline at end of file diff --git a/Runtime/GuruRemote/Runtime/RemoteConfigManager.cs b/Runtime/GuruRemote/Runtime/RemoteConfigManager.cs new file mode 100644 index 0000000..ed6aae4 --- /dev/null +++ b/Runtime/GuruRemote/Runtime/RemoteConfigManager.cs @@ -0,0 +1,268 @@ +using System.Collections.Generic; +using System.Linq; + +namespace Guru +{ + using System; + using UnityEngine; + using Firebase.RemoteConfig; + using Firebase.Extensions; + + /// + /// 运控配置管理器 + /// + public class RemoteConfigManager + { + public const double DefaultUpdateHours = 12; + private const string Tag = "[Remote]"; + private static bool _initOnce = false; + private static RemoteConfigManager _instance; + public static RemoteConfigManager Instance => _instance ??= new RemoteConfigManager(); + + private FirebaseRemoteConfig _firebaseRemote; + private static bool _isDebug = false; + private static double _updateHours = DefaultUpdateHours; + + private RemoteConfigModel _model; + + internal RemoteConfigModel Model + { + get + { + if(_model == null) _model = RemoteConfigModel.LoadOrCreate(); + return _model; + } + } + private Dictionary _defaults; + + public static event Action OnFetchCompleted; + + public static void Init(Dictionary defaults, double updateHours = DefaultUpdateHours, bool isDebug = false) + { + if (_initOnce) return; + Instance.InitAssets(defaults, updateHours, isDebug); + } + + // 拉取所有的线上配置数据 + // onFetchComplete 传参 true: 拉取成功 false: 拉取失败 + public static void FetchAll(Action onFetchComplete = null) + { + OnFetchCompleted += onFetchComplete; + if (!_initOnce) Init(null, _updateHours, _isDebug); + Instance.FetchAllConfigs(); + } + + #region 初始化 + + private void InitAssets(Dictionary defaults, double updateHours = DefaultUpdateHours, bool isDebug = false) + { + _defaults = defaults; + _updateHours = updateHours; + _isDebug = isDebug; + _firebaseRemote = FirebaseRemoteConfig.DefaultInstance; + if (_firebaseRemote == null) + { + LogE("Can't find FirebaseRemoteConfig.DefaultInstance, init failed."); + return; + } + + + if (_defaults != null) + { + _firebaseRemote.SetDefaultsAsync(_defaults); + } + _initOnce = true; + + FetchAllConfigs(); + } + + + + + // private void OnConfigUpdateListener() + // { + // if (evt.Error == RemoteConfigError.None) + // { + // LogI($"------- Config Update -------"); + // var list = evt.UpdatedKeys.ToArray(); + // for (int i = 0; i < list.Length; i++) + // { + // LogI($"[{i}] : {list[i]}"); + // } + // } + // } + + + private void FetchAllConfigs() + { + var span = _isDebug? TimeSpan.Zero : TimeSpan.FromHours(_updateHours); + _firebaseRemote.FetchAsync(span) + .ContinueWithOnMainThread(task => + { + bool success = true; + if (task.IsFaulted || task.IsCanceled) + { + string res = task.IsFaulted? "Faulted" : "Canceled"; + LogE($" --- FetchAllConfigs fails: {res}"); + success = false; + } + OnFetchCompleted?.Invoke(success); + }); + } + + + #endregion + + #region 数据接口 + + public string GetStringValue(string key, string defaultValue = "") + { + if (_firebaseRemote != null) + { + try + { + return _firebaseRemote.GetValue(key).StringValue; + } + catch (Exception e) + { + LogException(e); + } + } + return defaultValue; + } + + public int GetIntValue(string key, int defaultValue = 0) + { + if (_firebaseRemote != null) + { + try + { + return (int) _firebaseRemote.GetValue(key).LongValue; + } + catch (Exception e) + { + LogException(e); + } + } + return defaultValue; + } + + public long GetLongValue(string key, long defaultValue = 0) + { + if (_firebaseRemote != null) + { + try + { + return _firebaseRemote.GetValue(key).LongValue; + } + catch (Exception e) + { + LogException(e); + } + } + return defaultValue; + } + + public double GetDoubleValue(string key, double defaultValue = 0) + { + if (_firebaseRemote != null) + { + try + { + return _firebaseRemote.GetValue(key).DoubleValue; + } + catch (Exception e) + { + LogException(e); + } + } + return defaultValue; + } + + public bool GetBoolValue(string key, bool defaultValue = false) + { + if (_firebaseRemote != null) + { + try + { + return _firebaseRemote.GetValue(key).BooleanValue; + } + catch (Exception e) + { + LogException(e); + } + } + return defaultValue; + } + + #endregion + + + #region 云控值获取 + + public static string GetString(string key, string defaultValue = "") + { + return Instance.GetStringValue(key, defaultValue); + } + + public static int GetInt(string key, int defaultValue = 0) + { + return Instance.GetIntValue(key, defaultValue); + } + + public static long GetLong(string key, long defaultValue = 0) + { + return Instance.GetLongValue(key, defaultValue); + } + + public static double GetDouble(string key, double defaultValue = 0) + { + return Instance.GetDoubleValue(key, defaultValue); + } + + public static float GetFloat(string key, float defaultValue = 0) + { + return (float) Instance.GetDoubleValue(key, defaultValue); + } + + public static bool GetBool(string key, bool defaultValue = false) + { + return Instance.GetBoolValue(key, defaultValue); + } + + #endregion + + + #region 云控配置获取 + + + + + + #endregion + + #region Log + + private static void LogI(string msg, params object[] args) + { + Log.I(Tag, msg, args); + } + + private static void LogE(string msg, params object[] args) + { + Log.E(Tag, msg, args); + } + + private static void LogW(string msg, params object[] args) + { + Log.W(Tag, msg, args); + } + + private static void LogException(Exception e) + { + Log.Exception(e); + } + + #endregion + } +} \ No newline at end of file diff --git a/Runtime/GuruRemote/Runtime/RemoteConfigManager.cs.meta b/Runtime/GuruRemote/Runtime/RemoteConfigManager.cs.meta new file mode 100644 index 0000000..305f55b --- /dev/null +++ b/Runtime/GuruRemote/Runtime/RemoteConfigManager.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 6e4356ccb799475581be3992b0dd9c24 +timeCreated: 1703494289 \ No newline at end of file diff --git a/Runtime/GuruRemote/Runtime/RemoteConfigModel.cs b/Runtime/GuruRemote/Runtime/RemoteConfigModel.cs new file mode 100644 index 0000000..abee9d4 --- /dev/null +++ b/Runtime/GuruRemote/Runtime/RemoteConfigModel.cs @@ -0,0 +1,143 @@ +using System; +using System.Collections.Generic; +using UnityEngine; + +namespace Guru +{ + + + + [Serializable] + internal class RemoteConfigModel + { + private static float SaveInterval = 2f; + private const string SaveKey = "comr.guru.remote.model.save"; + public Dictionary Data; + public long last_modified = 0; + + private float _lastSavedTime = 0; + + /// + /// 创建或加载 + /// + /// + public static RemoteConfigModel LoadOrCreate() + { + RemoteConfigModel model = null; + if (PlayerPrefs.HasKey(SaveKey)) + { + string json = LoadStringValue(SaveKey); + model = JsonParser.ToObject(json); + } + if (model == null) model = new RemoteConfigModel(); + return model; + } + + + /// + /// 默认赋值数据 + /// + private Dictionary _defaultData; + + /// + /// 加载数据 + /// + /// + /// + /// + private static string LoadStringValue(string key, string defaultValue = "") + { + if (PlayerPrefs.HasKey(key)) + { + return PlayerPrefs.GetString(key, defaultValue); + } + return defaultValue; + } + + /// + /// 保存数据 + /// + /// + /// + private static void SaveToPlayerPrefs(string key, string value) + { + PlayerPrefs.SetString(key, value); + } + + + /// + /// 初始化 + /// + public RemoteConfigModel() + { + _defaultData = new Dictionary(20); + Data = new Dictionary(20); + } + + /// + /// 是否有数据 + /// + /// + /// + public bool HasKey(string key) => Data.ContainsKey(key); + + /// + /// 保存数据 + /// + /// + public void Save(bool forceSave = false) + { + if (forceSave || (Time.realtimeSinceStartup - _lastSavedTime > SaveInterval)) + { + _lastSavedTime = Time.realtimeSinceStartup; + last_modified = TimeUtil.GetCurrentTimeStamp(); + SaveToPlayerPrefs(SaveKey, JsonParser.ToJson(this)); + } + } + + /// + /// 设置默认值 + /// + /// + /// + public void SetDefault(string key, string value) + { + _defaultData[key] = value; + if (!HasKey(key)) + { + Data[key] = value; + Save(); + } + } + + /// + /// 设置当前值 + /// + /// + /// + public void SetDefault(string key, T config) where T : IRemoteConfig + { + var json = config.ToJson(); + SetDefault(key, json); + } + + /// + /// 获取配置对象 + /// + /// + /// + /// + public T Get(string key) where T : IRemoteConfig + { + if(HasKey(key)) return JsonParser.ToObject(Data[key]); + return default(T); + } + + public void Set(string key, T config) where T : IRemoteConfig + { + + } + + + } +} \ No newline at end of file diff --git a/Runtime/GuruRemote/Runtime/RemoteConfigModel.cs.meta b/Runtime/GuruRemote/Runtime/RemoteConfigModel.cs.meta new file mode 100644 index 0000000..beaf375 --- /dev/null +++ b/Runtime/GuruRemote/Runtime/RemoteConfigModel.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 33988330bc494306a653edce939dbec5 +timeCreated: 1703505516 \ No newline at end of file diff --git a/Runtime/GuruWebview.meta b/Runtime/GuruWebview.meta new file mode 100644 index 0000000..cf5c1b6 --- /dev/null +++ b/Runtime/GuruWebview.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 79e1f533cf5084cb8a0f87db7ec60492 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/GuruWebview/Editor.meta b/Runtime/GuruWebview/Editor.meta new file mode 100644 index 0000000..93f1ed2 --- /dev/null +++ b/Runtime/GuruWebview/Editor.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 8675d9ff75734e21badf096265a187e4 +timeCreated: 1702956639 \ No newline at end of file diff --git a/Runtime/GuruWebview/Editor/Proguards.txt b/Runtime/GuruWebview/Editor/Proguards.txt new file mode 100644 index 0000000..93bfe50 --- /dev/null +++ b/Runtime/GuruWebview/Editor/Proguards.txt @@ -0,0 +1,2 @@ +-keep class com.onevcat.uniwebview.* { *; } +-keep class com.iab.omid.* { *; } \ No newline at end of file diff --git a/Runtime/GuruWebview/Editor/Proguards.txt.meta b/Runtime/GuruWebview/Editor/Proguards.txt.meta new file mode 100644 index 0000000..c0f9b5e --- /dev/null +++ b/Runtime/GuruWebview/Editor/Proguards.txt.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: e0f4629ef62c4d598c6b613b401f0b35 +timeCreated: 1702956650 \ No newline at end of file diff --git a/Runtime/GuruWebview/Proguard.meta b/Runtime/GuruWebview/Proguard.meta new file mode 100644 index 0000000..107cc7c --- /dev/null +++ b/Runtime/GuruWebview/Proguard.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 488dfee5dd3bc48e8ad6aff272082c65 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/GuruWebview/Proguard/uniwebview.txt b/Runtime/GuruWebview/Proguard/uniwebview.txt new file mode 100644 index 0000000..93bfe50 --- /dev/null +++ b/Runtime/GuruWebview/Proguard/uniwebview.txt @@ -0,0 +1,2 @@ +-keep class com.onevcat.uniwebview.* { *; } +-keep class com.iab.omid.* { *; } \ No newline at end of file diff --git a/Runtime/GuruWebview/Proguard/uniwebview.txt.meta b/Runtime/GuruWebview/Proguard/uniwebview.txt.meta new file mode 100644 index 0000000..c39bf1c --- /dev/null +++ b/Runtime/GuruWebview/Proguard/uniwebview.txt.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: b767b9e9fa694e78b2ca8ccc6dab4261 +timeCreated: 1702954531 \ No newline at end of file diff --git a/Runtime/GuruWebview/README.md b/Runtime/GuruWebview/README.md new file mode 100644 index 0000000..37857a1 --- /dev/null +++ b/Runtime/GuruWebview/README.md @@ -0,0 +1,36 @@ +# Guru WebView + +Verison 0.0.1 + +内置网页浏览器插件, 底层使用了用了 `UniWebView` 来实现内置网页浏览器功能 + +目前仅支持内部打开网页. + +# Integration + +代码调用: + +```C# +// 参数 url 用来填写打开的地址 +// 参数 showToolbar 用来控制状态栏是否显示 +// 关闭按钮 Done 的文本,当前版本在Android平台不可设置 +// 默认情况下, showToolbar = true +GuruWebView.OpenPage("https://m.baidu.com"); +``` + +## Android 平台 + +单独使用时, 在项目开启了`Minify`功能时, 请在 `Plaugins/Android/proguard-user.txt` 添加如下代码: +```yaml +-keep class com.onevcat.uniwebview.* { *; } +-keep class com.iab.omid.* { *; } +``` +对于直接使用中台框架的项目可以自动注入混淆保护 + + +## UniWebView + +插件版本 +- Version: 5.0.3 + + diff --git a/Runtime/GuruWebview/README.md.meta b/Runtime/GuruWebview/README.md.meta new file mode 100644 index 0000000..6b95ef6 --- /dev/null +++ b/Runtime/GuruWebview/README.md.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: e6a372715a12485e9bf2fd243d0335e9 +timeCreated: 1702966903 \ No newline at end of file diff --git a/Runtime/GuruWebview/Runtime.meta b/Runtime/GuruWebview/Runtime.meta new file mode 100644 index 0000000..58e1bcc --- /dev/null +++ b/Runtime/GuruWebview/Runtime.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 10a00cb80706f48bf913b0bbfe4373f3 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/GuruWebview/Runtime/GuruWebview.cs b/Runtime/GuruWebview/Runtime/GuruWebview.cs new file mode 100644 index 0000000..4c512a9 --- /dev/null +++ b/Runtime/GuruWebview/Runtime/GuruWebview.cs @@ -0,0 +1,53 @@ +using System.Collections; +using System.Collections.Generic; +using UnityEngine; + +namespace Guru +{ + + /// + /// Guru 内置浏览器 + /// + public class GuruWebview + { + public const string Version = "0.0.1"; + + private static UniWebView CreateWebView() + { + var go = new GameObject("guru_webview"); + Object.DontDestroyOnLoad(go); + var view = go.AddComponent(); + view.Frame = new Rect(0,0, Screen.width, Screen.height); + view.SetUserInteractionEnabled(true); + return view; + } + + /// + /// 打开链接 + /// + /// + public static void OpenPage(string url, bool showToolbar = true) + { + Debug.Log($"---- Guru Open Url: {url}"); + var view = CreateWebView(); + + if (showToolbar) + { + view.SetShowToolbarNavigationButtons(true); + view.SetBackButtonEnabled(true); + view.EmbeddedToolbar.Show(); + // view.SetToolbarDoneButtonText("X"); + } + + view.Load(url); + view.OnPageFinished += (v, code, msg) => + { + // 加载完成后展示页面 + view.Show(true); + }; + + } + + } +} + diff --git a/Runtime/GuruWebview/Runtime/GuruWebview.cs.meta b/Runtime/GuruWebview/Runtime/GuruWebview.cs.meta new file mode 100644 index 0000000..2b9e4ed --- /dev/null +++ b/Runtime/GuruWebview/Runtime/GuruWebview.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 40ebe2767f11944c3a67ca120bf45bdf +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/GuruWebview/UniWebView5.meta b/Runtime/GuruWebview/UniWebView5.meta new file mode 100644 index 0000000..29505e6 --- /dev/null +++ b/Runtime/GuruWebview/UniWebView5.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 330b22af8d4364e22b248f6afae54d3f +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/GuruWebview/UniWebView5/Plugins.meta b/Runtime/GuruWebview/UniWebView5/Plugins.meta new file mode 100644 index 0000000..c7cbf6d --- /dev/null +++ b/Runtime/GuruWebview/UniWebView5/Plugins.meta @@ -0,0 +1,9 @@ +fileFormatVersion: 2 +guid: 4920deff2cffa43c98b6c11f1d4c90fd +folderAsset: yes +timeCreated: 1489325633 +licenseType: Free +DefaultImporter: + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/GuruWebview/UniWebView5/Plugins/Android.meta b/Runtime/GuruWebview/UniWebView5/Plugins/Android.meta new file mode 100644 index 0000000..d16eb63 --- /dev/null +++ b/Runtime/GuruWebview/UniWebView5/Plugins/Android.meta @@ -0,0 +1,9 @@ +fileFormatVersion: 2 +guid: 5f6c0bbe8d0744418b9d67f7b9fd7fb4 +folderAsset: yes +timeCreated: 1489325654 +licenseType: Free +DefaultImporter: + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/GuruWebview/UniWebView5/Plugins/Android/UniWebView.aar b/Runtime/GuruWebview/UniWebView5/Plugins/Android/UniWebView.aar new file mode 100644 index 0000000..9f4490d Binary files /dev/null and b/Runtime/GuruWebview/UniWebView5/Plugins/Android/UniWebView.aar differ diff --git a/Runtime/GuruWebview/UniWebView5/Plugins/Android/UniWebView.aar.meta b/Runtime/GuruWebview/UniWebView5/Plugins/Android/UniWebView.aar.meta new file mode 100644 index 0000000..8e31aa5 --- /dev/null +++ b/Runtime/GuruWebview/UniWebView5/Plugins/Android/UniWebView.aar.meta @@ -0,0 +1,32 @@ +fileFormatVersion: 2 +guid: 70a24c5a1222c4ae4b48a1d4738ae6de +PluginImporter: + externalObjects: {} + serializedVersion: 2 + iconMap: {} + executionOrder: {} + defineConstraints: [] + isPreloaded: 0 + isOverridable: 0 + isExplicitlyReferenced: 0 + validateReferences: 1 + platformData: + - first: + Android: Android + second: + enabled: 1 + settings: {} + - first: + Any: + second: + enabled: 0 + settings: {} + - first: + Editor: Editor + second: + enabled: 0 + settings: + DefaultValueInitialized: true + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/GuruWebview/UniWebView5/Plugins/Android/omsdk-android-1.4.0-release.aar b/Runtime/GuruWebview/UniWebView5/Plugins/Android/omsdk-android-1.4.0-release.aar new file mode 100644 index 0000000..83bf102 Binary files /dev/null and b/Runtime/GuruWebview/UniWebView5/Plugins/Android/omsdk-android-1.4.0-release.aar differ diff --git a/Runtime/GuruWebview/UniWebView5/Plugins/Android/omsdk-android-1.4.0-release.aar.meta b/Runtime/GuruWebview/UniWebView5/Plugins/Android/omsdk-android-1.4.0-release.aar.meta new file mode 100644 index 0000000..2534f8d --- /dev/null +++ b/Runtime/GuruWebview/UniWebView5/Plugins/Android/omsdk-android-1.4.0-release.aar.meta @@ -0,0 +1,32 @@ +fileFormatVersion: 2 +guid: 421fd959e80dc41d1b4d5f714a7f0752 +PluginImporter: + externalObjects: {} + serializedVersion: 2 + iconMap: {} + executionOrder: {} + defineConstraints: [] + isPreloaded: 0 + isOverridable: 0 + isExplicitlyReferenced: 0 + validateReferences: 1 + platformData: + - first: + Android: Android + second: + enabled: 1 + settings: {} + - first: + Any: + second: + enabled: 0 + settings: {} + - first: + Editor: Editor + second: + enabled: 0 + settings: + DefaultValueInitialized: true + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/GuruWebview/UniWebView5/Plugins/UniWebView.bundle.meta b/Runtime/GuruWebview/UniWebView5/Plugins/UniWebView.bundle.meta new file mode 100644 index 0000000..58c187f --- /dev/null +++ b/Runtime/GuruWebview/UniWebView5/Plugins/UniWebView.bundle.meta @@ -0,0 +1,34 @@ +fileFormatVersion: 2 +guid: 12a93e06f58f74620aa3663f7e55ecb3 +folderAsset: yes +PluginImporter: + externalObjects: {} + serializedVersion: 2 + iconMap: {} + executionOrder: {} + defineConstraints: [] + isPreloaded: 0 + isOverridable: 0 + isExplicitlyReferenced: 0 + validateReferences: 1 + platformData: + - first: + Any: + second: + enabled: 0 + settings: {} + - first: + Editor: Editor + second: + enabled: 1 + settings: + DefaultValueInitialized: true + - first: + Standalone: OSXUniversal + second: + enabled: 1 + settings: + CPU: AnyCPU + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/GuruWebview/UniWebView5/Plugins/UniWebView.bundle/Contents.meta b/Runtime/GuruWebview/UniWebView5/Plugins/UniWebView.bundle/Contents.meta new file mode 100644 index 0000000..b0f0183 --- /dev/null +++ b/Runtime/GuruWebview/UniWebView5/Plugins/UniWebView.bundle/Contents.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 4fd2db5d630fd43298e453e00f5bf4b6 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/GuruWebview/UniWebView5/Plugins/UniWebView.bundle/Contents/Info.plist b/Runtime/GuruWebview/UniWebView5/Plugins/UniWebView.bundle/Contents/Info.plist new file mode 100644 index 0000000..2178f33 --- /dev/null +++ b/Runtime/GuruWebview/UniWebView5/Plugins/UniWebView.bundle/Contents/Info.plist @@ -0,0 +1,48 @@ + + + + + BuildMachineOSBuild + 22D68 + CFBundleDevelopmentRegion + en + CFBundleExecutable + UniWebView + CFBundleIdentifier + com.onevcat.UniWebViewMac + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + UniWebView + CFBundlePackageType + BNDL + CFBundleShortVersionString + 1.0 + CFBundleSupportedPlatforms + + MacOSX + + CFBundleVersion + 1 + DTCompiler + com.apple.compilers.llvm.clang.1_0 + DTPlatformBuild + 14C18 + DTPlatformName + macosx + DTPlatformVersion + 13.1 + DTSDKBuild + 22C55 + DTSDKName + macosx13.1 + DTXcode + 1420 + DTXcodeBuild + 14C18 + LSMinimumSystemVersion + 10.10 + NSHumanReadableCopyright + Copyright © 2017年 OneV's Den. All rights reserved. + + diff --git a/Runtime/GuruWebview/UniWebView5/Plugins/UniWebView.bundle/Contents/Info.plist.meta b/Runtime/GuruWebview/UniWebView5/Plugins/UniWebView.bundle/Contents/Info.plist.meta new file mode 100644 index 0000000..badb65e --- /dev/null +++ b/Runtime/GuruWebview/UniWebView5/Plugins/UniWebView.bundle/Contents/Info.plist.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 1a152c5424cf14075ba6d3833802d47c +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/GuruWebview/UniWebView5/Plugins/UniWebView.bundle/Contents/MacOS.meta b/Runtime/GuruWebview/UniWebView5/Plugins/UniWebView.bundle/Contents/MacOS.meta new file mode 100644 index 0000000..31ab75c --- /dev/null +++ b/Runtime/GuruWebview/UniWebView5/Plugins/UniWebView.bundle/Contents/MacOS.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 9e7db360725374b64b0ec3f96cce3fd3 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/GuruWebview/UniWebView5/Plugins/UniWebView.bundle/Contents/MacOS/UniWebView b/Runtime/GuruWebview/UniWebView5/Plugins/UniWebView.bundle/Contents/MacOS/UniWebView new file mode 100755 index 0000000..a6226f3 Binary files /dev/null and b/Runtime/GuruWebview/UniWebView5/Plugins/UniWebView.bundle/Contents/MacOS/UniWebView differ diff --git a/Runtime/GuruWebview/UniWebView5/Plugins/UniWebView.bundle/Contents/MacOS/UniWebView.meta b/Runtime/GuruWebview/UniWebView5/Plugins/UniWebView.bundle/Contents/MacOS/UniWebView.meta new file mode 100644 index 0000000..b4124e3 --- /dev/null +++ b/Runtime/GuruWebview/UniWebView5/Plugins/UniWebView.bundle/Contents/MacOS/UniWebView.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: b22bec296e7344274920d89d60046b86 +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/GuruWebview/UniWebView5/Plugins/UniWebView.bundle/Contents/_CodeSignature.meta b/Runtime/GuruWebview/UniWebView5/Plugins/UniWebView.bundle/Contents/_CodeSignature.meta new file mode 100644 index 0000000..19041c7 --- /dev/null +++ b/Runtime/GuruWebview/UniWebView5/Plugins/UniWebView.bundle/Contents/_CodeSignature.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 80556f40493b8401b9ed41de8942b552 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/GuruWebview/UniWebView5/Plugins/UniWebView.bundle/Contents/_CodeSignature/CodeResources b/Runtime/GuruWebview/UniWebView5/Plugins/UniWebView.bundle/Contents/_CodeSignature/CodeResources new file mode 100644 index 0000000..d5d0fd7 --- /dev/null +++ b/Runtime/GuruWebview/UniWebView5/Plugins/UniWebView.bundle/Contents/_CodeSignature/CodeResources @@ -0,0 +1,115 @@ + + + + + files + + files2 + + rules + + ^Resources/ + + ^Resources/.*\.lproj/ + + optional + + weight + 1000 + + ^Resources/.*\.lproj/locversion.plist$ + + omit + + weight + 1100 + + ^Resources/Base\.lproj/ + + weight + 1010 + + ^version.plist$ + + + rules2 + + .*\.dSYM($|/) + + weight + 11 + + ^(.*/)?\.DS_Store$ + + omit + + weight + 2000 + + ^(Frameworks|SharedFrameworks|PlugIns|Plug-ins|XPCServices|Helpers|MacOS|Library/(Automator|Spotlight|LoginItems))/ + + nested + + weight + 10 + + ^.* + + ^Info\.plist$ + + omit + + weight + 20 + + ^PkgInfo$ + + omit + + weight + 20 + + ^Resources/ + + weight + 20 + + ^Resources/.*\.lproj/ + + optional + + weight + 1000 + + ^Resources/.*\.lproj/locversion.plist$ + + omit + + weight + 1100 + + ^Resources/Base\.lproj/ + + weight + 1010 + + ^[^/]+$ + + nested + + weight + 10 + + ^embedded\.provisionprofile$ + + weight + 20 + + ^version\.plist$ + + weight + 20 + + + + diff --git a/Runtime/GuruWebview/UniWebView5/Plugins/UniWebView.bundle/Contents/_CodeSignature/CodeResources.meta b/Runtime/GuruWebview/UniWebView5/Plugins/UniWebView.bundle/Contents/_CodeSignature/CodeResources.meta new file mode 100644 index 0000000..3ad521f --- /dev/null +++ b/Runtime/GuruWebview/UniWebView5/Plugins/UniWebView.bundle/Contents/_CodeSignature/CodeResources.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: e0dc774ebc4264ab3982157e12fd428b +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/GuruWebview/UniWebView5/Plugins/iOS.meta b/Runtime/GuruWebview/UniWebView5/Plugins/iOS.meta new file mode 100644 index 0000000..d3cd1d8 --- /dev/null +++ b/Runtime/GuruWebview/UniWebView5/Plugins/iOS.meta @@ -0,0 +1,9 @@ +fileFormatVersion: 2 +guid: 921d907cf956b4498afa0a728241d961 +folderAsset: yes +timeCreated: 1489325641 +licenseType: Free +DefaultImporter: + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/GuruWebview/UniWebView5/Plugins/iOS/libUniWebView.a b/Runtime/GuruWebview/UniWebView5/Plugins/iOS/libUniWebView.a new file mode 100644 index 0000000..ec3199d Binary files /dev/null and b/Runtime/GuruWebview/UniWebView5/Plugins/iOS/libUniWebView.a differ diff --git a/Runtime/GuruWebview/UniWebView5/Plugins/iOS/libUniWebView.a.meta b/Runtime/GuruWebview/UniWebView5/Plugins/iOS/libUniWebView.a.meta new file mode 100644 index 0000000..2158153 --- /dev/null +++ b/Runtime/GuruWebview/UniWebView5/Plugins/iOS/libUniWebView.a.meta @@ -0,0 +1,80 @@ +fileFormatVersion: 2 +guid: c7db402cd440f49ce97da3366b3cb653 +PluginImporter: + externalObjects: {} + serializedVersion: 2 + iconMap: {} + executionOrder: {} + defineConstraints: [] + isPreloaded: 0 + isOverridable: 0 + isExplicitlyReferenced: 0 + validateReferences: 1 + platformData: + - first: + : Any + second: + enabled: 0 + settings: + Exclude Android: 1 + Exclude Editor: 1 + Exclude Linux64: 1 + Exclude OSXUniversal: 1 + Exclude Win: 1 + Exclude Win64: 1 + Exclude iOS: 0 + - first: + Android: Android + second: + enabled: 0 + settings: + CPU: ARMv7 + - first: + Any: + second: + enabled: 0 + settings: {} + - first: + Editor: Editor + second: + enabled: 0 + settings: + CPU: AnyCPU + DefaultValueInitialized: true + OS: AnyOS + - first: + Standalone: Linux64 + second: + enabled: 0 + settings: + CPU: None + - first: + Standalone: OSXUniversal + second: + enabled: 0 + settings: + CPU: None + - first: + Standalone: Win + second: + enabled: 0 + settings: + CPU: None + - first: + Standalone: Win64 + second: + enabled: 0 + settings: + CPU: None + - first: + iPhone: iOS + second: + enabled: 1 + settings: + AddToEmbeddedBinaries: false + CPU: AnyCPU + CompileFlags: + FrameworkDependencies: + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/GuruWebview/UniWebView5/UniWebView.meta b/Runtime/GuruWebview/UniWebView5/UniWebView.meta new file mode 100644 index 0000000..906f478 --- /dev/null +++ b/Runtime/GuruWebview/UniWebView5/UniWebView.meta @@ -0,0 +1,9 @@ +fileFormatVersion: 2 +guid: a1424e00355294877b7da28b5968d7ec +folderAsset: yes +timeCreated: 1490878496 +licenseType: Free +DefaultImporter: + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/GuruWebview/UniWebView5/UniWebView/CHANGELOG.md b/Runtime/GuruWebview/UniWebView5/UniWebView/CHANGELOG.md new file mode 100644 index 0000000..5a1203f --- /dev/null +++ b/Runtime/GuruWebview/UniWebView5/UniWebView/CHANGELOG.md @@ -0,0 +1,1474 @@ +# Release Note + +### 5.0.3 + +#### Fix + +* Improve the delay that on Android the received cookies are not flushed fast enough in some cases. Now a forcibly cookie flush is always performed when closing the web view. + +### 5.0.2 (24 Oct, 2022) + +#### Fix + +* A potential issue that the post-build Gradle file parses wrongly when using variables. +* The loading spinner is now not shown wrongly when a mail link tapped. + +### 5.0.1 (8 Oct, 2022) + +#### Fix + +* An issue that the exported iOS project cannot be built with Xcode 13. +* A problem which stops the project from compiling when ".Net Framework" profile is used in Unity. + +### 5.0.0 (18 Sep, 2022) + +::: danger +5.0.0 is a major update of UniWebView. Although most of APIs in UniWebView 5 are compatible with UniWebView 4 and it should be easy enough to migrate, there are still a few breaking changes. If you are using the toolbar in UniWebView 4, although it is still available, but it is deprecated and replaced by the new embedded toolbar. For migration from UniWebView 4 in detail, visit the [Migration Guide](https://docs.uniwebview.com/guide/migration-guide-v4-to-v5.html). To know the highlight of the version, check [this page](https://docs.uniwebview.com/guide/version-highlight.html) for more. +::: + +#### Add + +* **Built-in OAuth 2.0 support** for Google, Twitter, Facebook, LINE and GitHub. Now your user can login with their account easily and get access token for other API calls on those service. For more about OAuth 2.0 support in UniWebView, check [this guide](https://docs.uniwebview.com/guide/oauth2.html). +* **Customized OAuth 2.0 support** for any other service provider which defines a standard OAuth 2.0 Authorization Code Flow. +* Embedded toolbar for both iOS and Android. It replaces the old toolbar on iOS. The new toolbar contains a set of navigation buttons and a title, showing as a part of the web view on top or bottom of the web view frame. For more about embedded toolbar, check [this guide](https://docs.uniwebview.com/guide/embedded-toolbar.html). + +#### Fix + +* A layout issue which occasionally happens when opening the old toolbar. Now the buttons position should be always correct. +* An issue which causes web view's `Url` property not parsing correctly when it contains some special characters. + +#### Deprecate + +* The original iOS-only toolbar is now deprecated. Use the new embedded toolbar instead. +* As Xcode 14 drops the support of 32-bit iOS devices, UniWebView 5 will not support 32-bit iOS devices anymore. If you are using UniWebView 4 and still need to support 32-bit iOS devices for a while, please stay with UniWebView 4. + +#### Known Issues + +* The `WKWebView` on iOS 16 Simulator is broken: the web view process won't connect and no content can be loaded due to a bug inside WebKit and Apple's system. To develop on iOS simulators, please use iOS 15 or earlier versions for now. This issue does not affect any physical devices running iOS 16. + +### 4.13.0 (3 Sep, 2022) + +#### Add + +* A `SetAcceptThirdPartyCookies` method to allow third party cookies on Android. By default, the third party cookies are not allowed for security reason. + +### 4.12.1 (24 Jun, 2022) + +#### Fix + +* An issue that a stored cookie cannot be read when using `GetCookie` method on iOS if its domain starting with a dot. According to RFC6265, the leading dot should be ignored when matching a cookie. + +### 4.12.0 (17 Jun, 2022) + +#### Add + +* An `allowJavaScriptOpening` parameter in `SetSupportMultipleWindows` to allow the web view opens a new window even triggered by JavaScript instead of user action. + +#### Fix + +* An issue which prevents "uploading from camera" from working properly on some Android 11 and Android 12 devices. +* an issue which causes getting JavaScript evaluating result on the pages with UTF code format (a.k.a, characters as `\uXXXX`) will fail on Android. +* Now UniWebView 4 supports from Unity 2019.4. This is a requirement of Unity publisher submission guidelines. If you need to continue using UniWebView 4.12.0 and later versions, please consider to upgrade your Unity version. + +### 4.11.2 (6 Apr, 2022) + +#### Fix + +* An issue that Safe Browsing on iOS will leak its memory when using `Dismiss` to close the web page by code. + +### 4.11.1 (23 Feb, 2022) + +#### Fix + +* Now UniWebView can build with Unity's new Input System enabled. But due to a bug in Unity's Input System, the back button detection on some Android devices may break and cause a stuck state. Before Unity can fix that, it is strongly recommended to use the "Both" option for Input System to get a stable experience. +* An issue that when header is set, the HTML content in iframe tag not shows properly in some cases. +* Fix a problem that the Post Build script edits format of some embedded variables in gradle file incorrectly. + +### 4.11.0 (26 Jan, 2022) + +#### Add + +* An `IsWebViewSupported` property on `UniWebView` to check whether the current device supports web view. + +### 4.10.3 (3 Dec, 2021) + +#### Fix + +* A crash when scrolling outside of the web view area when `SetTransparencyClickingThroughEnabled` is set to `true`. +* Now Android supports RTL languages layout. + +### 4.10.2 (25 Nov, 2021) + +#### Fix + +* Now UniWebView also supports NTLM authentication method. +* Uses the intent-based way to detect availability of SafeBrowsing. Now the `IsSafeBrowsingSupported` API would give back a more accurate result. +* Removes the requirement of Gradle plugin version of 4.0.1 or later. This fixes some Android build errors on earlier Unity versions which ships with an older Gradle Plugin. However, if you are using `UniWebViewSafeBrowsing` and setting the Target API Level to Android 11 (Level 30), you still need a newer version of Unity and follow the [related guide](https://docs.uniwebview.com/guide/safe-browsing.html) to add `queries` to the `AndroidManifest.xml` file. +* An issue that the toolbar on iOS can be clicked through to the Unity scene. + + +### 4.10.1 (11 Nov, 2021) + +#### Fix + +* An issue that `UniWebViewSafeBrowsing.IsSafeBrowsingSupported` may always return `false` wrongly when build against Android API 30 or above. + +### 4.10.0 (2 Oct, 2021) + +#### Add + +* Add option to support for using regular expression matching in `AddDownloadURL` or `AddDownloadMIMEType` methods. + +#### Fix + +* Now the `Load` method allows a `null` URL on Android instead of crash the game. +* The file downloading respects the SSL exception settings of the host web view now. +* The UniWebView message system now support URLs without authority part. Now, a URL like `myscheme:this_is_the_message?foo=bar` can be parsed without problem. + +### 4.9.0 (26 Jul, 2021) + +#### Add + +* New feature Transparency Clicking Through. Now you can use `SetTransparencyClickingThroughEnabled` and set the `BackgroundColor` of web view to a clear color to allow the user tap through the web view background. Those taps will be delivered to Unity scene. + +#### Fix + +* A potential issue that in some cases the `Hide` or `Show` method freeze the game for a few seconds. + +### 4.8.0 (19 Jul, 2021) + +#### Add + +* A method to disable the keyboard avoidance behavior on Android. It can prevent the web view layout changing when showing keyboard on Android. + +#### Fix + +* A gradle parser issue that ignores tab in a gradle node. This may lead some gradle build failing if a tab is used in the gradle file. +* Now the SSL error exception works properly even for resource on the allowed page. +* An issue which causes the video auto play not working on Android. + +### 4.7.0 (14 Jun, 2021) + +#### Add + +* A new method `SetTextZoom` on Android. It delegates setting to Android `WebSettings.setTextZoom` for the web view . +* A new method `SetDownloadEventForContextMenuEnabled` on Android. This allows the image saving action in context menu also triggers the `OnFileDownloadStarted` and `OnFileDownloadFinished` events. + +#### Fix + +* A potential issue that on some old Android devices, the `Show` and `Hide` methods do not return correct value for the animation starting state. + +### 4.6.1 (8 May, 2021) + +#### Fix + +* An issue that extension format of `accept` field is not recognized when uploading through the input file form on Android. It causes the file picker not shown up in some cases. + +### 4.6.0 (4 Apr, 2021) + +#### Add + +* Methods to remove cookies under a URL and with a certain cookie name. It allows you to remove some of the stored cookies instead of clearing them all. Check the `RemoveCookies` and `RemoveCooke` methods respectively. + +### 4.5.1 (12 Mar, 2021) + +#### Fix + +* Now file uploading from Android contains a more readable file name in its `Content-Disposition` field. +* An issue which causes external keyboard input is not valid while opening the web view on iOS. + +### 4.5.0 (17 Feb, 2021) + +#### Add + +* A new method to capture the current web view content and store it on a temporary folder on disk. Check `CaptureSnapshot` method and `OnCaptureSnapshotFinished` event for more. + +#### Fix + +* A potential issue that Safe Browsing crashes on certain devices without Chrome and Google Mobile Services (GMS) properly installed. + +### 4.4.0 (26 Jan, 2021) + +#### Add + +* Support download files in the general web view. Now, a download task will be triggered if the loading of resource cannot be rendered in place. See the [Downloading Files Guide](https://docs.uniwebview.com/guide/download-files.html) for more information. + +#### Fix + +* Now Jetifier is not required when Android exporting. If you need Jetifier for other third-party libraries, set it in UniWebView's setting panel. +* The `Hide` method now correctly hides the web view even when a full-screen view is being used on Android. + +### 4.3.1 (18 Dec, 2020) + +#### Fix + +* An issue that causes the app to crash on iOS 9 and 10 when closing the web view. + +### 4.3.0 (14 Dec, 2020) + +#### Add + +* A property to check whether the Safe Browsing Mode is supported on the device. Some Android devices may not have Chrome installed, so Safe Browsing Mode is not supported. Instead of ignoring the requests of opening the web page in Safe Browsing Mode, now UniWebView will open it in the installed system browser. Use `UniWebViewSafeBrowsing.IsSafeBrowsingSupported` to get availability. +* Add Assembly Definitions (`asmdef`) support. Now UniWebView will organize itself as an assembly. This should boost your compile-time in Unity Editor. + +#### Fix + +* Better support for multiple windows. The pop-up new web page window can pop another new window now. +* Now the script should compile when using Unity Editor even on Linux. +* A Gradle issue which causes error while exporting project when some certain customized Gradle templates are used. +* A potential problem that accessing to web page cookie might freeze the app in older iOS devices or systems. + +### 4.2.0 (17 Oct, 2020) + +#### Add + +* A method `SetAllowUniversalAccessFromFileURLs` to allow local file loading by AJAX from a local HTML page. This ignores CORS checking on iOS `WKWebView` when loading both the page and resource locally. +* Now you can enable the screen swipe gesture on iOS or macOS to perform back or forward navigation. Call `SetAllowBackForwardNavigationGestures` to allow it. +* Add opening and closing events for multiple window. Now you can receive `OnMultipleWindowOpened` and `OnMultipleWindowClosed` event when a new window is opened or closed respectively. + +#### Fix + +* Now the Safe Browsing on Android only requires "androidx.browser:browser" version 1.0.0. This solves some conflicting with other "old and bad-behaved" packages which not upgrading to the latest dependency. +* The JavaScript pop-up alert now has a better cancel button style. Also fixed the duplicated prompt text for input alert. +* An issue which causes wrong parsing for Gradle build file when a string is defined inside a non-node block. + +### 4.1.0 (26 Sep, 2020) + +#### Add + +* Add a method to dismiss the `UniWebViewSafeBrowsing` on iOS. To close an opened safe browsing component, call the `Dismiss` method. +* Build support for iOS 14 and Xcode 12. + +#### Fix + +* An issue that `BackgroundColor` property not working on Android. + +### 4.0.4 (1 Sep, 2020) + +#### Fix + +* An issue causes the web view does not show up until switching the app from background on some customized Android distribution. +* Now setting for "Accept" header will give a warning since iOS does not support customize this header field. + +### 4.0.3 (18 Aug, 2020) + +#### Fix + +* Fix an issue that causes keyboard overlaying not handled correctly on Android. + +### 4.0.2 (14 Aug, 2020) + +#### Fix + +* Android X browser dependency ('androidx.browser:browser') for UniWebView Android can now be disabled in the preference panel. Turn it off if other plugins are already adding it for you. + +### 4.0.1 (12 Aug, 2020) + +#### Fix + +* A issue that prevents the Gradle file being processed correctly if inline comments are used. +* Kotlin dependency for UniWebView Android can now be disabled in the preference panel. Turn it off if other plugins are already adding it for you. + +### 4.0.0 (1 Aug, 2020) + +::: danger +4.0.0 is a major update of UniWebView. Although most of APIs in UniWebView 4 are compatible with UniWebView 3 and it should be easy enough to migrate, there are still a few breaking changes. For migration from UniWebView 3 in detail, visit the [Migration Guide](https://docs-v4.uniwebview.com/guide/migration-guide.html). To know the highlight of the version, check [this page](https://docs-v4.uniwebview.com/guide/version-highlight.html) for more. +::: + +#### Add + +* **Safe Browsing Mode support.** Now UniWebView also contains a wrapper for [SFSafariViewController](https://developer.apple.com/documentation/safariservices/sfsafariviewcontroller) on iOS and [Custom Tabs](https://developers.google.com/web/android/custom-tabs) on Android. It allows you to browse web pages in a more browser-like level, including sharing cookies or credentials with the system browser. Check the [Safe Browsing guide](https://docs.uniwebview.com/guide/safe-browsing.html) for more information. +* **Customizable built-in toolbar on iOS.** Now you can set the bar background color, button text, and the text color in the toolbar. Check the [Built-in Toolbar](https://docs.uniwebview.com/guide/built-in-toolbar.html) guide for more information. +* **Add support for Unity 2020.1.** In the latest Unity version, it changes its way of handling native touch events. It may cause some functionality broken in UniWebView 3. UniWebView 4 uses itself in a new way to handle events to ensure working fine on all supported Unity versions. UniWebView 4 itself **supports from Unity 2018.3**. +* **New Post Build Processing system and Preference Panel.** They provide a better way to define the dependencies and help you modify the "AndroidManifest.xml" file without touch it directly. +* **Better permission model.** UniWebView now does not require the write external permission for uploading a photo from the gallery or camera to the web. It also adopts to permission models for Android 10. Other possible needed permission can be setup in the UniWebView Preference Panel. +* An `OnPageProgressChanged` event which will be raised every time the loading progress changes in the web view. + +#### Fix + +* Now the web view resizes itself when a soft keyboard is showing up and about to cover the text input fields on the page. It keeps the text input visible. +* UniWebView now always respects Unity setting of immersive mode on Android. The navigation bar will not jump out or flicker when showing the web view in an immersive mode. +* Audio resource can stop correctly when the web view is closed now. +* The alert pop-up and its text edit views for JavaScript confirm window or HTTP auth now have a better style and alignment. + +#### Deprecate + +* `SetImmersiveMode` is not needed anymore since the Unity setting is now always respected. This method will not do anything but give an error log instead. +* `OnKeyCodeReceived` will not be called anymore since UniWebView is not intercepting any native events. Try Unity's `Input.GetKeyUp` in `Update` to get key code events instead. + +### 3.18.1 (4 Jul, 2020) + +#### Fix + +* A potential issue that the Android keyboard is not shown when switching back from a full screen view on Android 9 or above. +* Now the Unity Editor support also allows displaying a web view when using the Device Simulator package. +* An issue on iOS which causes the toolbar buttons placed in wrong position when switching back to the game while the web view is showing. + +### 3.18.0 (14 May, 2020) + +#### Add + +* An API to disable the whole user interaction of the web view. Call `SetUserInteractionEnabled(false)` on the web view if you do not want users to tap or scroll the web view. +* Allow "Go Back" feature on a pop-up window if `SetSupportMultipleWindows` is set to `true`. + +#### Fix + +* An issue that the `OnPageFinished` event is not called on Android when loading an HTML string. + +### 3.17.0 (14 Apr, 2020) + +#### Add + +* A method to set the default font size on Android. This can help to reduce the influence of the user's display setting of the font scale. Use `SetDefaultFontSize` to give a more reasonable default font size based on the scale setting. +* Now the visibility of navigation buttons (Go Back and Go Forward) on iOS toolbar can be set from Unity by `SetShowToolbarNavigationButtons`. + +#### Fix + +* A layout issue that breaks web view size when switching back to foreground when an action sheet shown in some cases. +* The customized user agent string now also applies to the pop-up window when `SetSupportMultipleWindows` was called with `true`. +* An issue that causes the new window cannot be dismissed correctly by `window.close()` on Android. +* Now the text input views should be automatically scrolled up on Android when the soft keyboard displays and overlaps them. +* The `ReferenceRectTransform` can calculate the correct final size and position for a non-default resolution setting for screen now. + +### 3.16.0 (5 Feb, 2020) + +#### Add + +* Support for `target="_blank"` links. By default, UniWebView will open the destination URL in the same page even it is attributed by a `_blank` target. If you want to open it in a new page, call `SetSupportMultipleWindows` with `true` and it will create a new web view above the current one to load the new request. + +#### Fix + +* Improve SSL error handling logic for Android to prevent warnings from Google. + +### 3.15.2 (8 Jan, 2020) + +#### Fix + +* URLs with "#" should be loaded correctly in iOS even the `skipEncoding` is not set to `true` in `Load` method. +* The keyboard will be dismissed automatically when hiding the web view by calling `Hide` now. + +### 3.15.1 (18 Dec, 2019) + +#### Fix + +* Unity 2019.3 support. Fixed a problem which causes your app hanging and being killed at starting on iOS devices when building against Unity 2019.03 and without Xcode connected. +* A possible crash when uploading and converting data between file choosers. +* A potential issue that the Camera can be ignored when choosing images for uploading. + +### 3.15.0 (11 Dec, 2019) + +#### Add + +* A new method `ScrollTo` to scroll web view to a certain point. +* Basic localization support for Android. Now the UniWebView related UI elements are localized for the following locals: `en`, `zh`, `zh-rTW`, `ja`, `ko`, `fr`, `de`, `es`, `ru`, `vi`, `pt`, `da`, `it`, `nl`, `sv`. + +#### Fix + +* Solve an issue that HTTP status cannot be retrieved correctly due to page loading events order are not correct on some Android systems. +* Now the web view window will be pinned to top on macOS editor. This aligns the fact that the web view will always be the top-layer view on iOS and Android. + +### 3.14.0 (20 Nov, 2019) + +#### Add + +* New API `SetContentInsetAdjustmentBehavior` for setting the adjustment behavior which indicates how safe area insets. It is a wrapper for `contentInsetAdjustmentBehavior` on iOS. + +#### Fix + +* A crash when uploading a single picture taken by camera on some Android devices. +* Unescape JavaScript result from Android. This helps get correct value when using `GetHTMLContent`. + +### 3.13.2 (29 Aug, 2019) + +#### Fix + +* An unintended symbol link to deprecated UIWebView on iOS. This prevents the deprecation warning when submitting an app to the App Store. + +### 3.13.1 (17 Aug, 2019) + +#### Fix + +* Enabled app cache support for web view on Android. + +### 3.13.0 (11 Jun, 2019) + +::: warning +From 3.13.0, UniWebView supports from Unity 2017.3. This would help to reduce some legacy code and achieve more stable behaviors. +If you need to continue use UniWebView 3 on Unity 5.6, Unity 2017.1 or Unity 2017.2, please keep to use UniWebView 3.12.1 or earlier. +::: + +#### Add + +* Support on Android for uploading multiple files at the same time. + +#### Fix + +* The `RawMessage` of `UniWebViewMessage` is now not escaped any more. It will return exactly the original URL it receives. Meanwhile, the values in `Args` keep escaped as is. +* An issue on iPad that the customized "Done" button text might be truncated in some cases. + +### 3.12.1 (26 Apr, 2019) + +#### Fix + +* An issue that web content does not go back to its original position after keyboard dismissed on iOS. +* A crash when dismissing the web view on iOS 9 devices. +* An issue causes some files cannot be selected when uploading with a form and the file chooser. + +### 3.12.0 (10 Apr, 2019) + +#### Add + +* Now you can use `SetDragInteractionEnabled` to disable the drag interaction on iPad running iOS 11 or above. + +#### Fix + +* A issue which causes the non full screen web view position moving in some cases when soft keyboard is showing up. + +### 3.11.0 (27 Mar, 2019) + +#### Add + +* Add `SetCalloutEnabled` method to control the behavior when the user taps an image or link with long press or force touch gesture. Now you can choose not to display the context callout menu. + +#### Fix + +* Now the navigation bar on Android will automatically hide immediately when the soft keyboard dismissed. +* A workaround for regression on iOS 12 which causes selecting input fields in forms does not trigger the web view auto scrolling. + +### 3.10.2 (28 Feb, 2019) + +#### Fix + +* Fix a crash when selecting files from Downloads folder on certain Android devices when uploading. +* Support showing web view in cutout mode in Android. Now UniWebView will follow the cutout render setting. + +### 3.10.1 (15 Feb, 2019) + +#### Fix + +* An issue that lack of a placeholder method which causes code not compile on Windows. + +### 3.10.0 (9 Feb, 2019) + +#### Add + +* A method to turn off automatically prompt alert showing when received an HTTP auth challenge from server. Use `SetAllowHTTPAuthPopUpWindow` to control the behavior. +* Support open third party app with links of corresponding URL schemes. Now you can use a link to open other apps as long as it was registered. + +#### Fix + +* Performance issues when using some sync getter APIs on Android native side. + +### 3.9.2 (10 Jan, 2019) + +#### Fix + +* A problem that `SetZoomEnabled` not works correctly on iOS 12. +* Now `CanGoBack`, `CanGoForward` and `Url` getters also work for single page app page. + +### 3.9.1 (18 Dec, 2018) + +#### Fix + +* A potential issue causes file chooser not response correctly when user dismiss the permission prompt without choosing. +* Remove JavaScript Interface support for suppressing security warning from Google. + +### 3.9.0 (30 Nov, 2018) + +#### Add + +* A parameter for `Load` method to customize the read access URL for local file loading. It helps to load local resources under a different URL other than the current load page. + +#### Fix + +* A problem that read access URL not encoded correctly when special characters contained. +* An issue causes crash when changing screen orientation by code when closing the web view with a toolbar displayed. + +### 3.8.1 (24 Oct, 2018) + +#### Fix + +* Fix a potential crash when reference `RectTransform` is used to determine web view frame, while there is no cavans for some reason on the transform. + +### 3.8.0 (5 Sep, 2018) + +#### Add + +* A method to allow file access from file URLs. This could solve some problem when request in a cross origin way from local pages. However, by setting it to true may cause some potential security issue, so make your choice at the risk. + +#### Fix + +* A problem causing immersive mode flickering in Android 8.x. +* Post request on back button would work correctly. + +### 3.7.1 (26 Jun, 2018) + +#### Fix + +* A typo on "OnOrientationChanged" event. It was "OnOreintationChanged" and now we correct this issue in both code and documentation. + +### 3.7.0 (14 Jun, 2018) + +::: danger +From Unity 2018, Gradle is used as the default build system, so we updated the integration method to make UniWebView works better in the new build system. If you are upgrading UniWebView from an earlier version, please refer to the [Adapting to AAR File](https://docs.uniwebview.com/guide/adapting-to-aar-file.html) documentation. +::: + +#### Add + +* Compatible with Unity 2018.1 and gradle build system. Please note that you need some migration if you want to upgrade from an earlier version. + +### 3.6.1 (26 Apr, 2018) + +#### Fix + +* Compatible with Unity 2017.4. Now videos could be played correctly on Android devices in project exported by Unity 2017.4. + +--- + +### 3.6.0 (3 Apr, 2018) + +#### Add + +* OnWebContentProcessTerminatedDelegate event is added. This event will be raised when iOS system stops loading the web content for some reason (Usually due to memory pressure). Prior UniWebView versions will try to refresh the page for you automatically. However, that would not fix the problem in most cases. This event provides a chance for you to free as much as resources and do reloading/handling as you need. + +#### Fix + +* As issue which causes web view frame not set properly when using prefab with a pre-set reference rectangle transform. +* Some other minor internal fix. + +--- + +### 3.5.2 (28 Feb, 2018) + +#### Fix + +* An issue which causes POST data from HTML form is missing when a customized header field is set on iOS. +* Now customized header fields will be also added to image download requests on Android. + +--- + +### 3.5.1 (31 Jan, 2018) + +#### Fix + +* A potential issue which may cause game unresponsive when switching back to foreground when a web view's game object or its parent objects are inactive. +* An issue causes web view cannot be added to correct view in the first game run loop while splash screen is disabled. + +--- + +### 3.5.0 (22 Jan, 2018) + +#### Add + +* Images from Internet now could be downloaded to Download folder in Android. +* More consistent toolbar layout mechanism for toolbar on iOS. Now Auto Layout is used for layout toolbar, and `adjustInset` option works better. + +#### Fix + +* An issue which crashes macOS Editor when loading a local file in macOS 10.11 or earlier. +* iOS context menu (action bar) now could cover toolbar properly. +* A regression that makes setting text for toolbar done button title not working. +* No need to add support-v4 package anymore to support uploading files to server. +* Toolbar intersection with web view will now respect your frame setting first, then adjust its own height or top anchor. + +--- + +### 3.4.2 (28 Dec, 2017) + +#### Fix + +* Use new message sending method between native and C# script. Now all messages could be received even when the web view game object is inactive. +* Prevent `Init` with an empty name for security. + +--- + +### 3.4.1 (12 Dec, 2017) + +#### Fix + +* Fix an issue on Android that getting some web view properties inside web view events may freeze the loading process. +* The toolbar will not show automatically when you are using a prefab with "Use Toolbar" on. Now the toolbar will follow the show and hide state of the web view itself. +* Fix an error in Editor when stop playing mode while a web view is being shown. +* Upgrade to Android build SDK to API Level 26. (But still support from Android 5.0.) + +--- + +### 3.4.0 (7 Nov, 2017) + +UniWebView now requires Xcode 9 with iOS SDK 11 to build. If you are still using Xcode 8 and iOS 10 SDK, please use UniWebView 3.3.x instead. + +#### Add + +* Better cookie management for iOS 11. On iOS 11, now UniWebView uses the newly added HTTP cookie store to get more stable cookie states. +* Update web view layout and toolbar for iPhone X screen. +* A method for printing current web view to a printer or air printer. + +#### Fix + +* A potential issue which may cause cookie lost during 301 or 302 redirecting. + +--- + +### 3.3.2 (6 Oct, 2017) + +#### Fix + +* A crash when games are built against iOS 11 SDK and setting screen orientation with Unity API while the web view showing. + +--- + +### 3.3.1 (6 Oct, 2017) + +#### Fix + +* A crash when setting screen orientation with Unity API while the web view showing. +* Compatible with iOS 11 SDK and Xcode 9. +* Due to a Unity High Sierra support issue, UniWebView 3 now require Unity 5.6.3 at least. + +--- + +### 3.3.0 (9 Sep, 2017) + +#### Add + +* Support HTTP Basic and HTTP Digest authentication. A native pop-up will be displayed when there is a challenge to ask users to provide user name and password. +* Support "intent" and "market" URL scheme. Now any valid "intent://" URL will be handled on Android. If target intent is not found, and there is no fallback URL provided, UniWebView will try to open the application page on Play Market. The "market://" URL will be navigated to Play Market. + +#### Fix + +* A problem which caused hidden web view also receiving touch event on Android when multiple web views are used. +* Fix an issue which causes crash when uploading a camera captured photo on some Android devices. +* Improvement on loading performance a bit on Android. + +--- + +### 3.2.0 (3 Sep, 2017) + +#### Add + +* An option to URL or HTML string loading APIs to skip encoding, it is useful when you need to encode the url yourself instead of using the default encoding rule in UniWebView. +* An option to cookie setting and getting APIs to skip encoding. + +#### Fix + +* An issue on Android which cause the default user agent cannot be retrieved correctly before the first request. +* A memory leak on macOS Editor that web view window not get closed correctly. +* More accurate scaling calculating in Zoomed Display mode on iOS. +* Event touch could be handled correctly in Android when multiple web views shown now. +* The URL encoding will not be applied to an already encoded input URL now. +* The local file loading will not be loaded again in current view. This prevents a wrong behavior when loading a local page containing iframe tag. +* The UniWebViewMessage will now respect the encoded url parameters and queries. + +--- + +### 3.1.4 (17 Aug, 2017) + +#### Fix + +* Fix a problem which could cause web view alpha value not correct in some situation after transition with fade animation. +* Fix position issue when showing web view with both edge and fade animation. + +--- + +### 3.1.3 (9 Aug, 2017) + +#### Fix + +* A linking issue when running with a lower iOS SDK in Xcode. + +--- + +### 3.1.2 (6 Aug, 2017) + +#### Fix + +* Now `SetWebContentsDebuggingEnabled` also works for macOS Editor build. Set it to true and you could right click in the web view in editor to show an inspector for debugging purpose. +* Fix a flickering of navigation bar when loading a new page with immersive mode on Android. +* A compiling error when build iOS target on a Windows editor. +* A problem that built-in some schemes (`mailto`, `sms` and `tel`) not handled correctly on iOS and macOS. +* Fix some deprecated methods of build system for Android. + +--- + +### 3.1.1 (21 Jul, 2017) + +#### Fix + +* Local file URL with special characters now can be corrected encoded when loading. +* Avoid unhandled Show method calling right after a Hide method. Now, the Hide method will be ignore if the web view is already hidden. +* All three kinds of render mode for canvas (`ScreenSpaceOverlay`, `ScreenSpaceCamera` and `WorldSpace`) are now supported when using `RectTransform` to determine web view position and size. +* Array query in `UniWebViewMessage` should work as expected. Now a query like "?a[]=1&a[]=2" will be parsed to "1,2" in the result message. + +--- + +### 3.1.0 (14 Jul, 2017) + +#### Add + +* A helper method `UniWebViewHelper.PersistentDataURLForPath` to return a url string for files under `persistentDataPath`. +* A method to enable user resizing for web view windows on macOS Editor. +* Upgrade to new build tool chain to get better optimized binary for both iOS and Android targets. + +#### Fix + +* An issue which might cause url encoding returns wrong result when the original url contains space for other special characters. + +--- + +### 3.0.1 (3 Jul, 2017) + +#### Fix + +* Setting cookies from JavaScript now could work correctly. +* Allowing back compatibility for mixed content loading in Android. +* Stopping loading now could trigger page loading error event. +* Fully bit code support is now enabled for iOS build. + +--- + +### 3.0.0 (27 Jun, 2017) + +::: danger +3.0.0 is a major update of UniWebView. We rewrote the whole software from scratch to bring your experience of using a web view in Unity to a next level. Be caution it is not compatible with UniWebView 2, there are quite a few breaking changes in this version. For migration from UniWebView 2 in detail, visit our [Migration Guide](https://docs-v3.uniwebview.com/guide/migration-guide.html) in documentation. To know the highlight of the version, check [this page](https://docs-v3.uniwebview.com/guide/version-highlight.html) for more. +::: + +#### Add + +* Better way to set frame of the web view. You can also use a reference `RectTransform` to set position and size. +* Use `WKWebView` instead of `UIWebView` on iOS. +* New pop-up style Unity Editor support on macOS. It is a fully functional tool for debugging purpose. +* A new way to setup bridging between Unity and Cocoa native. Now there is no message sending delay. +* You can now set the position of toolbar to top or bottom on iOS. +* A leveled logger to log all UniWebView related information. See [UniWebViewLogger](https://docs.uniwebview.com/api/uniwebviewlogger.html) documentation for more. +* Use a payload based callback API like transition and JavaScript related methods. It takes more data. See [UniWebViewNativeResultPayload]https://docs.uniwebview.com(/api/uniwebviewnativeresultpayload.html). +* SSL exception for white listed domain. It is useful for a un-trusted certification but you still want to access. +* A method to get current web view HTML content as a string. + +#### Fix + +* UniWebView Android no longer requires to be the main activity. +* Uploading now could work properly on all supported devices. +* The url scheme based message now supports UTF-8 and url encoding directly. +* Now the customized header field will be existing for all requests until removed, not only the first one. + +--- + +### 2.11.1 (22 May, 2017) + +#### Fix + +* A performance issue which causes the camera scene gets slow and laggy when used with Vuforia. + +--- + +### 2.11.0 (28 Apr, 2017) + +#### Add + +* An API to ignore SSL cert error for a certain host in Android. +* Add Firebase as built-in third party jar supporting. + +#### Fix + +* An icon used to demonstrate UniWebView. + +--- + +### 2.10.0 (28 Feb, 2017) + +#### Add + +* An API to set allowing video auto-play. Call `SetAllowAutoPlay` on the web view for it. Please note that you also need to add an "auto-play" property in the video tag to enable auto-play in the web page. +* An API to enable inline video play for iOS. By default, iOS video play will be pop to full-screen. By calling SetAllowInlinePlay with a true flag, you can play video inline on iOS. Please note you also need to add an "inline" property in your video tag to support it. +* SetCookie and GetCookie methods to set and get a cookie for a specified URL and key. + +#### Fix + +* A potential issue that cleaning cookie did not work in some case. Since the logic of current cleaning cookie is not correct, we also marked the original CleanCookie method to be obsoleted. You should now use SetCookie method to set a cookie value to empty for cleaning purpose. + +--- + +### 2.9.2 (31 Jan, 2017) + +#### Fix + +* Pre-compiled jar file for Google VR activity. +* An issue which causes web view cannot be opened correctly in some old Android devices. + +--- + +### 2.9.1 (7 Nov, 2016) + +#### Fix + +* Add a null check for Android web view. This prevented a potential crash when navigating from javascript + +--- + +### 2.9.0 (29 Sep, 2016) + +#### Add + +* Android remote web content debugging support. Now you can debug your web page in Android with Chrome. Just set `SetWebContentsDebuggingEnabled` with true to enable it. +* Ability for loading page with overview mode for Android. + +#### Fix + +* Now all key down events are forwarded to Unity side by OnKeyDown callback in Android, even for TV remote controller and real device keys. + +--- + +### 2.8.0 (29 Jul, 2016) + +#### Add + +* An option (`openLinksInExternalBrowser`) that all the links should be opened in an external browser or not. It will be useful if you want to open a URL in Mobile Safari or Chrome, instead of opening it in UniWebView. Default is false. +* Now you can set the Done button title from Unity. Use `SetDoneButtonText` to customize the button title. +* LGC SDK built-in support. + +--- + +### 2.7.1 (3 Apr, 2016) + +#### Fix + +* Now the keyboard will also be dismissed in iOS when you call Hide on the web view with the keyboard showing. +* A potential problem of activity type casting which might cause the app crashes when you use UniWebView with some other third party activity. +* Add "singleTop" to launch mode in Android to prevent crash from multiple activity racing. + +--- + +### 2.7.0 (11 Mar, 2016) + +#### Add + +* Add APIs for setting visibility of vertical and horizontal scroll bar of the web view. + +#### Fix + +* Update pre-compiled jar file for Prime31 and CardBoard, to be compatible to their latest SDKs. +* Return an empty string for asset path string API, so it will compile even you switch to a platform that UniWebView does not support. +* Fix a problem which prevents you from settings background color to transparent ones. It is a regression in version 2.6.0 and now it will not show a black background color anymore. +* Not disabling hardware accelerating when Settings background color with alpha for Android. This would improve the performance of using a transparent background a lot. +* A potential issue that will fail in type casting in Android activity type. This would improve the compatibility when working with other third party packages which tries to convert the Unity activity. + +--- + +### 2.6.0 (15 Feb, 2016) + +#### Add + +* Rewrite video playing for Android web view. Now you could expect a better and stable video playback for both Youtube and Vimeo. + +#### Fix + +* Remove custom activity which is not needed anymore. + +--- + +### 2.5.2 (27 Jan, 2016) + +#### Fix + +* Use UnityPlayerActivity instead. This makes UniWebView compatible with Unity 5.4 (current in beta). + +--- + +### 2.5.1 (22 Jan, 2016) + +#### Fix + +* A crash issue in editor when you use AddUrlScheme. + +--- + +### 2.5.0 (31 Dec, 2015) + +#### Add + +* Support for adding customize header field. +* A helper method to generate streaming asset url for local pages. +* Add AppFlyer jar file. + +#### Fix + +* Hide transition callback will work correctly for Android now. +* Improve performance for OS X Editor version. + +--- + +### 2.4.1 (11 Dec, 2015) + +#### Fix + +* Compatible with Unity 5.3.0's new OS X OpenGL Core for editor. +* Clean some debug log. +* Update default page in demo scene. +* Set default user agent of Editor version to an iPhone agent. So you could get the mobile version of web view in Editor. + +::: danger +Notice: Unity dropped Windows Phone 8 support officially from 5.3. UniWebView will not continue development for Windows Phone as well from this version. +::: + +Reason: The amount of our Windows Phone users is very few compared to iOS and Android (it is less than 1% share according to report). +However, if we want to to support Windows Store App with Windows Phone 8.1 SDK or Windows 10 SDK, it would take even more time than either of other platforms. + +Since the Windows Phone SDK itself is not backwards compatible, the transition from Windows 8 to 8.1 or 10 means we need to rewrite all code in the package. + +We have only limited resource. To make sure we could build the asset in a better quality for most of our users, we decided to focus back to iOS and Android only from next version. + +The current Windows Phone 8 support (UniWebViewWP.dll) will be kept in later versions of UniWebView for a while, but we are sorry that it doesn't seem to get updated anymore. + +--- + +### 2.4.0 (4 Dec, 2015) + +#### Add + +* Built-in show/hide transition. You could now show or hide the web view with a transition effect. Currently we support fade in/out and transition from/to one of screen edges. Please take a look at reference of Show() and Hide() for more. + Add: Recreate new demo scenes as a better tutorial. Now most of the useful features are contained in the demo scenes, with fully commented script to show how to use them. +* Now you can use "ESC" in OS X Editor to go back or close the web view. + +#### Fix + +* The color of iOS toolbar is now white. This fixed an issue that the clear background tool bar will become black if no web page is underneath. +* Fix an issue which cause show tool bar property not working in some situation. +* Adjust the behavior of insets, it is a preparation for later feature like insets translating. The API of insets remains the same now. + +#### Remove + +::: danger +Notice: Unity will drop Windows Phone 8 support officially from 5.3. UniWebView will not continue development for Windows Phone as well from the next version. +::: + +Reason: The amount of our Windows Phone users is very few compared to iOS and Android (it is less than 1% share according to report). +However, if we want to to support Windows Store App with Windows Phone 8.1 SDK or Windows 10 SDK, it would take even more time than either of other platforms. + +Since the Windows Phone SDK itself is not backwards compatible, the transition from Windows 8 to 8.1 or 10 means we need to rewrite all code in the package. + +We have only limited resource. To make sure we could build the asset in a better quality for most of our users, we decided to focus back to iOS and Android only from next version. + +The current Windows Phone 8 support (UniWebViewWP.dll) will be kept in later versions of UniWebView for a while, but we are sorry that it doesn't seem to get updated anymore. + +--- + +### 2.3.1 (5 Nov, 2015) + +#### Fix + +* An issue that script is missing in the prefab if you import the asset in a new version of Unity. +* Compatibility for Unity 5.2.2 editor. + +--- + +### 2.3.0 (13 Oct, 2015) + +#### Add + +* SMS link support for Android devices. + +--- + +### 2.2.5 (8 Oct, 2015) + +#### Fix + +* Compatibility for Unity 5.2.1 editor. + +--- + +### 2.2.4 (4 Oct, 2015) + +#### Fix + +* A status bar truncating issue which may appears on some Android devices when user show/hide the soft keyboard for several times. + +--- + +### 2.2.3 (27 Sep, 2015) + +* An API to add trusted sites for Android. You will need to add your site url to the white list if you need to request the protected resources of Android devices (such as microphone or camera). +* Add third party support for Craftar. + +#### Fix + +* A vulnerability that accept SSL connect even an error happens. Now UniWebView will use the default behavior of system and reject all un-trusted SSL connection. It will protect your web content from possible man-in-the-middle attacks. At the same time, it means you have to use a valid certification even if you are in test environment. + +* A null exception when input in a text field in Editor. + +--- + +### 2.2.2 (12 Sep, 2015) + +#### Fix + +* An issue in Unity 5.2 which causes the web view not showing correctly in Unity Editor. +* Alert compatibility for iOS in Unity 5.2. + +--- + +### 2.2.1 (4 Sep, 2015) + +#### Add + +* PIs to get navigation state of current web view. Now you can use CanGoBack() and CanGoForward() to determine whether there is a page to go back or forward. + +#### Fix + +* Optimize performance for 64-bit OSX Editor version. + +--- + +### 2.2.0 (22 Aug, 2015) + +#### Add + +* An API to set background color for the web view. + +#### Fix + +* The video will paused when app switched to background in Android devices now. +* An issue causes the loading indicator not removed in Windows Phone 8 devices when dismiss the web view. +* Add Neatplugin and RigidFace to default supported third party jar. + +--- + +### 2.1.3 (30 Apr, 2015) + +#### Fix + +* File chooser for Android 5.0+. + +--- + +### 2.1.2 (28 Apr, 2015) + +#### Fix + +* An issue on Android which causes screen remaining black when user taps back button when playing a full-screen Youtube video. + +--- + +### 2.1.1 (26 Apr, 2015) + +#### Add + +* API for setting and getting alpha value. + +#### Fix + +* An issue which causes spinner not hidden when web view dismissed by done button. + +--- + +### 2.1.0 (22 Apr, 2015) + +#### Add + +* Add immersive mode support for Android for API Level 19 and above. +* Google CardBoard support. You can now find a pre-built CardBoard jar library. + +#### Fix + +* A crash which causes crash when setting insets to an invalid value. + +--- + +### 2.0.0 (15 Mar, 2015) + +#### Add + +* Support for Windows Phone 8 and 8.1. Please note you need to add WebBrowser capability in Windows Phone's manifest file after exported from Unity. +* Support for Unity 5. If you upgrade from UniWebView 1.x, you need to remove the old package and reimport again. +* 64 bit support for Mac Editor version, since Unity 5 is now a 64-bit app. + +#### Fix + +* An issue which may cause the cache can not be cleaned completed in iOS. +* A serialize issue which may cause editor crash in some occasion. + +::: danger +Notice: This version is not compatible with Unity 4.x. If you need to use UniWebView on Unity 4.x, please use UniWebView 1.x instead. You can find more information about the earlier version on Asset Store: https://assetstore.unity3d.com/jp/#!/content/12476 +::: + +--- + +### 1.9.0 (10 Feb, 2015) + +::: warning +1.9.x will be the last version support Unity 4.x. Due to the huge difference between Unity 4 and 5, we decide to make a major update as well. Please keep an eye on our website (https://uniwebview.onevcat.com) to know more information about Unity 5 compatible version. +::: + +#### Fix + +* An issue cause Mac Editor can not be used in 64bit environment. +* Removed source folder and zipped them to avoid a potential compile error. +* Removed a deprecated method to avoid compile error. + +--- + +### 1.8.1 (9 Nov, 2014) + +#### Fix + +* Improve performance for Mac Editor a lot. +* An issue which cause spinner can not hide when the web view removed by code from Unity. +* Update Android build target. + +--- + +### 1.8.0 (5 Nov, 2014) + +#### Fix + +Add: A method to change the user agent of the web view. Now you can use the SetUserAgent method to set customized user agent for the web view. +Add: The Mac Editor now supports for Xcode 6.1 and OSX Yosemite. + +* An orientation issue when you set game as a landscape one, while the web view can not rotated to portrait on iOS 8. + +--- + +### 1.7.3 (8 Oct, 2014) + +#### Fix + +* A issue which may cause crash on some Android device with system version 2.x. + +--- + +### 1.7.2 + +#### Add + +* An API for Android to enable wide view port support. Call SetUseWideViewPort(true) before loading any webpage containing viewport meta tag on Android to make it available. + +#### Fix + +* Now the viewport support for Android is disabled by default. Use the new added SetUseWideViewPort API to enable it if you are using viewport tag in your webpage. + +--- + +### 1.7.1 (7 Oct, 2014) + +#### Add + +* Insets setting for portrait and landscape mode. You can now set different portrait and landscape inset easier. If you need show your web content in both orientation, you might want to use the new `InsetsForScreenOreitation` event. See demo code for the usage. + +#### Fix + +* Viewport issue for some Android devices. Now the html's viewport should work correctly. +* Web view truncated issue when the y position set to 0 on some Android devices. This is an advanced fix continuing for version 1.6.1. +* An issue which causes auto-rotation not work on iOS 8 in some situation. +* The position of toolbar and spinner should be correct on iOS 8 now. +* A build error which stops the exported project compiling in Xcode 5 and iOS 7 SDK. + +--- + +### 1.7.0 (26 Sep, 2014) + +::: danger +This version dropped support for Unity 4.1.4. Now UniWebView starts from 4.2.2. And 1.7.x will be the last versions support Unity version below 4.4 (because there is a compile error in iOS 8 SDK in the exported Xcode project, which is annoying and makes the development harder). +::: + +#### Add + +* UniWebViewHelper.screenHeight and UniWebViewHelper.screenWidth. You can retrieve the screen size in "point" instead of "pixel" with these two new API. See the migrate guide below for more. +* "tel:" link support for Android. Now you can open the telephone UI by a link like "tel:12345678". +* An example in demo file, to show how to handle the auto-orientation, with keeping the layout unchanged in both portrait and landscape. +* iOS 8 and 5.5 inch iPhone 6 Plus support. + +#### Fix + +* The auto-orientation issue on iOS 8. Now it works for both iOS 8 SDK. +* Trim the url leading and trailing spaces, making the behaviors the same for iOS and Android. + +#### Deprecate + +* UniWebViewHelper.RunningOnRetinaIOS() is now deprecated due to 3x size of iPhone 6 Plus. Use UniWebViewHelper.screenHeight and UniWebViewHelper.screenWidth to decide the insets for retina display now. See the migrate guide below for more. + +#### Remove + +* Remove the long-ago deprecated Dismiss(). +* Remove support for Unity 4.1.4. + +> Migrate Guide - If you are using UniWebViewHelper.RunningOnRetinaIOS() to decide the insets of UniWebView, you may want to look at the Migrate Guide: + +Due to introduction of iPhone 6 Plus and its new @3x scale, the old way would not work well. Now, all screen-size-based insets calculation for UniWebView should NOT use RunningOnRetinaIOS anymore. Instead you should use UniWebViewHelper.screenHeight and UniWebViewHelper.screenWidth and specify the point directly. For example, if you want to show a web view taking the upper half of screen, before version 1.7.0, it might be something like this: + + int uiFactor = UniWebViewHelper.RunningOnRetinaIOS() ? 2 : 1; + int bottomInset = Screen.height / ( 2 * uiFactor ); + _webView.insets = new UniWebViewEdgeInsets(0,0,bottomInset,0); + +Right now, with the new API, you can just use this: + + int bottomInset = (int)(UniWebViewHelper.screenHeight * 0.5f); + _webView.insets = new UniWebViewEdgeInsets(0,0,bottomInset,0); + +No more ui factor and device-specified calculation needed. + +To make sure you can notice this change, the usage of RunningOnRetinaIOS() will cause an error now. You can just delete all usage of that, and change your insets code like the example above. + +--- + +### 1.6.1 (2 Sep, 2014) + +#### Add + +* A pre-compiled package for Vuforia's QCARPlayerNativeActivity activity. You can find it under the /UniWebView/Source/ThirdPartyJar folder. + +#### Fix + +* An issue which cause the web view can not be full screen on some Android tablet. +* Drop support for Android 2.2.x and earlier. So you can recompile the jar file with Android SDK 18 and later. + +--- + +### 1.5.4 (29 Jul, 2014) + +#### Fix + +* Keyboard overlap issue when text filed on Android. Now the web content will be scrolled up automatically. + +--- + +### 1.5.3 (16 Jul, 2014) + +#### Fix + +* A bug which causes an unexpected behavior when starting from other activities in Android. + +--- + +### 1.5.2 (19 Jun, 2014) + +#### Fix + +* A bug when setting insets in some Android devices. + +--- + +### 1.5.0 (1 Jun, 2014) + +#### Add + +* New download feature support for Android. +* Compatibility with Unity 4.5. + +#### Fix + +* Update Android recompile guide for Unity 4.5. +* Rotation problem when playing full screen video on iOS. Now the layout can work perfectly. + +--- + +### 1.4.2 (30 Apr, 2014) + +#### Fix + +* Fix errors when export from Unity for the platforms not supported in UniWebView. Be caution, this fix does not make UniWebView supporting for other platforms than iOS and Android. It just disabled UniWebView code and suspended the errors. + +--- + +### 1.4.1 (31 Mar, 2014) + +#### Add + +* An API for adding some javascript snippet to Android and iOS in runtime. Now you can dynamically add js code to your page when the game is running, instead of defining it in the web page before loading the web content. +* API for cleaning a specified cookie, instead of cleaning them all. +* A key down event on Android while the web view is activated. + +#### Fix + +* Improved the performance for Mac Editor. +* Some other small issues. + +> The parameters of LoadBeginDelegate is changed from 1.4.0. There is now an url parameter in the delegate. If you are using web view.currentUrl in the corresponding event, now please use this parameter instead of that. The currentUrl is updated only when web page loading finished or failed now. + +--- + +### 1.4.0 (12 Mar, 2014) + +#### Add + +* Add an API for clean cookies for the web view. +* Add an API for stop current loading of the web view. + +#### Fix + +* Change the logic when loading web view, now UniWebView will send all loading begin event back to Unity. +* An issue which cause Youtube video can not be automatically played in iOS. + +--- + +### 1.3.0 (11 Feb, 2014) + +#### Add + +#### Fix + +New: Customize url schemes. You can now add and remove the url schemes UniWebView is using. By using new API of AddUrlScheme and RemoveUrlScheme, you can now integrate some more third party service easier. +New: Support for location service of Android. + +* Some memory leak on iOS. + +--- + +### 1.2.6 (21 Jan, 2014) + +#### Add + +* Add bouncesEnable property to Android. You can set the bounce effect for Android as well. Default is false, which means not show the bounce effect on Android, and not bounce the web view on iOS. +* Add a refresh button in iOS's built-in tool bar. +* Add zoom feature to both iOS and Android. Now you can set the zoomEnable property to let your users zoom-in or zoom-out web view by a pinch gesture. +* Add Reload method, you can reload the current page now from code. + +#### Fix + +* Ignore the ssl check in Android, it is useful when you are testing your page with a certification you create yourself. Otherwise, you can not get pass from it. +* Log order issue on Android. Now the create and show events can be logged correctly. + +--- + +### 1.2.5 (9 Jan, 2014) + +#### Add + +* Add a bouncesEnable property to control the bounces when scroll the web view in iOS. The bounces effect is turned off by default, if you want it back, set this property to true. + +#### Fix + +* A problem causing the OnComplete event can not be raised correctly when load fails. +* A problem which causes the name of web view game object being appended instead of replaced. +* A problem with hiding the web view. Now the web view area could be clicked through correctly when hidden. + +#### Deprecate + +* `Dismiss()` is now deprecated, use `Hide()` to hide the web view. + +--- + +### 1.2.3 (25 Dec, 2013) + +#### Add + +* Support for "mailto" link for Android. + +#### Fix + +* A potential issue which may cause unity scene turning to black when switching back to game while web view opened. It might shows on some old Android devices. +* A problem which causes the source can not get compiled in Mono Develop. +* Update the Dismiss method to let it work as designed in Mac Editor and Android devices. It now behaviors as making the web view hidden instead of close and destroy it. You can use Show to make it visible again after dismissing it. + +--- + +### 1.2.2 (18 Dec, 2013) + +#### Add + +* An option to disable the back button on Android device. It is useful when you don't want users dismiss the web view (for example you want to use web view as part of your UI). + +#### Fix + +* Change the local file loading back to previous because the new method can not handle base url very well. +* An issue which caused current url can not be retrieved properly for Android. + +--- + +### 1.2.1 (12 Dec, 2013) + +#### Add + +* A property to get the current page url. You can know on which page now when you using UniWebView. +* An event when the page begins to load. Not only the first page load from code, but also all the page loading by clicking links on the page. + +#### Fix + +* A mistake in transparent setting method in Android. +* Background can be setting to transparent now in Android, from 2.x to 4.x (and later I think). +* Make the local html load easier to use. Now just set the correct streaming asset path, and load. No more platform-specified step. + +--- + +### 1.2.0 (7 Dec, 2013) + +#### Add + +* A spinner is added to web view, which will show when the webpage is under loading. The spinner is on by default, if you don't want it, you can set it not show by the SetShowSpinnerWhenLoading method of UniWebView. +* We also added a text label in the spinner, to show some information to your users. +* Added an OnEvalJavaScriptFinished event for evaluate js script. Now EvaluatingJavaScript will not return a value, but raise the event. So you can use the same API for both iOS and Android. +* Local file load for Android 4.x. + +#### Fix + +* Rewrote all the Android native code, to make it working properly under Unity 4.3. +* A problem which may cause unity game scene disappeared when game goes to background with web view opened in some old Android devices. + +--- + +### 1.1.7 (2 Dec, 2013) + +#### Add + +* Support for loading local file in Android. + +#### Fix + +* A bug in the demo which causes an exception when you click the web page of the demo. +* A problem which causes the plugin not working sometimes in Editor. + +--- + +### 1.1.6 (28 Nov, 2013) + +#### Fix + +* A problem the file input tag can not trigger a file chooser in Android. + +--- + +### 1.1.3 (21 Nov, 2013) + +#### Add + +* Add a method to the web view for loading from a html string. +* Add some demo script to tell demonstrate how to load a local html file. + +#### Fix + +* A problem when users close the web view with soft keyboard showing in Android. + +--- + +### 1.1.2 (16 Nov, 2013) + +#### Add + +* A better and more interesting demo scene to explain how to make the UniWebView work. + +#### Fix + +* A problem which cause the keyboard not working on Android. +* A rotation issue which cause the web view can not rotate correctly in iOS. + +--- + +### 1.1.0 (9 Nov, 2013) + +#### Add + +* Add Youtube video playing support for Android. +* Add a tool bar to iOS. +* The users can use back button (Android) or a native toolbar (iOS) to close the web view now. +* Add an event to control whether the web view can be closed or not. + +#### Fix + +* Update the demo scene to make it clearer and more interesting. + +--- + +### 1.0.1 (1 Nov, 2013) + +Init release + + diff --git a/Runtime/GuruWebview/UniWebView5/UniWebView/CHANGELOG.md.meta b/Runtime/GuruWebview/UniWebView5/UniWebView/CHANGELOG.md.meta new file mode 100644 index 0000000..d4c364b --- /dev/null +++ b/Runtime/GuruWebview/UniWebView5/UniWebView/CHANGELOG.md.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: f7ee8c3eb642344b08f4ceeb53d70cd1 +timeCreated: 1497057465 +licenseType: Free +DefaultImporter: + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/GuruWebview/UniWebView5/UniWebView/Demo.meta b/Runtime/GuruWebview/UniWebView5/UniWebView/Demo.meta new file mode 100644 index 0000000..7f3c2c8 --- /dev/null +++ b/Runtime/GuruWebview/UniWebView5/UniWebView/Demo.meta @@ -0,0 +1,9 @@ +fileFormatVersion: 2 +guid: 565d674726c3d499cade2709dd955ad0 +folderAsset: yes +timeCreated: 1536753564 +licenseType: Free +DefaultImporter: + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/GuruWebview/UniWebView5/UniWebView/Demo/UniWebViewDemo.unity b/Runtime/GuruWebview/UniWebView5/UniWebView/Demo/UniWebViewDemo.unity new file mode 100644 index 0000000..deae377 --- /dev/null +++ b/Runtime/GuruWebview/UniWebView5/UniWebView/Demo/UniWebViewDemo.unity @@ -0,0 +1,242 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!29 &1 +OcclusionCullingSettings: + m_ObjectHideFlags: 0 + serializedVersion: 2 + m_OcclusionBakeSettings: + smallestOccluder: 5 + smallestHole: 0.25 + backfaceThreshold: 100 + m_SceneGUID: 00000000000000000000000000000000 + m_OcclusionCullingData: {fileID: 0} +--- !u!104 &2 +RenderSettings: + m_ObjectHideFlags: 0 + serializedVersion: 9 + m_Fog: 0 + m_FogColor: {r: 0.5, g: 0.5, b: 0.5, a: 1} + m_FogMode: 3 + m_FogDensity: 0.01 + m_LinearFogStart: 0 + m_LinearFogEnd: 300 + m_AmbientSkyColor: {r: 0.212, g: 0.227, b: 0.259, a: 1} + m_AmbientEquatorColor: {r: 0.114, g: 0.125, b: 0.133, a: 1} + m_AmbientGroundColor: {r: 0.047, g: 0.043, b: 0.035, a: 1} + m_AmbientIntensity: 1 + m_AmbientMode: 3 + m_SubtractiveShadowColor: {r: 0.42, g: 0.478, b: 0.627, a: 1} + m_SkyboxMaterial: {fileID: 0} + m_HaloStrength: 0.5 + m_FlareStrength: 1 + m_FlareFadeSpeed: 3 + m_HaloTexture: {fileID: 0} + m_SpotCookie: {fileID: 10001, guid: 0000000000000000e000000000000000, type: 0} + m_DefaultReflectionMode: 0 + m_DefaultReflectionResolution: 128 + m_ReflectionBounces: 1 + m_ReflectionIntensity: 1 + m_CustomReflection: {fileID: 0} + m_Sun: {fileID: 0} + m_IndirectSpecularColor: {r: 0, g: 0, b: 0, a: 1} + m_UseRadianceAmbientProbe: 0 +--- !u!157 &3 +LightmapSettings: + m_ObjectHideFlags: 0 + serializedVersion: 11 + m_GIWorkflowMode: 1 + m_GISettings: + serializedVersion: 2 + m_BounceScale: 1 + m_IndirectOutputScale: 1 + m_AlbedoBoost: 1 + m_EnvironmentLightingMode: 0 + m_EnableBakedLightmaps: 0 + m_EnableRealtimeLightmaps: 0 + m_LightmapEditorSettings: + serializedVersion: 10 + m_Resolution: 2 + m_BakeResolution: 40 + m_AtlasSize: 1024 + m_AO: 0 + m_AOMaxDistance: 1 + m_CompAOExponent: 1 + m_CompAOExponentDirect: 0 + m_Padding: 2 + m_LightmapParameters: {fileID: 0} + m_LightmapsBakeMode: 1 + m_TextureCompression: 1 + m_FinalGather: 0 + m_FinalGatherFiltering: 1 + m_FinalGatherRayCount: 256 + m_ReflectionCompression: 2 + m_MixedBakeMode: 2 + m_BakeBackend: 0 + m_PVRSampling: 1 + m_PVRDirectSampleCount: 32 + m_PVRSampleCount: 500 + m_PVRBounces: 2 + m_PVRFilterTypeDirect: 0 + m_PVRFilterTypeIndirect: 0 + m_PVRFilterTypeAO: 0 + m_PVRFilteringMode: 1 + m_PVRCulling: 1 + m_PVRFilteringGaussRadiusDirect: 1 + m_PVRFilteringGaussRadiusIndirect: 5 + m_PVRFilteringGaussRadiusAO: 2 + m_PVRFilteringAtrousPositionSigmaDirect: 0.5 + m_PVRFilteringAtrousPositionSigmaIndirect: 2 + m_PVRFilteringAtrousPositionSigmaAO: 1 + m_ShowResolutionOverlay: 1 + m_LightingDataAsset: {fileID: 0} + m_UseShadowmask: 1 +--- !u!196 &4 +NavMeshSettings: + serializedVersion: 2 + m_ObjectHideFlags: 0 + m_BuildSettings: + serializedVersion: 2 + agentTypeID: 0 + agentRadius: 0.5 + agentHeight: 2 + agentSlope: 45 + agentClimb: 0.4 + ledgeDropHeight: 0 + maxJumpAcrossDistance: 0 + minRegionArea: 2 + manualCellSize: 0 + cellSize: 0.16666667 + manualTileSize: 0 + tileSize: 256 + accuratePlacement: 0 + debug: + m_Flags: 0 + m_NavMeshData: {fileID: 0} +--- !u!1001 &783622082 +PrefabInstance: + m_ObjectHideFlags: 0 + serializedVersion: 2 + m_Modification: + m_TransformParent: {fileID: 0} + m_Modifications: + - target: {fileID: 4960404783511462, guid: 7e3f16a6f6303419cbd9837f6c746de4, type: 3} + propertyPath: m_LocalPosition.x + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 4960404783511462, guid: 7e3f16a6f6303419cbd9837f6c746de4, type: 3} + propertyPath: m_LocalPosition.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 4960404783511462, guid: 7e3f16a6f6303419cbd9837f6c746de4, type: 3} + propertyPath: m_LocalPosition.z + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 4960404783511462, guid: 7e3f16a6f6303419cbd9837f6c746de4, type: 3} + propertyPath: m_LocalRotation.x + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 4960404783511462, guid: 7e3f16a6f6303419cbd9837f6c746de4, type: 3} + propertyPath: m_LocalRotation.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 4960404783511462, guid: 7e3f16a6f6303419cbd9837f6c746de4, type: 3} + propertyPath: m_LocalRotation.z + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 4960404783511462, guid: 7e3f16a6f6303419cbd9837f6c746de4, type: 3} + propertyPath: m_LocalRotation.w + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 4960404783511462, guid: 7e3f16a6f6303419cbd9837f6c746de4, type: 3} + propertyPath: m_RootOrder + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 114939446366399424, guid: 7e3f16a6f6303419cbd9837f6c746de4, + type: 3} + propertyPath: urlOnStart + value: https://docs.uniwebview.com/guide/ + objectReference: {fileID: 0} + m_RemovedComponents: [] + m_SourcePrefab: {fileID: 100100000, guid: 7e3f16a6f6303419cbd9837f6c746de4, type: 3} +--- !u!1 &1313296981 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1313296986} + - component: {fileID: 1313296985} + - component: {fileID: 1313296982} + m_Layer: 0 + m_Name: Main Camera + m_TagString: MainCamera + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!81 &1313296982 +AudioListener: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1313296981} + m_Enabled: 1 +--- !u!20 &1313296985 +Camera: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1313296981} + m_Enabled: 1 + serializedVersion: 2 + m_ClearFlags: 1 + m_BackGroundColor: {r: 0.19215687, g: 0.3019608, b: 0.4745098, a: 0} + m_projectionMatrixMode: 1 + m_SensorSize: {x: 36, y: 24} + m_LensShift: {x: 0, y: 0} + m_GateFitMode: 2 + m_FocalLength: 50 + m_NormalizedViewPortRect: + serializedVersion: 2 + x: 0 + y: 0 + width: 1 + height: 1 + near clip plane: 0.3 + far clip plane: 1000 + field of view: 60 + orthographic: 1 + orthographic size: 5 + m_Depth: -1 + m_CullingMask: + serializedVersion: 2 + m_Bits: 4294967295 + m_RenderingPath: -1 + m_TargetTexture: {fileID: 0} + m_TargetDisplay: 0 + m_TargetEye: 3 + m_HDR: 1 + m_AllowMSAA: 1 + m_AllowDynamicResolution: 0 + m_ForceIntoRT: 0 + m_OcclusionCulling: 1 + m_StereoConvergence: 10 + m_StereoSeparation: 0.022 +--- !u!4 &1313296986 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1313296981} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: -10} + m_LocalScale: {x: 1, y: 1, z: 1} + m_Children: [] + m_Father: {fileID: 0} + m_RootOrder: 0 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} diff --git a/Runtime/GuruWebview/UniWebView5/UniWebView/Demo/UniWebViewDemo.unity.meta b/Runtime/GuruWebview/UniWebView5/UniWebView/Demo/UniWebViewDemo.unity.meta new file mode 100644 index 0000000..2b90c5f --- /dev/null +++ b/Runtime/GuruWebview/UniWebView5/UniWebView/Demo/UniWebViewDemo.unity.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 938c077f40a814d4584ce2ad17947cf3 +timeCreated: 1536753573 +licenseType: Free +DefaultImporter: + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/GuruWebview/UniWebView5/UniWebView/Editor.meta b/Runtime/GuruWebview/UniWebView5/UniWebView/Editor.meta new file mode 100644 index 0000000..50aa7ec --- /dev/null +++ b/Runtime/GuruWebview/UniWebView5/UniWebView/Editor.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 5f0047c1c8c8348de9d66b0496b353e1 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/GuruWebview/UniWebView5/UniWebView/Editor/AndroidManifest.cs b/Runtime/GuruWebview/UniWebView5/UniWebView/Editor/AndroidManifest.cs new file mode 100644 index 0000000..dcc396d --- /dev/null +++ b/Runtime/GuruWebview/UniWebView5/UniWebView/Editor/AndroidManifest.cs @@ -0,0 +1,213 @@ +using System.Xml; +using System.Collections; +using System.Text; +using System.IO; +using System; +using UnityEditor; +using UnityEngine; + +internal class UniWebViewAndroidXmlDocument : XmlDocument { + private string path; + protected XmlNamespaceManager nameSpaceManager; + public readonly string AndroidXmlNamespace = "http://schemas.android.com/apk/res/android"; + + public UniWebViewAndroidXmlDocument(string path) { + this.path = path; + using (var reader = new XmlTextReader(path)) { + reader.Read(); + Load(reader); + } + nameSpaceManager = new XmlNamespaceManager(NameTable); + nameSpaceManager.AddNamespace("android", AndroidXmlNamespace); + } + + public string Save() { + return SaveAs(path); + } + + public string SaveAs(string path) { + using (var writer = new XmlTextWriter(path, new UTF8Encoding(false))) { + writer.Formatting = Formatting.Indented; + Save(writer); + } + return path; + } +} + +internal class UniWebViewAndroidManifest : UniWebViewAndroidXmlDocument { + private readonly XmlElement ManifestElement; + private readonly XmlElement ApplicationElement; + + public UniWebViewAndroidManifest(string path) : base(path) { + ManifestElement = SelectSingleNode("/manifest") as XmlElement; + ApplicationElement = SelectSingleNode("/manifest/application") as XmlElement; + } + + private XmlAttribute CreateAndroidAttribute(string key, string value) { + XmlAttribute attr = CreateAttribute("android", key, AndroidXmlNamespace); + attr.Value = value; + return attr; + } + + internal XmlNode GetActivityWithLaunchIntent() { + return + SelectSingleNode( + "/manifest/application/activity[intent-filter/action/@android:name='android.intent.action.MAIN' and " + + "intent-filter/category/@android:name='android.intent.category.LAUNCHER']", + nameSpaceManager); + } + + internal bool SetUsesCleartextTraffic() { + bool changed = false; + if (ApplicationElement.GetAttribute("usesCleartextTraffic", AndroidXmlNamespace) != "true") { + ApplicationElement.SetAttribute("usesCleartextTraffic", AndroidXmlNamespace, "true"); + changed = true; + } + return changed; + } + + internal bool SetHardwareAccelerated() { + bool changed = false; + var activity = GetActivityWithLaunchIntent() as XmlElement; + if (activity.GetAttribute("hardwareAccelerated", AndroidXmlNamespace) != "true") { + activity.SetAttribute("hardwareAccelerated", AndroidXmlNamespace, "true"); + changed = true; + } + return changed; + } + + internal bool AddCameraPermission() { + bool changed = false; + if (SelectNodes("/manifest/uses-permission[@android:name='android.permission.CAMERA']", nameSpaceManager).Count == 0) { + var elem = CreateElement("uses-permission"); + elem.Attributes.Append(CreateAndroidAttribute("name", "android.permission.CAMERA")); + ManifestElement.AppendChild(elem); + changed = true; + } + if (SelectNodes("/manifest/uses-feature[@android:name='android.hardware.camera']", nameSpaceManager).Count == 0) { + var elem = CreateElement("uses-feature"); + elem.Attributes.Append(CreateAndroidAttribute("name", "android.hardware.camera")); + ManifestElement.AppendChild(elem); + changed = true; + } + return changed; + } + + internal bool AddMicrophonePermission() { + bool changed = false; + if (SelectNodes("/manifest/uses-permission[@android:name='android.permission.MICROPHONE']", nameSpaceManager).Count == 0) { + var elem = CreateElement("uses-permission"); + elem.Attributes.Append(CreateAndroidAttribute("name", "android.permission.MICROPHONE")); + ManifestElement.AppendChild(elem); + changed = true; + } + if (SelectNodes("/manifest/uses-feature[@android:name='android.hardware.microphone']", nameSpaceManager).Count == 0) { + var elem = CreateElement("uses-feature"); + elem.Attributes.Append(CreateAndroidAttribute("name", "android.hardware.microphone")); + ManifestElement.AppendChild(elem); + changed = true; + } + return changed; + } + + internal bool AddReadExternalStoragePermission() { + bool changed = false; + if (SelectNodes("/manifest/uses-permission[@android:name='android.permission.READ_EXTERNAL_STORAGE']", nameSpaceManager).Count == 0) { + var elem = CreateElement("uses-permission"); + elem.Attributes.Append(CreateAndroidAttribute("name", "android.permission.READ_EXTERNAL_STORAGE")); + ManifestElement.AppendChild(elem); + changed = true; + } + return changed; + } + + internal bool AddWriteExternalStoragePermission() { + bool changed = false; + if (SelectNodes("/manifest/uses-permission[@android:name='android.permission.WRITE_EXTERNAL_STORAGE']", nameSpaceManager).Count == 0) { + var elem = CreateElement("uses-permission"); + elem.Attributes.Append(CreateAndroidAttribute("name", "android.permission.WRITE_EXTERNAL_STORAGE")); + ManifestElement.AppendChild(elem); + changed = true; + } + return changed; + } + + internal bool AddAccessFineLocationPermission() { + bool changed = false; + if (SelectNodes("/manifest/uses-permission[@android:name='android.permission.ACCESS_FINE_LOCATION']", nameSpaceManager).Count == 0) { + var elem = CreateElement("uses-permission"); + elem.Attributes.Append(CreateAndroidAttribute("name", "android.permission.ACCESS_FINE_LOCATION")); + ManifestElement.AppendChild(elem); + changed = true; + } + return changed; + } + + internal bool AddAuthCallbacksIntentFilter(string[] authCallbackUrls) { + bool changed = false; + XmlElement authActivityNode; + if (authCallbackUrls.Length > 0) { + var list = SelectNodes("/manifest/application/activity[@android:name='com.onevcat.uniwebview.UniWebViewAuthenticationActivity']", nameSpaceManager); + if (list.Count == 0) { + var created = CreateElement("activity"); + created.SetAttribute("name", AndroidXmlNamespace, "com.onevcat.uniwebview.UniWebViewAuthenticationActivity"); + created.SetAttribute("exported", AndroidXmlNamespace, "true"); + created.SetAttribute("launchMode", AndroidXmlNamespace, "singleTop"); + authActivityNode = created; + } else { + authActivityNode = list[0] as XmlElement; + } + } else { + return changed; + } + + foreach (var url in authCallbackUrls) { + var intentFilter = CreateIntentFilter(url); + if (intentFilter != null) { + authActivityNode.AppendChild(intentFilter); + changed = true; + } + } + ApplicationElement.AppendChild(authActivityNode); + return changed; + } + + private XmlElement CreateIntentFilter(string url) { + + var uri = new Uri(url); + var scheme = uri.Scheme; + if (String.IsNullOrEmpty(scheme)) { + Debug.LogError(" Auth callback url contains an empty scheme. Please check the url: " + url); + return null; + } + + var filter = CreateElement("intent-filter"); + + var action = CreateElement("action"); + action.SetAttribute("name", AndroidXmlNamespace, "android.intent.action.VIEW"); + filter.AppendChild(action); + + var defaultCategory = CreateElement("category"); + defaultCategory.SetAttribute("name", AndroidXmlNamespace, "android.intent.category.DEFAULT"); + filter.AppendChild(defaultCategory); + + var browseCategory = CreateElement("category"); + browseCategory.SetAttribute("name", AndroidXmlNamespace, "android.intent.category.BROWSABLE"); + filter.AppendChild(browseCategory); + + var data = CreateElement("data"); + data.SetAttribute("scheme", AndroidXmlNamespace, scheme); + if (!String.IsNullOrEmpty(uri.Host)) { + data.SetAttribute("host", AndroidXmlNamespace, uri.Host); + } + if (uri.Port != -1) { + data.SetAttribute("port", AndroidXmlNamespace, uri.Port.ToString()); + } + if (!String.IsNullOrEmpty(uri.PathAndQuery) && uri.PathAndQuery != "/") { + data.SetAttribute("path", AndroidXmlNamespace, uri.PathAndQuery); + } + + filter.AppendChild(data); + return filter; + } +} \ No newline at end of file diff --git a/Runtime/GuruWebview/UniWebView5/UniWebView/Editor/AndroidManifest.cs.meta b/Runtime/GuruWebview/UniWebView5/UniWebView/Editor/AndroidManifest.cs.meta new file mode 100644 index 0000000..ccf5d02 --- /dev/null +++ b/Runtime/GuruWebview/UniWebView5/UniWebView/Editor/AndroidManifest.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: d09054fcc76964295a49868566075973 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/GuruWebview/UniWebView5/UniWebView/Editor/BuildGradle.cs b/Runtime/GuruWebview/UniWebView5/UniWebView/Editor/BuildGradle.cs new file mode 100644 index 0000000..1af5fcb --- /dev/null +++ b/Runtime/GuruWebview/UniWebView5/UniWebView/Editor/BuildGradle.cs @@ -0,0 +1,366 @@ +using System.Collections.Generic; +using UnityEngine; +using System.IO; +using System.Text; +using System; + +public class UniWebViewGradleConfig +{ + private UniWebViewGradleNode m_root; + private String m_filePath; + private UniWebViewGradleNode m_curNode; + + public UniWebViewGradleNode ROOT + { + get { return m_root; } + } + + public UniWebViewGradleConfig(string filePath) + { + string file = File.ReadAllText(filePath); + TextReader reader = new StringReader(file); + + m_filePath = filePath; + m_root = new UniWebViewGradleNode("root"); + m_curNode = m_root; + + StringBuilder str = new StringBuilder(); + bool inDoubleQuote = false; + bool inSingleQuote = false; + bool inDollarVariable = false; + + while (reader.Peek() > 0) + { + char c = (char)reader.Read(); + switch (c) + { + // case '/': + // if (reader.Peek() == '/') + // { + // reader.Read(); + // string comment = reader.ReadLine(); + // Debug.Log("Comment line: " + comment); + // m_curNode.AppendChildNode(new UniWebViewGradleCommentNode(comment, m_curNode)); + // } + // else + // { + // str.Append('/'); + // } + // break; + case '\n': + case '\r': + { + var strf = FormatStr(str); + if (!string.IsNullOrEmpty(strf)) + { + m_curNode.AppendChildNode(new UniWebViewGradleContentNode(strf, m_curNode)); + } + } + inDollarVariable = false; + str = new StringBuilder(); + break; + case '\t': + { + var strf = FormatStr(str); + if (!string.IsNullOrEmpty(strf)) + { + str.Append(" "); + } + break; + } + case '{': + { + if (inDoubleQuote || inSingleQuote) { + str.Append(c); + break; + } + var n = FormatStr(str); + if (!string.IsNullOrEmpty(n)) + { + UniWebViewGradleNode node = new UniWebViewGradleNode(n, m_curNode); + m_curNode.AppendChildNode(node); + m_curNode = node; + } + } + str = new StringBuilder(); + break; + case '}': + { + if (inDoubleQuote || inSingleQuote) { + str.Append(c); + break; + } + var strf = FormatStr(str); + if (!string.IsNullOrEmpty(strf)) + { + m_curNode.AppendChildNode(new UniWebViewGradleContentNode(strf, m_curNode)); + } + m_curNode = m_curNode.PARENT; + } + str = new StringBuilder(); + break; + case '\"': + if (inDollarVariable) { + str.Append(c); + break; + } + inDoubleQuote = !inDoubleQuote; + str.Append(c); + break; + case '\'': + if (inDollarVariable) { + str.Append(c); + break; + } + inSingleQuote = !inSingleQuote; + str.Append(c); + break; + case '$': + { + if (inDoubleQuote || inSingleQuote) { + str.Append(c); + break; + } + inDollarVariable = true; + str.Append(c); + break; + } + default: + str.Append(c); + break; + } + } + + //Debug.Log("Gradle parse done!"); + } + + public void Save(string path = null) + { + if (path == null) + path = m_filePath; + File.WriteAllText(path, Print()); + //Debug.Log("Gradle parse done! " + path); + } + + private string FormatStr(StringBuilder sb) + { + string str = sb.ToString(); + str = str.Trim(); + return str; + } + public string Print() + { + StringBuilder sb = new StringBuilder(); + printNode(sb, m_root, -1); + return sb.ToString(); + } + private string GetLevelIndent(int level) + { + if (level <= 0) return ""; + StringBuilder sb = new StringBuilder(""); + for (int i = 0; i < level; i++) + { + sb.Append('\t'); + } + return sb.ToString(); + } + private void printNode(StringBuilder stringBuilder, UniWebViewGradleNode node, int level) + { + if (node.PARENT != null) + { + if (node is UniWebViewGradleCommentNode) + { + stringBuilder.Append("\n" + GetLevelIndent(level) + @"//" + node.NAME); + } + else + { + stringBuilder.Append("\n" + GetLevelIndent(level) + node.NAME); + } + + } + + if (!(node is UniWebViewGradleContentNode) && !(node is UniWebViewGradleCommentNode)) + { + if (node.PARENT != null) + { + stringBuilder.Append(" {"); + } + foreach (var c in node.CHILDREN) + { + printNode(stringBuilder, c, level + 1); + } + if (node.PARENT != null) + { + stringBuilder.Append("\n" + GetLevelIndent(level) + "}"); + } + } + } +} + +public class UniWebViewGradleNode +{ + protected List m_children = new List(); + protected UniWebViewGradleNode m_parent; + protected String m_name; + public UniWebViewGradleNode PARENT + { + get { return m_parent; } + } + + public string NAME + { + get { return m_name; } + } + + public List CHILDREN + { + get { return m_children; } + } + + public UniWebViewGradleNode(string name, UniWebViewGradleNode parent = null) + { + m_parent = parent; + m_name = name; + } + + public void Each(Action f) + { + f(this); + foreach (var n in m_children) + { + n.Each(f); + } + } + + public void AppendChildNode(UniWebViewGradleNode node) + { + if (m_children == null) m_children = new List(); + m_children.Add(node); + node.m_parent = this; + } + + public UniWebViewGradleNode TryGetNode(string path) + { + string[] subpath = path.Split('/'); + UniWebViewGradleNode cnode = this; + for (int i = 0; i < subpath.Length; i++) + { + var p = subpath[i]; + if (string.IsNullOrEmpty(p)) continue; + UniWebViewGradleNode tnode = cnode.FindChildNodeByName(p); + if (tnode == null) + { + Debug.Log("Can't find Node:" + p); + return null; + } + + cnode = tnode; + tnode = null; + } + + return cnode; + } + + public UniWebViewGradleNode FindChildNodeByName(string name) + { + foreach (var n in m_children) + { + if (n is UniWebViewGradleCommentNode || n is UniWebViewGradleContentNode) + continue; + if (n.NAME == name) + return n; + } + return null; + } + + public bool ReplaceContenStartsWith(string patten, string value) + { + foreach (var n in m_children) + { + if (!(n is UniWebViewGradleContentNode)) continue; + if (n.m_name.StartsWith(patten)) + { + n.m_name = value; + return true; + } + } + return false; + } + + public UniWebViewGradleContentNode ReplaceContenOrAddStartsWith(string patten, string value) + { + foreach (var n in m_children) + { + if (!(n is UniWebViewGradleContentNode)) continue; + if (n.m_name.StartsWith(patten)) + { + n.m_name = value; + return (UniWebViewGradleContentNode)n; + } + } + return AppendContentNode(value); + } + + /// + /// 添加子节点 + /// + /// + /// + public UniWebViewGradleContentNode AppendContentNode(string content) + { + foreach (var n in m_children) + { + if (!(n is UniWebViewGradleContentNode)) continue; + if (n.m_name == content) + { + Debug.Log("UniWebViewGradleContentNode with " + content + " already exists!"); + return null; + } + } + UniWebViewGradleContentNode cnode = new UniWebViewGradleContentNode(content, this); + AppendChildNode(cnode); + return cnode; + } + + + public bool RemoveContentNode(string contentPattern) + { + for(int i=0;i text.Contains("android.useAndroidX")); + bool hasJetifierProperty = lines.Any(text => text.Contains("android.enableJetifier")); + + StringBuilder builder = new StringBuilder(); + + foreach(string each in lines) { + builder.AppendLine(each); + } + + if (!hasAndroidXProperty) { + builder.AppendLine("android.useAndroidX=true"); + } + + if (!hasJetifierProperty && UniWebViewEditorSettings.GetOrCreateSettings().enableJetifier) { + builder.AppendLine("android.enableJetifier=true"); + } + + File.WriteAllText(filePath, builder.ToString()); + } +} \ No newline at end of file diff --git a/Runtime/GuruWebview/UniWebView5/UniWebView/Editor/GradleProperty.cs.meta b/Runtime/GuruWebview/UniWebView5/UniWebView/Editor/GradleProperty.cs.meta new file mode 100644 index 0000000..0c95b96 --- /dev/null +++ b/Runtime/GuruWebview/UniWebView5/UniWebView/Editor/GradleProperty.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 62bf94600ac0f42c09fc261d7ab19e12 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/GuruWebview/UniWebView5/UniWebView/Editor/UniWebView-CSharp.Editor.asmdef b/Runtime/GuruWebview/UniWebView5/UniWebView/Editor/UniWebView-CSharp.Editor.asmdef new file mode 100644 index 0000000..4adb281 --- /dev/null +++ b/Runtime/GuruWebview/UniWebView5/UniWebView/Editor/UniWebView-CSharp.Editor.asmdef @@ -0,0 +1,15 @@ +{ + "name": "UniWebView-CSharp.Editor", + "references": ["GUID:343deaaf83e0cee4ca978e7df0b80d21","GUID:2bafac87e7f4b9b418d9448d219b01ab"], + "includePlatforms": [ + "Editor" + ], + "excludePlatforms": [], + "allowUnsafeCode": false, + "overrideReferences": false, + "precompiledReferences": [], + "autoReferenced": true, + "defineConstraints": [], + "versionDefines": [], + "noEngineReferences": false +} \ No newline at end of file diff --git a/Runtime/GuruWebview/UniWebView5/UniWebView/Editor/UniWebView-CSharp.Editor.asmdef.meta b/Runtime/GuruWebview/UniWebView5/UniWebView/Editor/UniWebView-CSharp.Editor.asmdef.meta new file mode 100644 index 0000000..16d33cc --- /dev/null +++ b/Runtime/GuruWebview/UniWebView5/UniWebView/Editor/UniWebView-CSharp.Editor.asmdef.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: b6d0e5b49a073436cbcd56804553ee20 +AssemblyDefinitionImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/GuruWebview/UniWebView5/UniWebView/Editor/UniWebViewEditorSettings.cs b/Runtime/GuruWebview/UniWebView5/UniWebView/Editor/UniWebViewEditorSettings.cs new file mode 100644 index 0000000..f5bc626 --- /dev/null +++ b/Runtime/GuruWebview/UniWebView5/UniWebView/Editor/UniWebViewEditorSettings.cs @@ -0,0 +1,152 @@ +using UnityEngine; +using UnityEditor; +using System.Collections.Generic; +using System; +using System.IO; + +class UniWebViewEditorSettings: ScriptableObject +{ + const string assetPath = "Assets/Guru/Editor/UniWebView/settings.asset"; + + [SerializeField] + internal bool usesCleartextTraffic = false; + + [SerializeField] + internal bool writeExternalStorage = false; + + [SerializeField] + internal bool accessFineLocation = false; + + [SerializeField] + internal bool addsKotlin = true; + + [SerializeField] + internal bool addsAndroidBrowser = true; + + [SerializeField] + internal bool enableJetifier = true; + + [SerializeField] + internal string[] authCallbackUrls = { }; + + [SerializeField] + internal bool supportLINELogin = false; + + internal static UniWebViewEditorSettings GetOrCreateSettings() { + var settings = AssetDatabase.LoadAssetAtPath(assetPath); + + if (settings == null) { + settings = ScriptableObject.CreateInstance(); + + Directory.CreateDirectory("Assets/Guru/Editor/UniWebView/"); + AssetDatabase.CreateAsset(settings, assetPath); + AssetDatabase.SaveAssets(); + } + + return settings; + } + + internal static SerializedObject GetSerializedSettings() { + return new SerializedObject(GetOrCreateSettings()); + } +} + +static class UniWebViewSettingsProvider { + static SerializedObject settings; + + #if UNITY_2018_3_OR_NEWER + private class Provider : SettingsProvider { + public Provider(string path, SettingsScope scope = SettingsScope.User): base(path, scope) {} + public override void OnGUI(string searchContext) { + DrawPref(); + } + } + [SettingsProvider] + static SettingsProvider UniWebViewPref() { + return new Provider("Preferences/UniWebView"); + } + #else + [PreferenceItem("UniWebView")] + #endif + static void DrawPref() { + EditorGUIUtility.labelWidth = 320; + if (settings == null) { + settings = UniWebViewEditorSettings.GetSerializedSettings(); + } + settings.Update(); + EditorGUI.BeginChangeCheck(); + + // Manifest + EditorGUILayout.Space(); + EditorGUILayout.BeginVertical(); + EditorGUILayout.LabelField("Android Manifest", EditorStyles.boldLabel); + + EditorGUI.indentLevel++; + EditorGUILayout.PropertyField(settings.FindProperty("usesCleartextTraffic")); + DrawDetailLabel("If you need to load plain HTTP content."); + + EditorGUILayout.PropertyField(settings.FindProperty("writeExternalStorage")); + DrawDetailLabel("If you need to download an image from web page."); + + EditorGUILayout.PropertyField(settings.FindProperty("accessFineLocation")); + DrawDetailLabel("If you need to enable location support in web view."); + EditorGUI.indentLevel--; + EditorGUILayout.EndVertical(); + + // Gradle + EditorGUILayout.Space(); + EditorGUILayout.BeginVertical(); + EditorGUILayout.LabelField("Gradle Build", EditorStyles.boldLabel); + EditorGUI.indentLevel++; + EditorGUILayout.PropertyField(settings.FindProperty("addsKotlin")); + DrawDetailLabel("Turn off this if another library is already adding Kotlin runtime."); + EditorGUILayout.PropertyField(settings.FindProperty("addsAndroidBrowser")); + DrawDetailLabel("Turn off this if another library is already adding 'androidx.browser:browser'."); + EditorGUILayout.PropertyField(settings.FindProperty("enableJetifier")); + DrawDetailLabel("Turn off this if you do not need Jetifier (for converting other legacy support dependencies to Android X)."); + EditorGUI.indentLevel--; + EditorGUILayout.EndVertical(); + + // Auth callbacks + EditorGUILayout.Space(); + EditorGUILayout.BeginVertical(); + EditorGUILayout.LabelField("Auth Callbacks", EditorStyles.boldLabel); + EditorGUI.indentLevel++; + EditorGUILayout.PropertyField(settings.FindProperty("authCallbackUrls"), true); + DrawDetailLabel("Adds all available auth callback URLs here to use UniWebView's auth support."); + + EditorGUILayout.Space(); + EditorGUILayout.PropertyField(settings.FindProperty("supportLINELogin")); + DrawDetailLabel("LINE Login is using a custom fixed scheme. If you want to support LINE Login, turn on this."); + + EditorGUI.indentLevel--; + EditorGUILayout.EndVertical(); + + EditorGUILayout.Space(); + EditorGUILayout.BeginHorizontal(); + EditorGUI.indentLevel++; + EditorGUILayout.HelpBox("Read the help page to know more about all UniWebView preferences detail.", MessageType.Info); + + var style = new GUIStyle(GUI.skin.label); + style.normal.textColor = Color.blue; + if (GUILayout.Button("Help Page", style)) { + Application.OpenURL("https://docs.uniwebview.com/guide/installation.html#optional-steps"); + } + + EditorGUILayout.Space(); + EditorGUI.indentLevel--; + EditorGUILayout.EndHorizontal(); + + if (EditorGUI.EndChangeCheck()) { + settings.ApplyModifiedProperties(); + AssetDatabase.SaveAssets(); + } + EditorGUIUtility.labelWidth = 0; + } + + static void DrawDetailLabel(string text) { + EditorGUI.indentLevel++; + EditorGUILayout.LabelField(text, EditorStyles.miniLabel); + EditorGUI.indentLevel--; + } +} \ No newline at end of file diff --git a/Runtime/GuruWebview/UniWebView5/UniWebView/Editor/UniWebViewEditorSettings.cs.meta b/Runtime/GuruWebview/UniWebView5/UniWebView/Editor/UniWebViewEditorSettings.cs.meta new file mode 100644 index 0000000..eb5f99c --- /dev/null +++ b/Runtime/GuruWebview/UniWebView5/UniWebView/Editor/UniWebViewEditorSettings.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: a403a09e241a0480a957591ea60fb785 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/GuruWebview/UniWebView5/UniWebView/Editor/UniWebViewPostBuildProcessor.cs b/Runtime/GuruWebview/UniWebView5/UniWebView/Editor/UniWebViewPostBuildProcessor.cs new file mode 100644 index 0000000..5c7fecb --- /dev/null +++ b/Runtime/GuruWebview/UniWebView5/UniWebView/Editor/UniWebViewPostBuildProcessor.cs @@ -0,0 +1,123 @@ +using UnityEditor; +using UnityEditor.Android; +using UnityEngine; +using System.IO; +using System.Text; + + +class UniWebViewPostBuildProcessor : IPostGenerateGradleAndroidProject +{ + public int callbackOrder { get { return 1; } } + public void OnPostGenerateGradleAndroidProject(string path) { + Debug.Log(" UniWebView Post Build Scirpt is patching manifest file and gradle file..."); + PatchAndroidManifest(path); + PatchBuildGradle(path); + PatchGradleProperty(path); + } + + private void PatchAndroidManifest(string root) { + var manifestFilePath = GetManifestFilePath(root); + var manifest = new UniWebViewAndroidManifest(manifestFilePath); + + var changed = false; + + Debug.Log(" Set hardware accelerated to enable smooth web view experience and HTML5 support like video and canvas."); + changed = manifest.SetHardwareAccelerated() || changed; + + var settings = UniWebViewEditorSettings.GetOrCreateSettings(); + if (settings.usesCleartextTraffic) { + changed = manifest.SetUsesCleartextTraffic() || changed; + } + if (settings.writeExternalStorage) { + changed = manifest.AddWriteExternalStoragePermission() || changed; + } + if (settings.accessFineLocation) { + changed = manifest.AddAccessFineLocationPermission() || changed; + } + if (settings.authCallbackUrls.Length > 0) { + changed = manifest.AddAuthCallbacksIntentFilter(settings.authCallbackUrls) || changed; + } + + if (settings.supportLINELogin) { + changed = manifest.AddAuthCallbacksIntentFilter(new string[] { "lineauth://auth" }) || changed; + } + + if (changed) { + manifest.Save(); + } + } + + private void PatchBuildGradle(string root) { + var gradleFilePath = GetGradleFilePath(root); + var config = new UniWebViewGradleConfig(gradleFilePath); + + var kotlinPrefix = "implementation 'org.jetbrains.kotlin:kotlin-stdlib-jdk7:"; + var kotlinVersion = "1.6.20'"; + + var browserPrefix = "implementation 'androidx.browser:browser:"; + var browserVersion = "1.2.0'"; + + var settings = UniWebViewEditorSettings.GetOrCreateSettings(); + + var dependenciesNode = config.ROOT.FindChildNodeByName("dependencies"); + if (dependenciesNode != null) { + // Add kotlin + if (settings.addsKotlin) { + dependenciesNode.ReplaceContenOrAddStartsWith(kotlinPrefix, kotlinPrefix + kotlinVersion); + Debug.Log(" Updated Kotlin dependency in build.gradle."); + } + + // Add browser package + if (settings.addsAndroidBrowser) { + dependenciesNode.ReplaceContenOrAddStartsWith(browserPrefix, browserPrefix + browserVersion); + Debug.Log(" Updated Browser dependency in build.gradle."); + } + } else { + Debug.LogError("UniWebViewPostBuildProcessor didn't find the `dependencies` field in build.gradle."); + Debug.LogError("Although we can continue to add a `dependencies`, make sure you have setup Gradle and the template correctly."); + + var newNode = new UniWebViewGradleNode("dependencies", config.ROOT); + if (settings.addsKotlin) { + newNode.AppendContentNode(kotlinPrefix + kotlinVersion); + } + if (settings.addsAndroidBrowser) { + newNode.AppendContentNode(browserPrefix + browserVersion); + } + newNode.AppendContentNode("implementation(name: 'UniWebView', ext:'aar')"); + config.ROOT.AppendChildNode(newNode); + } + config.Save(); + } + + private void PatchGradleProperty(string root) { + var gradlePropertyFilePath = GetGradlePropertyFilePath(root); + UniWebViewGradlePropertyPatcher.Patch(gradlePropertyFilePath); + } + + private string CombinePaths(string[] paths) { + var path = ""; + foreach (var item in paths) { + path = Path.Combine(path, item); + } + return path; + } + + private string GetManifestFilePath(string root) { + string[] comps = {root, "src", "main", "AndroidManifest.xml"}; + return CombinePaths(comps); + } + + private string GetGradleFilePath(string root) { + string[] comps = {root, "build.gradle"}; + return CombinePaths(comps); + } + + private string GetGradlePropertyFilePath(string root) { + #if UNITY_2019_3_OR_NEWER + string[] compos = {root, "..", "gradle.properties"}; + #else + string[] compos = {root, "gradle.properties"}; + #endif + return CombinePaths(compos); + } +} \ No newline at end of file diff --git a/Runtime/GuruWebview/UniWebView5/UniWebView/Editor/UniWebViewPostBuildProcessor.cs.meta b/Runtime/GuruWebview/UniWebView5/UniWebView/Editor/UniWebViewPostBuildProcessor.cs.meta new file mode 100644 index 0000000..40665c7 --- /dev/null +++ b/Runtime/GuruWebview/UniWebView5/UniWebView/Editor/UniWebViewPostBuildProcessor.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 59d4a8d85c95843719d8b9df823c3da3 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/GuruWebview/UniWebView5/UniWebView/Interface.meta b/Runtime/GuruWebview/UniWebView5/UniWebView/Interface.meta new file mode 100644 index 0000000..807e044 --- /dev/null +++ b/Runtime/GuruWebview/UniWebView5/UniWebView/Interface.meta @@ -0,0 +1,9 @@ +fileFormatVersion: 2 +guid: dc0f52a2d219347b1a6390b753d6ac97 +folderAsset: yes +timeCreated: 1491898971 +licenseType: Free +DefaultImporter: + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/GuruWebview/UniWebView5/UniWebView/Interface/UniWebViewAndroid.cs b/Runtime/GuruWebview/UniWebView5/UniWebView/Interface/UniWebViewAndroid.cs new file mode 100644 index 0000000..7e4515b --- /dev/null +++ b/Runtime/GuruWebview/UniWebView5/UniWebView/Interface/UniWebViewAndroid.cs @@ -0,0 +1,517 @@ +#if UNITY_ANDROID && !UNITY_EDITOR + +using UnityEngine; + +public class UniWebViewInterface { + private static readonly AndroidJavaClass plugin; + private static bool correctPlatform = Application.platform == RuntimePlatform.Android; + + static UniWebViewInterface() { + var go = new GameObject("UniWebViewAndroidStaticListener"); + go.AddComponent(); + plugin = new AndroidJavaClass("com.onevcat.uniwebview.UniWebViewInterface"); + CheckPlatform(); + plugin.CallStatic("prepare"); + } + + //OMSDK part------- + + public static void InitOMSDK(string jsonSettings, string omidJSServiceContent) { + CheckPlatform(); + plugin.CallStatic("initOMSDK",jsonSettings,omidJSServiceContent); + } + + public static void InitOMSDKSession(string resourceUrl) { + CheckPlatform(); + plugin.CallStatic("initOMSDKSession",resourceUrl); + } + + public static void StartImpression(string resourceUrl) { + CheckPlatform(); + plugin.CallStatic("startImpression",resourceUrl); + } + + public static void StopImpression(string resourceUrl) { + CheckPlatform(); + plugin.CallStatic("stopImpression",resourceUrl); + } + + public static void StopOMIDAdSession(string name) { + CheckPlatform(); + plugin.CallStatic("stopOMIDAdSession",name); + } + + //-------- + + public static void SetLogLevel(int level) { + CheckPlatform(); + plugin.CallStatic("setLogLevel", level); + } + + public static bool IsWebViewSupported() { + CheckPlatform(); + return plugin.CallStatic("isWebViewSupported"); + } + + public static void Init(string name, int x, int y, int width, int height) { + CheckPlatform(); + plugin.CallStatic("init", name, x, y, width, height); + } + + public static void Destroy(string name) { + CheckPlatform(); + plugin.CallStatic("destroy", name); + } + + public static void Load(string name, string url, bool skipEncoding, string readAccessURL) { + CheckPlatform(); + plugin.CallStatic("load", name, url); + } + + public static void LoadHTMLString(string name, string html, string baseUrl, bool skipEncoding) { + CheckPlatform(); + plugin.CallStatic("loadHTMLString", name, html, baseUrl); + } + + public static void Reload(string name) { + CheckPlatform(); + plugin.CallStatic("reload", name); + } + + public static void Stop(string name) { + CheckPlatform(); + plugin.CallStatic("stop", name); + } + + public static string GetUrl(string name) { + CheckPlatform(); + return plugin.CallStatic("getUrl", name); + } + + public static void SetFrame(string name, int x, int y, int width, int height) { + CheckPlatform(); + plugin.CallStatic("setFrame", name, x, y, width, height); + } + + public static void SetPosition(string name, int x, int y) { + CheckPlatform(); + plugin.CallStatic("setPosition", name, x, y); + } + + public static void SetSize(string name, int width, int height) { + CheckPlatform(); + plugin.CallStatic("setSize", name, width, height); + } + + public static bool Show(string name, bool fade, int edge, float duration, bool useAsync, string identifier) { + CheckPlatform(); + if (useAsync) { + plugin.CallStatic("showAsync", name, fade, edge, duration, identifier); + return true; + } else { + return plugin.CallStatic("show", name, fade, edge, duration, identifier); + } + } + + public static bool Hide(string name, bool fade, int edge, float duration, bool useAsync, string identifier) { + CheckPlatform(); + if (useAsync) { + plugin.CallStatic("hideAsync", name, fade, edge, duration, identifier); + return true; + } else { + return plugin.CallStatic("hide", name, fade, edge, duration, identifier); + } + } + + public static bool AnimateTo(string name, int x, int y, int width, int height, float duration, float delay, string identifier) { + CheckPlatform(); + return plugin.CallStatic("animateTo", name, x, y, width, height, duration, delay, identifier); + } + + public static void AddJavaScript(string name, string jsString, string identifier) { + CheckPlatform(); + plugin.CallStatic("addJavaScript", name, jsString, identifier); + } + + public static void EvaluateJavaScript(string name, string jsString, string identifier) { + CheckPlatform(); + plugin.CallStatic("evaluateJavaScript", name, jsString, identifier); + } + + public static void AddUrlScheme(string name, string scheme) { + CheckPlatform(); + plugin.CallStatic("addUrlScheme", name, scheme); + } + + public static void RemoveUrlScheme(string name, string scheme) { + CheckPlatform(); + plugin.CallStatic("removeUrlScheme", name, scheme); + } + + public static void AddSslExceptionDomain(string name, string domain) { + CheckPlatform(); + plugin.CallStatic("addSslExceptionDomain", name, domain); + } + + public static void RemoveSslExceptionDomain(string name, string domain) { + CheckPlatform(); + plugin.CallStatic("removeSslExceptionDomain", name, domain); + } + + public static void AddPermissionTrustDomain(string name, string domain) { + CheckPlatform(); + plugin.CallStatic("addPermissionTrustDomain", name, domain); + } + + public static void RemovePermissionTrustDomain(string name, string domain) { + CheckPlatform(); + plugin.CallStatic("removePermissionTrustDomain", name, domain); + } + + public static void SetHeaderField(string name, string key, string value) { + CheckPlatform(); + plugin.CallStatic("setHeaderField", name, key, value); + } + + public static void SetUserAgent(string name, string userAgent) { + CheckPlatform(); + plugin.CallStatic("setUserAgent", name, userAgent); + } + + public static string GetUserAgent(string name) { + CheckPlatform(); + return plugin.CallStatic("getUserAgent", name); + } + + public static void SetAllowAutoPlay(bool flag) { + CheckPlatform(); + plugin.CallStatic("setAllowAutoPlay", flag); + } + + public static void SetAllowJavaScriptOpenWindow(bool flag) { + CheckPlatform(); + plugin.CallStatic("setAllowJavaScriptOpenWindow", flag); + } + + public static void SetAllowFileAccess(string name, bool flag) { + CheckPlatform(); + plugin.CallStatic("setAllowFileAccess", name, flag); + } + + public static void SetAcceptThirdPartyCookies(string name, bool flag) { + CheckPlatform(); + plugin.CallStatic("setAcceptThirdPartyCookies", name, flag); + } + + public static void SetAllowFileAccessFromFileURLs(string name, bool flag) { + CheckPlatform(); + plugin.CallStatic("setAllowFileAccessFromFileURLs", name, flag); + } + + public static void SetAllowUniversalAccessFromFileURLs(bool flag) { + CheckPlatform(); + plugin.CallStatic("setAllowUniversalAccessFromFileURLs", flag); + } + + public static void SetEnableKeyboardAvoidance(bool flag) { + CheckPlatform(); + plugin.CallStatic("setEnableKeyboardAvoidance", flag); + } + + public static void SetJavaScriptEnabled(bool enabled) { + CheckPlatform(); + plugin.CallStatic("setJavaScriptEnabled", enabled); + } + + public static void CleanCache(string name) { + CheckPlatform(); + plugin.CallStatic("cleanCache", name); + } + + public static void ClearCookies() { + CheckPlatform(); + plugin.CallStatic("clearCookies"); + } + + public static void SetCookie(string url, string cookie, bool skipEncoding) { + CheckPlatform(); + plugin.CallStatic("setCookie", url, cookie); + } + + public static string GetCookie(string url, string key, bool skipEncoding) { + CheckPlatform(); + return plugin.CallStatic("getCookie", url, key); + } + + public static void RemoveCookies(string url, bool skipEncoding) { + CheckPlatform(); + plugin.CallStatic("removeCookies", url); + } + + public static void RemoveCookie(string url, string key, bool skipEncoding) { + CheckPlatform(); + plugin.CallStatic("removeCookie", url, key); + } + + public static void ClearHttpAuthUsernamePassword(string host, string realm) { + CheckPlatform(); + plugin.CallStatic("clearHttpAuthUsernamePassword", host, realm); + } + + public static void SetBackgroundColor(string name, float r, float g, float b, float a) { + CheckPlatform(); + plugin.CallStatic("setBackgroundColor", name, r, g, b, a); + } + + public static void SetWebViewAlpha(string name, float alpha) { + CheckPlatform(); + plugin.CallStatic("setWebViewAlpha", name, alpha); + } + + public static float GetWebViewAlpha(string name) { + CheckPlatform(); + return plugin.CallStatic("getWebViewAlpha", name); + } + + public static void SetShowSpinnerWhileLoading(string name, bool show) { + CheckPlatform(); + plugin.CallStatic("setShowSpinnerWhileLoading", name, show); + } + + public static void SetSpinnerText(string name, string text) { + CheckPlatform(); + plugin.CallStatic("setSpinnerText", name, text); + } + + public static bool CanGoBack(string name) { + CheckPlatform(); + return plugin.CallStatic("canGoBack", name); + } + + public static bool CanGoForward(string name) { + CheckPlatform(); + return plugin.CallStatic("canGoForward", name); + } + + public static void GoBack(string name) { + CheckPlatform(); + plugin.CallStatic("goBack", name); + } + public static void GoForward(string name) { + CheckPlatform(); + plugin.CallStatic("goForward", name); + } + + public static void SetOpenLinksInExternalBrowser(string name, bool flag) { + CheckPlatform(); + plugin.CallStatic("setOpenLinksInExternalBrowser", name, flag); + } + + public static void SetHorizontalScrollBarEnabled(string name, bool enabled) { + CheckPlatform(); + plugin.CallStatic("setHorizontalScrollBarEnabled", name, enabled); + } + + public static void SetVerticalScrollBarEnabled(string name, bool enabled) { + CheckPlatform(); + plugin.CallStatic("setVerticalScrollBarEnabled", name, enabled); + } + + public static void SetBouncesEnabled(string name, bool enabled) { + CheckPlatform(); + plugin.CallStatic("setBouncesEnabled", name, enabled); + } + + public static void SetZoomEnabled(string name, bool enabled) { + CheckPlatform(); + plugin.CallStatic("setZoomEnabled", name, enabled); + } + + public static void SetUseWideViewPort(string name, bool use) { + CheckPlatform(); + plugin.CallStatic("setUseWideViewPort", name, use); + } + + public static void SetLoadWithOverviewMode(string name, bool overview) { + CheckPlatform(); + plugin.CallStatic("setLoadWithOverviewMode", name, overview); + } + + public static void SetImmersiveModeEnabled(string name, bool enabled) { + CheckPlatform(); + plugin.CallStatic("setImmersiveModeEnabled", name, enabled); + } + + public static void SetUserInteractionEnabled(string name, bool enabled) { + CheckPlatform(); + plugin.CallStatic("setUserInteractionEnabled", name, enabled); + } + + public static void SetTransparencyClickingThroughEnabled(string name, bool enabled) { + CheckPlatform(); + plugin.CallStatic("setTransparencyClickingThroughEnabled", name, enabled); + } + + public static void SetWebContentsDebuggingEnabled(bool enabled) { + CheckPlatform(); + plugin.CallStatic("setWebContentsDebuggingEnabled", enabled); + } + + public static void SetAllowHTTPAuthPopUpWindow(string name, bool flag) { + CheckPlatform(); + plugin.CallStatic("setAllowHTTPAuthPopUpWindow", name, flag); + } + + public static void Print(string name) { + CheckPlatform(); + plugin.CallStatic("print", name); + } + + public static void CaptureSnapshot(string name, string filename) { + CheckPlatform(); + plugin.CallStatic("captureSnapshot", name, filename); + } + + public static void ScrollTo(string name, int x, int y, bool animated) { + CheckPlatform(); + plugin.CallStatic("scrollTo", name, x, y, animated); + } + + public static void SetCalloutEnabled(string name, bool flag) { + CheckPlatform(); + plugin.CallStatic("setCalloutEnabled", name, flag); + } + + public static void SetSupportMultipleWindows(string name, bool enabled, bool allowJavaScriptOpening) { + CheckPlatform(); + plugin.CallStatic("setSupportMultipleWindows", name, enabled, allowJavaScriptOpening); + } + + public static void SetDefaultFontSize(string name, int size) { + CheckPlatform(); + plugin.CallStatic("setDefaultFontSize", name, size); + } + + public static void SetTextZoom(string name, int textZoom) { + CheckPlatform(); + plugin.CallStatic("setTextZoom", name, textZoom); + } + + public static float NativeScreenWidth() { + CheckPlatform(); + return plugin.CallStatic("screenWidth"); + } + + public static float NativeScreenHeight() { + CheckPlatform(); + return plugin.CallStatic("screenHeight"); + } + + public static void SetDownloadEventForContextMenuEnabled(string name, bool enabled) { + CheckPlatform(); + plugin.CallStatic("setDownloadEventForContextMenuEnabled", name, enabled); + } + + // Safe Browsing + + public static bool IsSafeBrowsingSupported() { + CheckPlatform(); + return plugin.CallStatic("isSafeBrowsingSupported"); + } + + public static void SafeBrowsingInit(string name, string url) { + CheckPlatform(); + plugin.CallStatic("safeBrowsingInit", name, url); + } + + public static void SafeBrowsingSetToolbarColor(string name, float r, float g, float b) { + CheckPlatform(); + plugin.CallStatic("safeBrowsingSetToolbarColor", name, r, g, b); + } + + public static void SafeBrowsingShow(string name) { + CheckPlatform(); + plugin.CallStatic("safeBrowsingShow", name); + } + + // Authentication + + public static bool IsAuthenticationIsSupported() { + CheckPlatform(); + return plugin.CallStatic("isAuthenticationIsSupported"); + } + + public static void AuthenticationInit(string name, string url, string scheme) { + CheckPlatform(); + plugin.CallStatic("authenticationInit", name, url, scheme); + } + + public static void AuthenticationStart(string name) { + CheckPlatform(); + plugin.CallStatic("authenticationStart", name); + } + + public static void AuthenticationSetPrivateMode(string name, bool enabled) { + CheckPlatform(); + plugin.CallStatic("authenticationSetPrivateMode", name, enabled); + } + + public static void SetShowEmbeddedToolbar(string name, bool show) { + CheckPlatform(); + plugin.CallStatic("setShowEmbeddedToolbar", name, show); + } + + public static void SetEmbeddedToolbarOnTop(string name, bool top) { + CheckPlatform(); + plugin.CallStatic("setEmbeddedToolbarOnTop", name, top); + } + + public static void SetEmbeddedToolbarDoneButtonText(string name, string text) { + CheckPlatform(); + plugin.CallStatic("setEmbeddedToolbarDoneButtonText", name, text); + } + + public static void SetEmbeddedToolbarGoBackButtonText(string name, string text) { + CheckPlatform(); + plugin.CallStatic("setEmbeddedToolbarGoBackButtonText", name, text); + } + + public static void SetEmbeddedToolbarGoForwardButtonText(string name, string text) { + CheckPlatform(); + plugin.CallStatic("setEmbeddedToolbarGoForwardButtonText", name, text); + } + + public static void SetEmbeddedToolbarTitleText(string name, string text) { + CheckPlatform(); + plugin.CallStatic("setEmbeddedToolbarTitleText", name, text); + } + + public static void SetEmbeddedToolbarBackgroundColor(string name, Color color) { + CheckPlatform(); + plugin.CallStatic("setEmbeddedToolbarBackgroundColor", name, color.r, color.g, color.b, color.a); + } + + public static void SetEmbeddedToolbarButtonTextColor(string name, Color color) { + CheckPlatform(); + plugin.CallStatic("setEmbeddedToolbarButtonTextColor", name, color.r, color.g, color.b, color.a); + } + + public static void SetEmbeddedToolbarTitleTextColor(string name, Color color) { + CheckPlatform(); + plugin.CallStatic("setEmbeddedToolbarTitleTextColor", name, color.r, color.g, color.b, color.a); + } + + public static void SetEmeddedToolbarNavigationButtonsShow(string name, bool show) { + CheckPlatform(); + plugin.CallStatic("setEmbeddedToolbarNavigationButtonsShow", name, show); + } + + // Platform + + public static void CheckPlatform() { + if (!correctPlatform) { + throw new System.InvalidOperationException("Method can only be performed on Android."); + } + } +} +#endif \ No newline at end of file diff --git a/Runtime/GuruWebview/UniWebView5/UniWebView/Interface/UniWebViewAndroid.cs.meta b/Runtime/GuruWebview/UniWebView5/UniWebView/Interface/UniWebViewAndroid.cs.meta new file mode 100644 index 0000000..4a8f17e --- /dev/null +++ b/Runtime/GuruWebview/UniWebView5/UniWebView/Interface/UniWebViewAndroid.cs.meta @@ -0,0 +1,12 @@ +fileFormatVersion: 2 +guid: 5a1d3cecc27d64565835e14b493c935b +timeCreated: 1490880130 +licenseType: Free +MonoImporter: + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/GuruWebview/UniWebView5/UniWebView/Interface/UniWebViewAndroidStaticListener.cs b/Runtime/GuruWebview/UniWebView5/UniWebView/Interface/UniWebViewAndroidStaticListener.cs new file mode 100644 index 0000000..39c7751 --- /dev/null +++ b/Runtime/GuruWebview/UniWebView5/UniWebView/Interface/UniWebViewAndroidStaticListener.cs @@ -0,0 +1,39 @@ + +// #if UNITY_ANDROID && !UNITY_EDITOR +using System; +using System.Reflection; +using UnityEngine; + +public class UniWebViewAndroidStaticListener: MonoBehaviour { + void Awake() { + DontDestroyOnLoad(gameObject); + } + + void OnJavaMessage(string message) { + // {listener_name}@{method_name}@parameters + string[] parts = message.Split("@"[0]); + if (parts.Length < 3) { + Debug.Log("Not enough parts for receiving a message."); + return; + } + + var listener = UniWebViewNativeListener.GetListener(parts[0]); + if (listener == null) { + return; + } + + MethodInfo methodInfo = typeof(UniWebViewNativeListener).GetMethod(parts[1]); + if (methodInfo == null) { + Debug.Log("Cannot find correct method to invoke: " + parts[1]); + } + + var leftLength = parts.Length - 2; + var left = new string[leftLength]; + for (int i = 0; i < leftLength; i++) { + left[i] = parts[i + 2]; + } + methodInfo.Invoke(listener, new object[] { String.Join("@", left) }); + } +} + +// #endif \ No newline at end of file diff --git a/Runtime/GuruWebview/UniWebView5/UniWebView/Interface/UniWebViewAndroidStaticListener.cs.meta b/Runtime/GuruWebview/UniWebView5/UniWebView/Interface/UniWebViewAndroidStaticListener.cs.meta new file mode 100644 index 0000000..2d10c95 --- /dev/null +++ b/Runtime/GuruWebview/UniWebView5/UniWebView/Interface/UniWebViewAndroidStaticListener.cs.meta @@ -0,0 +1,13 @@ +fileFormatVersion: 2 +guid: 2704cf8e127d541f1888d96429308645 +timeCreated: 1514387178 +licenseType: Free +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/GuruWebview/UniWebView5/UniWebView/Interface/UniWebViewCocoa.cs b/Runtime/GuruWebview/UniWebView5/UniWebView/Interface/UniWebViewCocoa.cs new file mode 100644 index 0000000..9d12ffa --- /dev/null +++ b/Runtime/GuruWebview/UniWebView5/UniWebView/Interface/UniWebViewCocoa.cs @@ -0,0 +1,846 @@ +// +// UniWebViewInterface.cs +// Created by Wang Wei(@onevcat) on 2017-04-11. +// +// This file is a part of UniWebView Project (https://uniwebview.com) +// By purchasing the asset, you are allowed to use this code in as many as projects +// you want, only if you publish the final products under the name of the same account +// used for the purchase. +// +// This asset and all corresponding files (such as source code) are provided on an +// “as is” basis, without warranty of any kind, express of 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 action of contract, tort or otherwise, +// arising from, out of or in connection with the software or the use of other dealing in the software. +// +#if (UNITY_EDITOR_OSX || UNITY_STANDALONE_OSX || UNITY_IOS) && !UNITY_EDITOR_WIN && !UNITY_EDITOR_LINUX + +using UnityEngine; +using System; +using System.Runtime.InteropServices; +using AOT; +using System.Reflection; + +public class UniWebViewInterface { + static UniWebViewInterface() { + ConnectMessageSender(); + } + + delegate void UnitySendMessageDelegate(IntPtr objectName, IntPtr methodName, IntPtr parameter); + + private const string DllLib = + #if UNITY_EDITOR_OSX || UNITY_STANDALONE_OSX + "UniWebView"; + #else + "__Internal"; + #endif + + private static bool correctPlatform = + #if UNITY_EDITOR_OSX + Application.platform == RuntimePlatform.OSXEditor || + Application.platform == RuntimePlatform.IPhonePlayer || // Support for Device Simulator package + Application.platform == RuntimePlatform.Android; // Support for Device Simulator package + #elif UNITY_STANDALONE_OSX + Application.platform == RuntimePlatform.OSXPlayer; + #else + Application.platform == RuntimePlatform.IPhonePlayer; + #endif + + [DllImport(DllLib)] + private static extern void uv_connectMessageSender( + [MarshalAs(UnmanagedType.FunctionPtr)] UnitySendMessageDelegate sendMessageDelegate + ); + static void ConnectMessageSender() { + UniWebViewLogger.Instance.Info("Connecting to native side message sender."); + CheckPlatform(); + uv_connectMessageSender(SendMessage); + } + + [MonoPInvokeCallback(typeof(UnitySendMessageDelegate))] + private static void SendMessage(IntPtr namePtr, IntPtr methodPtr, IntPtr parameterPtr) { + string name = Marshal.PtrToStringAuto(namePtr); + string method = Marshal.PtrToStringAuto(methodPtr); + string parameters = Marshal.PtrToStringAuto(parameterPtr); + + UniWebViewLogger.Instance.Verbose( + "Received message sent from native. Name: " + name + " Method: " + method + " Params: " + parameters + ); + + var listener = UniWebViewNativeListener.GetListener(name); + if (listener) { + MethodInfo methodInfo = typeof(UniWebViewNativeListener).GetMethod(method); + if (methodInfo != null) { + methodInfo.Invoke(listener, new object[] { parameters }); + } + } + } + + //------- + + [DllImport(DllLib)] + private static extern void uv_stopOMIDAdSession(string name); + public static void StopOMIDAdSession(string name) + { + CheckPlatform(); + + if (String.IsNullOrEmpty(name)) + { + return; + } + + uv_stopOMIDAdSession(name); + } + + [DllImport(DllLib)] + private static extern void uv_initOMSDK(string jsonSettings, string omidJSServiceContent); + public static void InitOMSDK(string jsonSettings, string omidJSServiceContent) + { + CheckPlatform(); + + uv_initOMSDK(jsonSettings, omidJSServiceContent); + } + + [DllImport(DllLib)] + private static extern void uv_initOMSDKSession(string resourceUrl); + public static void InitOMSDKSession(string resourceUrl) + { + CheckPlatform(); + + uv_initOMSDKSession(resourceUrl); + } + + [DllImport(DllLib)] + private static extern void uv_startImpression(string resourceUrl); + public static void StartImpression(string resourceUrl) + { + CheckPlatform(); + + uv_startImpression(resourceUrl); + } + + [DllImport(DllLib)] + private static extern void uv_stopImpression(string resourceUrl); + public static void StopImpression(string resourceUrl) + { + CheckPlatform(); + + uv_stopImpression(resourceUrl); + } + + + //-------- + + + [DllImport(DllLib)] + private static extern void uv_setLogLevel(int level); + public static void SetLogLevel(int level) { + CheckPlatform(); + uv_setLogLevel(level); + } + + [DllImport(DllLib)] + private static extern void uv_init(string name, int x, int y, int width, int height); + public static void Init(string name, int x, int y, int width, int height) { + CheckPlatform(); + if (String.IsNullOrEmpty(name)) { + return; + } + uv_init(name, x, y, width, height); + } + + [DllImport(DllLib)] + private static extern void uv_destroy(string name); + public static void Destroy(string name) { + CheckPlatform(); + uv_destroy(name); + } + + [DllImport(DllLib)] + private static extern void uv_load(string name, string url, bool skipEncoding, string readAccessURL); + public static void Load(string name, string url, bool skipEncoding, string readAccessURL) { + CheckPlatform(); + uv_load(name, url, skipEncoding, readAccessURL); + } + + [DllImport(DllLib)] + private static extern void uv_loadHTMLString(string name, string html, string baseUrl, bool skipEncoding); + public static void LoadHTMLString(string name, string html, string baseUrl, bool skipEncoding) { + CheckPlatform(); + uv_loadHTMLString(name, html, baseUrl, skipEncoding); + } + + [DllImport(DllLib)] + private static extern void uv_reload(string name); + public static void Reload(string name) { + CheckPlatform(); + uv_reload(name); + } + + [DllImport(DllLib)] + private static extern void uv_stop(string name); + public static void Stop(string name) { + CheckPlatform(); + uv_stop(name); + } + + [DllImport(DllLib)] + private static extern string uv_getUrl(string name); + public static string GetUrl(string name) { + CheckPlatform(); + return uv_getUrl(name); + } + + [DllImport(DllLib)] + private static extern void uv_setFrame(string name, int x, int y, int width, int height); + public static void SetFrame(string name, int x, int y, int width, int height) { + CheckPlatform(); + uv_setFrame(name, x, y, width, height); + } + + [DllImport(DllLib)] + private static extern void uv_setPosition(string name, int x, int y); + public static void SetPosition(string name, int x, int y) { + CheckPlatform(); + uv_setPosition(name, x, y); + } + + [DllImport(DllLib)] + private static extern void uv_setSize(string name, int width, int height); + public static void SetSize(string name, int width, int height) { + CheckPlatform(); + uv_setSize(name, width, height); + } + + [DllImport(DllLib)] + private static extern bool uv_show(string name, bool fade, int edge, float duration, string identifier); + public static bool Show(string name, bool fade, int edge, float duration, bool useAsync, string identifier) { + CheckPlatform(); + return uv_show(name, fade, edge, duration, identifier); + } + + [DllImport(DllLib)] + private static extern bool uv_hide(string name, bool fade, int edge, float duration, string identifier); + public static bool Hide(string name, bool fade, int edge, float duration, bool useAsync, string identifier) { + CheckPlatform(); + return uv_hide(name, fade, edge, duration, identifier); + } + + [DllImport(DllLib)] + private static extern bool uv_animateTo( + string name, int x, int y, int width, int height, float duration, float delay, string identifier + ); + public static bool AnimateTo( + string name, int x, int y, int width, int height, float duration, float delay, string identifier) + { + CheckPlatform(); + return uv_animateTo(name, x, y, width, height, duration, delay, identifier); + } + + [DllImport(DllLib)] + private static extern void uv_addJavaScript(string name, string jsString, string identifier); + public static void AddJavaScript(string name, string jsString, string identifier) { + CheckPlatform(); + uv_addJavaScript(name, jsString, identifier); + } + + [DllImport(DllLib)] + private static extern void uv_evaluateJavaScript(string name, string jsString, string identifier); + public static void EvaluateJavaScript(string name, string jsString, string identifier) { + CheckPlatform(); + uv_evaluateJavaScript(name, jsString, identifier); + } + + [DllImport(DllLib)] + private static extern void uv_addUrlScheme(string name, string scheme); + public static void AddUrlScheme(string name, string scheme) { + CheckPlatform(); + uv_addUrlScheme(name, scheme); + } + + [DllImport(DllLib)] + private static extern void uv_removeUrlScheme(string name, string scheme); + public static void RemoveUrlScheme(string name, string scheme) { + CheckPlatform(); + uv_removeUrlScheme(name, scheme); + } + + [DllImport(DllLib)] + private static extern void uv_addSslExceptionDomain(string name, string domain); + public static void AddSslExceptionDomain(string name, string domain) { + CheckPlatform(); + uv_addSslExceptionDomain(name, domain); + } + + [DllImport(DllLib)] + private static extern void uv_removeSslExceptionDomain(string name, string domain); + public static void RemoveSslExceptionDomain(string name, string domain) { + CheckPlatform(); + uv_removeSslExceptionDomain(name, domain); + } + + [DllImport(DllLib)] + private static extern void uv_setHeaderField(string name, string key, string value); + public static void SetHeaderField(string name, string key, string value) { + CheckPlatform(); + uv_setHeaderField(name, key, value); + } + + [DllImport(DllLib)] + private static extern void uv_setUserAgent(string name, string userAgent); + public static void SetUserAgent(string name, string userAgent) { + CheckPlatform(); + uv_setUserAgent(name, userAgent); + } + + [DllImport(DllLib)] + private static extern string uv_getUserAgent(string name); + public static string GetUserAgent(string name) { + CheckPlatform(); + return uv_getUserAgent(name); + } + + + [DllImport(DllLib)] + private static extern void uv_setContentInsetAdjustmentBehavior(string name, int behavior); + public static void SetContentInsetAdjustmentBehavior( + string name, UniWebViewContentInsetAdjustmentBehavior behavior + ) + { + CheckPlatform(); + uv_setContentInsetAdjustmentBehavior(name, (int)behavior); + } + + [DllImport(DllLib)] + private static extern void uv_setAllowAutoPlay(bool flag); + public static void SetAllowAutoPlay(bool flag) { + CheckPlatform(); + uv_setAllowAutoPlay(flag); + } + + [DllImport(DllLib)] + private static extern void uv_setAllowInlinePlay(bool flag); + public static void SetAllowInlinePlay(bool flag) { + CheckPlatform(); + uv_setAllowInlinePlay(flag); + } + + [DllImport(DllLib)] + private static extern void uv_setAllowFileAccess(string name, bool flag); + public static void SetAllowFileAccess(string name, bool flag) { + CheckPlatform(); + uv_setAllowFileAccess(name, flag); + } + [DllImport(DllLib)] + private static extern void uv_setAllowFileAccessFromFileURLs(string name, bool flag); + public static void SetAllowFileAccessFromFileURLs(string name, bool flag) { + CheckPlatform(); + uv_setAllowFileAccessFromFileURLs(name, flag); + } + + [DllImport(DllLib)] + private static extern void uv_setAllowUniversalAccessFromFileURLs(bool flag); + public static void SetAllowUniversalAccessFromFileURLs(bool flag) { + CheckPlatform(); + uv_setAllowUniversalAccessFromFileURLs(flag); + } + + [DllImport(DllLib)] + private static extern void uv_setAllowJavaScriptOpenWindow(bool flag); + public static void SetAllowJavaScriptOpenWindow(bool flag) { + CheckPlatform(); + uv_setAllowJavaScriptOpenWindow(flag); + } + + [DllImport(DllLib)] + private static extern void uv_setJavaScriptEnabled(bool flag); + public static void SetJavaScriptEnabled(bool flag) { + CheckPlatform(); + uv_setJavaScriptEnabled(flag); + } + + [DllImport(DllLib)] + private static extern void uv_cleanCache(string name); + public static void CleanCache(string name) { + CheckPlatform(); + uv_cleanCache(name); + } + + [DllImport(DllLib)] + private static extern void uv_clearCookies(); + public static void ClearCookies() { + CheckPlatform(); + uv_clearCookies(); + } + + [DllImport(DllLib)] + private static extern void uv_setCookie(string url, string cookie, bool skipEncoding); + public static void SetCookie(string url, string cookie, bool skipEncoding) { + CheckPlatform(); + uv_setCookie(url, cookie, skipEncoding); + } + + [DllImport(DllLib)] + private static extern void uv_removeCookies(string url, bool skipEncoding); + public static void RemoveCookies(string url, bool skipEncoding) { + CheckPlatform(); + uv_removeCookies(url, skipEncoding); + } + + [DllImport(DllLib)] + private static extern void uv_removeCookie(string url, string key, bool skipEncoding); + public static void RemoveCookie(string url, string key, bool skipEncoding) { + CheckPlatform(); + uv_removeCookie(url, key, skipEncoding); + } + + [DllImport(DllLib)] + private static extern string uv_getCookie(string url, string key, bool skipEncoding); + public static string GetCookie(string url, string key, bool skipEncoding) { + CheckPlatform(); + return uv_getCookie(url, key, skipEncoding); + } + + [DllImport(DllLib)] + private static extern void uv_clearHttpAuthUsernamePasswordHost(string host, string realm); + public static void ClearHttpAuthUsernamePassword(string host, string realm) { + CheckPlatform(); + uv_clearHttpAuthUsernamePasswordHost(host, realm); + } + + [DllImport(DllLib)] + private static extern void uv_setBackgroundColor(string name, float r, float g, float b, float a); + public static void SetBackgroundColor(string name, float r, float g, float b, float a) { + CheckPlatform(); + uv_setBackgroundColor(name, r, g, b, a); + } + + [DllImport(DllLib)] + private static extern void uv_setWebViewAlpha(string name, float alpha); + public static void SetWebViewAlpha(string name, float alpha) { + CheckPlatform(); + uv_setWebViewAlpha(name, alpha); + } + + [DllImport(DllLib)] + private static extern float uv_getWebViewAlpha(string name); + public static float GetWebViewAlpha(string name) { + CheckPlatform(); + return uv_getWebViewAlpha(name); + } + + [DllImport(DllLib)] + private static extern void uv_setShowSpinnerWhileLoading(string name, bool show); + public static void SetShowSpinnerWhileLoading(string name, bool show) { + CheckPlatform(); + uv_setShowSpinnerWhileLoading(name, show); + } + + [DllImport(DllLib)] + private static extern void uv_setSpinnerText(string name, string text); + public static void SetSpinnerText(string name, string text) { + CheckPlatform(); + uv_setSpinnerText(name, text); + } + + [DllImport(DllLib)] + private static extern bool uv_canGoBack(string name); + public static bool CanGoBack(string name) { + CheckPlatform(); + return uv_canGoBack(name); + } + + [DllImport(DllLib)] + private static extern bool uv_canGoForward(string name); + public static bool CanGoForward(string name) { + CheckPlatform(); + return uv_canGoForward(name); + } + + [DllImport(DllLib)] + private static extern void uv_goBack(string name); + public static void GoBack(string name) { + CheckPlatform(); + uv_goBack(name); + } + + [DllImport(DllLib)] + private static extern void uv_goForward(string name); + public static void GoForward(string name) { + CheckPlatform(); + uv_goForward(name); + } + + [DllImport(DllLib)] + private static extern void uv_setOpenLinksInExternalBrowser(string name, bool flag); + public static void SetOpenLinksInExternalBrowser(string name, bool flag) { + CheckPlatform(); + uv_setOpenLinksInExternalBrowser(name, flag); + } + + [DllImport(DllLib)] + private static extern void uv_setHorizontalScrollBarEnabled(string name, bool enabled); + public static void SetHorizontalScrollBarEnabled(string name, bool enabled) { + CheckPlatform(); + uv_setHorizontalScrollBarEnabled(name, enabled); + } + + [DllImport(DllLib)] + private static extern void uv_setVerticalScrollBarEnabled(string name, bool enabled); + public static void SetVerticalScrollBarEnabled(string name, bool enabled) { + CheckPlatform(); + uv_setVerticalScrollBarEnabled(name, enabled); + } + + [DllImport(DllLib)] + private static extern void uv_setBouncesEnabled(string name, bool enabled); + public static void SetBouncesEnabled(string name, bool enabled) { + CheckPlatform(); + uv_setBouncesEnabled(name, enabled); + } + + [DllImport(DllLib)] + private static extern void uv_setZoomEnabled(string name, bool enabled); + public static void SetZoomEnabled(string name, bool enabled) { + CheckPlatform(); + uv_setZoomEnabled(name, enabled); + } + + [DllImport(DllLib)] + private static extern void uv_setWindowUserResizeEnabled(string name, bool enabled); + public static void SetWindowUserResizeEnabled(string name, bool enabled) { + CheckPlatform(); + uv_setWindowUserResizeEnabled(name, enabled); + } + + [DllImport(DllLib)] + private static extern void uv_setUserInteractionEnabled(string name, bool enabled); + public static void SetUserInteractionEnabled(string name, bool enabled) { + CheckPlatform(); + uv_setUserInteractionEnabled(name, enabled); + } + + [DllImport(DllLib)] + private static extern void uv_setTransparencyClickingThroughEnabled(string name, bool enabled); + public static void SetTransparencyClickingThroughEnabled(string name, bool enabled) { + CheckPlatform(); + uv_setTransparencyClickingThroughEnabled(name, enabled); + } + + [DllImport(DllLib)] + private static extern void uv_setWebContentsDebuggingEnabled(bool enabled); + public static void SetWebContentsDebuggingEnabled(bool enabled) { + CheckPlatform(); + uv_setWebContentsDebuggingEnabled(enabled); + } + + [DllImport(DllLib)] + private static extern void uv_setAllowBackForwardNavigationGestures(string name, bool flag); + public static void SetAllowBackForwardNavigationGestures(string name, bool flag) { + CheckPlatform(); + uv_setAllowBackForwardNavigationGestures(name, flag); + } + + [DllImport(DllLib)] + private static extern void uv_setAllowHTTPAuthPopUpWindow(string name, bool flag); + public static void SetAllowHTTPAuthPopUpWindow(string name, bool flag) { + CheckPlatform(); + uv_setAllowHTTPAuthPopUpWindow(name, flag); + } + + [DllImport(DllLib)] + private static extern void uv_print(string name); + public static void Print(string name) { + CheckPlatform(); + uv_print(name); + } + + [DllImport(DllLib)] + private static extern void uv_captureSnapshot(string name, string fileName); + public static void CaptureSnapshot(string name, string fileName) { + CheckPlatform(); + uv_captureSnapshot(name, fileName); + } + + [DllImport(DllLib)] + private static extern void uv_scrollTo(string name, int x, int y, bool animated); + public static void ScrollTo(string name, int x, int y, bool animated) { + CheckPlatform(); + uv_scrollTo(name, x, y, animated); + } + + [DllImport(DllLib)] + private static extern void uv_setCalloutEnabled(string name, bool flag); + public static void SetCalloutEnabled(string name, bool flag) { + CheckPlatform(); + uv_setCalloutEnabled(name, flag); + } + + [DllImport(DllLib)] + private static extern void uv_setSupportMultipleWindows(string name, bool enabled, bool allowJavaScriptOpening); + public static void SetSupportMultipleWindows(string name, bool enabled, bool allowJavaScriptOpening) { + CheckPlatform(); + uv_setSupportMultipleWindows(name, enabled, allowJavaScriptOpening); + } + + [DllImport(DllLib)] + private static extern void uv_setDragInteractionEnabled(string name, bool flag); + public static void SetDragInteractionEnabled(string name, bool flag) { + CheckPlatform(); + uv_setDragInteractionEnabled(name, flag); + } + + [DllImport(DllLib)] + private static extern float uv_nativeScreenWidth(); + public static float NativeScreenWidth() { + #if UNITY_EDITOR_OSX + return Screen.width; + #else + return uv_nativeScreenWidth(); + #endif + } + + [DllImport(DllLib)] + private static extern float uv_nativeScreenHeight(); + public static float NativeScreenHeight() { + #if UNITY_EDITOR_OSX + return Screen.height; + #else + return uv_nativeScreenHeight(); + #endif + } + + [DllImport(DllLib)] + private static extern void uv_addDownloadURL(string name, string urlString, int type); + public static void AddDownloadURL(string name, string urlString, int type) { + CheckPlatform(); + uv_addDownloadURL(name, urlString, type); + } + + [DllImport(DllLib)] + private static extern void uv_removeDownloadURL(string name, string urlString, int type); + public static void RemoveDownloadURL(string name, string urlString, int type) { + CheckPlatform(); + uv_removeDownloadURL(name, urlString, type); + } + + [DllImport(DllLib)] + private static extern void uv_addDownloadMIMEType(string name, string MIMEType, int type); + public static void AddDownloadMIMEType(string name, string MIMEType, int type) { + CheckPlatform(); + uv_addDownloadMIMEType(name, MIMEType, type); + } + + [DllImport(DllLib)] + private static extern void uv_removeDownloadMIMETypes(string name, string MIMEType, int type); + public static void RemoveDownloadMIMETypes(string name, string MIMEType, int type) { + CheckPlatform(); + uv_removeDownloadMIMETypes(name, MIMEType, type); + } + + [DllImport(DllLib)] + private static extern void uv_setAllowUserChooseActionAfterDownloading(string name, bool allowed); + public static void SetAllowUserChooseActionAfterDownloading(string name, bool allowed) { + CheckPlatform(); + uv_setAllowUserChooseActionAfterDownloading(name, allowed); + } + + [DllImport(DllLib)] + private static extern void uv_safeBrowsingInit(string name, string url); + public static void SafeBrowsingInit(string name, string url) { + CheckPlatform(); + if (String.IsNullOrEmpty(name)) { + return; + } + uv_safeBrowsingInit(name, url); + } + + [DllImport(DllLib)] + private static extern void uv_safeBrowsingShow(string name); + public static void SafeBrowsingShow(string name) { + CheckPlatform(); + uv_safeBrowsingShow(name); + } + + [DllImport(DllLib)] + private static extern void uv_safeBrowsingSetToolbarColor(string name, float r, float g, float b); + public static void SafeBrowsingSetToolbarColor(string name, float r, float g, float b) { + CheckPlatform(); + uv_safeBrowsingSetToolbarColor(name, r, g, b); + } + + [DllImport(DllLib)] + private static extern void uv_safeBrowsingSetToolbarItemColor(string name, float r, float g, float b); + public static void SafeBrowsingSetToolbarItemColor(string name, float r, float g, float b) { + CheckPlatform(); + uv_safeBrowsingSetToolbarItemColor(name, r, g, b); + } + + [DllImport(DllLib)] + private static extern void uv_safeBrowsingDismiss(string name); + public static void SafeBrowsingDismiss(string name) { + CheckPlatform(); + uv_safeBrowsingDismiss(name); + } + + [DllImport(DllLib)] + private static extern bool uv_authenticationIsSupported(); + public static bool IsAuthenticationIsSupported() { + CheckPlatform(); + return uv_authenticationIsSupported(); + } + + [DllImport(DllLib)] + private static extern void uv_authenticationInit(string name, string url, string scheme); + public static void AuthenticationInit(string name, string url, string scheme) { + CheckPlatform(); + uv_authenticationInit(name, url, scheme); + } + + [DllImport(DllLib)] + private static extern void uv_authenticationStart(string name); + public static void AuthenticationStart(string name) { + CheckPlatform(); + uv_authenticationStart(name); + } + + [DllImport(DllLib)] + private static extern void uv_authenticationSetPrivateMode(string name, bool flag); + public static void AuthenticationSetPrivateMode(string name, bool flag) { + CheckPlatform(); + uv_authenticationSetPrivateMode(name, flag); + } + + [DllImport(DllLib)] + private static extern void uv_setShowEmbeddedToolbar(string name, bool show); + public static void SetShowEmbeddedToolbar(string name, bool show) { + CheckPlatform(); + uv_setShowEmbeddedToolbar(name, show); + } + + [DllImport(DllLib)] + private static extern void uv_setEmbeddedToolbarOnTop(string name, bool top); + public static void SetEmbeddedToolbarOnTop(string name, bool top) { + CheckPlatform(); + uv_setEmbeddedToolbarOnTop(name, top); + } + + [DllImport(DllLib)] + private static extern void uv_setEmbeddedToolbarDoneButtonText(string name, string text); + public static void SetEmbeddedToolbarDoneButtonText(string name, string text) { + CheckPlatform(); + uv_setEmbeddedToolbarDoneButtonText(name, text); + } + + [DllImport(DllLib)] + private static extern void uv_setEmbeddedToolbarGoBackButtonText(string name, string text); + public static void SetEmbeddedToolbarGoBackButtonText(string name, string text) { + CheckPlatform(); + uv_setEmbeddedToolbarGoBackButtonText(name, text); + } + + [DllImport(DllLib)] + private static extern void uv_setEmbeddedToolbarGoForwardButtonText(string name, string text); + public static void SetEmbeddedToolbarGoForwardButtonText(string name, string text) { + CheckPlatform(); + uv_setEmbeddedToolbarGoForwardButtonText(name, text); + } + + [DllImport(DllLib)] + private static extern void uv_setEmbeddedToolbarTitleText(string name, string text); + public static void SetEmbeddedToolbarTitleText(string name, string text) { + CheckPlatform(); + uv_setEmbeddedToolbarTitleText(name, text); + } + + [DllImport(DllLib)] + private static extern void uv_setEmbeddedToolbarBackgroundColor(string name, float r, float g, float b, float a); + public static void SetEmbeddedToolbarBackgroundColor(string name, Color color) { + CheckPlatform(); + uv_setEmbeddedToolbarBackgroundColor(name, color.r, color.g, color.b, color.a); + } + + [DllImport(DllLib)] + private static extern void uv_setEmbeddedToolbarButtonTextColor(string name, float r, float g, float b, float a); + public static void SetEmbeddedToolbarButtonTextColor(string name, Color color) { + CheckPlatform(); + uv_setEmbeddedToolbarButtonTextColor(name, color.r, color.g, color.b, color.a); + } + + [DllImport(DllLib)] + private static extern void uv_setEmbeddedToolbarTitleTextColor(string name, float r, float g, float b, float a); + public static void SetEmbeddedToolbarTitleTextColor(string name, Color color) { + CheckPlatform(); + uv_setEmbeddedToolbarTitleTextColor(name, color.r, color.g, color.b, color.a); + } + + [DllImport(DllLib)] + private static extern void uv_setEmbeddedToolbarNavigationButtonsShow(string name, bool show); + public static void SetEmeddedToolbarNavigationButtonsShow(string name, bool show) { + CheckPlatform(); + uv_setEmbeddedToolbarNavigationButtonsShow(name, show); + } + + #region Deprecated + + [DllImport(DllLib)] + private static extern void uv_setShowToolbar(string name, bool show, bool animated, bool onTop, bool adjustInset); + public static void SetShowToolbar(string name, bool show, bool animated, bool onTop, bool adjustInset) { + CheckPlatform(); + uv_setShowToolbar(name, show, animated, onTop, adjustInset); + } + + [DllImport(DllLib)] + private static extern void uv_setShowToolbarNavigationButtons(string name, bool show); + public static void SetShowToolbarNavigationButtons(string name, bool show) { + CheckPlatform(); + uv_setShowToolbarNavigationButtons(name, show); + } + + [DllImport(DllLib)] + private static extern void uv_setToolbarDoneButtonText(string name, string text); + public static void SetToolbarDoneButtonText(string name, string text) { + CheckPlatform(); + uv_setToolbarDoneButtonText(name, text); + } + + [DllImport(DllLib)] + private static extern void uv_setGoBackButtonText(string name, string text); + public static void SetToolbarGoBackButtonText(string name, string text) { + CheckPlatform(); + uv_setGoBackButtonText(name, text); + } + + [DllImport(DllLib)] + private static extern void uv_setGoForwardButtonText(string name, string text); + public static void SetToolbarGoForwardButtonText(string name, string text) { + CheckPlatform(); + uv_setGoForwardButtonText(name, text); + } + + + [DllImport(DllLib)] + private static extern void uv_setToolbarTintColor(string name, float r, float g, float b); + public static void SetToolbarTintColor(string name, float r, float g, float b) { + CheckPlatform(); + uv_setToolbarTintColor(name, r, g, b); + } + + [DllImport(DllLib)] + private static extern void uv_setToolbarTextColor(string name, float r, float g, float b); + public static void SetToolbarTextColor(string name, float r, float g, float b) { + CheckPlatform(); + uv_setToolbarTextColor(name, r, g, b); + } + + #endregion + + public static void CheckPlatform() { + if (!correctPlatform) { + throw new System.InvalidOperationException( + "Method can only be performed on correct platform. Current: " + Application.platform + ); + } + } +} +#endif \ No newline at end of file diff --git a/Runtime/GuruWebview/UniWebView5/UniWebView/Interface/UniWebViewCocoa.cs.meta b/Runtime/GuruWebview/UniWebView5/UniWebView/Interface/UniWebViewCocoa.cs.meta new file mode 100644 index 0000000..d0bce83 --- /dev/null +++ b/Runtime/GuruWebview/UniWebView5/UniWebView/Interface/UniWebViewCocoa.cs.meta @@ -0,0 +1,12 @@ +fileFormatVersion: 2 +guid: 2e905ba0b47304f4cbf9e3e3345f84eb +timeCreated: 1492400358 +licenseType: Free +MonoImporter: + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/GuruWebview/UniWebView5/UniWebView/Interface/UniWebViewPlaceholder.cs b/Runtime/GuruWebview/UniWebView5/UniWebView/Interface/UniWebViewPlaceholder.cs new file mode 100644 index 0000000..95e165a --- /dev/null +++ b/Runtime/GuruWebview/UniWebView5/UniWebView/Interface/UniWebViewPlaceholder.cs @@ -0,0 +1,109 @@ +#if UNITY_EDITOR_WIN || UNITY_EDITOR_LINUX || (!UNITY_EDITOR_OSX && !UNITY_STANDALONE_OSX && !UNITY_IOS && !UNITY_ANDROID) + +using UnityEngine; + +public class UniWebViewInterface { + + private static bool alreadyLoggedWarning = false; + + public static void StopOMIDAdSession(string name) { CheckPlatform(); } + public static void InitOMSDK(string jsonSettings, string omidJSServiceContent) { CheckPlatform(); } + public static void InitOMSDKSession(string resourceUrl) { CheckPlatform(); } + public static void StartImpression(string resourceUrl) { CheckPlatform(); } + public static void StopImpression(string resourceUrl) { CheckPlatform(); } + + public static void SetLogLevel(int level) { CheckPlatform(); } + public static void Init(string name, int x, int y, int width, int height) { CheckPlatform(); } + public static void Destroy(string name) { CheckPlatform(); } + public static void Load(string name, string url, bool skipEncoding, string readAccessURL) { CheckPlatform(); } + public static void LoadHTMLString(string name, string html, string baseUrl, bool skipEncoding) { CheckPlatform(); } + public static void Reload(string name) { CheckPlatform(); } + public static void Stop(string name) { CheckPlatform(); } + public static string GetUrl(string name) { CheckPlatform(); return ""; } + public static void SetFrame(string name, int x, int y, int width, int height) { CheckPlatform(); } + public static void SetPosition(string name, int x, int y) { CheckPlatform(); } + public static void SetSize(string name, int width, int height) { CheckPlatform(); } + public static bool Show(string name, bool fade, int edge, float duration, bool useAsync, string identifier) { CheckPlatform(); return false; } + public static bool Hide(string name, bool fade, int edge, float duration, bool useAsync, string identifier) { CheckPlatform(); return false; } + public static bool AnimateTo(string name, int x, int y, int width, int height, float duration, float delay, string identifier) { CheckPlatform(); return false; } + public static void AddJavaScript(string name, string jsString, string identifier) { CheckPlatform(); } + public static void EvaluateJavaScript(string name, string jsString, string identifier) { CheckPlatform(); } + public static void AddUrlScheme(string name, string scheme) { CheckPlatform(); } + public static void RemoveUrlScheme(string name, string scheme) { CheckPlatform(); } + public static void AddSslExceptionDomain(string name, string domain) { CheckPlatform(); } + public static void RemoveSslExceptionDomain(string name, string domain) { CheckPlatform(); } + public static void SetHeaderField(string name, string key, string value) { CheckPlatform(); } + public static void SetUserAgent(string name, string userAgent) { CheckPlatform(); } + public static string GetUserAgent(string name) { CheckPlatform(); return ""; } + public static void SetAllowAutoPlay(bool flag) { CheckPlatform(); } + public static void SetAllowInlinePlay(bool flag) { CheckPlatform(); } + public static void SetAllowJavaScriptOpenWindow(bool flag) { CheckPlatform(); } + public static void SetAllowFileAccess(string name, bool flag) { CheckPlatform(); } + public static void SetAllowFileAccessFromFileURLs(string name, bool flag) { CheckPlatform(); } + public static void SetAllowUniversalAccessFromFileURLs(bool flag) { CheckPlatform(); } + public static void SetJavaScriptEnabled(bool flag) { CheckPlatform(); } + public static void CleanCache(string name) { CheckPlatform(); } + public static void ClearCookies() { CheckPlatform(); } + public static void SetCookie(string url, string cookie, bool skipEncoding) { CheckPlatform(); } + public static void RemoveCookies(string url, bool skipEncoding) { CheckPlatform(); } + public static void RemoveCookie(string url, string key, bool skipEncoding) { CheckPlatform(); } + public static string GetCookie(string url, string key, bool skipEncoding) { CheckPlatform(); return ""; } + public static void ClearHttpAuthUsernamePassword(string host, string realm) { CheckPlatform(); } + public static void SetBackgroundColor(string name, float r, float g, float b, float a) { CheckPlatform(); } + public static void SetWebViewAlpha(string name, float alpha) { CheckPlatform(); } + public static float GetWebViewAlpha(string name) { CheckPlatform(); return 0.0f; } + public static void SetShowSpinnerWhileLoading(string name, bool show) { CheckPlatform(); } + public static void SetSpinnerText(string name, string text) { CheckPlatform(); } + public static bool CanGoBack(string name) { CheckPlatform(); return false; } + public static bool CanGoForward(string name) { CheckPlatform(); return false; } + public static void GoBack(string name) { CheckPlatform(); } + public static void GoForward(string name) { CheckPlatform(); } + public static void SetOpenLinksInExternalBrowser(string name, bool flag) { CheckPlatform(); } + public static void SetHorizontalScrollBarEnabled(string name, bool enabled) { CheckPlatform(); } + public static void SetVerticalScrollBarEnabled(string name, bool enabled) { CheckPlatform(); } + public static void SetBouncesEnabled(string name, bool enabled) { CheckPlatform(); } + public static void SetZoomEnabled(string name, bool enabled) { CheckPlatform(); } + public static void SetShowToolbar(string name, bool show, bool animated, bool onTop, bool adjustInset) { CheckPlatform(); } + public static void SetToolbarDoneButtonText(string name, string text) { CheckPlatform(); } + public static void SetToolbarGoBackButtonText(string name, string text) { CheckPlatform(); } + public static void SetToolbarGoForwardButtonText(string name, string text) { CheckPlatform(); } + public static void SetToolbarTintColor(string name, float r, float g, float b) { CheckPlatform(); } + public static void SetToolbarTextColor(string name, float r, float g, float b) { CheckPlatform(); } + public static void SetUserInteractionEnabled(string name, bool enabled) { CheckPlatform(); } + public static void SetTransparencyClickingThroughEnabled(string name, bool enabled) { CheckPlatform(); } + public static void SetWebContentsDebuggingEnabled(bool enabled) { CheckPlatform(); } + public static void SetAllowHTTPAuthPopUpWindow(string name, bool flag) { CheckPlatform(); } + public static void Print(string name) { CheckPlatform(); } + public static void CaptureSnapshot(string name, string filename) { CheckPlatform(); } + public static void SetCalloutEnabled(string name, bool flag) { CheckPlatform(); } + public static void SetSupportMultipleWindows(string name, bool enabled, bool allowJavaScriptOpening) { CheckPlatform(); } + public static void SetDragInteractionEnabled(string name, bool flag) { CheckPlatform(); } + public static void ScrollTo(string name, int x, int y, bool animated) { CheckPlatform(); } + public static float NativeScreenWidth() { CheckPlatform(); return 0.0f; } + public static float NativeScreenHeight() { CheckPlatform(); return 0.0f; } + public static void SafeBrowsingInit(string name, string url) { CheckPlatform(); } + public static void SafeBrowsingSetToolbarColor(string name, float r, float g, float b) { CheckPlatform(); } + public static void SafeBrowsingShow(string name) { CheckPlatform(); } + public static bool IsAuthenticationIsSupported() { CheckPlatform(); return false; } + public static void AuthenticationInit(string name, string url, string scheme) { CheckPlatform(); } + public static void AuthenticationStart(string name) { CheckPlatform(); } + public static void AuthenticationSetPrivateMode(string name, bool enabled) { CheckPlatform(); } + public static void SetShowEmbeddedToolbar(string name, bool show) { CheckPlatform(); } + public static void SetEmbeddedToolbarOnTop(string name, bool top) { CheckPlatform(); } + public static void SetEmbeddedToolbarDoneButtonText(string name, string text) { CheckPlatform(); } + public static void SetEmbeddedToolbarGoBackButtonText(string name, string text) { CheckPlatform(); } + public static void SetEmbeddedToolbarGoForwardButtonText(string name, string text) { CheckPlatform(); } + public static void SetEmbeddedToolbarTitleText(string name, string text) { CheckPlatform(); } + public static void SetEmbeddedToolbarBackgroundColor(string name, Color color) { CheckPlatform(); } + public static void SetEmbeddedToolbarButtonTextColor(string name, Color color) { CheckPlatform(); } + public static void SetEmbeddedToolbarTitleTextColor(string name, Color color) { CheckPlatform(); } + public static void SetEmeddedToolbarNavigationButtonsShow(string name, bool show) { CheckPlatform(); } + + public static void CheckPlatform() { + if (!alreadyLoggedWarning) { + alreadyLoggedWarning = true; + Debug.LogWarning("UniWebView only supports iOS/Android/macOS Editor. You current platform " + Application.platform + " is not supported."); + } + } +} +#endif \ No newline at end of file diff --git a/Runtime/GuruWebview/UniWebView5/UniWebView/Interface/UniWebViewPlaceholder.cs.meta b/Runtime/GuruWebview/UniWebView5/UniWebView/Interface/UniWebViewPlaceholder.cs.meta new file mode 100644 index 0000000..c65d504 --- /dev/null +++ b/Runtime/GuruWebview/UniWebView5/UniWebView/Interface/UniWebViewPlaceholder.cs.meta @@ -0,0 +1,12 @@ +fileFormatVersion: 2 +guid: 0b3bad20d12b1433ab8927c3effc605b +timeCreated: 1497403102 +licenseType: Free +MonoImporter: + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/GuruWebview/UniWebView5/UniWebView/Prefab.meta b/Runtime/GuruWebview/UniWebView5/UniWebView/Prefab.meta new file mode 100644 index 0000000..b8b3163 --- /dev/null +++ b/Runtime/GuruWebview/UniWebView5/UniWebView/Prefab.meta @@ -0,0 +1,9 @@ +fileFormatVersion: 2 +guid: d5d657f2ee1114e20bccaace74235a99 +folderAsset: yes +timeCreated: 1491898971 +licenseType: Free +DefaultImporter: + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/GuruWebview/UniWebView5/UniWebView/Prefab/UniWebView.prefab b/Runtime/GuruWebview/UniWebView5/UniWebView/Prefab/UniWebView.prefab new file mode 100644 index 0000000..c52ce82 --- /dev/null +++ b/Runtime/GuruWebview/UniWebView5/UniWebView/Prefab/UniWebView.prefab @@ -0,0 +1,60 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!1 &1900085666445226 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 4960404783511462} + - component: {fileID: 114939446366399424} + m_Layer: 0 + m_Name: UniWebView + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!4 &4960404783511462 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1900085666445226} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 0} + m_RootOrder: 0 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!114 &114939446366399424 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1900085666445226} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 598e18fb001004a81960f552978ecf4e, type: 3} + m_Name: + m_EditorClassIdentifier: + urlOnStart: + showOnStart: 1 + fullScreen: 1 + useToolbar: 0 + toolbarPosition: 0 + useEmbeddedToolbar: 1 + embeddedToolbarPosition: 0 + frame: + serializedVersion: 2 + x: 0 + y: 0 + width: 0 + height: 0 + referenceRectTransform: {fileID: 0} diff --git a/Runtime/GuruWebview/UniWebView5/UniWebView/Prefab/UniWebView.prefab.meta b/Runtime/GuruWebview/UniWebView5/UniWebView/Prefab/UniWebView.prefab.meta new file mode 100644 index 0000000..28ac7bd --- /dev/null +++ b/Runtime/GuruWebview/UniWebView5/UniWebView/Prefab/UniWebView.prefab.meta @@ -0,0 +1,9 @@ +fileFormatVersion: 2 +guid: 7e3f16a6f6303419cbd9837f6c746de4 +timeCreated: 1496204510 +licenseType: Free +NativeFormatImporter: + mainObjectFileID: 100100000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/GuruWebview/UniWebView5/UniWebView/Prefab/UniWebViewSafeBrowsing.prefab b/Runtime/GuruWebview/UniWebView5/UniWebView/Prefab/UniWebViewSafeBrowsing.prefab new file mode 100644 index 0000000..08e434f --- /dev/null +++ b/Runtime/GuruWebview/UniWebView5/UniWebView/Prefab/UniWebViewSafeBrowsing.prefab @@ -0,0 +1,46 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!1 &8236771505572270655 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 8236771505572270653} + - component: {fileID: 8236771505572270654} + m_Layer: 0 + m_Name: UniWebViewSafeBrowsing + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!4 &8236771505572270653 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 8236771505572270655} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_Children: [] + m_Father: {fileID: 0} + m_RootOrder: 0 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!114 &8236771505572270654 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 8236771505572270655} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 5843488507315421aa0a7d92c0604d10, type: 3} + m_Name: + m_EditorClassIdentifier: + url: diff --git a/Runtime/GuruWebview/UniWebView5/UniWebView/Prefab/UniWebViewSafeBrowsing.prefab.meta b/Runtime/GuruWebview/UniWebView5/UniWebView/Prefab/UniWebViewSafeBrowsing.prefab.meta new file mode 100644 index 0000000..145400e --- /dev/null +++ b/Runtime/GuruWebview/UniWebView5/UniWebView/Prefab/UniWebViewSafeBrowsing.prefab.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 2d6c4899a63004f16bb4f791176f4ad3 +PrefabImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/GuruWebview/UniWebView5/UniWebView/README.txt b/Runtime/GuruWebview/UniWebView5/UniWebView/README.txt new file mode 100644 index 0000000..09281fb --- /dev/null +++ b/Runtime/GuruWebview/UniWebView5/UniWebView/README.txt @@ -0,0 +1,30 @@ +# UniWebView + +Thank you for purchasing UniWebView. + +UniWebView is a Unity3D plugin built on native iOS/Android technology. It helps your users to enjoy web content and +interact with your game through the web views. + +## Documentation + +To get started, please visit our documentation site: [https://docs.uniwebview.com](https://docs.uniwebview.com). You +could find step-by-step guides on how to import UniWebView to your project, as well as some basic usage of this asset. +You could also find a full script reference on the same site in this page: [https://docs.uniwebview.com/api/](https://docs.uniwebview.com/api/). + +## Upgrading + +All purchased customers could get all updates for the same major version for free. Please check the place you have +purchased this asset to see whether there is an update or not. A release note is also contained in this asset, in the +"CHANGELOG.md" file. You could also find the same version list in [this page](https://docs.uniwebview.com/release-note). + +## Getting Support + +For frequently asked questions, we have a page to answer them. If you encountered any problems while using UniWebView, +we strongly suggest to visit the [FAQ page](https://docs.uniwebview.com/guide/faq.html) first. Also feel free to +[submit a ticket](https://onevcat.atlassian.net/servicedesk/customer/portal/2) if you cannot find a solution, we will +do our best to get you out! + +## Other + +For more information, please visit the [official web site](https://uniwebview.com) of UniWebView, or contact us through +a ticket. diff --git a/Runtime/GuruWebview/UniWebView5/UniWebView/README.txt.meta b/Runtime/GuruWebview/UniWebView5/UniWebView/README.txt.meta new file mode 100644 index 0000000..94a494f --- /dev/null +++ b/Runtime/GuruWebview/UniWebView5/UniWebView/README.txt.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 95440ce70c5e14d6e96e166c45f7cf6d +timeCreated: 1499302025 +licenseType: Free +TextScriptImporter: + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/GuruWebview/UniWebView5/UniWebView/Script.meta b/Runtime/GuruWebview/UniWebView5/UniWebView/Script.meta new file mode 100644 index 0000000..80a72b1 --- /dev/null +++ b/Runtime/GuruWebview/UniWebView5/UniWebView/Script.meta @@ -0,0 +1,9 @@ +fileFormatVersion: 2 +guid: b34f36f9836464e04893f632434d0862 +folderAsset: yes +timeCreated: 1491898971 +licenseType: Free +DefaultImporter: + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/GuruWebview/UniWebView5/UniWebView/Script/UniWebView.cs b/Runtime/GuruWebview/UniWebView5/UniWebView/Script/UniWebView.cs new file mode 100644 index 0000000..cf6f398 --- /dev/null +++ b/Runtime/GuruWebview/UniWebView5/UniWebView/Script/UniWebView.cs @@ -0,0 +1,1843 @@ +// +// UniWebView.cs +// Created by Wang Wei (@onevcat) on 2017-04-11. +// +// This file is a part of UniWebView Project (https://uniwebview.com) +// By purchasing the asset, you are allowed to use this code in as many as projects +// you want, only if you publish the final products under the name of the same account +// used for the purchase. +// +// This asset and all corresponding files (such as source code) are provided on an +// “as is” basis, without warranty of any kind, express of 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 action of contract, tort or otherwise, +// arising from, out of or in connection with the software or the use of other dealing in the software. +// +using UnityEngine; +using System.Collections; +using System.Collections.Generic; +using System; + +/// +/// Main class of UniWebView. Any `GameObject` instance with this script can represent a webview object in the scene. +/// Use this class to create, load, show and interact with a general-purpose web view. +/// +public class UniWebView: MonoBehaviour { + /// + /// Delegate for page started event. + /// + /// The web view component which raises this event. + /// The url which the web view is about to load. + public delegate void PageStartedDelegate(UniWebView webView, string url); + + /// + /// Raised when the web view starts loading a url. + /// + /// This event will be invoked for both url loading with `Load` method or by a link navigating from page. + /// + public event PageStartedDelegate OnPageStarted; + + /// + /// Delegate for page finished event. + /// + /// The web view component which raises this event. + /// HTTP status code received from response. + /// The url which the web view loaded. + public delegate void PageFinishedDelegate(UniWebView webView, int statusCode, string url); + /// + /// Raised when the web view finished to load a url successfully. + /// + /// This method will be invoked when a valid response received from the url, regardless of the response status. + /// If a url loading fails before reaching to the server and getting a response, `OnPageErrorReceived` will be + /// raised instead. + /// + public event PageFinishedDelegate OnPageFinished; + + /// + /// Delegate for page error received event. + /// + /// The web view component which raises this event. + /// + /// The error code which indicates the error type. + /// It can be different from systems and platforms. + /// + /// The error message which indicates the error. + public delegate void PageErrorReceivedDelegate(UniWebView webView, int errorCode, string errorMessage); + /// + /// Raised when an error encountered during the loading process. + /// Such as the "host not found" error or "no Internet connection" error will raise this event. + /// + public event PageErrorReceivedDelegate OnPageErrorReceived; + + /// + /// Delegate for page progress changed event. + /// + /// The web view component which raises this event. + /// A value indicates the loading progress of current page. It is a value between 0.0f and 1.0f. + public delegate void PageProgressChangedDelegate(UniWebView webView, float progress); + /// + /// Raised when the loading progress value changes in current web view. + /// + public event PageProgressChangedDelegate OnPageProgressChanged; + + /// + /// Delegate for message received event. + /// + /// The web view component which raises this event. + /// Message received from web view. + public delegate void MessageReceivedDelegate(UniWebView webView, UniWebViewMessage message); + /// + /// Raised when a message from web view is received. + /// + /// Generally, the message comes from a navigation to + /// a scheme which is observed by current web view. You could use `AddUrlScheme` and + /// `RemoveUrlScheme` to manipulate the scheme list. + /// + /// "uniwebview://" scheme is default in the list, so a clicking on link starting with "uniwebview://" + /// will raise this event, if it is not removed. + /// + public event MessageReceivedDelegate OnMessageReceived; + + /// + /// Delegate for should close event. + /// + /// The web view component which raises this event. + /// Whether the web view should be closed and destroyed. + public delegate bool ShouldCloseDelegate(UniWebView webView); + /// + /// Raised when the web view is about to close itself. + /// + /// This event is raised when the users close the web view by Back button on Android, Done button on iOS, + /// or Close button on Unity Editor. It gives a chance to make final decision whether the web view should + /// be closed and destroyed. You can also clean all related resources you created (such as a reference to + /// the web view) in this event. + /// + public event ShouldCloseDelegate OnShouldClose; + + /// + /// Delegate for orientation changed event. + /// + /// The web view component which raises this event. + /// The screen orientation for current state. + public delegate void OrientationChangedDelegate(UniWebView webView, ScreenOrientation orientation); + /// + /// Raised when the screen orientation is changed. It is a good time to set the web view frame if you + /// need to support multiple orientations in your game. + /// + public event OrientationChangedDelegate OnOrientationChanged; + + /// + /// Delegate for content loading terminated event. + /// + /// The web view component which raises this event. + public delegate void OnWebContentProcessTerminatedDelegate(UniWebView webView); + /// + /// Raised when on iOS, when system calls `webViewWebContentProcessDidTerminate` method. + /// It is usually due to a low memory when loading the web content and leave you a blank white screen. + /// You need to free as much as memory you could and then do a page reload. + /// + public event OnWebContentProcessTerminatedDelegate OnWebContentProcessTerminated; + + /// + /// Delegate for file download task starting event. + /// + /// The web view component which raises this event. + /// The remote URL of this download task. This is also the download URL for the task. + /// The file name which user chooses to use. + public delegate void FileDownloadStarted(UniWebView webView, string remoteUrl, string fileName); + /// + /// Raised when a file download task starts. + /// + public event FileDownloadStarted OnFileDownloadStarted; + + /// + /// Delegate for file download task finishing event. + /// + /// The web view component which raises this event. + /// + /// The error code of the download task result. Value `0` means the download finishes without a problem. + /// Any other non-`0` value indicates an issue. The detail meaning of the error code depends on system. + /// On iOS, it is usually the `errorCode` of the received `NSURLError`. On Android, the value usually represents + /// an `ERROR_*` value in `DownloadManager`. + /// + /// The remote URL of this download task. + /// + /// The file path of the downloaded file. On iOS, the downloader file is in a temporary folder of your app sandbox. + /// On Android, it is in the "Download" folder of your app. + /// + public delegate void FileDownloadFinished(UniWebView webView, int errorCode, string remoteUrl, string diskPath); + /// + /// Raised when a file download task finishes with either an error or success. + /// + public event FileDownloadFinished OnFileDownloadFinished; + + /// + /// Delegate for capturing snapshot finished event. + /// + /// The web view component which raises this event. + /// + /// The error code of the event. If the snapshot is captured and stored without a problem, the error code is 0. + /// Any other number indicates an error happened. In most cases, the screenshot capturing only fails due to lack + /// of disk storage. + /// + /// + /// An accessible disk path to the captured snapshot image. If an error happens, it is an empty string. + /// + public delegate void CaptureSnapshotFinished(UniWebView webView, int errorCode, string diskPath); + /// + /// Raised when an image captured and stored in a cache path on disk. + /// + public event CaptureSnapshotFinished OnCaptureSnapshotFinished; + + /// + /// Delegate for multiple window opening event. + /// + /// The web view component which opens the new multiple (pop-up) window. + /// The identifier of the opened new window. + public delegate void MultipleWindowOpenedDelegate(UniWebView webView, string multipleWindowId); + /// + /// Raised when a new window is opened. This happens when you enable the `SetSupportMultipleWindows` and open a + /// new pop-up window. + /// + public event MultipleWindowOpenedDelegate OnMultipleWindowOpened; + + /// + /// Delegate for multiple window closing event. + /// + /// The web view component which closes the multiple window. + /// The identifier of the closed window. + public delegate void MultipleWindowClosedDelegate(UniWebView webView, string multipleWindowId); + /// + /// Raised when the multiple window is closed. This happens when the pop-up window is closed by navigation operation + /// or by a invocation of `close()` on the page. + /// + public event MultipleWindowClosedDelegate OnMultipleWindowClosed; + + private string id = Guid.NewGuid().ToString(); + private UniWebViewNativeListener listener; + + /// + /// Represents the embedded toolbar in the current web view. + /// + public UniWebViewEmbeddedToolbar EmbeddedToolbar { get; private set; } + + private bool isPortrait; + [SerializeField] + + #pragma warning disable 0649 + private string urlOnStart; + [SerializeField] + private bool showOnStart = false; + + [SerializeField] + private bool fullScreen; + + [Obsolete("Use Toolbar is deprecated. Use the embedded toolbar instead.", false)] + [SerializeField] + private bool useToolbar; + + [Obsolete("Use Toolbar is deprecated. Use the embedded toolbar instead.", false)] + [SerializeField] + private UniWebViewToolbarPosition toolbarPosition; + + [SerializeField] + private bool useEmbeddedToolbar; + + [SerializeField] + private UniWebViewToolbarPosition embeddedToolbarPosition; + + #pragma warning restore 0649 + + // Action callback holders + private Dictionary actions = new Dictionary(); + private Dictionary> payloadActions = new Dictionary>(); + + [SerializeField] + private Rect frame; + /// + /// Gets or sets the frame of current web view. The value is based on current `Screen.width` and `Screen.height`. + /// The first two values of `Rect` is `x` and `y` position and the followed two `width` and `height`. + /// + public Rect Frame { + get { return frame; } + set { + frame = value; + UpdateFrame(); + } + } + + [SerializeField] + private RectTransform referenceRectTransform; + /// + /// A reference rect transform which the web view should change its position and size to. + /// Set it to a Unity UI element (which contains a `RectTransform`) under a canvas to determine + /// the web view frame by a certain UI element. + /// + /// By using this, you could get benefit from [Multiple Resolutions UI](https://docs.unity3d.com/Manual/HOWTO-UIMultiResolution.html). + /// + /// + public RectTransform ReferenceRectTransform { + get { + return referenceRectTransform; + } + set { + referenceRectTransform = value; + UpdateFrame(); + } + } + + private bool started; + + private bool backButtonEnabled = true; + + /// + /// The url of current loaded web page. + /// + public string Url { + get { return UniWebViewInterface.GetUrl(listener.Name); } + } + + /// + /// Updates and sets current frame of web view to match the setting. + /// + /// This is useful if the `referenceRectTransform` is changed and you need to sync the frame change + /// to the web view. This method follows the frame determining rules. + /// + public void UpdateFrame() { + Rect rect = NextFrameRect(); + UniWebViewInterface.SetFrame(listener.Name, (int)rect.x, (int)rect.y, (int)rect.width, (int)rect.height); + } + + Rect NextFrameRect() { + if (referenceRectTransform == null) { + UniWebViewLogger.Instance.Info("Using Frame setting to determine web view frame."); + return frame; + } else { + UniWebViewLogger.Instance.Info("Using reference RectTransform to determine web view frame."); + var worldCorners = new Vector3[4]; + + referenceRectTransform.GetWorldCorners(worldCorners); + + var bottomLeft = worldCorners[0]; + var topLeft = worldCorners[1]; + var topRight = worldCorners[2]; + var bottomRight = worldCorners[3]; + + var canvas = referenceRectTransform.GetComponentInParent(); + if (canvas == null) { + return frame; + } + + switch (canvas.renderMode) { + case RenderMode.ScreenSpaceOverlay: + break; + case RenderMode.ScreenSpaceCamera: + case RenderMode.WorldSpace: + var camera = canvas.worldCamera; + if (camera == null) { + UniWebViewLogger.Instance.Critical(@"You need a render camera + or event camera to use RectTransform to determine correct + frame for UniWebView."); + UniWebViewLogger.Instance.Info("No camera found. Fall back to ScreenSpaceOverlay mode."); + } else { + bottomLeft = camera.WorldToScreenPoint(bottomLeft); + topLeft = camera.WorldToScreenPoint(topLeft); + topRight = camera.WorldToScreenPoint(topRight); + bottomRight = camera.WorldToScreenPoint(bottomRight); + } + break; + } + + float widthFactor = (float)UniWebViewInterface.NativeScreenWidth() / (float)Screen.width; + float heightFactor = (float)UniWebViewInterface.NativeScreenHeight() / (float)Screen.height; + + float x = topLeft.x * widthFactor; + float y = (Screen.height - topLeft.y) * heightFactor; + float width = (bottomRight.x - topLeft.x) * widthFactor; + float height = (topLeft.y - bottomRight.y) * heightFactor; + return new Rect(x, y, width, height); + } + } + + void Awake() { + var listenerObject = new GameObject(id); + listener = listenerObject.AddComponent(); + listenerObject.transform.parent = transform; + listener.webView = this; + UniWebViewNativeListener.AddListener(listener); + + EmbeddedToolbar = new UniWebViewEmbeddedToolbar(listener); + + Rect rect; + if (fullScreen) { + rect = new Rect(0, 0, Screen.width, Screen.height); + } else { + rect = NextFrameRect(); + } + + UniWebViewInterface.Init(listener.Name, (int)rect.x, (int)rect. y, (int)rect.width, (int)rect.height); + isPortrait = Screen.height >= Screen.width; + } + + void Start() { + if (showOnStart) { + Show(); + } + + if (useEmbeddedToolbar) { + EmbeddedToolbar.SetPosition(embeddedToolbarPosition); + EmbeddedToolbar.Show(); + } + + if (!string.IsNullOrEmpty(urlOnStart)) { + Load(urlOnStart); + } + started = true; + if (referenceRectTransform != null) { + UpdateFrame(); + } + } + + void Update() { + var newIsPortrait = Screen.height >= Screen.width; + if (isPortrait != newIsPortrait) { + isPortrait = newIsPortrait; + if (OnOrientationChanged != null) { + OnOrientationChanged(this, Screen.orientation); + } + if (referenceRectTransform != null) { + UpdateFrame(); + } + } + + // Only the new input system is enabled. Related flags: https://docs.unity3d.com/Packages/com.unity.inputsystem@1.0/manual/Installation.html#enabling-the-new-input-backends + // + // The new input system is not handling touchscreen events nicely as the old one. + // The gesture detection hangs out regularly. Wait for an improvement of Unity. + // So we choose to use the old one whenever it is available. + #if ENABLE_INPUT_SYSTEM && !ENABLE_LEGACY_INPUT_MANAGER + var backDetected = backButtonEnabled && UnityEngine.InputSystem.Keyboard.current.escapeKey.wasPressedThisFrame; + #else + var backDetected = backButtonEnabled && Input.GetKeyUp(KeyCode.Escape); + #endif + + if (backDetected) { + UniWebViewLogger.Instance.Info("Received Back button, handling GoBack or close web view."); + if (CanGoBack) { + GoBack(); + } else { + InternalOnShouldClose(); + } + } + } + + void OnEnable() { + if (started) { + _Show(useAsync: true); + } + } + + void OnDisable() { + if (started) { + _Hide(useAsync: true); + } + } + + /// + /// Whether the web view is supported in current runtime or not. + /// + /// On some certain Android customized builds, the manufacturer prefers not containing the web view package in the + /// system or blocks the web view package from being installed. If this happens, using of any web view related APIs will + /// throw a `MissingWebViewPackageException` exception. + /// + /// Use this method to check whether the web view is available on the current running system. If this parameter returns `false`, + /// you should not use the web view. + /// + /// This property always returns `true` on other supported platforms, such as iOS or macOS editor. It only performs + /// runtime checking on Android. On other not supported platforms such as Windows or Linux, it always returns `false`. + /// + /// Returns `true` if web view is supported on the current platform. Otherwise, `false`. + public static bool IsWebViewSupported { + get { + #if UNITY_EDITOR_OSX + return true; + #elif UNITY_EDITOR + return false; + #elif UNITY_IOS + return true; + #elif UNITY_STANDALONE_OSX + return true; + #elif UNITY_ANDROID + return UniWebViewInterface.IsWebViewSupported(); + #else + return false; + #endif + } + } + + /// + /// Loads a url in current web view. + /// + /// The url to be loaded. This url should start with `http://` or `https://` scheme. You could even load a non-ascii url text if it is valid. + /// + /// Whether UniWebView should skip encoding the url or not. If set to `false`, UniWebView will try to encode the url parameter before + /// loading it. Otherwise, your original url string will be used as the url if it is valid. Default is `false`. + /// + /// + /// The URL to allow read access to. This parameter is only used when loading from the filesystem in iOS, and passed + /// to `loadFileURL:allowingReadAccessToURL:` method of WebKit. By default, the parent folder of the `url` parameter will be read accessible. + /// + public void Load(string url, bool skipEncoding = false, string readAccessURL = null) { + UniWebViewInterface.Load(listener.Name, url, skipEncoding, readAccessURL); + } + + /// + /// Loads an HTML string in current web view. + /// + /// The HTML string to use as the contents of the webpage. + /// The url to use as the page's base url. + /// + /// Whether UniWebView should skip encoding the baseUrl or not. If set to `false`, UniWebView will try to encode the baseUrl parameter before + /// using it. Otherwise, your original url string will be used as the baseUrl if it is valid. Default is `false`. + /// + public void LoadHTMLString(string htmlString, string baseUrl, bool skipEncoding = false) { + UniWebViewInterface.LoadHTMLString(listener.Name, htmlString, baseUrl, skipEncoding); + } + + /// + /// Stop current OMID ad session + /// + public void StopOMIDAdSession() + { + UniWebViewInterface.StopOMIDAdSession(listener.Name); + } + + /// + /// Reloads the current page. + /// + public void Reload() { + UniWebViewInterface.Reload(listener.Name); + } + + /// + /// Stops loading all resources on the current page. + /// + public void Stop() { + UniWebViewInterface.Stop(listener.Name); + } + + /// + /// Gets whether there is a back page in the back-forward list that can be navigated to. + /// + public bool CanGoBack { + get { + return UniWebViewInterface.CanGoBack(listener.Name); + } + } + + /// + /// Gets whether there is a forward page in the back-forward list that can be navigated to. + /// + public bool CanGoForward { + get { + return UniWebViewInterface.CanGoForward(listener.Name); + } + } + + /// + /// Navigates to the back item in the back-forward list. + /// + public void GoBack() { + UniWebViewInterface.GoBack(listener.Name); + } + + /// + /// Navigates to the forward item in the back-forward list. + /// + public void GoForward() { + UniWebViewInterface.GoForward(listener.Name); + } + + /// + /// Sets whether the link clicking in the web view should open the page in an external browser. + /// + /// The flag indicates whether a link should be opened externally. + public void SetOpenLinksInExternalBrowser(bool flag) { + UniWebViewInterface.SetOpenLinksInExternalBrowser(listener.Name, flag); + } + + /// + /// Sets the web view visible on screen. + /// + /// If you pass `false` and `UniWebViewTransitionEdge.None` to the first two parameters, it means no animation will + /// be applied when showing. So the `duration` parameter will not be taken into account. Otherwise, when + /// either or both `fade` and `edge` set, the showing operation will be animated. + /// + /// Regardless of there is an animation or not, the `completionHandler` will be called if not `null` when the web + /// view showing finishes. + /// + /// Whether show with a fade in animation. Default is `false`. + /// The edge from which the web view showing. It simulates a modal effect when showing a web view. Default is `UniWebViewTransitionEdge.None`. + /// Duration of the showing animation. Default is `0.4f`. + /// Completion handler which will be called when showing finishes. Default is `null`. + /// A bool value indicates whether the showing operation started. + public bool Show(bool fade = false, UniWebViewTransitionEdge edge = UniWebViewTransitionEdge.None, + float duration = 0.4f, Action completionHandler = null) + { + return _Show(fade, edge, duration, false, completionHandler); + } + + public bool _Show(bool fade = false, UniWebViewTransitionEdge edge = UniWebViewTransitionEdge.None, + float duration = 0.4f, bool useAsync = false, Action completionHandler = null) + { + var identifier = Guid.NewGuid().ToString(); + var showStarted = UniWebViewInterface.Show(listener.Name, fade, (int)edge, duration, useAsync, identifier); + if (showStarted && completionHandler != null) { + var hasAnimation = (fade || edge != UniWebViewTransitionEdge.None); + if (hasAnimation) { + actions.Add(identifier, completionHandler); + } else { + completionHandler(); + } + } + +#pragma warning disable 618 + if (showStarted && useToolbar) { + var top = (toolbarPosition == UniWebViewToolbarPosition.Top); + SetShowToolbar(true, false, top, fullScreen); + } +#pragma warning restore 618 + return showStarted; + } + + /// + /// Sets the web view invisible from screen. + /// + /// If you pass `false` and `UniWebViewTransitionEdge.None` to the first two parameters, it means no animation will + /// be applied when hiding. So the `duration` parameter will not be taken into account. Otherwise, when either or + /// both `fade` and `edge` set, the hiding operation will be animated. + /// + /// Regardless there is an animation or not, the `completionHandler` will be called if not `null` when the web view + /// hiding finishes. + /// + /// Whether hide with a fade in animation. Default is `false`. + /// The edge from which the web view hiding. It simulates a modal effect when hiding a web view. Default is `UniWebViewTransitionEdge.None`. + /// Duration of hiding animation. Default is `0.4f`. + /// Completion handler which will be called when hiding finishes. Default is `null`. + /// A bool value indicates whether the hiding operation started. + public bool Hide(bool fade = false, UniWebViewTransitionEdge edge = UniWebViewTransitionEdge.None, + float duration = 0.4f, Action completionHandler = null) + { + return _Hide(fade, edge, duration, false, completionHandler); + } + + public bool _Hide(bool fade = false, UniWebViewTransitionEdge edge = UniWebViewTransitionEdge.None, + float duration = 0.4f, bool useAsync = false, Action completionHandler = null) + { + var identifier = Guid.NewGuid().ToString(); + var hideStarted = UniWebViewInterface.Hide(listener.Name, fade, (int)edge, duration, useAsync, identifier); + if (hideStarted && completionHandler != null) { + var hasAnimation = (fade || edge != UniWebViewTransitionEdge.None); + if (hasAnimation) { + actions.Add(identifier, completionHandler); + } else { + completionHandler(); + } + } +#pragma warning disable 618 + if (hideStarted && useToolbar) { + var top = (toolbarPosition == UniWebViewToolbarPosition.Top); + SetShowToolbar(false, false, top, fullScreen); + } +#pragma warning restore 618 + return hideStarted; + } + + /// + /// Animates the web view from current position and size to another position and size. + /// + /// The new `Frame` which the web view should be. + /// Duration of the animation. + /// Delay before the animation begins. Default is `0.0f`, which means the animation will start immediately. + /// Completion handler which will be called when animation finishes. Default is `null`. + /// + public bool AnimateTo(Rect frame, float duration, float delay = 0.0f, Action completionHandler = null) { + var identifier = Guid.NewGuid().ToString(); + var animationStarted = UniWebViewInterface.AnimateTo(listener.Name, + (int)frame.x, (int)frame.y, (int)frame.width, (int)frame.height, duration, delay, identifier); + if (animationStarted) { + this.frame = frame; + if (completionHandler != null) { + actions.Add(identifier, completionHandler); + } + } + return animationStarted; + } + + /// + /// Adds a JavaScript to current page. + /// + /// The JavaScript code to add. It should be a valid JavaScript statement string. + /// Called when adding JavaScript operation finishes. Default is `null`. + public void AddJavaScript(string jsString, Action completionHandler = null) { + var identifier = Guid.NewGuid().ToString(); + UniWebViewInterface.AddJavaScript(listener.Name, jsString, identifier); + if (completionHandler != null) { + payloadActions.Add(identifier, completionHandler); + } + } + + /// + /// Evaluates a JavaScript string on current page. + /// + /// The JavaScript string to evaluate. + /// Called when evaluating JavaScript operation finishes. Default is `null`. + public void EvaluateJavaScript(string jsString, Action completionHandler = null) { + var identifier = Guid.NewGuid().ToString(); + UniWebViewInterface.EvaluateJavaScript(listener.Name, jsString, identifier); + if (completionHandler != null) { + payloadActions.Add(identifier, completionHandler); + } + } + + /// + /// Adds a url scheme to UniWebView message system interpreter. + /// All following url navigation to this scheme will be sent as a message to UniWebView instead. + /// + /// The url scheme to add. It should not contain "://" part. You could even add "http" and/or + /// "https" to prevent all resource loading on the page. "uniwebview" is added by default. Nothing will happen if + /// you try to add a duplicated scheme. + public void AddUrlScheme(string scheme) { + if (scheme == null) { + UniWebViewLogger.Instance.Critical("The scheme should not be null."); + return; + } + + if (scheme.Contains("://")) { + UniWebViewLogger.Instance.Critical("The scheme should not include invalid characters '://'"); + return; + } + UniWebViewInterface.AddUrlScheme(listener.Name, scheme); + } + + /// + /// Removes a url scheme from UniWebView message system interpreter. + /// + /// The url scheme to remove. Nothing will happen if the scheme is not in the message system. + public void RemoveUrlScheme(string scheme) { + if (scheme == null) { + UniWebViewLogger.Instance.Critical("The scheme should not be null."); + return; + } + if (scheme.Contains("://")) { + UniWebViewLogger.Instance.Critical("The scheme should not include invalid characters '://'"); + return; + } + UniWebViewInterface.RemoveUrlScheme(listener.Name, scheme); + } + + /// + /// Adds a domain to the SSL checking white list. + /// If you are trying to access a web site with untrusted or expired certification, + /// the web view will prevent its loading. If you could confirm that this site is trusted, + /// you can add the domain as an SSL exception, so you could visit it. + /// + /// The domain to add. It should not contain any scheme or path part in url. + public void AddSslExceptionDomain(string domain) { + if (domain == null) { + UniWebViewLogger.Instance.Critical("The domain should not be null."); + return; + } + if (domain.Contains("://")) { + UniWebViewLogger.Instance.Critical("The domain should not include invalid characters '://'"); + return; + } + UniWebViewInterface.AddSslExceptionDomain(listener.Name, domain); + } + + /// + /// Removes a domain from the SSL checking white list. + /// + /// The domain to remove. It should not contain any scheme or path part in url. + public void RemoveSslExceptionDomain(string domain) { + if (domain == null) { + UniWebViewLogger.Instance.Critical("The domain should not be null."); + return; + } + if (domain.Contains("://")) { + UniWebViewLogger.Instance.Critical("The domain should not include invalid characters '://'"); + return; + } + UniWebViewInterface.RemoveSslExceptionDomain(listener.Name, domain); + } + + /// + /// Sets a customized header field for web view requests. + /// + /// The header field will be used for all subsequence request. + /// Pass `null` as value to unset a header field. + /// + /// Some reserved headers like user agent are not be able to override by setting here, + /// use the `SetUserAgent` method for them instead. + /// + /// The key of customized header field. + /// The value of customized header field. `null` if you want to unset the field. + public void SetHeaderField(string key, string value) { + if (key == null) { + UniWebViewLogger.Instance.Critical("Header key should not be null."); + return; + } + UniWebViewInterface.SetHeaderField(listener.Name, key, value); + } + + /// + /// Sets the user agent used in the web view. + /// If the string is null or empty, the system default value will be used. + /// + /// The new user agent string to use. + public void SetUserAgent(string agent) { + UniWebViewInterface.SetUserAgent(listener.Name, agent); + } + + /// + /// Gets the user agent string currently used in web view. + /// If a customized user agent is not set, the default user agent in current platform will be returned. + /// + /// The user agent string in use. + public string GetUserAgent() { + return UniWebViewInterface.GetUserAgent(listener.Name); + } + + /// + /// Sets the adjustment behavior which indicates how safe area insets + /// are added to the adjusted content inset. It is a wrapper of `contentInsetAdjustmentBehavior` on iOS. + /// + /// It only works on iOS 11 and above. You need to call this method as soon as you create a web view, + /// before you call any other methods related to web view layout (like `Show` or `SetShowToolbar`). + /// + /// The behavior for determining the adjusted content offsets. + public void SetContentInsetAdjustmentBehavior( + UniWebViewContentInsetAdjustmentBehavior behavior + ) + { + #if (UNITY_EDITOR_OSX || UNITY_STANDALONE_OSX || UNITY_IOS) && !UNITY_EDITOR_WIN && !UNITY_EDITOR_LINUX + UniWebViewInterface.SetContentInsetAdjustmentBehavior(listener.Name, behavior); + #endif + } + + /// + /// Sets allow auto play for current web view. By default, + /// users need to touch the play button to start playing a media resource. + /// + /// By setting this to `true`, you can start the playing automatically through + /// corresponding media tag attributes. + /// + /// A flag indicates whether autoplaying of media is allowed or not. + public static void SetAllowAutoPlay(bool flag) { + UniWebViewInterface.SetAllowAutoPlay(flag); + } + + /// + /// Sets allow inline play for current web view. By default, on iOS, the video + /// can only be played in a new full screen view. + /// By setting this to `true`, you could play a video inline the page, instead of opening + /// a new full screen window. + /// + /// This only works for iOS and macOS Editor. + /// On Android, you could play videos inline by default and calling this method does nothing. + /// + /// A flag indicates whether inline playing of media is allowed or not. + public static void SetAllowInlinePlay(bool flag) { + #if (UNITY_EDITOR_OSX || UNITY_STANDALONE_OSX || UNITY_IOS) && !UNITY_EDITOR_WIN && !UNITY_EDITOR_LINUX + UniWebViewInterface.SetAllowInlinePlay(flag); + #endif + } + + /// + /// Sets whether loading a local file is allowed. + /// + /// If set to `false`, any load from a file URL `file://` for `Load` method will be rejected and trigger an + /// `OnPageErrorReceived` event. That means you cannot load a web page from any local file. If you are not going to + /// load any local files, setting it to `false` helps to reduce the interface of web view and improve the security. + /// + /// By default, it is `true` and the local file URL loading is allowed. + /// + /// Whether the local file access by web view loading is allowed or not. + public void SetAllowFileAccess(bool flag) { + UniWebViewInterface.SetAllowFileAccess(listener.Name, flag); + } + + /// + /// Sets whether file access from file URLs is allowed. + /// + /// By setting with `true`, access to file URLs inside the web view will be enabled and you could access + /// sub-resources or make cross origin requests from local HTML files. + /// + /// On iOS, it uses some "hidden" way by setting `allowFileAccessFromFileURLs` in config preferences for WebKit. + /// So it is possible that it stops working in a future version. + /// + /// On Android, it sets the `WebSettings.setAllowFileAccessFromFileURLs` for the current web view. + /// + /// Whether the file access inside web view from file URLs is allowed or not. + public void SetAllowFileAccessFromFileURLs(bool flag) { + UniWebViewInterface.SetAllowFileAccessFromFileURLs(listener.Name, flag); + } + + /// + /// Sets whether the UniWebView should allow third party cookies to be set. By default, on Android, the third party + /// cookies are disallowed due to security reason. Setting this to `true` will allow the cookie manager to accept + /// third party cookies you set. + /// + /// This method only works for Android. On iOS, this method does nothing and the third party cookies are always + /// allowed. + /// + /// Whether the third party cookies should be allowed. + public void SetAcceptThirdPartyCookies(bool flag) { + #if UNITY_ANDROID && !UNITY_EDITOR + UniWebViewInterface.SetAcceptThirdPartyCookies(listener.Name, flag); + #endif + } + + /// + /// Sets allow universal access from file URLs. By default, on iOS, the `WKWebView` forbids any load of local files + /// through AJAX even when opening a local HTML file. It checks the CORS rules and fails at web view level. + /// This is useful when you want access these files by setting the `allowUniversalAccessFromFileURLs` key of web view + /// configuration. + /// + /// On iOS and macOS Editor. It uses some "hidden" way by setting `allowUniversalAccessFromFileURLs` in config + /// for WebKit. So it is possible that it stops working in a future version. + /// + /// On Android, it sets the `WebSettings.setAllowUniversalAccessFromFileURLs` and any later-created web views uses + /// that value. + /// + /// A flag indicates whether the universal access for files are allowed or not. + public static void SetAllowUniversalAccessFromFileURLs(bool flag) { + UniWebViewInterface.SetAllowUniversalAccessFromFileURLs(flag); + } + + /// + /// Sets whether the web view area should avoid soft keyboard. It `true`, when the keyboard shows up, the web views + /// content view will resize itself to avoid keyboard overlap the web content. Otherwise, the web view will not resize + /// and just leave the content below under the soft keyboard. + /// + /// This method is only for Android. On iOS, the keyboard avoidance is built into the system directly and there is + /// no way to change its behavior. + /// + /// Whether the keyboard should avoid web view content. + public static void SetEnableKeyboardAvoidance(bool flag) { + #if UNITY_ANDROID && !UNITY_EDITOR + UniWebViewInterface.SetEnableKeyboardAvoidance(flag); + #endif + } + + /// + /// Sets whether JavaScript should be enabled in current web view. Default is enabled. + /// + /// Whether JavaScript should be enabled. + public static void SetJavaScriptEnabled(bool enabled) { + UniWebViewInterface.SetJavaScriptEnabled(enabled); + } + + /// + /// Sets whether JavaScript can open windows without user interaction. + /// + /// By setting this to `true`, an automatically JavaScript navigation will be allowed in the web view. + /// + /// Whether JavaScript could open window automatically. + public static void SetAllowJavaScriptOpenWindow(bool flag) { + UniWebViewInterface.SetAllowJavaScriptOpenWindow(flag); + } + + /// + /// Cleans web view cache. This removes cached local data of web view. + /// + /// If you need to clear all cookies, use `ClearCookies` instead. + /// + public void CleanCache() { + UniWebViewInterface.CleanCache(listener.Name); + } + + /// + /// Clears all cookies from web view. + /// + /// This will clear cookies from all domains in the web view and previous. + /// If you only need to remove cookies from a certain domain, use `SetCookie` instead. + /// + public static void ClearCookies() { + UniWebViewInterface.ClearCookies(); + } + + /// + /// Sets a cookie for a certain url. + /// + /// The url to which cookie will be set. + /// The cookie string to set. + /// + /// Whether UniWebView should skip encoding the url or not. If set to `false`, UniWebView will try to encode the url parameter before + /// using it. Otherwise, your original url string will be used to set the cookie if it is valid. Default is `false`. + /// + public static void SetCookie(string url, string cookie, bool skipEncoding = false) { + UniWebViewInterface.SetCookie(url, cookie, skipEncoding); + } + + /// + /// Gets the cookie value under a url and key. + /// + /// The url (domain) where the target cookie is. + /// The key for target cookie value. + /// + /// Whether UniWebView should skip encoding the url or not. If set to `false`, UniWebView will try to encode the url parameter before + /// using it. Otherwise, your original url string will be used to get the cookie if it is valid. Default is `false`. + /// + /// Value of the target cookie under url. + public static string GetCookie(string url, string key, bool skipEncoding = false) { + return UniWebViewInterface.GetCookie(url, key, skipEncoding); + } + + /// + /// Removes all the cookies under a url. + /// + /// The url (domain) where the cookies is under. + /// + /// Whether UniWebView should skip encoding the url or not. If set to `false`, UniWebView will try to encode the url parameter before + /// using it. Otherwise, your original url string will be used to get the cookie if it is valid. Default is `false`. + /// + public static void RemoveCookies(string url, bool skipEncoding = false) { + UniWebViewInterface.RemoveCookies(url, skipEncoding); + } + + /// + /// Removes the certain cookie under a url for the specified key. + /// + /// The url (domain) where the cookies is under. + /// The key for target cookie. + /// + /// Whether UniWebView should skip encoding the url or not. If set to `false`, UniWebView will try to encode the url parameter before + /// using it. Otherwise, your original url string will be used to get the cookie if it is valid. Default is `false`. + /// + public static void RemoveCooke(string url, string key, bool skipEncoding = false) { + UniWebViewInterface.RemoveCookie(url, key, skipEncoding); + } + + /// + /// Clears any saved credentials for HTTP authentication for both Basic and Digest. + /// + /// On both iOS and Android, the user input credentials will be stored permanently across session. + /// It could prevent your users to input username and password again until they changed. If you need the + /// credentials only living in a shorter lifetime, call this method at proper timing. + /// + /// On iOS, it will clear the credentials immediately and completely from both disk and network cache. + /// On Android, it only clears from disk database, the authentication might be still cached in the network stack + /// and will not be removed until next session (app restarting). + /// + /// The client logout mechanism should be implemented by the Web site designer (such as server sending a HTTP + /// 401 for invalidating credentials). + /// + /// + /// The host to which the credentials apply. It should not contain any thing like scheme or path part. + /// The realm to which the credentials apply. + public static void ClearHttpAuthUsernamePassword(string host, string realm) { + UniWebViewInterface.ClearHttpAuthUsernamePassword(host, realm); + } + + private Color backgroundColor = Color.white; + /// + /// Gets or sets the background color of web view. The default value is `Color.white`. + /// + public Color BackgroundColor { + get { + return backgroundColor; + } + set { + backgroundColor = value; + UniWebViewInterface.SetBackgroundColor(listener.Name, value.r, value.g, value.b, value.a); + } + } + + /// + /// Gets or sets the alpha value of the whole web view. + /// + /// You can make the game scene behind web view visible to make the web view transparent. + /// + /// Default is `1.0f`, which means totally opaque. Set it to `0.0f` will make the web view totally transparent. + /// + public float Alpha { + get { + return UniWebViewInterface.GetWebViewAlpha(listener.Name); + } + set { + UniWebViewInterface.SetWebViewAlpha(listener.Name, value); + } + } + + /// + /// Sets whether to show a loading indicator while the loading is in progress. + /// + /// Whether an indicator should show. + public void SetShowSpinnerWhileLoading(bool flag) { + UniWebViewInterface.SetShowSpinnerWhileLoading(listener.Name, flag); + } + + /// + /// Sets the text displayed in the loading indicator, if `SetShowSpinnerWhileLoading` is set to `true`. + /// + /// The text to display while loading indicator visible. Default is "Loading..." + public void SetSpinnerText(string text) { + UniWebViewInterface.SetSpinnerText(listener.Name, text); + } + + /// + /// Sets whether the horizontal scroll bar should show when the web content beyonds web view bounds. + /// + /// This only works on mobile platforms. It will do nothing on macOS Editor. + /// + /// Whether enable the scroll bar or not. + public void SetHorizontalScrollBarEnabled(bool enabled) { + UniWebViewInterface.SetHorizontalScrollBarEnabled(listener.Name, enabled); + } + + /// + /// Sets whether the vertical scroll bar should show when the web content beyonds web view bounds. + /// + /// This only works on mobile platforms. It will do nothing on macOS Editor. + /// + /// Whether enable the scroll bar or not. + public void SetVerticalScrollBarEnabled(bool enabled) { + UniWebViewInterface.SetVerticalScrollBarEnabled(listener.Name, enabled); + } + + /// + /// Sets whether the web view should show with a bounces effect when scrolling to page edge. + /// + /// This only works on mobile platforms. It will do nothing on macOS Editor. + /// + /// Whether the bounces effect should be applied or not. + public void SetBouncesEnabled(bool enabled) { + UniWebViewInterface.SetBouncesEnabled(listener.Name, enabled); + } + + /// + /// Sets whether the web view supports zoom gesture to change content size. + /// Default is `false`, which means the zoom gesture is not supported. + /// + /// Whether the zoom gesture is allowed or not. + public void SetZoomEnabled(bool enabled) { + UniWebViewInterface.SetZoomEnabled(listener.Name, enabled); + } + + /// + /// Adds a trusted domain to white list and allow permission requests from the domain. + /// + /// You only need this on Android devices with system before 6.0 when a site needs the location or camera + /// permission. It will allow the permission gets approved so you could access the corresponding devices. + /// From Android 6.0, the permission requests method is changed and this is not needed anymore. + /// + /// The domain to add to the white list. + public void AddPermissionTrustDomain(string domain) { + #if UNITY_ANDROID && !UNITY_EDITOR + UniWebViewInterface.AddPermissionTrustDomain(listener.Name, domain); + #endif + } + + /// + /// Removes a trusted domain from white list. + /// + /// The domain to remove from white list. + public void RemovePermissionTrustDomain(string domain) { + #if UNITY_ANDROID && !UNITY_EDITOR + UniWebViewInterface.RemovePermissionTrustDomain(listener.Name, domain); + #endif + } + + /// + /// Sets whether the device back button should be enabled to execute "go back" or "closing" operation. + /// + /// On Android, the device back button in navigation bar will navigate users to a back page. If there is + /// no any back page avaliable, the back button clicking will try to raise a `OnShouldClose` event and try + /// to close the web view if `true` is return from the event. If the `OnShouldClose` is not listened, + /// the web view will be closed and the UniWebView component will be destroyed to release using resource. + /// + /// Listen to `OnKeyCodeReceived` if you need to disable the back button, but still want to get the back + /// button key pressing event. + /// + /// Default is enabled. + /// + /// Whether the back button should perform go back or closing operation to web view. + public void SetBackButtonEnabled(bool enabled) { + this.backButtonEnabled = enabled; + } + + /// + /// Sets whether the web view should enable support for the "viewport" HTML meta tag or should use a wide viewport. + /// + /// Whether to enable support for the viewport meta tag. + public void SetUseWideViewPort(bool flag) { + #if UNITY_ANDROID && !UNITY_EDITOR + UniWebViewInterface.SetUseWideViewPort(listener.Name, flag); + #endif + } + + /// + /// Sets whether the web view loads pages in overview mode, that is, zooms out the content to fit on screen by width. + /// + /// This method is only for Android. Default is disabled. + /// + /// + public void SetLoadWithOverviewMode(bool flag) { + #if UNITY_ANDROID && !UNITY_EDITOR + UniWebViewInterface.SetLoadWithOverviewMode(listener.Name, flag); + #endif + } + + /// + /// Sets whether to show a toolbar which contains navigation buttons and Done button. + /// + /// You could choose to show or hide the tool bar. By configuring the `animated` and `onTop` + /// parameters, you can control the animating and position of the toolbar. If the toolbar is + /// overlapping with some part of your web view, pass `adjustInset` with `true` to have the + /// web view relocating itself to avoid the overlap. + /// + /// This method is only for iOS. The toolbar is hidden by default. + /// + /// Whether the toolbar should show or hide. + /// Whether the toolbar state changing should be with animation. Default is `false`. + /// Whether the toolbar should snap to top of screen or to bottom of screen. + /// Default is `true` + /// Whether the toolbar transition should also adjust web view position and size + /// if overlapped. Default is `false` + public void SetShowToolbar(bool show, bool animated = false, bool onTop = true, bool adjustInset = false) { + #if (UNITY_EDITOR_OSX || UNITY_STANDALONE_OSX || UNITY_IOS) && !UNITY_EDITOR_WIN && !UNITY_EDITOR_LINUX + UniWebViewInterface.SetShowToolbar(listener.Name, show, animated, onTop, adjustInset); + #endif + } + + /// + /// Sets the done button text in toolbar. + /// + /// By default, UniWebView will show a "Done" button at right size in the + /// toolbar. You could change its title by passing a text. + /// + /// This method is only for iOS, since there is no toolbar on Android. + /// + /// The text needed to be set as done button title. + public void SetToolbarDoneButtonText(string text) { + #if UNITY_IOS && !UNITY_EDITOR + UniWebViewInterface.SetToolbarDoneButtonText(listener.Name, text); + #endif + } + + /// + /// Sets the go back button text in toolbar. + /// + /// By default, UniWebView will show a back arrow at the left side in the + /// toolbar. You could change its text. + /// + /// This method is only for iOS and macOS Editor, since there is no toolbar on Android. + /// + /// The text needed to be set as go back button. + public void SetToolbarGoBackButtonText(string text) { + #if (UNITY_EDITOR_OSX || UNITY_STANDALONE_OSX || UNITY_IOS) && !UNITY_EDITOR_WIN && !UNITY_EDITOR_LINUX + UniWebViewInterface.SetToolbarGoBackButtonText(listener.Name, text); + #endif + } + + /// + /// Sets the go forward button text in toolbar. + /// + /// By default, UniWebView will show a forward arrow at the left side in the + /// toolbar. You could change its text. + /// + /// This method is only for iOS and macOS Editor, since there is no toolbar on Android. + /// + /// The text needed to be set as go forward button. + public void SetToolbarGoForwardButtonText(string text) { + #if (UNITY_EDITOR_OSX || UNITY_STANDALONE_OSX || UNITY_IOS) && !UNITY_EDITOR_WIN && !UNITY_EDITOR_LINUX + UniWebViewInterface.SetToolbarGoForwardButtonText(listener.Name, text); + #endif + } + + /// + /// Sets the background tint color for the toolbar. + /// + /// By default, UniWebView uses a default half-transparent iOS standard background for toolbar. + /// You can change it by setting a new opaque color. + /// + /// This method is only for iOS, since there is no toolbar on Android. + /// + /// The color should be used for the background tint of the toolbar. + public void SetToolbarTintColor(Color color) { + #if UNITY_IOS && !UNITY_EDITOR + UniWebViewInterface.SetToolbarTintColor(listener.Name, color.r, color.g, color.b); + #endif + } + + /// + /// Sets the button text color for the toolbar. + /// + /// By default, UniWebView uses the default text color on iOS, which is blue for most cases. + /// You can change it by setting a new opaque color. + /// + /// This method is only for iOS, since there is no toolbar on Android. + /// + /// The color should be used for the button text of the toolbar. + public void SetToolbarTextColor(Color color) { + #if UNITY_IOS && !UNITY_EDITOR + UniWebViewInterface.SetToolbarTextColor(listener.Name, color.r, color.g, color.b); + #endif + } + + /// + /// Sets the visibility of navigation buttons, such as "Go Back" and "Go Forward", on toolbar. + /// + /// By default, UniWebView will show the "Go Back" and "Go Forward" navigation buttons on the toolbar. + /// Users can use these buttons to perform go back or go forward action just like in a browser. If the navigation + /// model is not for your case, call this method with `false` as `show` parameter to hide them. + /// + /// This method is only for iOS, since there is no toolbar on Android. + /// + /// Whether the navigation buttons on the toolbar should show or hide. + public void SetShowToolbarNavigationButtons(bool show) { + #if (UNITY_EDITOR_OSX || UNITY_STANDALONE_OSX || UNITY_IOS) && !UNITY_EDITOR_WIN && !UNITY_EDITOR_LINUX + UniWebViewInterface.SetShowToolbarNavigationButtons(listener.Name, show); + #endif + } + + /// + /// Sets whether the web view can receive user interaction or not. + /// + /// By setting this to `false`, the web view will not accept any user touch event so your users cannot tap links or + /// scroll the page. + /// + /// + /// Whether the user interaction should be enabled or not. + public void SetUserInteractionEnabled(bool enabled) { + UniWebViewInterface.SetUserInteractionEnabled(listener.Name, enabled); + } + + /// + /// Sets whether the web view should pass through clicks at clear pixels to Unity scene. + /// + /// Setting this method is a pre-condition for the whole passing-through feature to work. To allow your touch passing through + /// to Unity scene, the following conditions should be met at the same time: + /// + /// 1. This method is called with `true` and the web view accepts passing-through clicks. + /// 2. The web view has a transparent background in body style for its content by CSS. + /// 3. The web view itself has a transparent background color by setting `BackgroundColor` with a clear color. + /// + /// Then, when user clicks on the clear pixel on the web view, the touch events will not be handled by the web view. + /// Instead, these events are passed to Unity scene. By using this feature, it is possible to create a native UI with the + /// web view. + /// + /// Only clicks on transparent part on the web view will be delivered to Unity scene. The web view still intercepts + /// and handles other touches on visible pixels on the web view. + /// + /// Whether the transparency clicking through feature should be enabled in this web view. + public void SetTransparencyClickingThroughEnabled(bool enabled) { + UniWebViewInterface.SetTransparencyClickingThroughEnabled(listener.Name, enabled); + } + + /// + /// Enables debugging of web contents. You could inspect of the content of a + /// web view by using a browser development tool of Chrome for Android or Safari for macOS. + /// + /// This method is only for Android and macOS Editor. On iOS, you do not need additional step. + /// You could open Safari's developer tools to debug a web view on iOS. + /// + /// Whether the content debugging should be enabled. + public static void SetWebContentsDebuggingEnabled(bool enabled) { + UniWebViewInterface.SetWebContentsDebuggingEnabled(enabled); + } + + /// + /// Enables user resizing for web view window. By default, you can only set the window size + /// by setting its frame on mac Editor. By enabling user resizing, you would be able to resize + /// the window by dragging its border as a normal macOS window. + /// + /// This method only works for macOS for debugging purpose. It does nothing on iOS and Android. + /// + /// Whether the window could be able to be resized by cursor. + public void SetWindowUserResizeEnabled(bool enabled) { + #if (UNITY_EDITOR_OSX || UNITY_STANDALONE_OSX || UNITY_IOS) && !UNITY_EDITOR_WIN && !UNITY_EDITOR_LINUX + UniWebViewInterface.SetWindowUserResizeEnabled(listener.Name, enabled); + #endif + } + + /// + /// Gets the HTML content from current page by accessing its outerHTML with JavaScript. + /// + /// Called after the JavaScript executed. The parameter string is the content read + /// from page. + public void GetHTMLContent(Action handler) { + EvaluateJavaScript("document.documentElement.outerHTML", payload => { + if (handler != null) { + handler(payload.data); + } + }); + } + + /// + /// Sets whether horizontal swipe gestures should trigger back-forward list navigation. + /// + /// By setting with `true`, users can swipe from screen edge to perform a back or forward navigation. + /// This method only works on iOS and macOS Editor. Default is `false`. + /// + /// On Android, the screen navigation gestures are simulating the traditional back button and it is enabled by + /// default. To disable gesture navigation on Android, you have to also disable the device back button. See + /// `SetBackButtonEnabled` for that purpose. + /// + /// + /// The value indicates whether a swipe gestures driven navigation should be allowed. Default is `false`. + /// + public void SetAllowBackForwardNavigationGestures(bool flag) { + #if (UNITY_EDITOR_OSX || UNITY_STANDALONE_OSX || UNITY_IOS) && !UNITY_EDITOR_WIN && !UNITY_EDITOR_LINUX + UniWebViewInterface.SetAllowBackForwardNavigationGestures(listener.Name, flag); + #endif + } + + /// + /// Sets whether a prompt alert should be displayed for collection username and password when the web view receives an + /// HTTP authentication challenge (HTTP Basic or HTTP Digest) from server. + /// + /// By setting with `false`, no prompt will be shown and the user cannot login with input credentials. In this case, + /// you can only access this page by providing username and password through the URL like: "http://username:password@example.com". + /// If the username and password does not match, normally an error with 401 as status code would be returned (this behavior depends + /// on the server implementation). If set with `true`, a prompt will be shown when there is no credentials provided or it is not + /// correct in the URL. + /// + /// Default is `true`. + /// + /// Whether a prompt alert should be shown for HTTP authentication challenge or not. + public void SetAllowHTTPAuthPopUpWindow(bool flag) { + UniWebViewInterface.SetAllowHTTPAuthPopUpWindow(listener.Name, flag); + } + + /// + /// Sets whether a callout (context) menu should be displayed when user long tapping on certain web view content. + /// + /// When enabled, when user long presses an image or link in the web page, a context menu would be show up to ask + /// user's action. On iOS, it is a action sheet to ask whether opening the target link or saving the image. On + /// Android it is a pop up dialog to ask whether saving the image to local disk. On iOS, the preview page triggered + /// by force touch on iOS is also considered as a callout menu. + /// + /// Default is `true`, means that the callout menu will be displayed. Call this method with `false` to disable + /// it on the web view. + /// + /// + /// Whether a callout menu should be displayed when user long pressing or force touching a certain web page element. + /// + public void SetCalloutEnabled(bool enabled) { + UniWebViewInterface.SetCalloutEnabled(listener.Name, enabled); + } + + + [ObsoleteAttribute("Deprecated. Use `SetSupportMultipleWindows(bool enabled, bool allowJavaScriptOpen)` to set `allowJavaScriptOpen` explicitly.")] + public void SetSupportMultipleWindows(bool enabled) { + SetSupportMultipleWindows(enabled, true); + } + + /// + /// Sets whether the web view should support a pop up web view triggered by user in a new tab. + /// + /// In a general web browser (such as Google Chrome or Safari), a URL with `target="_blank"` attribute is intended + /// to be opened in a new tab. However, in the context of web view, there is no way to handle new tabs without + /// proper configurations. Due to that, by default UniWebView will ignore the `target="_blank"` and try to open + /// the page in the same web view if that kind of link is pressed. + /// + /// It works for most cases, but if this is a problem to your app logic, you can change this behavior by calling + /// this method with `enabled` set to `true`. It enables the "opening in new tab" behavior in a limited way, by + /// adding the new tab web view above to the current web view, with the same size and position. When the opened new + /// tab is closed, it will be removed from the view hierarchy automatically. + /// + /// By default, only user triggered action is allowed to open a new window for security reason. That means, if you + /// are using some JavaScript like `window.open`, unless you set `allowJavaScriptOpening` to `true`, it won't work. + /// This default behavior prevents any other third party JavaScript code from opening a window arbitrarily. + /// + /// + /// + /// Whether to support multiple windows. If `true`, the `target="_blank"` link will be opened in a new web view. + /// Default is `false`. + /// + /// + /// Whether to support open the new window with JavaScript by `window.open`. Setting this to `true` means any JavaScript + /// code, even from third party (in an iframe or a library on the page), can open a new window. Use it as your risk. + /// + public void SetSupportMultipleWindows(bool enabled, bool allowJavaScriptOpening) { + UniWebViewInterface.SetSupportMultipleWindows(listener.Name, enabled, allowJavaScriptOpening); + } + + /// + /// Sets the default font size used in the web view. + /// + /// On Android, the web view font size can be affected by the system font scale setting. Use this method to set the + /// font size in a more reasonable way, by giving the web view another default font size with the system font scale + /// considered. It can removes or reduces the effect of system font scale when displaying the web content. + /// + /// This method only works on Android. On iOS, this method does nothing since the web view will respect the font + /// size setting in your CSS styles. + /// + /// The target default font size set to the web view. + public void SetDefaultFontSize(int size) { + #if UNITY_ANDROID && !UNITY_EDITOR + UniWebViewInterface.SetDefaultFontSize(listener.Name, size); + #endif + } + + /// + /// Sets the text zoom used in the web view. + /// + /// On Android, this method call `WebSettings.setTextZoom` to the the text zoom used in the web view. + /// + /// This method only works on Android. + /// + /// The text zoom in percent. + public void SetTextZoom(int textZoom) { + #if UNITY_ANDROID && !UNITY_EDITOR + UniWebViewInterface.SetTextZoom(listener.Name, textZoom); + #endif + } + + /// + /// Sets whether the drag interaction should be enabled on iOS. + /// + /// From iOS 11, the iPad web view supports the drag interaction when user long presses an image, link or text. + /// Setting this to `false` would disable the drag feather on the web view. + /// + /// This method only works on iOS. It does nothing on Android or macOS editor. Default is `true`, which means + /// drag interaction on iPad is enabled. + /// + /// + /// Whether the drag interaction should be enabled. + /// + public void SetDragInteractionEnabled(bool enabled) { + #if (UNITY_EDITOR_OSX || UNITY_STANDALONE_OSX || UNITY_IOS) && !UNITY_EDITOR_WIN && !UNITY_EDITOR_LINUX + UniWebViewInterface.SetDragInteractionEnabled(listener.Name, enabled); + #endif + } + + /// + /// Prints current page. + /// + /// By calling this method, a native print preview panel will be brought up on iOS and Android. + /// This method does nothing on macOS editor. + /// On iOS and Android, the web view does not support JavaScript (window.print()), + /// you can only initialize a print job from Unity by this method. + /// + public void Print() { + UniWebViewInterface.Print(listener.Name); + } + + /// + /// Capture the content of web view and store it to the cache path on disk with the given file name. + /// + /// When the capturing finishes, `OnCaptureSnapshotFinished` event will be raised, with an error code to indicate + /// whether the operation succeeded and an accessible disk path of the image. + /// + /// The captured image will be stored as a PNG file under the `fileName` in app's cache folder. If a file with the + /// same file name already exists, it will be overridden by the new captured image. + /// + /// + /// The file name to which the captured image is stored to, for example "screenshot.png". If empty, UniWebView will + /// pick a random UUID with "png" file extension as the file name. + /// + public void CaptureSnapshot(string fileName) { + UniWebViewInterface.CaptureSnapshot(listener.Name, fileName); + } + + /// + /// Scrolls the web view to a certain point. + /// + /// Use 0 for both `x` and `y` value to scroll the web view to its origin. + /// In a normal vertical web page, it is equivalent as scrolling to top. + /// + /// You can use the `animated` parameter to control whether scrolling the page with or without animation. + /// This parameter only works on iOS and Android. On macOS editor, the scrolling always happens without animation. + /// + /// X value of the target scrolling point. + /// Y value of the target scrolling point. + /// If `true`, the scrolling happens with animation. Otherwise, it happens without + /// animation and the content is set directly. + /// + public void ScrollTo(int x, int y, bool animated) { + UniWebViewInterface.ScrollTo(listener.Name, x, y, animated); + } + + /// + /// Adds the URL to download inspecting list. + /// + /// If a response is received in main frame and its URL is already in the inspecting list, a download task will be + /// triggered. Check "Download Files" guide for more. + /// + /// This method only works on iOS and macOS Editor. + /// + /// The inspected URL. + /// The download matching type used to match the URL. Default is `ExactValue`. + public void AddDownloadURL(string urlString, UniWebViewDownloadMatchingType type = UniWebViewDownloadMatchingType.ExactValue) { + #if (UNITY_EDITOR_OSX || UNITY_STANDALONE_OSX || UNITY_IOS) && !UNITY_EDITOR_WIN && !UNITY_EDITOR_LINUX + UniWebViewInterface.AddDownloadURL(listener.Name, urlString, (int)type); + #endif + } + + /// + /// Removes the URL from download inspecting list. + /// + /// If a response is received in main frame and its URL is already in the inspecting list, a download task will be + /// triggered. Check "Download Files" guide for more. + /// + /// This method only works on iOS and macOS Editor. + /// + /// The inspected URL. + /// The download matching type used to match the URL. Default is `ExactValue`. + /// + public void RemoveDownloadURL(string urlString, UniWebViewDownloadMatchingType type = UniWebViewDownloadMatchingType.ExactValue) { + #if (UNITY_EDITOR_OSX || UNITY_STANDALONE_OSX || UNITY_IOS) && !UNITY_EDITOR_WIN && !UNITY_EDITOR_LINUX + UniWebViewInterface.RemoveDownloadURL(listener.Name, urlString, (int)type); + #endif + } + + /// + /// Adds the MIME type to download inspecting list. + /// + /// If a response is received in main frame and its MIME type is already in the inspecting list, a + /// download task will be triggered. Check "Download Files" guide for more. + /// + /// This method only works on iOS and macOS Editor. + /// + /// The inspected MIME type of the response. + /// The download matching type used to match the MIME type. Default is `ExactValue`. + public void AddDownloadMIMEType(string MIMEType, UniWebViewDownloadMatchingType type = UniWebViewDownloadMatchingType.ExactValue) { + #if (UNITY_EDITOR_OSX || UNITY_STANDALONE_OSX || UNITY_IOS) && !UNITY_EDITOR_WIN && !UNITY_EDITOR_LINUX + UniWebViewInterface.AddDownloadMIMEType(listener.Name, MIMEType, (int)type); + #endif + } + + /// + /// Removes the MIME type from download inspecting list. + /// + /// If a response is received in main frame and its MIME type is already in the inspecting list, a + /// download task will be triggered. Check "Download Files" guide for more. + /// + /// This method only works on iOS and macOS Editor. + /// + /// The inspected MIME type of the response. + /// The download matching type used to match the MIME type. Default is `ExactValue`. + public void RemoveDownloadMIMETypes(string MIMEType, UniWebViewDownloadMatchingType type = UniWebViewDownloadMatchingType.ExactValue) { + #if (UNITY_EDITOR_OSX || UNITY_STANDALONE_OSX || UNITY_IOS) && !UNITY_EDITOR_WIN && !UNITY_EDITOR_LINUX + UniWebViewInterface.RemoveDownloadMIMETypes(listener.Name, MIMEType, (int)type); + #endif + } + + /// + /// Sets whether allowing users to choose the way to handle the downloaded file. Default is `true`. + /// + /// On iOS, the downloaded file will be stored in a temporary folder. Setting this to `true` will show a system + /// default share sheet and give the user a chance to send and store the file to another location (such as the + /// File app or iCloud). + /// + /// On macOS Editor, setting this to `true` will allow UniWebView to open the file in Finder. + /// + /// This method does not have any effect on Android. On Android, the file is downloaded to the Download folder. + /// + /// + /// + public void SetAllowUserChooseActionAfterDownloading(bool allowed) { + #if (UNITY_EDITOR_OSX || UNITY_STANDALONE_OSX || UNITY_IOS) && !UNITY_EDITOR_WIN && !UNITY_EDITOR_LINUX + UniWebViewInterface.SetAllowUserChooseActionAfterDownloading(listener.Name, allowed); + #endif + } + + /// + /// Sets whether the `OnFileDownloadStarted` and `OnFileDownloadFinished` events should be raised even for an image + /// saving action triggered by the callout (context) menu on Android. + /// + /// By default, the image saving goes through a different route and it does not trigger the `OnFileDownloadStarted` + /// and `OnFileDownloadFinished` events like other normal download tasks. Setting this with enabled with `true` if + /// you also need to get notified when user long-presses on the image and taps "Save Image" button. By default, the + /// image will be saved to the Downloads directory and you can get the path from the parameter + /// of `OnFileDownloadFinished` event. + /// + /// This only works on Android. On iOS, there is no way to get a callback or any event from the "Add to Photos" + /// button in the callout menu. + /// + /// Whether the context menu image saving action triggers the download related events. + public void SetDownloadEventForContextMenuEnabled(bool enabled) { + #if UNITY_ANDROID && !UNITY_EDITOR + UniWebViewInterface.SetDownloadEventForContextMenuEnabled(listener.Name, enabled); + #endif + } + + void OnDestroy() { + UniWebViewNativeListener.RemoveListener(listener.Name); + UniWebViewInterface.Destroy(listener.Name); + Destroy(listener.gameObject); + } + + /* ////////////////////////////////////////////////////// + // Internal Listener Interface + ////////////////////////////////////////////////////// */ + internal void InternalOnShowTransitionFinished(string identifier) { + Action action; + if (actions.TryGetValue(identifier, out action)) { + action(); + actions.Remove(identifier); + } + } + + internal void InternalOnHideTransitionFinished(string identifier) { + Action action; + if (actions.TryGetValue(identifier, out action)) { + action(); + actions.Remove(identifier); + } + } + + internal void InternalOnAnimateToFinished(string identifier) { + Action action; + if (actions.TryGetValue(identifier, out action)) { + action(); + actions.Remove(identifier); + } + } + + internal void InternalOnAddJavaScriptFinished(UniWebViewNativeResultPayload payload) { + Action action; + var identifier = payload.identifier; + if (payloadActions.TryGetValue(identifier, out action)) { + action(payload); + payloadActions.Remove(identifier); + } + } + + internal void InternalOnEvalJavaScriptFinished(UniWebViewNativeResultPayload payload) { + Action action; + var identifier = payload.identifier; + if (payloadActions.TryGetValue(identifier, out action)) { + action(payload); + payloadActions.Remove(identifier); + } + } + + internal void InternalOnPageFinished(UniWebViewNativeResultPayload payload) { + if (OnPageFinished != null) { + int code = -1; + if (int.TryParse(payload.resultCode, out code)) { + OnPageFinished(this, code, payload.data); + } else { + UniWebViewLogger.Instance.Critical("Invalid status code received: " + payload.resultCode); + } + } + } + + internal void InternalOnPageStarted(string url) { + if (OnPageStarted != null) { + OnPageStarted(this, url); + } + } + + internal void InternalOnPageErrorReceived(UniWebViewNativeResultPayload payload) { + if (OnPageErrorReceived != null) { + int code = -1; + if (int.TryParse(payload.resultCode, out code)) { + OnPageErrorReceived(this, code, payload.data); + } else { + UniWebViewLogger.Instance.Critical("Invalid error code received: " + payload.resultCode); + } + } + } + + internal void InternalOnPageProgressChanged(float progress) { + if (OnPageProgressChanged != null) { + OnPageProgressChanged(this, progress); + } + } + + internal void InternalOnMessageReceived(string result) { + if (OnMessageReceived != null) { + var message = new UniWebViewMessage(result); + OnMessageReceived(this, message); + } + } + + internal void InternalOnShouldClose() { + if (OnShouldClose != null) { + var shouldClose = OnShouldClose(this); + if (shouldClose) { + Destroy(this); + } + } else { + Destroy(this); + } + } + + internal void InternalOnWebContentProcessDidTerminate() { + if (OnWebContentProcessTerminated != null) { + OnWebContentProcessTerminated(this); + } + } + + internal void InternalOnMultipleWindowOpened(string multiWindowId) { + if (OnMultipleWindowOpened != null) { + OnMultipleWindowOpened(this, multiWindowId); + } + } + + internal void InternalOnMultipleWindowClosed(string multiWindowId) { + if (OnMultipleWindowClosed != null) { + OnMultipleWindowClosed(this, multiWindowId); + } + } + + internal void InternalOnFileDownloadStarted(UniWebViewNativeResultPayload payload) { + if (OnFileDownloadStarted != null) { + OnFileDownloadStarted(this, payload.identifier, payload.data); + } + } + + internal void InternalOnFileDownloadFinished(UniWebViewNativeResultPayload payload) { + if (OnFileDownloadFinished != null) { + int errorCode = int.TryParse(payload.resultCode, out errorCode) ? errorCode : -1; + OnFileDownloadFinished(this, errorCode, payload.identifier, payload.data); + } + } + + internal void InternalOnCaptureSnapshotFinished(UniWebViewNativeResultPayload payload) { + if (OnCaptureSnapshotFinished != null) { + int errorCode = int.TryParse(payload.resultCode, out errorCode) ? errorCode : -1; + OnCaptureSnapshotFinished(this, errorCode, payload.data); + } + } + + /// + /// Sets whether the web view should behave in immersive mode, that is, + /// hides the status bar and navigation bar with a sticky style. + /// + /// This method is only for Android. Default is enabled. + /// + /// + [Obsolete("SetImmersiveModeEnabled is deprecated. Now UniWebView always respect navigation bar/status bar settings from Unity.", false)] + public void SetImmersiveModeEnabled(bool enabled) { + Debug.LogError( + "SetImmersiveModeEnabled is removed in UniWebView 4." + + "Now UniWebView always respect navigation bar/status bar settings from Unity." + ); + } + /// + /// Delegate for code keycode received event. + /// + /// The web view component which raises this event. + /// The key code of pressed key. See [Android API for keycode](https://developer.android.com/reference/android/view/KeyEvent.html#KEYCODE_0) to know the possible values. + [Obsolete("KeyCodeReceivedDelegate is deprecated. Now UniWebView never intercepts device key code events. Check `Input.GetKeyUp` instead.", false)] + public delegate void KeyCodeReceivedDelegate(UniWebView webView, int keyCode); + + /// + /// Raised when a key (like back button or volume up) on the device is pressed. + /// + /// This event only raised on Android. It is useful when you disabled the back button but still need to + /// get the back button event. On iOS, user's key action is not avaliable and this event will never be + /// raised. + /// + [Obsolete("OnKeyCodeReceived is deprecated and never called. Now UniWebView never intercepts device key code events. Check `Input.GetKeyUp` instead.", false)] +#pragma warning disable CS0067 + public event KeyCodeReceivedDelegate OnKeyCodeReceived; + +} \ No newline at end of file diff --git a/Runtime/GuruWebview/UniWebView5/UniWebView/Script/UniWebView.cs.meta b/Runtime/GuruWebview/UniWebView5/UniWebView/Script/UniWebView.cs.meta new file mode 100644 index 0000000..c09896b --- /dev/null +++ b/Runtime/GuruWebview/UniWebView5/UniWebView/Script/UniWebView.cs.meta @@ -0,0 +1,12 @@ +fileFormatVersion: 2 +guid: 598e18fb001004a81960f552978ecf4e +timeCreated: 1491898971 +licenseType: Free +MonoImporter: + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/GuruWebview/UniWebView5/UniWebView/Script/UniWebViewAuthentication.meta b/Runtime/GuruWebview/UniWebView5/UniWebView/Script/UniWebViewAuthentication.meta new file mode 100644 index 0000000..42b6300 --- /dev/null +++ b/Runtime/GuruWebview/UniWebView5/UniWebView/Script/UniWebViewAuthentication.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 5b17dc622ffd649da85854192714e429 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/GuruWebview/UniWebView5/UniWebView/Script/UniWebViewAuthentication/UniWebViewAuthenticationCommonFlow.cs b/Runtime/GuruWebview/UniWebView5/UniWebView/Script/UniWebViewAuthentication/UniWebViewAuthenticationCommonFlow.cs new file mode 100644 index 0000000..db12498 --- /dev/null +++ b/Runtime/GuruWebview/UniWebView5/UniWebView/Script/UniWebViewAuthentication/UniWebViewAuthenticationCommonFlow.cs @@ -0,0 +1,86 @@ +// +// UniWebViewAuthenticationCommonFlow.cs +// Created by Wang Wei (@onevcat) on 2022-06-25. +// +// This file is a part of UniWebView Project (https://uniwebview.com) +// By purchasing the asset, you are allowed to use this code in as many as projects +// you want, only if you publish the final products under the name of the same account +// used for the purchase. +// +// This asset and all corresponding files (such as source code) are provided on an +// “as is” basis, without warranty of any kind, express of 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 action of contract, tort or otherwise, +// arising from, out of or in connection with the software or the use of other dealing in the software. +// + +using System.Collections.Generic; +using UnityEngine; + +/// +/// Abstract class and general control for other authentication flows. This class determines the global behaviors of the +/// authentication flow, such as whether to start authentication as soon as the script `Start`s, and whether to use private +/// mode to authenticate the user. + +/// This is a super and abstract class for all concrete auth flow. You are not expected to use this class directly. +/// Instead, to start a customized auth flow, you can use the `UniWebViewAuthenticationFlowCustomize` class. +/// +public abstract class UniWebViewAuthenticationCommonFlow: MonoBehaviour { + /// + /// Whether to start authentication as soon as the script `Start`s. + /// + public bool authorizeOnStart; + /// + /// Whether to use private mode to authenticate the user. If `true` and the device supports, the authentication + /// will begin under the incognito mode. + /// + /// On iOS, this works on iOS 13 and later. + /// + /// On Android, it depends on the Chrome version and might require users to enable the incognito mode (and support + /// for third-party use) in Chrome's settings. Check settings with `chrome://flags/#cct-incognito` and + /// `chrome://flags/#cct-incognito-available-to-third-party` in Chrome to see the current status. + /// + public bool privateMode; + + // Security. Store the state. + private string state; + // Security. Store the code challenge verifier. + private string codeVerify; + + protected string CodeVerify => codeVerify; + + public void Start() { + if (authorizeOnStart) { + StartAuthenticationFlow(); + } + } + + // Subclass should override this method to start the authentication flow. Usually it starts + // a `UniWebViewAuthenticationFlow`. But you can also choose whatever you need to do. + public abstract void StartAuthenticationFlow(); + + // Child classes are expected to call this method to request a `state` (and store it for later check) if the + // `state` verification is enabled. + protected string GenerateAndStoreState() { + state = UniWebViewAuthenticationUtils.GenerateRandomBase64URLString(); + return state; + } + + // Child classes are expected to call this method to request a `code_challenge`. Later when exchanging the access + // token, the `code_verifier` will be used to verify the `code_challenge`. Subclass can read it from `CodeVerify`. + protected string GenerateCodeChallengeAndStoreCodeVerify(UniWebViewAuthenticationPKCE method) { + codeVerify = UniWebViewAuthenticationUtils.GenerateCodeVerifier(); + return UniWebViewAuthenticationUtils.CalculateCodeChallenge(codeVerify, method); + } + + // Perform verifying for `state`. + protected void VerifyState(Dictionary parameters, string key = "state") { + if (state == null) { + throw AuthenticationResponseException.InvalidState; + } + if (!parameters.TryGetValue(key, out var stateInResponse) || state != stateInResponse) { + throw AuthenticationResponseException.InvalidState; + } + } +} diff --git a/Runtime/GuruWebview/UniWebView5/UniWebView/Script/UniWebViewAuthentication/UniWebViewAuthenticationCommonFlow.cs.meta b/Runtime/GuruWebview/UniWebView5/UniWebView/Script/UniWebViewAuthentication/UniWebViewAuthenticationCommonFlow.cs.meta new file mode 100644 index 0000000..7641a98 --- /dev/null +++ b/Runtime/GuruWebview/UniWebView5/UniWebView/Script/UniWebViewAuthentication/UniWebViewAuthenticationCommonFlow.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 6bfa5bb9dc237400298563d614b62705 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/GuruWebview/UniWebView5/UniWebView/Script/UniWebViewAuthentication/UniWebViewAuthenticationFlow.cs b/Runtime/GuruWebview/UniWebView5/UniWebView/Script/UniWebViewAuthentication/UniWebViewAuthenticationFlow.cs new file mode 100644 index 0000000..2769759 --- /dev/null +++ b/Runtime/GuruWebview/UniWebView5/UniWebView/Script/UniWebViewAuthentication/UniWebViewAuthenticationFlow.cs @@ -0,0 +1,291 @@ +// +// UniWebViewAuthenticationFlow.cs +// Created by Wang Wei (@onevcat) on 2022-06-25. +// +// This file is a part of UniWebView Project (https://uniwebview.com) +// By purchasing the asset, you are allowed to use this code in as many as projects +// you want, only if you publish the final products under the name of the same account +// used for the purchase. +// +// This asset and all corresponding files (such as source code) are provided on an +// “as is” basis, without warranty of any kind, express of 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 action of contract, tort or otherwise, +// arising from, out of or in connection with the software or the use of other dealing in the software. +// + +using System; +using System.Collections; +using UnityEngine.Networking; +using System.Collections.Generic; +using UnityEngine; +using UnityEngine.Events; + +/// +/// Interface for implementing a custom authentication flow. An authentication flow, in UniWebView, usually a "code" based +/// OAuth 2.0 flow, contains a standard set of steps: +/// +/// 1. User is navigated to a web page that requires authentication; +/// 2. A temporary code is generated by service provider and provided to client by a redirect URL with customized scheme. +/// 3. Client requests an access token using the temporary code by performing an "access token" exchange request. +/// +/// To use the common flow, any customize authentication flow must implement this interface and becomes a subclass of +/// UniWebViewAuthenticationCommonFlow. +/// +/// +public interface IUniWebViewAuthenticationFlow +{ + /// + /// Returns the redirect URL that is used to redirect the user after authenticated. This is used as the `redirect_uri` + /// parameter when navigating user to the authentication page. + /// + /// Usually this is a URL with customize scheme that later service provider may call. It takes intermediate code in its + /// query and can be used to open the current app in client. The native side of UniWebView will catch and handle it, + /// then send it to Unity side as the result of `UniWebViewAuthenticationSession`. + /// + /// + /// The redirect URL set in the OAuth settings. + /// + string GetCallbackUrl(); + + /// + /// Returns the config of the authentication flow. It usually defines the authentication requests entry points. + /// + /// The config object of an authentication flow. + UniWebViewAuthenticationConfiguration GetAuthenticationConfiguration(); + + /// + /// Returns a dictionary contains the parameters that are used to perform the authentication request. + /// The key value pairs in the dictionary are used to construct the query string of the authentication request. + /// + /// This usually contains fields like `client_id`, `redirect_uri`, `response_type`, etc. + /// + /// The dictionary indicates parameters that are used to perform the authentication request. + Dictionary GetAuthenticationUriArguments(); + + /// + /// Returns a dictionary contains the parameters that are used to perform the access token exchange request. + /// The key value pairs in the dictionary are used to construct the HTTP form body of the access token exchange request. + /// + /// + /// The response from authentication request. If the authentication succeeds, it is + /// usually a custom scheme URL with a `code` query as its parameter. Base on this, you could construct the body of the + /// access token exchange request. + /// + /// + /// The dictionary indicates parameters that are used to perform the access token exchange request. + /// + Dictionary GetAccessTokenRequestParameters(string authResponse); + + /// + /// Returns the strong-typed token for the authentication process. + /// + /// When the token exchange request finishes without problem, the response body will be passed to this method and + /// any conforming class should construct the token object from the response body. + /// + /// + /// The body response of the access token exchange request. Usually it contains the desired `access_token` and other + /// necessary fields to describe the authenticated result. + /// + /// + /// A token object with `TToken` type that represents the authenticated result. + /// + TTokenType GenerateTokenFromExchangeResponse(string exchangeResponse); + + /// + /// Called when the authentication flow succeeds and a valid token is generated. + /// + UnityEvent OnAuthenticationFinished { get; } + + /// + /// Called when any error (including user cancellation) happens during the authentication flow. + /// + UnityEvent OnAuthenticationErrored { get; } +} + +/// +/// The manager object of an authentication flow. This defines and runs the common flow of an authentication process +/// with `code` response type. +/// +/// The responsive token type expected for this authentication flow. +public class UniWebViewAuthenticationFlow { + + private IUniWebViewAuthenticationFlow service; + + public UniWebViewAuthenticationFlow( + IUniWebViewAuthenticationFlow service + ) + { + this.service = service; + } + + /// + /// Start the authentication flow. + /// + public void StartAuth() + { + var callbackUri = new Uri(service.GetCallbackUrl()); + var authUrl = GetAuthUrl(); + var session = UniWebViewAuthenticationSession.Create(authUrl, callbackUri.Scheme); + var flow = service as UniWebViewAuthenticationCommonFlow; + if (flow != null && flow.privateMode) { + session.SetPrivateMode(true); + } + session.OnAuthenticationFinished += (_, resultUrl) => { + UniWebViewLogger.Instance.Verbose("Auth flow received callback url: " + resultUrl); + ExchangeToken(resultUrl); + }; + + session.OnAuthenticationErrorReceived += (_, errorCode, message) => { + FlowErrored(errorCode, message); + }; + + UniWebViewLogger.Instance.Verbose("Starting auth flow with url: " + authUrl + "; Callback scheme: " + callbackUri.Scheme); + session.Start(); + } + + private void ExchangeToken(string response) { + try { + var args = service.GetAccessTokenRequestParameters(response); + var request = GetTokenRequest(args); + MonoBehaviour context = (MonoBehaviour)service; + context.StartCoroutine(SendTokenRequest(request)); + } catch (Exception e) { + var message = e.Message; + var code = -1; + if (e is AuthenticationResponseException ex) { + code = ex.Code; + } + UniWebViewLogger.Instance.Critical("Exception on parsing response: " + e + ". Code: " + code + ". Message: " + message); + FlowErrored(code, message); + } + } + + private string GetAuthUrl() { + var builder = new UriBuilder(service.GetAuthenticationConfiguration().authorizationEndpoint); + var query = System.Web.HttpUtility.ParseQueryString(""); + foreach (var kv in service.GetAuthenticationUriArguments()) { + query.Add(kv.Key, kv.Value); + } + builder.Query = query.ToString(); + return builder.ToString(); + } + + private UnityWebRequest GetTokenRequest(Dictionaryargs) { + var builder = new UriBuilder(service.GetAuthenticationConfiguration().tokenEndpoint); + var form = new WWWForm(); + foreach (var kv in args) { + form.AddField(kv.Key, kv.Value); + } + return UnityWebRequest.Post(builder.ToString(), form); + } + + private IEnumerator SendTokenRequest(UnityWebRequest request) { + using (var www = request) { + yield return www.SendWebRequest(); + if (www.result != UnityWebRequest.Result.Success) { + string errorMessage = null; + string errorBody = null; + if (www.error != null) { + errorMessage = www.error; + } + if (www.downloadHandler != null && www.downloadHandler.text != null) { + errorBody = www.downloadHandler.text; + } + UniWebViewLogger.Instance.Critical("Failed to get access token. Error: " + errorMessage + ". " + errorBody); + FlowErrored(www.responseCode, errorBody ?? errorMessage); + } else { + var responseText = www.downloadHandler.text; + UniWebViewLogger.Instance.Info("Token exchange request succeeded. Response: " + responseText); + try { + var token = service.GenerateTokenFromExchangeResponse(www.downloadHandler.text); + FlowFinished(token); + } catch (Exception e) { + var message = e.Message; + var code = -1; + if (e is AuthenticationResponseException ex) { + code = ex.Code; + } + UniWebViewLogger.Instance.Critical( + "Exception on parsing token response: " + e + ". Code: " + code + ". Message: " + + message + ". Response: " + responseText); + FlowErrored(code, message); + } + } + } + } + + private void FlowFinished(TTokenType token) { + if (service.OnAuthenticationFinished != null) { + service.OnAuthenticationFinished.Invoke(token); + } + service = null;; + } + + private void FlowErrored(long code, string message) { + UniWebViewLogger.Instance.Info("Auth flow errored: " + code + ". Detail: " + message); + if (service.OnAuthenticationErrored != null) { + service.OnAuthenticationErrored.Invoke(code, message); + } + service = null; + } +} + +/// +/// The configuration object of an authentication flow. This defines the authentication entry points. +/// +public class UniWebViewAuthenticationConfiguration { + internal readonly string authorizationEndpoint; + internal readonly string tokenEndpoint; + + /// + /// Creates a new authentication configuration object with the given entry points. + /// + /// The entry point to navigate end user to for authentication. + /// The entry point which is used to exchange the received code to a valid access token. + public UniWebViewAuthenticationConfiguration(string authorizationEndpoint, string tokenEndpoint) { + this.authorizationEndpoint = authorizationEndpoint; + this.tokenEndpoint = tokenEndpoint; + } +} + +/// +/// The exception thrown when the authentication flow fails when handling the response. +/// +public class AuthenticationResponseException : Exception { + /// + /// Exception error code to identify the error type. See the static instance of this class to know detail of error codes. + /// + public int Code { get; } + + /// + /// Creates an authentication response exception. + /// + /// The error code. + /// A message that contains error detail. + public AuthenticationResponseException(int code, string message): base(message) { + Code = code; + } + + /// + /// An unexpected authentication callback is received. Error code 7001. + /// + public static AuthenticationResponseException UnexpectedAuthCallbackUrl + = new AuthenticationResponseException(7001, "The received callback url is not expected."); + + /// + /// The `state` value in the callback url is not the same as the one in the request. Error code 7002. + /// + public static AuthenticationResponseException InvalidState + = new AuthenticationResponseException(7002, "The `state` is not valid."); + + /// + /// The response is not a valid one. It does not contains a `code` field or cannot be parsed. Error code 7003. + /// + /// The query will be delivered as a part of error message. + /// The created response exception that can be thrown out and handled by package user. + public static AuthenticationResponseException InvalidResponse(string query) { + return new AuthenticationResponseException(7003, "The service auth response is not valid: " + query); + } +} \ No newline at end of file diff --git a/Runtime/GuruWebview/UniWebView5/UniWebView/Script/UniWebViewAuthentication/UniWebViewAuthenticationFlow.cs.meta b/Runtime/GuruWebview/UniWebView5/UniWebView/Script/UniWebViewAuthentication/UniWebViewAuthenticationFlow.cs.meta new file mode 100644 index 0000000..af679d9 --- /dev/null +++ b/Runtime/GuruWebview/UniWebView5/UniWebView/Script/UniWebViewAuthentication/UniWebViewAuthenticationFlow.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 4add539e6d306455c9da09b069e248bd +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/GuruWebview/UniWebView5/UniWebView/Script/UniWebViewAuthentication/UniWebViewAuthenticationFlowCustomize.cs b/Runtime/GuruWebview/UniWebView5/UniWebView/Script/UniWebViewAuthentication/UniWebViewAuthenticationFlowCustomize.cs new file mode 100644 index 0000000..fb03b07 --- /dev/null +++ b/Runtime/GuruWebview/UniWebView5/UniWebView/Script/UniWebViewAuthentication/UniWebViewAuthenticationFlowCustomize.cs @@ -0,0 +1,187 @@ +// +// UniWebViewAuthenticationFlowCustomize.cs +// Created by Wang Wei (@onevcat) on 2022-06-25. +// +// This file is a part of UniWebView Project (https://uniwebview.com) +// By purchasing the asset, you are allowed to use this code in as many as projects +// you want, only if you publish the final products under the name of the same account +// used for the purchase. +// +// This asset and all corresponding files (such as source code) are provided on an +// “as is” basis, without warranty of any kind, express of 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 action of contract, tort or otherwise, +// arising from, out of or in connection with the software or the use of other dealing in the software. +// + +using System; +using System.Collections.Generic; +using UnityEngine; +using UnityEngine.Events; + +/// +/// A customizable authentication flow behavior. +/// +/// +/// Besides of the predefined authentication flows, such as Twitter (`UniWebViewAuthenticationFlowTwitter`) or Google +/// (`UniWebViewAuthenticationFlowGoogle`), this class allows you to determine the details of the authentication flow, +/// such as entry points, grant types, scopes and more. But similar to other target-specified flows, it follows the same +/// OAuth 2.0 code auth pattern. +/// +/// If you need to support other authentication flows for the platform targets other than the predefined ones, you can +/// use this class and set all necessary parameters. It runs the standard OAuth 2.0 flow and gives out a +/// `UniWebViewAuthenticationStandardToken` as the result. +/// +/// If you need to support authentication flows other than `code` based OAuth 2.0, try to derive from +/// `UniWebViewAuthenticationCommonFlow` and implement `IUniWebViewAuthenticationFlow` interface, or even use the +/// underneath `UniWebViewAuthenticationSession` to get a highly customizable flow. +/// +/// +public class UniWebViewAuthenticationFlowCustomize : UniWebViewAuthenticationCommonFlow, IUniWebViewAuthenticationFlow { + + /// + /// The config object which defines the basic information of the authentication flow. + /// + public UniWebViewAuthenticationFlowCustomizeConfig config = new UniWebViewAuthenticationFlowCustomizeConfig(); + + /// + /// The client Id of your OAuth application. + /// + public string clientId = ""; + + /// + /// The redirect URI of your OAuth application. The service provider is expected to call this URI to pass back the + /// authorization code. It should be something also set to your OAuth application. + /// + /// Also remember to add it to the "Auth Callback Urls" field in UniWebView's preference panel. + /// + public string redirectUri = ""; + + /// + /// The scope of the authentication request. + /// + public string scope = ""; + + /// + /// The optional object which defines some optional parameters of the authentication flow, such as whether supports + /// `state` or `PKCE`. + /// + public UniWebViewAuthenticationFlowCustomizeOptional optional; + + /// + /// Starts the authentication flow with the standard OAuth 2.0. + /// This implements the abstract method in `UniWebViewAuthenticationCommonFlow`. + /// + public override void StartAuthenticationFlow() { + var flow = new UniWebViewAuthenticationFlow(this); + flow.StartAuth(); + } + + /// + /// Implements required method in `IUniWebViewAuthenticationFlow`. + /// + public UniWebViewAuthenticationConfiguration GetAuthenticationConfiguration() { + return new UniWebViewAuthenticationConfiguration( + config.authorizationEndpoint, + config.tokenEndpoint + ); + } + + /// + /// Implements required method in `IUniWebViewAuthenticationFlow`. + /// + public string GetCallbackUrl() { + return redirectUri; + } + + /// + /// Implements required method in `IUniWebViewAuthenticationFlow`. + /// + public Dictionary GetAuthenticationUriArguments() { + var authorizeArgs = new Dictionary { + { "client_id", clientId }, + { "redirect_uri", redirectUri }, + { "scope", scope }, + { "response_type", config.responseType } + }; + if (optional != null) { + if (optional.enableState) { + var state = GenerateAndStoreState(); + authorizeArgs.Add("state", state); + } + + if (optional.PKCESupport != UniWebViewAuthenticationPKCE.None) { + var codeChallenge = GenerateCodeChallengeAndStoreCodeVerify(optional.PKCESupport); + authorizeArgs.Add("code_challenge", codeChallenge); + + var method = UniWebViewAuthenticationUtils.ConvertPKCEToString(optional.PKCESupport); + authorizeArgs.Add("code_challenge_method", method); + } + } + + return authorizeArgs; + } + + /// + /// Implements required method in `IUniWebViewAuthenticationFlow`. + /// + public Dictionary GetAccessTokenRequestParameters(string authResponse) { + if (!authResponse.StartsWith(redirectUri)) { + throw AuthenticationResponseException.UnexpectedAuthCallbackUrl; + } + + var uri = new Uri(authResponse); + var response = UniWebViewAuthenticationUtils.ParseFormUrlEncodedString(uri.Query); + if (!response.TryGetValue("code", out var code)) { + throw AuthenticationResponseException.InvalidResponse(authResponse); + } + if (optional.enableState) { + VerifyState(response); + } + var parameters = new Dictionary { + { "client_id", clientId }, + { "code", code }, + { "redirect_uri", redirectUri }, + { "grant_type", config.grantType }, + }; + if (CodeVerify != null) { + parameters.Add("code_verifier", CodeVerify); + } + + return parameters; + } + + /// + /// Implements required method in `IUniWebViewAuthenticationFlow`. + /// + public UniWebViewAuthenticationStandardToken GenerateTokenFromExchangeResponse(string exchangeResponse) { + return UniWebViewAuthenticationTokenFactory.Parse(exchangeResponse); + } + + /// + /// Implements required method in `IUniWebViewAuthenticationFlow`. + /// + [field: SerializeField] + public UnityEvent OnAuthenticationFinished { get; set; } + + /// + /// Implements required method in `IUniWebViewAuthenticationFlow`. + /// + [field: SerializeField] + public UnityEvent OnAuthenticationErrored { get; set; } +} + +[Serializable] +public class UniWebViewAuthenticationFlowCustomizeConfig { + public string authorizationEndpoint = ""; + public string tokenEndpoint = ""; + public string responseType = "code"; + public string grantType = "authorization_code"; +} + +[Serializable] +public class UniWebViewAuthenticationFlowCustomizeOptional { + public UniWebViewAuthenticationPKCE PKCESupport = UniWebViewAuthenticationPKCE.None; + public bool enableState = false; +} \ No newline at end of file diff --git a/Runtime/GuruWebview/UniWebView5/UniWebView/Script/UniWebViewAuthentication/UniWebViewAuthenticationFlowCustomize.cs.meta b/Runtime/GuruWebview/UniWebView5/UniWebView/Script/UniWebViewAuthentication/UniWebViewAuthenticationFlowCustomize.cs.meta new file mode 100644 index 0000000..b464e17 --- /dev/null +++ b/Runtime/GuruWebview/UniWebView5/UniWebView/Script/UniWebViewAuthentication/UniWebViewAuthenticationFlowCustomize.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: bfcc799dc1a0c4ef58bc3486a8af7afa +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/GuruWebview/UniWebView5/UniWebView/Script/UniWebViewAuthentication/UniWebViewAuthenticationFlowDiscord.cs b/Runtime/GuruWebview/UniWebView5/UniWebView/Script/UniWebViewAuthentication/UniWebViewAuthenticationFlowDiscord.cs new file mode 100644 index 0000000..d7a43b8 --- /dev/null +++ b/Runtime/GuruWebview/UniWebView5/UniWebView/Script/UniWebViewAuthentication/UniWebViewAuthenticationFlowDiscord.cs @@ -0,0 +1,181 @@ +// +// UniWebViewAuthenticationFlowDiscord.cs +// Created by Wang Wei (@onevcat) on 2022-06-25. +// +// This file is a part of UniWebView Project (https://uniwebview.com) +// By purchasing the asset, you are allowed to use this code in as many as projects +// you want, only if you publish the final products under the name of the same account +// used for the purchase. +// +// This asset and all corresponding files (such as source code) are provided on an +// “as is” basis, without warranty of any kind, express of 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 action of contract, tort or otherwise, +// arising from, out of or in connection with the software or the use of other dealing in the software. +// + +using System; +using System.Collections.Generic; +using UnityEngine; +using UnityEngine.Events; + +/// +/// A predefined authentication flow for Discord. +/// +/// This implementation follows the flow described here: +/// https://discord.com/developers/docs/topics/oauth2 +/// +/// See https://docs.uniwebview.com/guide/oauth2.html for a more detailed guide of authentication in UniWebView. +/// +public class UniWebViewAuthenticationFlowDiscord : UniWebViewAuthenticationCommonFlow, IUniWebViewAuthenticationFlow { + /// + /// The client ID of your Discord application. + /// + public string clientId = ""; + + /// + /// The client secret of your Discord application. + /// + public string clientSecret = ""; + + /// + /// The redirect URI of this Discord application. + /// + public string redirectUri = ""; + + /// + /// The scope string of all your required scopes. + /// + public string scope = ""; + + /// + /// Optional to control this flow's behaviour. + /// + public UniWebViewAuthenticationFlowDiscordOptional optional; + + private string responseType = "code"; + private string grantType = "authorization_code"; + + private readonly UniWebViewAuthenticationConfiguration config = + new UniWebViewAuthenticationConfiguration( + "https://discord.com/api/oauth2/authorize", + "https://discord.com/api/oauth2/token" + ); + + /// + /// Starts the authentication flow with the standard OAuth 2.0. + /// This implements the abstract method in `UniWebViewAuthenticationCommonFlow`. + /// + public override void StartAuthenticationFlow() { + var flow = new UniWebViewAuthenticationFlow(this); + flow.StartAuth(); + } + + /// + /// Implements required method in `IUniWebViewAuthenticationFlow`. + /// + public UniWebViewAuthenticationConfiguration GetAuthenticationConfiguration() { + return config; + } + + /// + /// Implements required method in `IUniWebViewAuthenticationFlow`. + /// + public string GetCallbackUrl() { + return redirectUri; + } + + /// + /// Implements required method in `IUniWebViewAuthenticationFlow`. + /// + public Dictionary GetAuthenticationUriArguments() { + var authorizeArgs = new Dictionary { + { "client_id", clientId }, + { "redirect_uri", redirectUri }, + { "scope", scope }, + { "response_type", responseType } + }; + if (optional != null) { + if (optional.enableState) { + var state = GenerateAndStoreState(); + authorizeArgs.Add("state", state); + } + + if (optional.PKCESupport != UniWebViewAuthenticationPKCE.None) { + var codeChallenge = GenerateCodeChallengeAndStoreCodeVerify(optional.PKCESupport); + authorizeArgs.Add("code_challenge", codeChallenge); + + var method = UniWebViewAuthenticationUtils.ConvertPKCEToString(optional.PKCESupport); + authorizeArgs.Add("code_challenge_method", method); + } + } + + return authorizeArgs; + } + + /// + /// Implements required method in `IUniWebViewAuthenticationFlow`. + /// + public Dictionary GetAccessTokenRequestParameters(string authResponse) { + if (!authResponse.StartsWith(redirectUri)) { + throw AuthenticationResponseException.UnexpectedAuthCallbackUrl; + } + + var uri = new Uri(authResponse); + var response = UniWebViewAuthenticationUtils.ParseFormUrlEncodedString(uri.Query); + if (!response.TryGetValue("code", out var code)) { + throw AuthenticationResponseException.InvalidResponse(authResponse); + } + if (optional.enableState) { + VerifyState(response); + } + var parameters = new Dictionary { + { "client_id", clientId }, + { "client_secret", clientSecret }, + { "code", code }, + { "redirect_uri", redirectUri }, + { "grant_type", grantType }, + }; + if (CodeVerify != null) { + parameters.Add("code_verifier", CodeVerify); + } + + return parameters; + } + + /// + /// Implements required method in `IUniWebViewAuthenticationFlow`. + /// + public UniWebViewAuthenticationDiscordToken GenerateTokenFromExchangeResponse(string exchangeResponse) { + return UniWebViewAuthenticationTokenFactory.Parse(exchangeResponse); + } + + [field: SerializeField] + public UnityEvent OnAuthenticationFinished { get; set; } + [field: SerializeField] + public UnityEvent OnAuthenticationErrored { get; set; } +} + +/// +/// The authentication flow's optional settings for Discord. +/// +[Serializable] +public class UniWebViewAuthenticationFlowDiscordOptional { + /// + /// Whether to enable PKCE when performing authentication. On mobile platforms, this has to be enabled as `S256`, + /// otherwise, Discord will reject the authentication request. + /// + public UniWebViewAuthenticationPKCE PKCESupport = UniWebViewAuthenticationPKCE.S256; + /// + /// Whether to enable the state verification. If enabled, the state will be generated and verified in the + /// authentication callback. Default is `true`. + /// + public bool enableState = true; +} + +/// +/// The token object from Discord. Check `UniWebViewAuthenticationStandardToken` for more. +/// +public class UniWebViewAuthenticationDiscordToken : UniWebViewAuthenticationStandardToken { } + diff --git a/Runtime/GuruWebview/UniWebView5/UniWebView/Script/UniWebViewAuthentication/UniWebViewAuthenticationFlowDiscord.cs.meta b/Runtime/GuruWebview/UniWebView5/UniWebView/Script/UniWebViewAuthentication/UniWebViewAuthenticationFlowDiscord.cs.meta new file mode 100644 index 0000000..d1f4c71 --- /dev/null +++ b/Runtime/GuruWebview/UniWebView5/UniWebView/Script/UniWebViewAuthentication/UniWebViewAuthenticationFlowDiscord.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 69ed5d1df2635470ca5c939d11d16924 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/GuruWebview/UniWebView5/UniWebView/Script/UniWebViewAuthentication/UniWebViewAuthenticationFlowFacebook.cs b/Runtime/GuruWebview/UniWebView5/UniWebView/Script/UniWebViewAuthentication/UniWebViewAuthenticationFlowFacebook.cs new file mode 100644 index 0000000..710cd99 --- /dev/null +++ b/Runtime/GuruWebview/UniWebView5/UniWebView/Script/UniWebViewAuthentication/UniWebViewAuthenticationFlowFacebook.cs @@ -0,0 +1,198 @@ +// +// UniWebViewAuthenticationFlowFacebook.cs +// Created by Wang Wei (@onevcat) on 2022-06-25. +// +// This file is a part of UniWebView Project (https://uniwebview.com) +// By purchasing the asset, you are allowed to use this code in as many as projects +// you want, only if you publish the final products under the name of the same account +// used for the purchase. +// +// This asset and all corresponding files (such as source code) are provided on an +// “as is” basis, without warranty of any kind, express of 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 action of contract, tort or otherwise, +// arising from, out of or in connection with the software or the use of other dealing in the software. +// + +using System; +using System.Collections.Generic; +using UnityEngine; +using UnityEngine.Events; + +/// +/// A predefined authentication flow for Facebook Login. +/// +/// It is not a standard OAuth2 flow, and using a plain web view. There once was a policy that Facebook did not allow +/// any third-party customize authentication flow other than using their official SDK. Recently Facebook started to provide +/// a so-called manual flow way to perform authentication. But it is originally only for Desktop apps, it is not stable +/// and not standard. +/// +/// Facebook suggests "For mobile apps, use the Facebook SDKs for iOS and Android, and follow the separate guides for +/// these platforms." So on mobile, use this class with your own risk since it might be invalidated or forbidden by +/// Facebook in the future. +/// +/// This implementation is based on the manual flow described in the following document: +/// https://developers.facebook.com/docs/facebook-login/guides/advanced/manual-flow +/// +/// See https://docs.uniwebview.com/guide/oauth2.html for a more detailed guide of authentication in UniWebView. +/// +public class UniWebViewAuthenticationFlowFacebook: UniWebViewAuthenticationCommonFlow { + /// + /// The App ID of your Facebook application + /// + public string appId = ""; + + /// + /// Optional to control this flow's behaviour. + /// + public UniWebViewAuthenticationFlowFacebookOptional optional; + + // The redirect URL should be exactly this one. Web view should inspect the loading of this URL to handle the result. + private string redirectUri = "https://www.facebook.com/connect/login_success.html"; + // Only `token` response type is supported to use Facebook Login as the manual flow. + private string responseType = "token"; + + [field: SerializeField] + public UnityEvent OnAuthenticationFinished { get; set; } + [field: SerializeField] + public UnityEvent OnAuthenticationErrored { get; set; } + + private readonly UniWebViewAuthenticationConfiguration config = + new UniWebViewAuthenticationConfiguration( + "https://www.facebook.com/v14.0/dialog/oauth", + // This `access_token` entry point is in fact not used in current auth model. + "https://graph.facebook.com/v14.0/oauth/access_token" + ); + + /// + /// Starts the authentication flow. + /// + /// This flow is executed in a customized web view and it is not a standard OAuth2 flow. + /// + public override void StartAuthenticationFlow() { + var webView = gameObject.AddComponent(); + webView.OnPageFinished += (view, status, url) => { + if (status != 200) { + if (OnAuthenticationErrored != null) { + OnAuthenticationErrored.Invoke(status, "Error while loading auth page."); + } + webView.Hide(false, UniWebViewTransitionEdge.Bottom, 0.25f, () => { + Destroy(webView); + }); + return; + } + + if (url.StartsWith(redirectUri)) { + UniWebViewLogger.Instance.Info("Received redirect url: " + url); + var uri = new Uri(url); + var response = UniWebViewAuthenticationUtils.ParseFormUrlEncodedString(uri.Fragment); + try { + VerifyState(response); + var token = new UniWebViewAuthenticationFacebookToken(url, response); + if (OnAuthenticationFinished != null) { + OnAuthenticationFinished.Invoke(token); + } + } + catch (Exception e) { + var message = e.Message; + var code = -1; + if (e is AuthenticationResponseException ex) { + code = ex.Code; + } + + UniWebViewLogger.Instance.Critical("Exception on parsing response: " + e + ". Code: " + code + + ". Message: " + message); + if (OnAuthenticationErrored != null) { + OnAuthenticationErrored.Invoke(code, message); + } + } + finally { + webView.Hide(false, UniWebViewTransitionEdge.Bottom, 0.25f, () => { + Destroy(webView); + }); + } + } + }; + webView.OnPageErrorReceived += (view, code, message) => { + if (OnAuthenticationErrored != null) { + OnAuthenticationErrored.Invoke(code, message); + } + }; + webView.Frame = new Rect(0, 0, Screen.width, Screen.height); + webView.Load(GetAuthUrl()); + webView.SetShowToolbar(true, adjustInset: true); + webView.Show(false, UniWebViewTransitionEdge.Bottom, 0.25f); + } + + private string GetAuthUrl() { + var builder = new UriBuilder(config.authorizationEndpoint); + var query = System.Web.HttpUtility.ParseQueryString(""); + foreach (var kv in GetAuthenticationUriArguments()) { + query.Add(kv.Key, kv.Value); + } + builder.Query = query.ToString(); + return builder.ToString(); + } + + private Dictionary GetAuthenticationUriArguments() { + + var state = GenerateAndStoreState(); + var authorizeArgs = new Dictionary { + { "client_id", appId }, + { "redirect_uri", redirectUri }, + { "state", state}, + { "response_type", responseType } + }; + if (optional != null) { + if (!String.IsNullOrEmpty(optional.scope)) { + authorizeArgs.Add("scope", optional.scope); + } + } + + return authorizeArgs; + } +} + +/// +/// The authentication flow's optional settings for Facebook. +/// +[Serializable] +public class UniWebViewAuthenticationFlowFacebookOptional { + /// + /// The scope string of all your required scopes. + /// + public string scope = ""; +} + +/// The token object from Facebook. +public class UniWebViewAuthenticationFacebookToken { + /// + /// The access token received from Facebook Login. + /// + public string AccessToken { get; } + /// + /// The expiration duration that your app can access requested items of user data. + /// + public long DataAccessExpirationTime { get; } + /// + /// The expiration duration that the access token is valid for authentication purpose. + /// + public long ExpiresIn { get; } + /// + /// The raw value of the response of the exchange token request. + /// If the predefined fields are not enough, you can parse the raw value to get the extra information. + /// + public string RawValue { get; } + + public UniWebViewAuthenticationFacebookToken(string response, Dictionary values) { + RawValue = response; + AccessToken = values.ContainsKey("access_token") ? values["access_token"] as string : null ; + if (AccessToken == null) { + throw AuthenticationResponseException.InvalidResponse(response); + } + DataAccessExpirationTime = values.ContainsKey("data_access_expiration_time") ? long.Parse(values["data_access_expiration_time"]) : 0; + ExpiresIn = values.ContainsKey("expires_in") ? long.Parse(values["expires_in"]) : 0; + } +} + diff --git a/Runtime/GuruWebview/UniWebView5/UniWebView/Script/UniWebViewAuthentication/UniWebViewAuthenticationFlowFacebook.cs.meta b/Runtime/GuruWebview/UniWebView5/UniWebView/Script/UniWebViewAuthentication/UniWebViewAuthenticationFlowFacebook.cs.meta new file mode 100644 index 0000000..d71ee94 --- /dev/null +++ b/Runtime/GuruWebview/UniWebView5/UniWebView/Script/UniWebViewAuthentication/UniWebViewAuthenticationFlowFacebook.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: ba2d51a8adc8e42b4aaff50218dfe058 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/GuruWebview/UniWebView5/UniWebView/Script/UniWebViewAuthentication/UniWebViewAuthenticationFlowGitHub.cs b/Runtime/GuruWebview/UniWebView5/UniWebView/Script/UniWebViewAuthentication/UniWebViewAuthenticationFlowGitHub.cs new file mode 100644 index 0000000..8338bee --- /dev/null +++ b/Runtime/GuruWebview/UniWebView5/UniWebView/Script/UniWebViewAuthentication/UniWebViewAuthenticationFlowGitHub.cs @@ -0,0 +1,214 @@ +// +// UniWebViewAuthenticationFlowGitHub.cs +// Created by Wang Wei (@onevcat) on 2022-06-25. +// +// This file is a part of UniWebView Project (https://uniwebview.com) +// By purchasing the asset, you are allowed to use this code in as many as projects +// you want, only if you publish the final products under the name of the same account +// used for the purchase. +// +// This asset and all corresponding files (such as source code) are provided on an +// “as is” basis, without warranty of any kind, express of 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 action of contract, tort or otherwise, +// arising from, out of or in connection with the software or the use of other dealing in the software. +// + +using UnityEngine; +using UnityEngine.Events; +using System.Collections.Generic; +using System; + +/// +/// A predefined authentication flow for GitHub. +/// +/// This implementation follows the flow described here: +/// https://docs.github.com/en/developers/apps/building-oauth-apps/authorizing-oauth-apps +/// +/// See https://docs.uniwebview.com/guide/oauth2.html for a more detailed guide of authentication in UniWebView. +/// +public class UniWebViewAuthenticationFlowGitHub: UniWebViewAuthenticationCommonFlow, IUniWebViewAuthenticationFlow { + + /// + /// The client ID of your GitHub application. + /// + public string clientId = ""; + + /// + /// The client secret of your GitHub application. + /// + public string clientSecret = ""; + + /// + /// The callback URL of your GitHub application. + /// + public string callbackUrl = ""; + + /// + /// Optional to control this flow's behaviour. + /// + public UniWebViewAuthenticationFlowGitHubOptional optional; + + private readonly UniWebViewAuthenticationConfiguration config = + new UniWebViewAuthenticationConfiguration( + "https://github.com/login/oauth/authorize", + "https://github.com/login/oauth/access_token" + ); + + [field: SerializeField] + public UnityEvent OnAuthenticationFinished { get; set; } + [field: SerializeField] + public UnityEvent OnAuthenticationErrored { get; set; } + + /// + /// Starts the authentication flow with the standard OAuth 2.0. + /// This implements the abstract method in `UniWebViewAuthenticationCommonFlow`. + /// + public override void StartAuthenticationFlow() { + var flow = new UniWebViewAuthenticationFlow(this); + flow.StartAuth(); + } + + /// + /// Implements required method in `IUniWebViewAuthenticationFlow`. + /// + public Dictionary GetAuthenticationUriArguments() { + var authorizeArgs = new Dictionary { { "client_id", clientId } }; + if (optional != null) { + if (!String.IsNullOrEmpty(optional.redirectUri)) { + authorizeArgs.Add("redirect_uri", optional.redirectUri); + } + if (!String.IsNullOrEmpty(optional.login)) { + authorizeArgs.Add("login", optional.login); + } + if (!String.IsNullOrEmpty(optional.scope)) { + authorizeArgs.Add("scope", optional.scope); + } + if (optional.enableState) { + var state = GenerateAndStoreState(); + authorizeArgs.Add("state", state); + } + if (!optional.allowSignup) { // The default value is true. + authorizeArgs.Add("allow_signup", "false"); + } + } + + return authorizeArgs; + } + + /// + /// Implements required method in `IUniWebViewAuthenticationFlow`. + /// + public string GetCallbackUrl() { + return callbackUrl; + } + + /// + /// Implements required method in `IUniWebViewAuthenticationFlow`. + /// + public UniWebViewAuthenticationConfiguration GetAuthenticationConfiguration() { + return config; + } + + /// + /// Implements required method in `IUniWebViewAuthenticationFlow`. + /// + public Dictionary GetAccessTokenRequestParameters(string authResponse) { + if (!authResponse.StartsWith(callbackUrl)) { + throw AuthenticationResponseException.UnexpectedAuthCallbackUrl; + } + var uri = new Uri(authResponse); + var response = UniWebViewAuthenticationUtils.ParseFormUrlEncodedString(uri.Query); + if (!response.TryGetValue("code", out var code)) { + throw AuthenticationResponseException.InvalidResponse(authResponse); + } + if (optional.enableState) { + VerifyState(response); + } + var result = new Dictionary { + { "client_id", clientId }, + { "client_secret", clientSecret }, + { "code", code } + }; + + if (optional != null && String.IsNullOrEmpty(optional.redirectUri)) { + result.Add("redirect_uri", optional.redirectUri); + } + return result; + } + + /// + /// Implements required method in `IUniWebViewAuthenticationFlow`. + /// + public UniWebViewAuthenticationGitHubToken GenerateTokenFromExchangeResponse(string exchangeResponse) { + return new UniWebViewAuthenticationGitHubToken(exchangeResponse); + } +} + +/// +/// The authentication flow's optional settings for GitHub. +/// +[Serializable] +public class UniWebViewAuthenticationFlowGitHubOptional { + /// + /// The redirect URI should be used in exchange token request. + /// + public string redirectUri = ""; + /// + /// Suggests a specific account to use for signing in and authorizing the app. + /// + public string login = ""; + /// + /// The scope string of all your required scopes. + /// + public string scope = ""; + /// + /// Whether to enable the state verification. If enabled, the state will be generated and verified in the + /// authentication callback. + /// + public bool enableState = false; + /// + /// Whether or not unauthenticated users will be offered an option to sign up for GitHub during the OAuth flow. + /// + public bool allowSignup = true; +} + +/// +/// The token object from GitHub. +/// +public class UniWebViewAuthenticationGitHubToken { + /// The access token retrieved from the service provider. + public string AccessToken { get; } + + /// The granted scopes of the token. + public string Scope { get; } + + /// The token type. Usually `bearer`. + public string TokenType { get; } + + /// The refresh token retrieved from the service provider. + public string RefreshToken { get; } + + /// Expiration duration for the refresh token. + public long RefreshTokenExpiresIn { get; } + + /// + /// The raw value of the response of the exchange token request. + /// If the predefined fields are not enough, you can parse the raw value to get the extra information. + /// + public string RawValue { get; } + + public UniWebViewAuthenticationGitHubToken(string result) { + RawValue = result; + var values = UniWebViewAuthenticationUtils.ParseFormUrlEncodedString(result); + AccessToken = values.ContainsKey("access_token") ? values["access_token"] : null ; + if (AccessToken == null) { + throw AuthenticationResponseException.InvalidResponse(result); + } + Scope = values.ContainsKey("scope") ? values["scope"] : null; + TokenType = values.ContainsKey("token_type") ? values["token_type"] : null; + RefreshToken = values.ContainsKey("refresh_token") ? values["refresh_token"] : null; + RefreshTokenExpiresIn = values.ContainsKey("refresh_token_expires_in") ? long.Parse(values["refresh_token_expires_in"]) : 0; + } +} \ No newline at end of file diff --git a/Runtime/GuruWebview/UniWebView5/UniWebView/Script/UniWebViewAuthentication/UniWebViewAuthenticationFlowGitHub.cs.meta b/Runtime/GuruWebview/UniWebView5/UniWebView/Script/UniWebViewAuthentication/UniWebViewAuthenticationFlowGitHub.cs.meta new file mode 100644 index 0000000..4ad5baf --- /dev/null +++ b/Runtime/GuruWebview/UniWebView5/UniWebView/Script/UniWebViewAuthentication/UniWebViewAuthenticationFlowGitHub.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 1925cdd8b9d284ec48146d7d03c41672 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/GuruWebview/UniWebView5/UniWebView/Script/UniWebViewAuthentication/UniWebViewAuthenticationFlowGoogle.cs b/Runtime/GuruWebview/UniWebView5/UniWebView/Script/UniWebViewAuthentication/UniWebViewAuthenticationFlowGoogle.cs new file mode 100644 index 0000000..6dcb426 --- /dev/null +++ b/Runtime/GuruWebview/UniWebView5/UniWebView/Script/UniWebViewAuthentication/UniWebViewAuthenticationFlowGoogle.cs @@ -0,0 +1,191 @@ +// +// UniWebViewAuthenticationFlowGoogle.cs +// Created by Wang Wei (@onevcat) on 2022-06-25. +// +// This file is a part of UniWebView Project (https://uniwebview.com) +// By purchasing the asset, you are allowed to use this code in as many as projects +// you want, only if you publish the final products under the name of the same account +// used for the purchase. +// +// This asset and all corresponding files (such as source code) are provided on an +// “as is” basis, without warranty of any kind, express of 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 action of contract, tort or otherwise, +// arising from, out of or in connection with the software or the use of other dealing in the software. +// + +using System; +using System.Collections.Generic; +using UnityEngine; +using UnityEngine.Events; + +/// +/// A predefined authentication flow for Google Identity. +/// +/// This implementation follows the flow described here: +/// https://developers.google.com/identity/protocols/oauth2/native-app +/// +/// Google authentication flow is a bit different from the other standard authentication flows. Please read the link +/// above carefully to understand it. +/// +/// See https://docs.uniwebview.com/guide/oauth2.html for a more detailed guide of authentication in UniWebView. +/// +public class UniWebViewAuthenticationFlowGoogle : UniWebViewAuthenticationCommonFlow, IUniWebViewAuthenticationFlow { + /// + /// The client ID of your Google application. + /// + public string clientId = ""; + + /// + /// The redirect URI of your Google application. + /// + /// It might be something like "com.googleusercontent.apps.${clientId}:${redirect_uri_path}". Be caution that the URI does not + /// contain regular double slashes `//`, but should be only one. + /// + public string redirectUri = ""; + + /// + /// The scope of your Google application. + /// + /// It might be some full URL in recent Google services, such as "https://www.googleapis.com/auth/userinfo.profile" + /// + public string scope = ""; + + /// + /// Optional to control this flow's behaviour. + /// + public UniWebViewAuthenticationFlowGoogleOptional optional; + + private string responseType = "code"; + private string grantType = "authorization_code"; + + private readonly UniWebViewAuthenticationConfiguration config = + new UniWebViewAuthenticationConfiguration( + "https://accounts.google.com/o/oauth2/v2/auth", + "https://oauth2.googleapis.com/token" + ); + + /// + /// Starts the authentication flow with the standard OAuth 2.0. + /// This implements the abstract method in `UniWebViewAuthenticationCommonFlow`. + /// + public override void StartAuthenticationFlow() { + var flow = new UniWebViewAuthenticationFlow(this); + flow.StartAuth(); + } + + /// + /// Implements required method in `IUniWebViewAuthenticationFlow`. + /// + public UniWebViewAuthenticationConfiguration GetAuthenticationConfiguration() { + return config; + } + + /// + /// Implements required method in `IUniWebViewAuthenticationFlow`. + /// + public string GetCallbackUrl() { + return redirectUri; + } + + /// + /// Implements required method in `IUniWebViewAuthenticationFlow`. + /// + public Dictionary GetAuthenticationUriArguments() { + var authorizeArgs = new Dictionary { + { "client_id", clientId }, + { "redirect_uri", redirectUri }, + { "scope", scope }, + { "response_type", responseType } + }; + if (optional != null) { + if (optional.enableState) { + var state = GenerateAndStoreState(); + authorizeArgs.Add("state", state); + } + + if (optional.PKCESupport != UniWebViewAuthenticationPKCE.None) { + var codeChallenge = GenerateCodeChallengeAndStoreCodeVerify(optional.PKCESupport); + authorizeArgs.Add("code_challenge", codeChallenge); + + var method = UniWebViewAuthenticationUtils.ConvertPKCEToString(optional.PKCESupport); + authorizeArgs.Add("code_challenge_method", method); + } + + if (!String.IsNullOrEmpty(optional.loginHint)) { + authorizeArgs.Add("login_hint", optional.loginHint); + } + } + + return authorizeArgs; + } + + /// + /// Implements required method in `IUniWebViewAuthenticationFlow`. + /// + public Dictionary GetAccessTokenRequestParameters(string authResponse) { + if (!authResponse.StartsWith(redirectUri)) { + throw AuthenticationResponseException.UnexpectedAuthCallbackUrl; + } + + var uri = new Uri(authResponse); + var response = UniWebViewAuthenticationUtils.ParseFormUrlEncodedString(uri.Query); + if (!response.TryGetValue("code", out var code)) { + throw AuthenticationResponseException.InvalidResponse(authResponse); + } + if (optional.enableState) { + VerifyState(response); + } + var parameters = new Dictionary { + { "client_id", clientId }, + { "code", code }, + { "redirect_uri", redirectUri }, + { "grant_type", grantType }, + }; + if (CodeVerify != null) { + parameters.Add("code_verifier", CodeVerify); + } + + return parameters; + } + + /// + /// Implements required method in `IUniWebViewAuthenticationFlow`. + /// + public UniWebViewAuthenticationGoogleToken GenerateTokenFromExchangeResponse(string exchangeResponse) { + return UniWebViewAuthenticationTokenFactory.Parse(exchangeResponse); + } + + [field: SerializeField] + public UnityEvent OnAuthenticationFinished { get; set; } + [field: SerializeField] + public UnityEvent OnAuthenticationErrored { get; set; } +} + +/// +/// The authentication flow's optional settings for Google. +/// +[Serializable] +public class UniWebViewAuthenticationFlowGoogleOptional { + /// + /// Whether to enable PKCE when performing authentication. Default is `S256`. + /// + public UniWebViewAuthenticationPKCE PKCESupport = UniWebViewAuthenticationPKCE.S256; + /// + /// Whether to enable the state verification. If enabled, the state will be generated and verified in the + /// authentication callback. Default is `true`. + /// + public bool enableState = true; + /// + /// If your application knows which user is trying to authenticate, it can use this parameter to provide a hint to + /// the Google Authentication Server. + /// + public string loginHint = ""; +} + +/// +/// The token object from Google. Check `UniWebViewAuthenticationStandardToken` for more. +/// +public class UniWebViewAuthenticationGoogleToken : UniWebViewAuthenticationStandardToken { } + diff --git a/Runtime/GuruWebview/UniWebView5/UniWebView/Script/UniWebViewAuthentication/UniWebViewAuthenticationFlowGoogle.cs.meta b/Runtime/GuruWebview/UniWebView5/UniWebView/Script/UniWebViewAuthentication/UniWebViewAuthenticationFlowGoogle.cs.meta new file mode 100644 index 0000000..41e422f --- /dev/null +++ b/Runtime/GuruWebview/UniWebView5/UniWebView/Script/UniWebViewAuthentication/UniWebViewAuthenticationFlowGoogle.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 9a51fd6394dc49ed954d995b9f7b0bbc +timeCreated: 1655992593 \ No newline at end of file diff --git a/Runtime/GuruWebview/UniWebView5/UniWebView/Script/UniWebViewAuthentication/UniWebViewAuthenticationFlowLine.cs b/Runtime/GuruWebview/UniWebView5/UniWebView/Script/UniWebViewAuthentication/UniWebViewAuthenticationFlowLine.cs new file mode 100644 index 0000000..4e83078 --- /dev/null +++ b/Runtime/GuruWebview/UniWebView5/UniWebView/Script/UniWebViewAuthentication/UniWebViewAuthenticationFlowLine.cs @@ -0,0 +1,200 @@ +// +// UniWebViewAuthenticationFlowLine.cs +// Created by Wang Wei (@onevcat) on 2022-06-25. +// +// This file is a part of UniWebView Project (https://uniwebview.com) +// By purchasing the asset, you are allowed to use this code in as many as projects +// you want, only if you publish the final products under the name of the same account +// used for the purchase. +// +// This asset and all corresponding files (such as source code) are provided on an +// “as is” basis, without warranty of any kind, express of 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 action of contract, tort or otherwise, +// arising from, out of or in connection with the software or the use of other dealing in the software. +// + +using System; +using System.Collections.Generic; +using UnityEngine; +using UnityEngine.Events; + +/// +/// A predefined authentication flow LINE Login. +/// +/// This implementation follows the flow described here: +/// https://developers.line.biz/en/reference/line-login/ +/// +/// Google authentication flow is a bit different from the other standard authentication flows. Please read the link +/// above carefully to understand it. +/// +/// See https://docs.uniwebview.com/guide/oauth2.html for a more detailed guide of authentication in UniWebView. +/// +public class UniWebViewAuthenticationFlowLine : UniWebViewAuthenticationCommonFlow, IUniWebViewAuthenticationFlow { + /// + /// The client ID (Channel ID) of your LINE Login application. + /// + public string clientId = ""; + + /// + /// The iOS bundle Id you set in LINE developer console. + /// + public string iOSBundleId = ""; + + /// + /// The Android package name you set in LINE developer console. + /// + public string androidPackageName = ""; + + /// + /// The scope of your LINE application. + /// + public string scope = ""; + + /// + /// Optional to control this flow's behaviour. + /// + public UniWebViewAuthenticationFlowLineOptional optional; + + private string responseType = "code"; + private string grantType = "authorization_code"; + + private string RedirectUri { + get { + if (Application.platform == RuntimePlatform.IPhonePlayer || Application.platform == RuntimePlatform.OSXEditor) { + return $"line3rdp.{iOSBundleId}://auth"; + } + + if (Application.platform == RuntimePlatform.Android) { + return "intent://auth#Intent;package=" + androidPackageName + ";scheme=lineauth;end"; + } + UniWebViewLogger.Instance.Critical("Not supported platform for LINE Login."); + return ""; + } + } + + private readonly UniWebViewAuthenticationConfiguration config = + new UniWebViewAuthenticationConfiguration( + "https://access.line.me/oauth2/v2.1/login", + "https://api.line.me/oauth2/v2.1/token" + ); + + /// + /// Starts the authentication flow with the standard OAuth 2.0. + /// This implements the abstract method in `UniWebViewAuthenticationCommonFlow`. + /// + public override void StartAuthenticationFlow() { + var flow = new UniWebViewAuthenticationFlow(this); + flow.StartAuth(); + } + + /// + /// Implements required method in `IUniWebViewAuthenticationFlow`. + /// + public UniWebViewAuthenticationConfiguration GetAuthenticationConfiguration() { + return config; + } + + /// + /// Implements required method in `IUniWebViewAuthenticationFlow`. + /// + public string GetCallbackUrl() { + return RedirectUri; + } + + /// + /// Implements required method in `IUniWebViewAuthenticationFlow`. + /// + public Dictionary GetAuthenticationUriArguments() { + var authorizeArgs = new Dictionary { + { "loginChannelId", clientId }, + { "returnUri", GenerateReturnUri() }, + }; + + return authorizeArgs; + } + + private string GenerateReturnUri() { + var query = System.Web.HttpUtility.ParseQueryString(""); + query.Add("response_type", responseType); + query.Add("client_id", clientId); + query.Add("redirect_uri", RedirectUri); + + // State is a must in LINE Login. + var state = GenerateAndStoreState(); + query.Add("state", state); + + if (!String.IsNullOrEmpty(scope)) { + query.Add("scope", scope); + } else { + query.Add("scope", "profile"); + } + if (optional != null) { + if (optional.PKCESupport != UniWebViewAuthenticationPKCE.None) { + var codeChallenge = GenerateCodeChallengeAndStoreCodeVerify(optional.PKCESupport); + query.Add("code_challenge", codeChallenge); + + var method = UniWebViewAuthenticationUtils.ConvertPKCEToString(optional.PKCESupport); + query.Add("code_challenge_method", method); + } + } + return "/oauth2/v2.1/authorize/consent?" + query; + } + + /// + /// Implements required method in `IUniWebViewAuthenticationFlow`. + /// + public Dictionary GetAccessTokenRequestParameters(string authResponse) { + var normalizedRedirectUri = UniWebViewAuthenticationUtils.ConvertIntentUri(RedirectUri); + if (!authResponse.StartsWith(normalizedRedirectUri)) { + throw AuthenticationResponseException.UnexpectedAuthCallbackUrl; + } + + var uri = new Uri(authResponse); + var response = UniWebViewAuthenticationUtils.ParseFormUrlEncodedString(uri.Query); + VerifyState(response); + if (!response.TryGetValue("code", out var code)) { + throw AuthenticationResponseException.InvalidResponse(authResponse); + } + var parameters = new Dictionary { + { "client_id", clientId }, + { "code", code }, + { "redirect_uri", RedirectUri }, + { "grant_type", grantType }, + }; + if (CodeVerify != null) { + parameters.Add("code_verifier", CodeVerify); + } + + return parameters; + } + + /// + /// Implements required method in `IUniWebViewAuthenticationFlow`. + /// + public UniWebViewAuthenticationLineToken GenerateTokenFromExchangeResponse(string exchangeResponse) { + return UniWebViewAuthenticationTokenFactory.Parse(exchangeResponse); + } + + [field: SerializeField] + public UnityEvent OnAuthenticationFinished { get; set; } + [field: SerializeField] + public UnityEvent OnAuthenticationErrored { get; set; } +} + +/// +/// The authentication flow's optional settings for LINE. +/// +[Serializable] +public class UniWebViewAuthenticationFlowLineOptional { + /// + /// Whether to enable PKCE when performing authentication. Default is `S256`. + /// + public UniWebViewAuthenticationPKCE PKCESupport = UniWebViewAuthenticationPKCE.S256; +} + +/// +/// The token object from LINE. Check `UniWebViewAuthenticationStandardToken` for more. +/// +public class UniWebViewAuthenticationLineToken : UniWebViewAuthenticationStandardToken { } diff --git a/Runtime/GuruWebview/UniWebView5/UniWebView/Script/UniWebViewAuthentication/UniWebViewAuthenticationFlowLine.cs.meta b/Runtime/GuruWebview/UniWebView5/UniWebView/Script/UniWebViewAuthentication/UniWebViewAuthenticationFlowLine.cs.meta new file mode 100644 index 0000000..02d150b --- /dev/null +++ b/Runtime/GuruWebview/UniWebView5/UniWebView/Script/UniWebViewAuthentication/UniWebViewAuthenticationFlowLine.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 69f3e0ea73db84403850bcdbc6531a62 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/GuruWebview/UniWebView5/UniWebView/Script/UniWebViewAuthentication/UniWebViewAuthenticationFlowTwitter.cs b/Runtime/GuruWebview/UniWebView5/UniWebView/Script/UniWebViewAuthentication/UniWebViewAuthenticationFlowTwitter.cs new file mode 100644 index 0000000..167430f --- /dev/null +++ b/Runtime/GuruWebview/UniWebView5/UniWebView/Script/UniWebViewAuthentication/UniWebViewAuthenticationFlowTwitter.cs @@ -0,0 +1,171 @@ +// +// UniWebViewAuthenticationFlowTwitter.cs +// Created by Wang Wei (@onevcat) on 2022-06-25. +// +// This file is a part of UniWebView Project (https://uniwebview.com) +// By purchasing the asset, you are allowed to use this code in as many as projects +// you want, only if you publish the final products under the name of the same account +// used for the purchase. +// +// This asset and all corresponding files (such as source code) are provided on an +// “as is” basis, without warranty of any kind, express of 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 action of contract, tort or otherwise, +// arising from, out of or in connection with the software or the use of other dealing in the software. +// + +using System; +using System.Collections.Generic; +using UnityEngine; +using UnityEngine.Events; + +/// +/// A predefined authentication flow for Twitter. +/// +/// This implementation follows the flow described here: +/// https://developer.twitter.com/en/docs/authentication/oauth-2-0/authorization-code +/// +/// See https://docs.uniwebview.com/guide/oauth2.html for a more detailed guide of authentication in UniWebView. +/// +public class UniWebViewAuthenticationFlowTwitter : UniWebViewAuthenticationCommonFlow, IUniWebViewAuthenticationFlow { + /// + /// The client ID of your Twitter application. + /// + public string clientId = ""; + /// + /// The redirect URI of your Twitter application. + /// + public string redirectUri = ""; + /// + /// The scope string of all your required scopes. + /// + public string scope = ""; + /// + /// Optional to control this flow's behaviour. + /// + public UniWebViewAuthenticationFlowTwitterOptional optional; + + private string responseType = "code"; + private string grantType = "authorization_code"; + + private readonly UniWebViewAuthenticationConfiguration config = + new UniWebViewAuthenticationConfiguration( + "https://twitter.com/i/oauth2/authorize", + "https://api.twitter.com/2/oauth2/token" + ); + + /// + /// Starts the authentication flow with the standard OAuth 2.0. + /// This implements the abstract method in `UniWebViewAuthenticationCommonFlow`. + /// + public override void StartAuthenticationFlow() { + var flow = new UniWebViewAuthenticationFlow(this); + flow.StartAuth(); + } + + /// + /// Implements required method in `IUniWebViewAuthenticationFlow`. + /// + public UniWebViewAuthenticationConfiguration GetAuthenticationConfiguration() { + return config; + } + + /// + /// Implements required method in `IUniWebViewAuthenticationFlow`. + /// + public string GetCallbackUrl() { + return redirectUri; + } + + /// + /// Implements required method in `IUniWebViewAuthenticationFlow`. + /// + public Dictionary GetAuthenticationUriArguments() { + var authorizeArgs = new Dictionary { + { "client_id", clientId }, + { "redirect_uri", redirectUri }, + { "scope", scope }, + { "response_type", responseType } + }; + if (optional != null) { + if (optional.enableState) { + var state = GenerateAndStoreState(); + authorizeArgs.Add("state", state); + } + + if (optional.PKCESupport != UniWebViewAuthenticationPKCE.None) { + var codeChallenge = GenerateCodeChallengeAndStoreCodeVerify(optional.PKCESupport); + authorizeArgs.Add("code_challenge", codeChallenge); + + var method = UniWebViewAuthenticationUtils.ConvertPKCEToString(optional.PKCESupport); + authorizeArgs.Add("code_challenge_method", method); + } + } + + return authorizeArgs; + } + + /// + /// Implements required method in `IUniWebViewAuthenticationFlow`. + /// + public Dictionary GetAccessTokenRequestParameters(string authResponse) { + if (!authResponse.StartsWith(redirectUri)) { + throw AuthenticationResponseException.UnexpectedAuthCallbackUrl; + } + + var uri = new Uri(authResponse); + var response = UniWebViewAuthenticationUtils.ParseFormUrlEncodedString(uri.Query); + if (!response.TryGetValue("code", out var code)) { + throw AuthenticationResponseException.InvalidResponse(authResponse); + } + if (optional.enableState) { + VerifyState(response); + } + var parameters = new Dictionary { + { "client_id", clientId }, + { "code", code }, + { "redirect_uri", redirectUri }, + { "grant_type", grantType }, + }; + if (CodeVerify != null) { + parameters.Add("code_verifier", CodeVerify); + } + + return parameters; + } + + /// + /// Implements required method in `IUniWebViewAuthenticationFlow`. + /// + public UniWebViewAuthenticationTwitterToken GenerateTokenFromExchangeResponse(string exchangeResponse) { + return UniWebViewAuthenticationTokenFactory.Parse(exchangeResponse); + } + + [field: SerializeField] + public UnityEvent OnAuthenticationFinished { get; set; } + [field: SerializeField] + public UnityEvent OnAuthenticationErrored { get; set; } +} + +/// +/// The authentication flow's optional settings for Twitter. +/// +[Serializable] +public class UniWebViewAuthenticationFlowTwitterOptional { + /// + /// Whether to enable PKCE when performing authentication.This has to be enabled as `S256`, + /// otherwise, Twitter will reject the authentication request. + /// + public UniWebViewAuthenticationPKCE PKCESupport = UniWebViewAuthenticationPKCE.S256; + /// + /// Whether to enable the state verification. If enabled, the state will be generated and verified in the + /// authentication callback. This has to be `true`, otherwise, Twitter will reject the authentication request. + /// + public bool enableState = true; +} + +/// +/// The token object from Twitter. Check `UniWebViewAuthenticationStandardToken` for more. +/// +public class UniWebViewAuthenticationTwitterToken : UniWebViewAuthenticationStandardToken { } \ No newline at end of file diff --git a/Runtime/GuruWebview/UniWebView5/UniWebView/Script/UniWebViewAuthentication/UniWebViewAuthenticationFlowTwitter.cs.meta b/Runtime/GuruWebview/UniWebView5/UniWebView/Script/UniWebViewAuthentication/UniWebViewAuthenticationFlowTwitter.cs.meta new file mode 100644 index 0000000..9f9d19e --- /dev/null +++ b/Runtime/GuruWebview/UniWebView5/UniWebView/Script/UniWebViewAuthentication/UniWebViewAuthenticationFlowTwitter.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 86840692470304a76b6d2ac73771b7af +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/GuruWebview/UniWebView5/UniWebView/Script/UniWebViewAuthentication/UniWebViewAuthenticationSession.cs b/Runtime/GuruWebview/UniWebView5/UniWebView/Script/UniWebViewAuthentication/UniWebViewAuthenticationSession.cs new file mode 100644 index 0000000..43dbb36 --- /dev/null +++ b/Runtime/GuruWebview/UniWebView5/UniWebView/Script/UniWebViewAuthentication/UniWebViewAuthenticationSession.cs @@ -0,0 +1,186 @@ +// +// UniWebViewAuthenticationSession.cs +// Created by Wang Wei(@onevcat) on 2022-06-21. +// +// This file is a part of UniWebView Project (https://uniwebview.com) +// By purchasing the asset, you are allowed to use this code in as many as projects +// you want, only if you publish the final products under the name of the same account +// used for the purchase. +// +// This asset and all corresponding files (such as source code) are provided on an +// “as is” basis, without warranty of any kind, express of 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 action of contract, tort or otherwise, +// arising from, out of or in connection with the software or the use of other dealing in the software. +// + +using UnityEngine; +using System; + +/// +/// Represents a session that can be used to authenticate a user through a web service. +/// +/// +/// Initialize the session with a URL that points to the authentication webpage. A browser or a secure web view loads +/// and displays the page. On completion, the service sends a callback URL to the session with an authentication token, +/// and this triggers the `OnAuthenticationFinished` with the received URL. To make your app be invoked by the system, +/// you need to also add the correct callback URL starting with the value of `CallbackScheme` to UniWebView's preferences. +/// +/// Usually this session processes an OAuth 2 flow. It will be used along with a following "exchange token" request, to +/// finally get the user's access token to allow you use the service APIs on behalf of the user. This token exchange can +/// happen in the client app, or you can pass the code to your server and let your server do the left work. +/// +/// UniWebView also provides built-in integrated authentication flows for several popular service. The the +/// `UniWebViewAuthenticationFlow` cluster classes to use them and simplify your work. If the built-in models do not +/// fit your work, you can use this class as a starting point of your own authentication integration. +/// +/// See https://docs.uniwebview.com/guide/oauth2.html for a more detailed guide of authentication in UniWebView. +/// +public class UniWebViewAuthenticationSession: UnityEngine.Object { + /// + /// Delegate for authentication session finished event. + /// + /// The session which raised this event. + /// + /// The received URL from service. It might contain a valid `code` from the service, or an error. + /// + public delegate void AuthenticationFinishedDelegate(UniWebViewAuthenticationSession session, string url); + + /// + /// Raised when the session finishes authentication. + /// + /// This event will be invoked when the service provider calls the callback URL. regardless of the authentication code + /// is retrieved or an error is returned in the callback URL. + /// + public event AuthenticationFinishedDelegate OnAuthenticationFinished; + + /// + /// Delegate for authentication session error encounter event. + /// + /// The session which raised this event. + /// The error code represents the error type. + /// The error message describes the error in detail. + public delegate void AuthErrorReceivedDelegate(UniWebViewAuthenticationSession session, int errorCode, string errorMessage); + + /// + /// Raised when the session encounters an error. + /// + /// This event will be invoked when the authentication session cannot finishes with a URL callback. This usually + /// happens when a network error or the user dismisses the authentication page from native UI. + /// + public event AuthErrorReceivedDelegate OnAuthenticationErrorReceived; + + private string id = Guid.NewGuid().ToString(); + private UniWebViewNativeListener listener; + + /// + /// The URL of the authentication webpage. This is the value you used to create this session. + /// + public string Url { get; private set; } + + /// + /// The callback scheme of the authentication webpage. This is the value you used to create this session. The service + /// is expected to use a URL with this scheme to return to your app. + /// + public string CallbackScheme { get; private set; } + + internal void InternalAuthenticationFinished(string url) { + if (OnAuthenticationFinished != null) { + OnAuthenticationFinished(this, url); + } + UniWebViewNativeListener.RemoveListener(listener.Name); + Destroy(listener.gameObject); + } + + internal void InternalAuthenticationErrorReceived(UniWebViewNativeResultPayload payload) { + if (OnAuthenticationErrorReceived != null) { + int errorCode = int.TryParse(payload.resultCode, out errorCode) ? errorCode : -1; + OnAuthenticationErrorReceived(this, errorCode, payload.data); + } + + UniWebViewNativeListener.RemoveListener(listener.Name); + Destroy(listener.gameObject); + } + + private UniWebViewAuthenticationSession() { + var listenerObject = new GameObject("UniWebViewAuthSession-" + id); + listener = listenerObject.AddComponent(); + UniWebViewNativeListener.AddListener(listener); + } + + /// + /// Check whether the current device and system supports the authentication session. + /// + /// + /// This property always returns `true` on iOS 11, macOS 10.15 and later. On Android, it depends on whether there + /// is an Intent can handle the safe browsing request, which is use to display the authentication page. Usually + /// it is provided by Chrome. If there is no Intent can open the URL in safe browsing mode, this property will + /// return `false`. + /// + /// To use this API on Android when you set your Target SDK to Android 11 or later, you need to declare the correct + /// intent query explicitly in your AndroidManifest.xml, to follow the Package Visibility + /// (https://developer.android.com/about/versions/11/privacy/package-visibility): + /// + /// ```xml + /// + /// + /// + /// + /// + /// ``` + /// + /// + /// Returns `true` if the safe browsing mode is supported and the page will be opened in safe browsing + /// mode. Otherwise, `false`. + /// + public static bool IsAuthenticationSupported { + get { + return UniWebViewInterface.IsAuthenticationIsSupported(); + } + } + + /// + /// Creates a new authentication session with a given authentication page URL and a callback scheme. + /// + /// + /// The authentication page which is provided by the service. It should be a URL with some information like your app's + /// client id and required scopes, etc. + /// + /// The URL scheme which the service will use to navigate back to your client app. + /// + public static UniWebViewAuthenticationSession Create(string url, string callbackScheme) { + var session = new UniWebViewAuthenticationSession(); + session.listener.session = session; + session.Url = url; + session.CallbackScheme = callbackScheme; + + UniWebViewInterface.AuthenticationInit(session.listener.Name, url, callbackScheme); + + return session; + } + + /// + /// Start the authentication session process. It will show up a secured web page and navigate users to the `Url`. + /// + public void Start() { + UniWebViewInterface.AuthenticationStart(listener.Name); + } + + /// + /// Sets to use the private mode for the authentication. If running under private mode, the previous stored + /// authentication information will not be used. + /// + /// On Apple's platform, this works from iOS 13 and macOS 10.15. On Android, this depends on the Chrome setting on the + /// device. The users should enable the "incognito" and "third-party incognito" to allow to use this feature. + /// + /// Check them in Chrome app: + /// chrome://flags/#cct-incognito + /// chrome://flags/#cct-incognito-available-to-third-party + /// + /// + /// Whether the session should run in private mode or not. + public void SetPrivateMode(bool flag) { + UniWebViewInterface.AuthenticationSetPrivateMode(listener.Name, flag); + } +} \ No newline at end of file diff --git a/Runtime/GuruWebview/UniWebView5/UniWebView/Script/UniWebViewAuthentication/UniWebViewAuthenticationSession.cs.meta b/Runtime/GuruWebview/UniWebView5/UniWebView/Script/UniWebViewAuthentication/UniWebViewAuthenticationSession.cs.meta new file mode 100644 index 0000000..b7e3689 --- /dev/null +++ b/Runtime/GuruWebview/UniWebView5/UniWebView/Script/UniWebViewAuthentication/UniWebViewAuthenticationSession.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: e5e52f46b3fba4e1c868230b0da9fd5f +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/GuruWebview/UniWebView5/UniWebView/Script/UniWebViewAuthentication/UniWebViewAuthenticationStandardToken.cs b/Runtime/GuruWebview/UniWebView5/UniWebView/Script/UniWebViewAuthentication/UniWebViewAuthenticationStandardToken.cs new file mode 100644 index 0000000..a1bc19a --- /dev/null +++ b/Runtime/GuruWebview/UniWebView5/UniWebView/Script/UniWebViewAuthentication/UniWebViewAuthenticationStandardToken.cs @@ -0,0 +1,116 @@ +// +// UniWebViewAuthenticationStandardToken.cs +// Created by Wang Wei (@onevcat) on 2022-06-25. +// +// This file is a part of UniWebView Project (https://uniwebview.com) +// By purchasing the asset, you are allowed to use this code in as many as projects +// you want, only if you publish the final products under the name of the same account +// used for the purchase. +// +// This asset and all corresponding files (such as source code) are provided on an +// “as is” basis, without warranty of any kind, express of 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 action of contract, tort or otherwise, +// arising from, out of or in connection with the software or the use of other dealing in the software. +// + +using System; +using UnityEngine; + +/// +/// Represents the standard token used in the OAuth 2 process. +/// +[Serializable] +public class UniWebViewAuthenticationStandardToken { + // Unity's JsonUtility.FromJson is quite stupid on this. + // Switch to Newtonsoft.Json when we can support from Unity 2021. + [SerializeField] + private string access_token = default; + /// + /// The access token retrieved from the service provider. + /// + /// This usually comes from the `access_token` field in the response. + /// Use this token to access the service provider's API. + /// + /// If you do not need the token "offline", just use it and discard. UniWebView will not store this token, if you + /// need to keep it for other purpose, please make sure you do not violate any policies and put it to a secure + /// place yourself. + /// + public string AccessToken => access_token; + + [SerializeField] + private string scope = default; + /// + /// The granted scopes of the token. This is usually comes from the `scope` field in the response. + /// + /// If there are optional scopes in the initial auth request, the user can choose to not give you some of the + /// permissions. Check this field before you use the access token to perform certain actions to avoid failure + /// before actual attempts. + /// + public string Scope => scope; + + [SerializeField] + private string token_type = default; + /// + /// The token type. This usually comes from the `token_type` field in the response. + /// + /// For most OAuth 2.0 services, it is fixed to `Bearer`. + /// + public string TokenType => token_type; + + [SerializeField] + private string refresh_token = default; + /// + /// The refresh token retrieved from the service provider. This usually comes from the `refresh_token` field in the + /// response. + /// + /// If the access token is refreshable, you can use this + /// refresh token to perform a refresh operation and get a new access token without the user's consent again. + /// + /// The refresh policy can be different from the service providers. Read the documentation of the service provider + /// to determine the use of refresh token. + /// + /// If the response does not contain a refresh token, this field will be `null`. + /// + public string RefreshToken => refresh_token; + + [SerializeField] + private long expires_in = default; + /// + /// How long does this token remain valid. This usually comes from the `expires_in` field in the response. + /// + public long ExpiresIn => expires_in; + + [SerializeField] + private string id_token = default; + /// + /// The ID token retrieved from the service provider. This usually comes from the `id_token` field in the response. + /// + /// If the service provider does not support ID token or you did not apply for it, this field will be `null`. + /// The ID token is usually a JWT token that contains information about the user. + /// + public string IdToken => id_token; + + /// + /// The raw value of the response of the exchange token request. + /// + /// If the predefined fields are not enough, you can parse the raw value to get the extra information. + /// + public string RawValue { get; set; } +} + +/// +/// Util class to generate the standard token from a JSON based exchange token response. +/// +/// The type of target token. +public abstract class UniWebViewAuthenticationTokenFactory where TToken : UniWebViewAuthenticationStandardToken { + public static TToken Parse(string result) { + var json = JsonUtility.FromJson(result); + json.RawValue = result; + if (json.AccessToken == null) { + throw AuthenticationResponseException.InvalidResponse(result); + } + return json; + } +} \ No newline at end of file diff --git a/Runtime/GuruWebview/UniWebView5/UniWebView/Script/UniWebViewAuthentication/UniWebViewAuthenticationStandardToken.cs.meta b/Runtime/GuruWebview/UniWebView5/UniWebView/Script/UniWebViewAuthentication/UniWebViewAuthenticationStandardToken.cs.meta new file mode 100644 index 0000000..e3051c4 --- /dev/null +++ b/Runtime/GuruWebview/UniWebView5/UniWebView/Script/UniWebViewAuthentication/UniWebViewAuthenticationStandardToken.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: bda3fe59d0bb48c890f1545b0b22dee4 +timeCreated: 1656308530 \ No newline at end of file diff --git a/Runtime/GuruWebview/UniWebView5/UniWebView/Script/UniWebViewAuthentication/UniWebViewAuthenticationUtils.cs b/Runtime/GuruWebview/UniWebView5/UniWebView/Script/UniWebViewAuthentication/UniWebViewAuthenticationUtils.cs new file mode 100644 index 0000000..4153bca --- /dev/null +++ b/Runtime/GuruWebview/UniWebView5/UniWebView/Script/UniWebViewAuthentication/UniWebViewAuthenticationUtils.cs @@ -0,0 +1,182 @@ +// +// UniWebViewAuthenticationUtils.cs +// Created by Wang Wei (@onevcat) on 2022-06-25. +// +// This file is a part of UniWebView Project (https://uniwebview.com) +// By purchasing the asset, you are allowed to use this code in as many as projects +// you want, only if you publish the final products under the name of the same account +// used for the purchase. +// +// This asset and all corresponding files (such as source code) are provided on an +// “as is” basis, without warranty of any kind, express of 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 action of contract, tort or otherwise, +// arising from, out of or in connection with the software or the use of other dealing in the software. +// + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Security.Cryptography; +using UnityEngine.Networking; + +/// +/// This class provides some helper utils for performing the authentication flow. +/// +/// They are used inside the built-in flows, but you can also use them to implement your own flow. +/// +public class UniWebViewAuthenticationUtils { + internal static Dictionary ParseFormUrlEncodedString(string input) { + var result = new Dictionary(); + if (input.StartsWith("?") || input.StartsWith("#")) { + input = input.Substring(1); + } + + var pairs = input.Split('&'); + foreach (var pair in pairs) { + var kv = pair.Split('='); + result.Add(UnityWebRequest.UnEscapeURL(kv[0]), UnityWebRequest.UnEscapeURL(kv[1])); + } + + return result; + } + + /// + /// Generates a random Base64 encoded string. + /// + /// A random Base64 encoded string. + public static string GenerateRandomBase64String() { + var randomNumber = new byte[32]; + string value = ""; + using (var rng = RandomNumberGenerator.Create()) { + rng.GetBytes(randomNumber); + value = Convert.ToBase64String(randomNumber); + } + + return value; + } + + /// + /// Generates a random Base64URL encoded string. + /// + /// A random Base64URL encoded string. + public static string GenerateRandomBase64URLString() { + var value = GenerateRandomBase64String(); + return ConvertToBase64URLString(value); + } + + static readonly char[] padding = { '=' }; + + /// + /// Converts a Base64 encoded string to a Base64URL encoded string. + /// + /// The Base64 encoded string. + /// A string with Base64URL encoded for the input. + public static string ConvertToBase64URLString(string input) { + return input.TrimEnd(padding).Replace('+', '-').Replace('/', '_'); + } + + /// + /// Converts a Base64URL encoded string to a Base64 encoded string. + /// + /// The Base64URL encoded string. + /// A string with Base64 encoded for the input. + public static string ConvertToBase64String(string input) { + string result = input.Replace('_', '/').Replace('-', '+'); + switch (input.Length % 4) { + case 2: + result += "=="; + break; + case 3: + result += "="; + break; + } + + return result; + } + + /// + /// Generates a code verifier for PKCE usage. + /// + /// The length of the target code verifier. Default is 64. + /// A generated code verifier for PKCE usage. + public static string GenerateCodeVerifier(int length = 64) { + var randomNumber = new byte[32]; + string value = ""; + + using (var rng = RandomNumberGenerator.Create()) { + rng.GetBytes(randomNumber); + String chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.~"; + Random random = new Random(System.BitConverter.ToInt32(randomNumber, 0)); + value = new String(Enumerable.Repeat(chars, length).Select(s => s[random.Next(s.Length)]).ToArray()); + } + + return value; + } + + /// + /// Calculates the code challenge for PKCE usage, with a given code verifier and hash method. + /// + /// The code verifier you generated. + /// The hash method you want to use. + /// The result of the code challenge. + public static string CalculateCodeChallenge(string codeVerifier, UniWebViewAuthenticationPKCE method) { + switch (method) { + case UniWebViewAuthenticationPKCE.None: + throw new ArgumentOutOfRangeException(nameof(method), method, null); + case UniWebViewAuthenticationPKCE.S256: + var sha256 = SHA256.Create(); + var hash = sha256.ComputeHash(System.Text.Encoding.UTF8.GetBytes(codeVerifier)); + return ConvertToBase64URLString(System.Convert.ToBase64String(hash)); + case UniWebViewAuthenticationPKCE.Plain: + return codeVerifier; + default: + throw new ArgumentOutOfRangeException(nameof(method), method, null); + } + } + + public static string ConvertPKCEToString(UniWebViewAuthenticationPKCE method) { + switch (method) { + case UniWebViewAuthenticationPKCE.None: + return null; + case UniWebViewAuthenticationPKCE.S256: + return "S256"; + case UniWebViewAuthenticationPKCE.Plain: + return "plain"; + } + + return null; + } + + public static string ConvertIntentUri(string input) { + var uri = new Uri(input); + if (uri.Scheme != "intent") { + return input; + } + + var host = uri.Host; + string scheme = null; + var fragmentPairs = new Dictionary(); + var fragments = uri.Fragment; + fragments.Split(';').ToList().ForEach(fragment => { + var kv = fragment.Split('='); + if (kv.Length == 2 && kv[0] == "scheme") { + scheme = kv[1]; + } + }); + + if (!String.IsNullOrEmpty(scheme)) { + return scheme + "://" + host; + } + + return input; + } +} + +public enum UniWebViewAuthenticationPKCE +{ + None, + S256, + Plain +} \ No newline at end of file diff --git a/Runtime/GuruWebview/UniWebView5/UniWebView/Script/UniWebViewAuthentication/UniWebViewAuthenticationUtils.cs.meta b/Runtime/GuruWebview/UniWebView5/UniWebView/Script/UniWebViewAuthentication/UniWebViewAuthenticationUtils.cs.meta new file mode 100644 index 0000000..480d65e --- /dev/null +++ b/Runtime/GuruWebview/UniWebView5/UniWebView/Script/UniWebViewAuthentication/UniWebViewAuthenticationUtils.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: cb86a1823d1e54d558d6f77e3c36719b +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/GuruWebview/UniWebView5/UniWebView/Script/UniWebViewContentInsetAdjustmentBehavior.cs b/Runtime/GuruWebview/UniWebView5/UniWebView/Script/UniWebViewContentInsetAdjustmentBehavior.cs new file mode 100644 index 0000000..e2b467c --- /dev/null +++ b/Runtime/GuruWebview/UniWebView5/UniWebView/Script/UniWebViewContentInsetAdjustmentBehavior.cs @@ -0,0 +1,42 @@ +// +// UniWebViewContentInsetAdjustmentBehavior.cs +// Created by Wang Wei(@onevcat) on 2019-09-20. +// +// This file is a part of UniWebView Project (https://uniwebview.com) +// By purchasing the asset, you are allowed to use this code in as many as projects +// you want, only if you publish the final products under the name of the same account +// used for the purchase. +// +// This asset and all corresponding files (such as source code) are provided on an +// “as is” basis, without warranty of any kind, express of 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 action of contract, tort or otherwise, +// arising from, out of or in connection with the software or the use of other dealing in the software. + +/// +/// Constants indicating how safe area insets are added to the adjusted content inset. +/// This is only for iOS. +/// +public enum UniWebViewContentInsetAdjustmentBehavior +{ + /// + /// Automatically adjust the scroll view insets. + /// + Automatic = 0, + + /// + /// Adjust the insets only in the scrollable directions. + /// + ScrollableAxes = 1, + + /// + /// Do not adjust the scroll view insets. + /// + Never = 2, + + /// + /// Always include the safe area insets in the content adjustment. + /// + Always = 3 +} \ No newline at end of file diff --git a/Runtime/GuruWebview/UniWebView5/UniWebView/Script/UniWebViewContentInsetAdjustmentBehavior.cs.meta b/Runtime/GuruWebview/UniWebView5/UniWebView/Script/UniWebViewContentInsetAdjustmentBehavior.cs.meta new file mode 100644 index 0000000..c05136b --- /dev/null +++ b/Runtime/GuruWebview/UniWebView5/UniWebView/Script/UniWebViewContentInsetAdjustmentBehavior.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: dc6b999829de442f2aa381a8ff78a917 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/GuruWebview/UniWebView5/UniWebView/Script/UniWebViewDownloadMatchingType.cs b/Runtime/GuruWebview/UniWebView5/UniWebView/Script/UniWebViewDownloadMatchingType.cs new file mode 100644 index 0000000..8e7abad --- /dev/null +++ b/Runtime/GuruWebview/UniWebView5/UniWebView/Script/UniWebViewDownloadMatchingType.cs @@ -0,0 +1,32 @@ +// +// UniWebViewDownloadMatchingType.cs +// Created by Wang Wei(@onevcat) on 2021-09-02. +// +// This file is a part of UniWebView Project (https://uniwebview.com) +// By purchasing the asset, you are allowed to use this code in as many as projects +// you want, only if you publish the final products under the name of the same account +// used for the purchase. +// +// This asset and all corresponding files (such as source code) are provided on an +// “as is” basis, without warranty of any kind, express of 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 action of contract, tort or otherwise, +// arising from, out of or in connection with the software or the use of other dealing in the software. +// + +/// +/// The matching type used when UniWebView determines whether to download from a URL or MIME type. +/// +public enum UniWebViewDownloadMatchingType +{ + /// + /// Matches exact the whole value. + /// + ExactValue = 1, + /// + /// Uses the value as a regular expression. + /// + RegularExpression = 2 +} + diff --git a/Runtime/GuruWebview/UniWebView5/UniWebView/Script/UniWebViewDownloadMatchingType.cs.meta b/Runtime/GuruWebview/UniWebView5/UniWebView/Script/UniWebViewDownloadMatchingType.cs.meta new file mode 100644 index 0000000..f5e52c7 --- /dev/null +++ b/Runtime/GuruWebview/UniWebView5/UniWebView/Script/UniWebViewDownloadMatchingType.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 0ba73f1cc351846878c5731b4e22b132 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/GuruWebview/UniWebView5/UniWebView/Script/UniWebViewEmbeddedToolbar.cs b/Runtime/GuruWebview/UniWebView5/UniWebView/Script/UniWebViewEmbeddedToolbar.cs new file mode 100644 index 0000000..d0b65f2 --- /dev/null +++ b/Runtime/GuruWebview/UniWebView5/UniWebView/Script/UniWebViewEmbeddedToolbar.cs @@ -0,0 +1,119 @@ +using UnityEngine; + +/// +/// Represents the embedded toolbar in a web view. +/// +/// You do not create an instance of this class directly. Instead, use the `EmbeddedWebView` property in `UniWebView` to +/// get the current embedded toolbar in the web view and interact with it. +/// +/// The embedded toolbar of a web view expands its width to match the web view's frame width. It is displayed at either +/// top or bottom of the web view, based on the setting received through `SetPosition`. By default, the toolbar contains +/// a main title, a back button, a forward button and a done button to close the web view. You can use methods in this +/// class to customize the toolbar to match your app's style. +/// +public class UniWebViewEmbeddedToolbar { + + private readonly UniWebViewNativeListener listener; + + internal UniWebViewEmbeddedToolbar(UniWebViewNativeListener listener) { + this.listener = listener; + } + + /// + /// Sets the position of the embedded toolbar. You can put the toolbar at either top or bottom of your web view. + /// The default position is `Top`. + /// + /// The desired position of the toolbar. + public void SetPosition(UniWebViewToolbarPosition position) { + UniWebViewInterface.SetEmbeddedToolbarOnTop(listener.Name, position == UniWebViewToolbarPosition.Top); + } + + /// + /// Shows the toolbar. + /// + public void Show() { + UniWebViewInterface.SetShowEmbeddedToolbar(listener.Name, true); + } + + /// + /// Hides the toolbar. + /// + public void Hide() { + UniWebViewInterface.SetShowEmbeddedToolbar(listener.Name, false); + } + + /// + /// Sets the text of the done button. The default text is "Done". + /// + /// The desired text to display as the done button. + public void SetDoneButtonText(string text) { + UniWebViewInterface.SetEmbeddedToolbarDoneButtonText(listener.Name, text); + } + + /// + /// Sets the text of the back button. The default text is "❮" ("\u276E"). + /// + /// The desired text to display as the back button. + public void SetGoBackButtonText(string text) { + UniWebViewInterface.SetEmbeddedToolbarGoBackButtonText(listener.Name, text); + } + + /// + /// Sets the text of the forward button. The default text is "❯" ("\u276F"). + /// + /// The desired text to display as the forward button. + public void SetGoForwardButtonText(string text) { + UniWebViewInterface.SetEmbeddedToolbarGoForwardButtonText(listener.Name, text); + } + + /// + /// Sets the text of toolbar title. The default is empty. The space is limited, setting a long text as title might + /// not fit in the space. + /// + /// The desired text to display as the title in the toolbar. + public void SetTitleText(string text) { + UniWebViewInterface.SetEmbeddedToolbarTitleText(listener.Name, text); + } + + /// + /// Sets the background color of the toolbar. + /// + /// The desired color of toolbar's background. + public void SetBackgroundColor(Color color) { + UniWebViewInterface.SetEmbeddedToolbarBackgroundColor(listener.Name, color); + } + + /// + /// Sets the buttons color of the toolbar. This color affects the back, forward and done button. + /// + /// The desired color of toolbar's buttons. + public void SetButtonTextColor(Color color) { + UniWebViewInterface.SetEmbeddedToolbarButtonTextColor(listener.Name, color); + } + + /// + /// Sets the text color of the toolbar title. + /// + /// The desired color of the toolbar's title. + public void SetTitleTextColor(Color color) { + UniWebViewInterface.SetEmbeddedToolbarTitleTextColor(listener.Name, color); + } + + /// + /// Hides the navigation buttons on the toolbar. When called, the back button and forward button will not be shown. + /// + /// By default, the navigation buttons are shown. + /// + public void HideNavigationButtons() { + UniWebViewInterface.SetEmeddedToolbarNavigationButtonsShow(listener.Name, false); + } + + /// + /// Shows the navigation buttons on the toolbar. When called, the back button and forward button will be shown. + /// + /// By default, the navigation buttons are shown. + /// + public void ShowNavigationButtons() { + UniWebViewInterface.SetEmeddedToolbarNavigationButtonsShow(listener.Name, true); + } +} \ No newline at end of file diff --git a/Runtime/GuruWebview/UniWebView5/UniWebView/Script/UniWebViewEmbeddedToolbar.cs.meta b/Runtime/GuruWebview/UniWebView5/UniWebView/Script/UniWebViewEmbeddedToolbar.cs.meta new file mode 100644 index 0000000..121a1f0 --- /dev/null +++ b/Runtime/GuruWebview/UniWebView5/UniWebView/Script/UniWebViewEmbeddedToolbar.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 19945236afb94be88f39e5f6c566ce71 +timeCreated: 1658065034 \ No newline at end of file diff --git a/Runtime/GuruWebview/UniWebView5/UniWebView/Script/UniWebViewHelper.cs b/Runtime/GuruWebview/UniWebView5/UniWebView/Script/UniWebViewHelper.cs new file mode 100644 index 0000000..0cbf3eb --- /dev/null +++ b/Runtime/GuruWebview/UniWebView5/UniWebView/Script/UniWebViewHelper.cs @@ -0,0 +1,67 @@ +// +// UniWebViewHelper.cs +// Created by Wang Wei(@onevcat) on 2017-04-11. +// +// This file is a part of UniWebView Project (https://uniwebview.com) +// By purchasing the asset, you are allowed to use this code in as many as projects +// you want, only if you publish the final products under the name of the same account +// used for the purchase. +// +// This asset and all corresponding files (such as source code) are provided on an +// “as is” basis, without warranty of any kind, express of 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 action of contract, tort or otherwise, +// arising from, out of or in connection with the software or the use of other dealing in the software. +// +using UnityEngine; +using System.IO; + +/// +/// Provides some helper utility methods for UniWebView. +/// +public class UniWebViewHelper { + /// + /// Get the local streaming asset path for a given file path related to the StreamingAssets folder. + /// + /// This method will help you to create a URL string for a file under your StreamingAssets folder for different platforms. + /// The relative path to the Assets/StreamingAssets of your file. + /// For example, if you placed a html file under Assets/StreamingAssets/www/index.html, you should pass `www/index.html` as parameter. + /// + /// The path you could use as the url for the web view. + public static string StreamingAssetURLForPath(string path) + { +#if (UNITY_EDITOR_OSX || UNITY_STANDALONE_OSX || UNITY_IOS) && !UNITY_EDITOR_WIN && !UNITY_EDITOR_LINUX + return Path.Combine("file://" + Application.streamingAssetsPath, path); +#elif UNITY_ANDROID && !UNITY_EDITOR_WIN && !UNITY_EDITOR_LINUX + return Path.Combine("file:///android_asset/", path); +#else + UniWebViewLogger.Instance.Critical("The current build target is not supported."); + return string.Empty; +#endif + } + + /// + /// Get the local persistent data path for a given file path related to the data folder of your host app. + /// + /// This method will help you to create a URL string for a file under you stored in the `persistentDataPath`. + /// + /// + /// The relative path to the persistent data path of your file. + /// + /// The path you could use as the url for the web view. + public static string PersistentDataURLForPath(string path) + { + return Path.Combine("file://" + Application.persistentDataPath, path); + } + + internal static bool IsEditor { + get { + #if UNITY_EDITOR_OSX || UNITY_STANDALONE_OSX + return true; + #else + return false; + #endif + } + } +} diff --git a/Runtime/GuruWebview/UniWebView5/UniWebView/Script/UniWebViewHelper.cs.meta b/Runtime/GuruWebview/UniWebView5/UniWebView/Script/UniWebViewHelper.cs.meta new file mode 100644 index 0000000..927842b --- /dev/null +++ b/Runtime/GuruWebview/UniWebView5/UniWebView/Script/UniWebViewHelper.cs.meta @@ -0,0 +1,12 @@ +fileFormatVersion: 2 +guid: 824ddd9d1592945268d857265662a174 +timeCreated: 1495373327 +licenseType: Free +MonoImporter: + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/GuruWebview/UniWebView5/UniWebView/Script/UniWebViewLogger.cs b/Runtime/GuruWebview/UniWebView5/UniWebView/Script/UniWebViewLogger.cs new file mode 100644 index 0000000..e2be3e1 --- /dev/null +++ b/Runtime/GuruWebview/UniWebView5/UniWebView/Script/UniWebViewLogger.cs @@ -0,0 +1,120 @@ +// +// UniWebViewLogger.cs +// Created by Wang Wei(@onevcat) on 2017-04-11. +// +// This file is a part of UniWebView Project (https://uniwebview.com) +// By purchasing the asset, you are allowed to use this code in as many as projects +// you want, only if you publish the final products under the name of the same account +// used for the purchase. +// +// This asset and all corresponding files (such as source code) are provided on an +// “as is” basis, without warranty of any kind, express of 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 action of contract, tort or otherwise, +// arising from, out of or in connection with the software or the use of other dealing in the software. +// + +/// +/// A leveled logger which could log UniWebView related messages in +/// both development environment and final product. +/// +public class UniWebViewLogger { + /// + /// Logger level. + /// + public enum Level { + /// + /// Lowest level. When set to `Verbose`, the logger will log out all messages. + /// + Verbose = 0, + + /// + /// Debug level. When set to `Debug`, the logger will log out most of messages up to this level. + /// + Debug = 10, + + /// + /// Info level. When set to `Info`, the logger will log out up to info messages. + /// + Info = 20, + + /// + /// Critical level. When set to `Critical`, the logger will only log out errors or exceptions. + /// + Critical = 80, + + /// + /// Off level. When set to `Off`, the logger will log out nothing. + /// + Off = 99 + } + + private static UniWebViewLogger instance; + private Level level; + + /// + /// Current level of this logger. All messages above current level will be logged out. + /// Default is `Critical`, which means the logger only prints errors and exceptions. + /// + public Level LogLevel { + get { return level; } + set { + Log(Level.Off, "Setting UniWebView logger level to: " + value); + level = value; + UniWebViewInterface.SetLogLevel((int)value); + } + } + + private UniWebViewLogger(Level level) { + this.level = level; + } + + /// + /// Instance of the UniWebView logger across the process. Normally you should use this for logging purpose + /// in UniWebView, instead of creating a new logger yourself. + /// + public static UniWebViewLogger Instance { + get { + if (instance == null) { + instance = new UniWebViewLogger(Level.Critical); + } + return instance; + } + } + + /// + /// Log a verbose message. + /// + /// The message to log. + public void Verbose(string message) { Log(Level.Verbose, message); } + + /// + /// Log a debug message. + /// + /// The message to log. + public void Debug(string message) { Log(Level.Debug, message); } + + /// + /// Log an info message. + /// + /// The message to log. + public void Info(string message) { Log(Level.Info, message); } + + /// + /// Log a critical message. + /// + /// The message to log. + public void Critical(string message) { Log(Level.Critical, message); } + + private void Log(Level level, string message) { + if (level >= this.LogLevel) { + var logMessage = " " + message; + if (level == Level.Critical) { + UnityEngine.Debug.LogError(logMessage); + } else { + UnityEngine.Debug.Log(logMessage); + } + } + } +} \ No newline at end of file diff --git a/Runtime/GuruWebview/UniWebView5/UniWebView/Script/UniWebViewLogger.cs.meta b/Runtime/GuruWebview/UniWebView5/UniWebView/Script/UniWebViewLogger.cs.meta new file mode 100644 index 0000000..6528b5f --- /dev/null +++ b/Runtime/GuruWebview/UniWebView5/UniWebView/Script/UniWebViewLogger.cs.meta @@ -0,0 +1,12 @@ +fileFormatVersion: 2 +guid: 06ca7a8564d9842ba99c77d43e9ce4f5 +timeCreated: 1491898971 +licenseType: Free +MonoImporter: + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/GuruWebview/UniWebView5/UniWebView/Script/UniWebViewMessage.cs b/Runtime/GuruWebview/UniWebView5/UniWebView/Script/UniWebViewMessage.cs new file mode 100644 index 0000000..b8a26a2 --- /dev/null +++ b/Runtime/GuruWebview/UniWebView5/UniWebView/Script/UniWebViewMessage.cs @@ -0,0 +1,111 @@ +// +// UniWebViewMessage.cs +// Created by Wang Wei(@onevcat) on 2017-05-12. +// +// This file is a part of UniWebView Project (https://uniwebview.com) +// By purchasing the asset, you are allowed to use this code in as many as projects +// you want, only if you publish the final products under the name of the same account +// used for the purchase. +// +// This asset and all corresponding files (such as source code) are provided on an +// “as is” basis, without warranty of any kind, express of 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 action of contract, tort or otherwise, +// arising from, out of or in connection with the software or the use of other dealing in the software. +// +using System.Collections.Generic; +using UnityEngine; +using UnityEngine.Networking; + +#if UNITY_2017_3_OR_NEWER +using Net = UnityEngine.Networking.UnityWebRequest; +#else +using Net = UnityEngine.WWW; +#endif + +/// +/// A structure represents a message from webview. +/// +public struct UniWebViewMessage { + /// + /// Gets the raw message. It is the original url which initialized this message. + /// + public string RawMessage {get; private set;} + + /// + /// The url scheme of this UniWebViewMessage. "uniwebview" was added to message scheme list + /// by default. You can add your own scheme by using `UniWebView.AddUrlScheme`. + /// + public string Scheme {get; private set;} + + /// + /// The path of this UniWebViewMessage. + /// This will be the decoded value for the path of original url. + /// + public string Path {get; private set;} + + /// + /// The arguments of this UniWebViewMessage. + /// + /// When received url "uniwebview://yourPath?param1=value1¶m2=value2", + /// the args is a Dictionary with: Args["param1"] = value1, Args["param2"] = value2 + /// + /// Both the key and valud will be url decoded from the original url. + /// + public Dictionary Args{get; private set;} + + /// + /// Initializes a new instance of the `UniWebViewMessage` struct. + /// + /// Raw message which will be parsed to a UniWebViewMessage. + public UniWebViewMessage(string rawMessage): this() { + UniWebViewLogger.Instance.Debug("Try to parse raw message: " + rawMessage); + this.RawMessage = rawMessage; + + string[] schemeSplit = rawMessage.Split(new string[] {"://"}, System.StringSplitOptions.None); + if (schemeSplit.Length == 1) { + // `://` not existing. Try `:/` instead. + schemeSplit = rawMessage.Split(new string[] {":/"}, System.StringSplitOptions.None); + } + if (schemeSplit.Length == 1) { + // `:/` not existing. Try `:` instead. + schemeSplit = rawMessage.Split(new string[] {":"}, System.StringSplitOptions.None); + } + + if (schemeSplit.Length >= 2) { + this.Scheme = schemeSplit[0]; + UniWebViewLogger.Instance.Debug("Get scheme: " + this.Scheme); + + string pathAndArgsString = ""; + int index = 1; + while (index < schemeSplit.Length) { + pathAndArgsString = string.Concat(pathAndArgsString, schemeSplit[index]); + index++; + } + UniWebViewLogger.Instance.Verbose("Build path and args string: " + pathAndArgsString); + + string[] split = pathAndArgsString.Split("?"[0]); + + this.Path = Net.UnEscapeURL(split[0].TrimEnd('/')); + this.Args = new Dictionary(); + if (split.Length > 1) { + foreach (string pair in split[1].Split("&"[0])) { + string[] elems = pair.Split("="[0]); + if (elems.Length > 1) { + var key = Net.UnEscapeURL(elems[0]); + if (Args.ContainsKey(key)) { + var existingValue = Args[key]; + Args[key] = existingValue + "," + Net.UnEscapeURL(elems[1]); + } else { + Args[key] = Net.UnEscapeURL(elems[1]); + } + UniWebViewLogger.Instance.Debug("Get arg, key: " + key + " value: " + Args[key]); + } + } + } + } else { + UniWebViewLogger.Instance.Critical("Bad url scheme. Can not be parsed to UniWebViewMessage: " + rawMessage); + } + } +} \ No newline at end of file diff --git a/Runtime/GuruWebview/UniWebView5/UniWebView/Script/UniWebViewMessage.cs.meta b/Runtime/GuruWebview/UniWebView5/UniWebView/Script/UniWebViewMessage.cs.meta new file mode 100644 index 0000000..d93de6f --- /dev/null +++ b/Runtime/GuruWebview/UniWebView5/UniWebView/Script/UniWebViewMessage.cs.meta @@ -0,0 +1,12 @@ +fileFormatVersion: 2 +guid: 0dbdb1ed01b9747a1ad6eb7bcc7b4014 +timeCreated: 1491898971 +licenseType: Free +MonoImporter: + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/GuruWebview/UniWebView5/UniWebView/Script/UniWebViewNativeListener.cs b/Runtime/GuruWebview/UniWebView5/UniWebView/Script/UniWebViewNativeListener.cs new file mode 100644 index 0000000..d8c4f12 --- /dev/null +++ b/Runtime/GuruWebview/UniWebView5/UniWebView/Script/UniWebViewNativeListener.cs @@ -0,0 +1,210 @@ +// +// UniWebViewNativeListener.cs +// Created by Wang Wei(@onevcat) on 2017-04-11. +// +// This file is a part of UniWebView Project (https://uniwebview.com) +// By purchasing the asset, you are allowed to use this code in as many as projects +// you want, only if you publish the final products under the name of the same account +// used for the purchase. +// +// This asset and all corresponding files (such as source code) are provided on an +// “as is” basis, without warranty of any kind, express of 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 action of contract, tort or otherwise, +// arising from, out of or in connection with the software or the use of other dealing in the software. +// +using UnityEngine; +using System.Collections; +using System.Collections.Generic; +using System; + +/// +/// A listener script for message sent from native side of UniWebView. +/// Normally this component will be attached to a sub `GameObject` under the `UniWebView` one. +/// It will be added automatically and destroyed as needed. So there is rarely a need for you +/// to manipulate on this class. +/// +public class UniWebViewNativeListener: MonoBehaviour { + + private static Dictionary listeners = new Dictionary(); + public static void AddListener(UniWebViewNativeListener target) { + listeners.Add(target.Name, target); + } + + public static void RemoveListener(String name) { + listeners.Remove(name); + } + + public static UniWebViewNativeListener GetListener(String name) { + UniWebViewNativeListener result = null; + if (listeners.TryGetValue(name, out result)) { + return result; + } else { + return null; + } + } + + /// + /// The web view holder of this listener. + /// It will be linked to original web view in web view context, so you should never set it yourself. + /// Either `webView` or `safeBrowsing` will be valid in this listener. + /// + [HideInInspector] + public UniWebView webView; + + // The safe browsing of this listener. + /// It will be linked to original safe browsing in browsing context, so you should never set it yourself. + /// Either `webView` or `safeBrowsing` will be valid in this listener. + [HideInInspector] + public UniWebViewSafeBrowsing safeBrowsing; + + [HideInInspector] + public UniWebViewAuthenticationSession session; + + /// + /// Name of current listener. This is a UUID string by which native side could use to find + /// the message destination. + /// + public string Name { + get { + return gameObject.name; + } + } + + public void PageStarted(string url) { + UniWebViewLogger.Instance.Info("Page Started Event. Url: " + url); + webView.InternalOnPageStarted(url); + } + + public void PageFinished(string result) { + UniWebViewLogger.Instance.Info("Page Finished Event. Url: " + result); + var payload = JsonUtility.FromJson(result); + webView.InternalOnPageFinished(payload); + } + + public void PageErrorReceived(string result) { + UniWebViewLogger.Instance.Info("Page Error Received Event. Result: " + result); + var payload = JsonUtility.FromJson(result); + webView.InternalOnPageErrorReceived(payload); + } + + public void PageProgressChanged(string result) { + float progress; + if (float.TryParse(result, out progress)) { + webView.InternalOnPageProgressChanged(progress); + } + } + + public void ShowTransitionFinished(string identifer) { + UniWebViewLogger.Instance.Info("Show Transition Finished Event. Identifier: " + identifer); + webView.InternalOnShowTransitionFinished(identifer); + } + + public void HideTransitionFinished(string identifer) { + UniWebViewLogger.Instance.Info("Hide Transition Finished Event. Identifier: " + identifer); + webView.InternalOnHideTransitionFinished(identifer); + } + + public void AnimateToFinished(string identifer) { + UniWebViewLogger.Instance.Info("Animate To Finished Event. Identifier: " + identifer); + webView.InternalOnAnimateToFinished(identifer); + } + + public void AddJavaScriptFinished(string result) { + UniWebViewLogger.Instance.Info("Add JavaScript Finished Event. Result: " + result); + var payload = JsonUtility.FromJson(result); + webView.InternalOnAddJavaScriptFinished(payload); + } + + public void EvalJavaScriptFinished(string result) { + UniWebViewLogger.Instance.Info("Eval JavaScript Finished Event. Result: " + result); + + var payload = JsonUtility.FromJson(result); + webView.InternalOnEvalJavaScriptFinished(payload); + } + + public void MessageReceived(string result) { + UniWebViewLogger.Instance.Info("Message Received Event. Result: " + result); + webView.InternalOnMessageReceived(result); + } + + public void WebViewDone(string param) { + UniWebViewLogger.Instance.Info("Web View Done Event."); + webView.InternalOnShouldClose(); + } + + public void WebContentProcessDidTerminate(string param) { + UniWebViewLogger.Instance.Info("Web Content Process Terminate Event."); + webView.InternalOnWebContentProcessDidTerminate(); + } + + public void SafeBrowsingFinished(string param) { + UniWebViewLogger.Instance.Info("Safe Browsing Finished."); + safeBrowsing.InternalSafeBrowsingFinished(); + } + + public void MultipleWindowOpened(string param) { + UniWebViewLogger.Instance.Info("MultipleWindowOpened Event. Multi Window: " + param); + webView.InternalOnMultipleWindowOpened(param); + } + + public void MultipleWindowClosed(string param) { + UniWebViewLogger.Instance.Info("MultipleWindowClose Event. Multi Window: " + param); + webView.InternalOnMultipleWindowClosed(param); + } + + public void FileDownloadStarted(string result) { + UniWebViewLogger.Instance.Info("FileDownloadStarted Event. Result: " + result); + + var payload = JsonUtility.FromJson(result); + webView.InternalOnFileDownloadStarted(payload); + } + + public void FileDownloadFinished(string result) { + UniWebViewLogger.Instance.Info("FileDownloadFinished Event. Result: " + result); + + var payload = JsonUtility.FromJson(result); + webView.InternalOnFileDownloadFinished(payload); + } + + public void CaptureSnapshotFinished(string result) { + UniWebViewLogger.Instance.Info("CaptureSnapshotFinished Event. Result: " + result); + + var payload = JsonUtility.FromJson(result); + webView.InternalOnCaptureSnapshotFinished(payload); + } + + public void AuthFinished(string result) { + UniWebViewLogger.Instance.Info("Auth Session Finished. Url: " + result); + session.InternalAuthenticationFinished(result); + } + + public void AuthErrorReceived(string result) { + UniWebViewLogger.Instance.Info("Auth Session Error Received. Result: " + result); + var payload = JsonUtility.FromJson(result); + session.InternalAuthenticationErrorReceived(payload); + } +} + +/// +/// A payload received from native side. It contains information to identify the message sender, +/// as well as some necessary field to bring data from native side to Unity. +/// +[System.Serializable] +public class UniWebViewNativeResultPayload { + /// + /// The identifier bound to this payload. It would be used internally to identify the callback. + /// + public string identifier; + /// + /// The result code contained in this payload. Generally, "0" means the operation finished without + /// problem, while a non-zero value means somethings goes wrong. + /// + public string resultCode; + /// + /// Return value or data from native. You should look at + /// corresponding APIs to know what exactly contained in this. + /// + public string data; +} \ No newline at end of file diff --git a/Runtime/GuruWebview/UniWebView5/UniWebView/Script/UniWebViewNativeListener.cs.meta b/Runtime/GuruWebview/UniWebView5/UniWebView/Script/UniWebViewNativeListener.cs.meta new file mode 100644 index 0000000..e39b7f3 --- /dev/null +++ b/Runtime/GuruWebview/UniWebView5/UniWebView/Script/UniWebViewNativeListener.cs.meta @@ -0,0 +1,12 @@ +fileFormatVersion: 2 +guid: 5538b25d1713f4de994dacdc6eaacb95 +timeCreated: 1491961733 +licenseType: Free +MonoImporter: + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/GuruWebview/UniWebView5/UniWebView/Script/UniWebViewSafeBrowingComponent.cs b/Runtime/GuruWebview/UniWebView5/UniWebView/Script/UniWebViewSafeBrowingComponent.cs new file mode 100644 index 0000000..4f7b985 --- /dev/null +++ b/Runtime/GuruWebview/UniWebView5/UniWebView/Script/UniWebViewSafeBrowingComponent.cs @@ -0,0 +1,20 @@ +using System.Collections; +using System.Collections.Generic; +using UnityEngine; + +public class UniWebViewSafeBrowingComponent : MonoBehaviour +{ + [SerializeField] + #pragma warning disable 0649 + private string url; + + void Start() + { + if (string.IsNullOrEmpty(url)) { + Debug.LogError("The `url` is empty or null. Set a valid url in the prefab before you initialize it."); + return; + } + var safeBrowsing = UniWebViewSafeBrowsing.Create(url); + safeBrowsing.Show(); + } +} diff --git a/Runtime/GuruWebview/UniWebView5/UniWebView/Script/UniWebViewSafeBrowingComponent.cs.meta b/Runtime/GuruWebview/UniWebView5/UniWebView/Script/UniWebViewSafeBrowingComponent.cs.meta new file mode 100644 index 0000000..bdd0b29 --- /dev/null +++ b/Runtime/GuruWebview/UniWebView5/UniWebView/Script/UniWebViewSafeBrowingComponent.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 5843488507315421aa0a7d92c0604d10 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/GuruWebview/UniWebView5/UniWebView/Script/UniWebViewSafeBrowsing.cs b/Runtime/GuruWebview/UniWebView5/UniWebView/Script/UniWebViewSafeBrowsing.cs new file mode 100644 index 0000000..97f9ad3 --- /dev/null +++ b/Runtime/GuruWebview/UniWebView5/UniWebView/Script/UniWebViewSafeBrowsing.cs @@ -0,0 +1,194 @@ +// +// UniWebViewSafeBrowsing.cs +// Created by Wang Wei(@onevcat) on 2020-07-18. +// +// This file is a part of UniWebView Project (https://uniwebview.com) +// By purchasing the asset, you are allowed to use this code in as many as projects +// you want, only if you publish the final products under the name of the same account +// used for the purchase. +// +// This asset and all corresponding files (such as source code) are provided on an +// “as is” basis, without warranty of any kind, express of 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 action of contract, tort or otherwise, +// arising from, out of or in connection with the software or the use of other dealing in the software. +// + +using UnityEngine; +using System; + +/// +/// UniWebView Safe Browsing provides a way for browsing the web content in a more browser-like way, such as Safari on +/// iOS and Chrome on Android. +/// +/// This class wraps `SFSafariViewController` on iOS and "Custom Tabs" on Android. It shares cookies, auto-fill +/// completion and other more data with the browser on device. Most of permissions are also built-in supported. You can +/// use this class for some tasks that are limited for a normal web view, such as using Apple Pay or Progressive Web +/// Apps (PWA). +/// +/// You create a `UniWebViewSafeBrowsing` instance by calling the static `UniWebViewSafeBrowsing.Create` method with a +/// destination URL. You cannot change this URL once the instance is created. To show the safe browsing, call `Show` on +/// the instance. The web content will be displayed in full screen with a toolbar containing the loaded URL, as well +/// as some basic controls like Go Back, Go Forward and Done. +/// +/// Browsing web content in `UniWebViewSafeBrowsing` is only supported on iOS and Android. There is no such component in +/// Unity Editor. Creating and showing a `UniWebViewSafeBrowsing` on Unity Editor will fall back to open the URL in +/// external browser by using Unity's `Application.OpenURL`. +/// +/// +public class UniWebViewSafeBrowsing: UnityEngine.Object { + + /// + /// Delegate for safe browsing finish event. + /// + /// The `UniWebViewSafeBrowsing` object raised this event. + public delegate void OnSafeBrowsingFinishedDelegate(UniWebViewSafeBrowsing browsing); + /// + /// Raised when user dismisses safe browsing by tapping the Done button or Back button. + /// + /// The dismissed safe browsing instance will be invalid after this event being raised, and you should not use + /// it for another browsing purpose. Instead, create a new one for a new browsing session. + /// + /// This event will not happen in Unity Editor, because the whole `UniWebViewSafeBrowsing` will fall back to an + /// external browser. + /// + public event OnSafeBrowsingFinishedDelegate OnSafeBrowsingFinished; + + private string id = Guid.NewGuid().ToString(); + private UniWebViewNativeListener listener; + + // This is only for editor, to open the url in system browser. + private string url; + + /// + /// Whether the safe browsing mode is supported in current runtime or not. + /// + /// If supported, the safe browsing mode will be used when `Show` is called on a `UniWebViewSafeBrowsing` instance. + /// Otherwise, the system default browser will be used to open the page when `Show` is called. + /// + /// This property always returns `true` on iOS runtime platform. On Android, it depends on whether there is an Intent + /// can handle the safe browsing request. Usually it is provided by Chrome. If there is no Intent can open the URL + /// in safe browsing mode, this property will return `false`. + /// + /// To use this API on Android when you set your Target SDK to Android 11 or later, you need to declare the correct + /// intent query explicitly in your AndroidManifest.xml, to follow the Package Visibility + /// (https://developer.android.com/about/versions/11/privacy/package-visibility): + /// + /// ```xml + /// + /// + /// + /// + /// + /// ``` + /// + /// + /// Returns `true` if the safe browsing mode is supported and the page will be opened in safe browsing + /// mode. Otherwise, `false`. + /// + public static bool IsSafeBrowsingSupported { + get { + #if UNITY_EDITOR + return false; + #elif UNITY_IOS + return true; + #elif UNITY_ANDROID + return UniWebViewInterface.IsSafeBrowsingSupported(); + #else + return false; + #endif + } + } + + /// + /// Creates a new `UniWebViewSafeBrowsing` instance with a given URL. + /// + /// The URL to navigate to. The URL must use the `http` or `https` scheme. + /// A newly created `UniWebViewSafeBrowsing` instance. + public static UniWebViewSafeBrowsing Create(string url) { + var safeBrowsing = new UniWebViewSafeBrowsing(); + if (!UniWebViewHelper.IsEditor) { + safeBrowsing.listener.safeBrowsing = safeBrowsing; + safeBrowsing.Init(url); + } + safeBrowsing.url = url; + + return safeBrowsing; + } + + /// + /// Shows the safe browsing content above current screen. + /// + public void Show() { + if (UniWebViewSafeBrowsing.IsSafeBrowsingSupported) { + UniWebViewInterface.SafeBrowsingShow(listener.Name); + } else { + if (!UniWebViewHelper.IsEditor) { + UniWebViewLogger.Instance.Critical(@"UniWebViewSafeBrowsing.Show is called but the current device does + not support Safe Browsing. + This might be due to Chrome or any other processing app is not installed, or the manifest file not + configured correctly. Check SafeBrowsing Mode guide for more: https://docs.uniwebview.com/guide/safe-browsing.html"); + } + Application.OpenURL(url); + } + } + + /// + /// Dismisses the safe browsing component. + /// + /// This method only works on iOS. On Android, there is no way to dismiss the safe browsing component + /// programatically as the result of the limitation from the native (Android) side. + /// + public void Dismiss() { + #if UNITY_IOS && !UNITY_EDITOR + UniWebViewInterface.SafeBrowsingDismiss(listener.Name); + #endif + } + + /// + /// Sets the color for toolbar background in the safe browsing component. The changes are ignored after `Show` + /// method is called. + /// + /// The color to tint the toolbar. + public void SetToolbarColor(Color color) { + if (!UniWebViewHelper.IsEditor) { + UniWebViewInterface.SafeBrowsingSetToolbarColor(listener.Name, color.r, color.g, color.b); + } + } + + /// + /// Sets the color for toolbar controls in the safe browsing component. The changes are ignored after `Show` method + /// is called. + /// + /// This method only works on iOS. On Android, the controls color is determined by system to keep a reasonable + /// contrast, based on the toolbar background color you provided in `SetToolbarColor`. + /// + /// The color to tint the controls on toolbar. + public void SetToolbarItemColor(Color color) { + #if UNITY_IOS && !UNITY_EDITOR + UniWebViewInterface.SafeBrowsingSetToolbarItemColor(listener.Name, color.r, color.g, color.b); + #endif + } + + private UniWebViewSafeBrowsing() { + if (!UniWebViewHelper.IsEditor) { + var listenerObject = new GameObject(id); + listener = listenerObject.AddComponent(); + UniWebViewNativeListener.AddListener(listener); + } + } + + private void Init(string url) { + UniWebViewInterface.SafeBrowsingInit(listener.Name, url); + } + + internal void InternalSafeBrowsingFinished() { + if (OnSafeBrowsingFinished != null) { + OnSafeBrowsingFinished(this); + } + + UniWebViewNativeListener.RemoveListener(listener.Name); + Destroy(listener.gameObject); + } +} \ No newline at end of file diff --git a/Runtime/GuruWebview/UniWebView5/UniWebView/Script/UniWebViewSafeBrowsing.cs.meta b/Runtime/GuruWebview/UniWebView5/UniWebView/Script/UniWebViewSafeBrowsing.cs.meta new file mode 100644 index 0000000..3d7276c --- /dev/null +++ b/Runtime/GuruWebview/UniWebView5/UniWebView/Script/UniWebViewSafeBrowsing.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: dca3ba926a98540d3a2bff6312899a75 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/GuruWebview/UniWebView5/UniWebView/Script/UniWebViewToolbarPosition.cs b/Runtime/GuruWebview/UniWebView5/UniWebView/Script/UniWebViewToolbarPosition.cs new file mode 100644 index 0000000..6d48c14 --- /dev/null +++ b/Runtime/GuruWebview/UniWebView5/UniWebView/Script/UniWebViewToolbarPosition.cs @@ -0,0 +1,32 @@ +// +// UniWebViewToolbarPosition.cs +// Created by Wang Wei(@onevcat) on 2017-05-31. +// +// This file is a part of UniWebView Project (https://uniwebview.com) +// By purchasing the asset, you are allowed to use this code in as many as projects +// you want, only if you publish the final products under the name of the same account +// used for the purchase. +// +// This asset and all corresponding files (such as source code) are provided on an +// “as is” basis, without warranty of any kind, express of 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 action of contract, tort or otherwise, +// arising from, out of or in connection with the software or the use of other dealing in the software. +// + +/// +/// Toolbar position of webview. You can set the snapping edge for the built-in toolbar in iOS. +/// +public enum UniWebViewToolbarPosition +{ + /// + /// Top screen edge. + /// + Top = 0, + /// + /// Bottom screen edge. + /// + Bottom +} + diff --git a/Runtime/GuruWebview/UniWebView5/UniWebView/Script/UniWebViewToolbarPosition.cs.meta b/Runtime/GuruWebview/UniWebView5/UniWebView/Script/UniWebViewToolbarPosition.cs.meta new file mode 100644 index 0000000..a3f1f7c --- /dev/null +++ b/Runtime/GuruWebview/UniWebView5/UniWebView/Script/UniWebViewToolbarPosition.cs.meta @@ -0,0 +1,12 @@ +fileFormatVersion: 2 +guid: 9e4c706c92e5e4dc381385bf4edbd3dd +timeCreated: 1496199547 +licenseType: Free +MonoImporter: + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/GuruWebview/UniWebView5/UniWebView/Script/UniWebViewTransitionEdge.cs b/Runtime/GuruWebview/UniWebView5/UniWebView/Script/UniWebViewTransitionEdge.cs new file mode 100644 index 0000000..9871428 --- /dev/null +++ b/Runtime/GuruWebview/UniWebView5/UniWebView/Script/UniWebViewTransitionEdge.cs @@ -0,0 +1,43 @@ +// +// UniWebViewTransitionEdge.cs +// Created by Wang Wei(@onevcat) on 2017-05-04. +// +// This file is a part of UniWebView Project (https://uniwebview.com) +// By purchasing the asset, you are allowed to use this code in as many as projects +// you want, only if you publish the final products under the name of the same account +// used for the purchase. +// +// This asset and all corresponding files (such as source code) are provided on an +// “as is” basis, without warranty of any kind, express of 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 action of contract, tort or otherwise, +// arising from, out of or in connection with the software or the use of other dealing in the software. + +/// +/// An enum to identify transition edge from or to when the UniWebView +/// transition happens. You can specify an edge in Show() or Hide() methods of web view. +/// +public enum UniWebViewTransitionEdge +{ + /// + /// No transition when showing or hiding. + /// + None = 0, + /// + /// Transit the web view from/to top. + /// + Top, + /// + /// Transit the web view from/to left. + /// + Left, + /// + /// Transit the web view from/to bottom. + /// + Bottom, + /// + /// Transit the web view from/to right. + /// + Right +} diff --git a/Runtime/GuruWebview/UniWebView5/UniWebView/Script/UniWebViewTransitionEdge.cs.meta b/Runtime/GuruWebview/UniWebView5/UniWebView/Script/UniWebViewTransitionEdge.cs.meta new file mode 100644 index 0000000..29ce0d8 --- /dev/null +++ b/Runtime/GuruWebview/UniWebView5/UniWebView/Script/UniWebViewTransitionEdge.cs.meta @@ -0,0 +1,12 @@ +fileFormatVersion: 2 +guid: 1256e7eea8f184fae9b700f8f78e014c +timeCreated: 1493888384 +licenseType: Free +MonoImporter: + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/GuruWebview/UniWebView5/UniWebView/UniWebView-CSharp.asmdef b/Runtime/GuruWebview/UniWebView5/UniWebView/UniWebView-CSharp.asmdef new file mode 100644 index 0000000..cd058cc --- /dev/null +++ b/Runtime/GuruWebview/UniWebView5/UniWebView/UniWebView-CSharp.asmdef @@ -0,0 +1,14 @@ +{ + "name": "UniWebView-CSharp", + "rootNamespace": "", + "references": ["GUID:343deaaf83e0cee4ca978e7df0b80d21","GUID:2bafac87e7f4b9b418d9448d219b01ab"], + "includePlatforms": [], + "excludePlatforms": [], + "allowUnsafeCode": false, + "overrideReferences": false, + "precompiledReferences": [], + "autoReferenced": true, + "defineConstraints": [], + "versionDefines": [], + "noEngineReferences": false +} \ No newline at end of file diff --git a/Runtime/GuruWebview/UniWebView5/UniWebView/UniWebView-CSharp.asmdef.meta b/Runtime/GuruWebview/UniWebView5/UniWebView/UniWebView-CSharp.asmdef.meta new file mode 100644 index 0000000..de361b1 --- /dev/null +++ b/Runtime/GuruWebview/UniWebView5/UniWebView/UniWebView-CSharp.asmdef.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 4ca033ee547ec451c8a2d3c33d7620ad +AssemblyDefinitionImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/GuruWebview/UniWebView5/UniWebView/csc.rsp b/Runtime/GuruWebview/UniWebView5/UniWebView/csc.rsp new file mode 100644 index 0000000..349a1e2 --- /dev/null +++ b/Runtime/GuruWebview/UniWebView5/UniWebView/csc.rsp @@ -0,0 +1 @@ +-r:System.Web.dll \ No newline at end of file diff --git a/Runtime/GuruWebview/UniWebView5/UniWebView/csc.rsp.meta b/Runtime/GuruWebview/UniWebView5/UniWebView/csc.rsp.meta new file mode 100644 index 0000000..a1db554 --- /dev/null +++ b/Runtime/GuruWebview/UniWebView5/UniWebView/csc.rsp.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 81b32579db9e845999209664ba8e2ec3 +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Keywords.meta b/Runtime/Keywords.meta new file mode 100644 index 0000000..ae54d4e --- /dev/null +++ b/Runtime/Keywords.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 65b00648a283f42a3b84dc6d28ace0ea +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Keywords/README.md b/Runtime/Keywords/README.md new file mode 100644 index 0000000..064c3f0 --- /dev/null +++ b/Runtime/Keywords/README.md @@ -0,0 +1,61 @@ +# Guru Keywords 管理器 + +Version 0.4.0 + +## 简介 + +配合 `AppLovin MAX` 接口, 通过上报特定的 `Keywords` 来获取预置好的广告瀑布流的模块 + +主要统计维度: `用户留存天数` 和 `是否付费` + + + + +## 接入说明 + +1. 在游戏启动类内调用如下代码: +```c# + // 启动类逻辑 + private void Start() + { + ... + + // 启动时更新一下用户的付费状态 + bool isIapUser = UserData.Instance.hasUserPaied; + + // 启动模块 + Guru.KeywordsManager.Install(isIapUser); + + ... + } +``` +2. 在支付成功时, 或者判定用户未付费用户时调用如下代码: +```c# + // IAP管理器内逻辑 + // 支付成功回调 + private void OnPurcahseSuccess(Product product) + { + ... + UserData.Instance.hasUserPaied = true; + + // 更新用户状态 + Guru.KeywordsManager.IsIapUser = true; + ... + + } +``` + +3. 此功能由Firebase云控控制开关, 请按需设置参数值和条件 +```javascript + + // remote-config key: + keywords_config + + // remote-config value: + { "enable": true } + + + // 项目内默认值: + enable = false + +``` \ No newline at end of file diff --git a/Runtime/Keywords/README.md.meta b/Runtime/Keywords/README.md.meta new file mode 100644 index 0000000..5c3b37c --- /dev/null +++ b/Runtime/Keywords/README.md.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 7a7d9ec112c447eda662241e36231769 +timeCreated: 1701077666 \ No newline at end of file diff --git a/Runtime/Keywords/Runtime.meta b/Runtime/Keywords/Runtime.meta new file mode 100644 index 0000000..95212c2 --- /dev/null +++ b/Runtime/Keywords/Runtime.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 7173ff46584e4a24aa55c97200778627 +timeCreated: 1701077593 \ No newline at end of file diff --git a/Runtime/Keywords/Runtime/KeywordsManager.cs b/Runtime/Keywords/Runtime/KeywordsManager.cs new file mode 100644 index 0000000..29e2c73 --- /dev/null +++ b/Runtime/Keywords/Runtime/KeywordsManager.cs @@ -0,0 +1,429 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using Firebase.Crashlytics; +using UnityEngine; + + +namespace Guru +{ + + + /// + /// MAX KeyWord管理器 + /// + public class KeywordsManager + { + public const string Version = "0.4.0"; + + public static readonly string Tag = "[KWM]"; + + public static readonly DateTime BaseDate = new DateTime(1970, 1, 1).ToUniversalTime(); + + /// + /// 日期转时间戳 (Ticks) + /// + /// + /// + public static long DateToTicks(DateTime date) => (date.ToUniversalTime() - BaseDate).Ticks; + + /// + /// 时间戳 (Ticks) 转日期 + /// + /// + /// + public static DateTime TicksToDate(long stamp) => BaseDate.AddTicks(stamp); + + /// + /// 首次安装时间 + /// + public static DateTime FirstInstallDate + { + get + { + var key = nameof(FirstInstallDate); + if (PlayerPrefs.HasKey(key)) + { + var value = PlayerPrefs.GetString(key, ""); + if (!string.IsNullOrEmpty(value) && long.TryParse(value, out var ticks)) + { + return TicksToDate(ticks); + } + } + + Debug.Log($"{Tag} Can't find first install date, using NOW!!"); + FirstInstallDate = UTCNow; + return UTCNow; + } + + set => PlayerPrefs.SetString(nameof(FirstInstallDate), DateToTicks(value).ToString(new CultureInfo("en"))); + } + public static DateTime UTCNow => DateTime.Now.ToUniversalTime(); + + + /// + /// 当前的LT + /// + public static int CurrentLT => Instance?.GetLTValue() ?? -1; + + private static KeywordsManager _instance; + public static KeywordsManager Instance { + get { + if(_instance == null) _instance = new KeywordsManager(); + return _instance; + } + } + + public static readonly List ItDays = new List() + { + 0, 1, 2, 3, 4, 5, 6, 14, 30, 60, 90, 120, 180 + }; + + private static string _badsWaterfallName = "null"; + private static string _iadsWaterfallName = "null"; + private static string _radsWaterfallName = "null"; + + // Banner 瀑布流名称 + public static string BadsWaterfallName => _badsWaterfallName; + // Interstital 瀑布流名称 + public static string IadsWaterfallName => _iadsWaterfallName; + // Reward 瀑布流名称 + public static string RadsWaterfallName => _radsWaterfallName; + /// + /// 是否具有用户标签 + /// + public static bool IsUserMarked + { + get + { + if(!PlayerPrefs.HasKey(nameof(IsUserMarked))) return false; + return !string.IsNullOrEmpty(PlayerPrefs.GetString(nameof(IsUserMarked), "")); + } + set => PlayerPrefs.SetString(nameof(IsUserMarked), value? "marked": ""); + } + + /// + /// 是否是付费用户 + /// 在用户执行任何一次购买的时候需要更新该接口 + /// + public static bool IsIapUser + { + get => PlayerPrefs.GetInt(nameof(IsIapUser), 0) == 1; + set => PlayerPrefs.SetInt(nameof(IsIapUser), value ? 1 : 0); + } + + + #region 初始化 + + /// + /// 安装服务 + /// + /// 标记用户是否为付费用户 + /// 当前完成关卡数. 不做关卡过滤时. 默认赋值0即可 + public static void Install(bool isIAPUser = false, int finishedLevels = 0) + { + try + { + // 首先检查用户属性 + if (finishedLevels > 0 && !CheckTargetUser(finishedLevels)) + { + Debug.Log($"{Tag} User is not new installing. Skip user...."); + return; + } + + if (KeywordsConfig.Enabled()) // 云控开关, 如果不需要云控, 可注释此行 + { + Instance.Init(); + if (!IsIapUser && isIAPUser) IsIapUser = true; // 判断未赋值时, 更新此变量 + } + } + catch (Exception e) + { + Crashlytics.LogException(e); + } + } + + /// + /// 初始化 + /// + private void Init() + { + var lt = GetLTValue(); // 初始化立即更新LT + Debug.Log($"{Tag} is inited. LT:{lt}"); // 显示启动日志 + SetupMaxCallbacks(); + } + + + private void SetupMaxCallbacks() + { + MaxSdkCallbacks.OnSdkInitializedEvent += OnMaxSdkInitializedCallBack; + + MaxSdkCallbacks.Banner.OnAdLoadedEvent += OnBannerLoaded; + MaxSdkCallbacks.Banner.OnAdLoadFailedEvent += OnBannerLoadFailed; + MaxSdkCallbacks.Interstitial.OnAdLoadedEvent += OnInterstitialLoaded; + MaxSdkCallbacks.Interstitial.OnAdLoadFailedEvent += OnInterstitialLoadFailed; + MaxSdkCallbacks.Rewarded.OnAdLoadedEvent += OnRewardedLoaded; + MaxSdkCallbacks.Rewarded.OnAdLoadFailedEvent += OnRewardedLoadedFailed; + } + + /// + /// 检查是否是目标用户 + /// + /// + /// + public static bool CheckTargetUser(int finishedLevels) + { + // 如果用户已经被标记, 则继续上报 + if (IsUserMarked) return true; + + // 用户未标记, 则根据完成的关卡判断用户是否是新用户 + // 新用户则给标记 + if (finishedLevels <= 2) + { + IsUserMarked = true; + Debug.Log($"{Tag} User is new installing. Mark user as target...."); + return true; + } + + // 用户未标记且已经开始过游戏了, 则视为老用户 + return false; + } + + #endregion + + #region Keywords + + /// + /// 上报关键字 + /// + private void ReportKeyword() + { + var lt = GetLTValue(); + bool paid = IsIapUser; + string version = Application.version; + var keywords = new [] { $"lt:{lt}", $"paid:{paid.ToString().ToLower()}", $"app_version:{version}" }; + SetMaxKeywords(keywords); + } + + /// + /// 获取LT值 + /// + /// + private int GetLTValue(int days = 0) + { + // Debug.Log($"{Tag} Get first install date: {FirstInstallDate}"); + if(0 == days) days = (int)(UTCNow - FirstInstallDate).TotalDays; + if(days > ItDays[ItDays.Count-1]) return ItDays[ItDays.Count-1]; + + for (int i = 0; i < ItDays.Count; i++) + { + if (days < ItDays[i]) + { + if (i > 0) + { + return ItDays[i - 1]; + } + else + { + return ItDays[0]; + } + } + } + return ItDays[0]; + } + + + /// + /// 设置Keywords + /// + /// + private void SetMaxKeywords(string[] keywords) + { + if ( null == keywords ) return; + Debug.Log($"{Tag} set keywords: [{string.Join(",", keywords)}]"); + MaxSdk.TargetingData.ClearAll(); + MaxSdk.TargetingData.Keywords = keywords; + } + + + #endregion + + #region WaterfallName + + /// + /// Banner WaterfallInfo + /// + /// + private void OnGetBannerWaterInfo(MaxSdkBase.WaterfallInfo waterfallInfo) + { + _badsWaterfallName = waterfallInfo.Name; + } + + /// + /// IV WaterfallInfo + /// + /// + private void OnGetIVWaterInfo(MaxSdkBase.WaterfallInfo waterfallInfo) + { + _iadsWaterfallName = waterfallInfo.Name; + } + + /// + /// RV WaterfallInfo + /// + /// + private void OnGetRVWaterInfo(MaxSdkBase.WaterfallInfo waterfallInfo) + { + _radsWaterfallName = waterfallInfo.Name; + } + + #endregion + + #region Max 生命周期监控 + + private void OnMaxSdkInitializedCallBack(MaxSdkBase.SdkConfiguration obj) + { + ReportKeyword(); + } + + + private void OnBannerLoaded(string adUnitId, MaxSdkBase.AdInfo adInfo) + { + OnGetBannerWaterInfo(adInfo.WaterfallInfo); + } + + private void OnBannerLoadFailed(string adUnitId, MaxSdkBase.ErrorInfo errorInfo) + { + OnGetBannerWaterInfo(errorInfo.WaterfallInfo); + } + + private void OnRewardedLoadedFailed(string adUnitId, MaxSdkBase.ErrorInfo errorInfo) + { + OnGetRVWaterInfo(errorInfo.WaterfallInfo); + } + + private void OnRewardedLoaded(string adUnitId, MaxSdkBase.AdInfo adInfo) + { + OnGetRVWaterInfo(adInfo.WaterfallInfo); + } + + private void OnInterstitialLoadFailed(string adUnitId, MaxSdkBase.ErrorInfo errorInfo) + { + OnGetIVWaterInfo(errorInfo.WaterfallInfo); + } + + private void OnInterstitialLoaded(string adUnitId, MaxSdkBase.AdInfo adInfo) + { + OnGetIVWaterInfo(adInfo.WaterfallInfo); + } + + #endregion + + #region Unit Test + +#if UNITY_EDITOR + + public static void DebugKeywords(string[] kw) => Instance?.SetMaxKeywords(kw); + + /// + /// LT取值测试 + /// + [UnityEditor.MenuItem("Test/Test LT")] + public static void TestFetchLT() + { + var days = 0; + days = 0; + Debug.Log($"Days: {days} => TL: {Instance.GetLTValue(days)}"); + days = 1; + Debug.Log($"Days: {days} => TL: {Instance.GetLTValue(days)}"); + days = 2; + Debug.Log($"Days: {days} => TL: {Instance.GetLTValue(days)}"); + days = 5; + Debug.Log($"Days: {days} => TL: {Instance.GetLTValue(days)}"); + days = 12; + Debug.Log($"Days: {days} => TL: {Instance.GetLTValue(days)}"); + days = 15; + Debug.Log($"Days: {days} => TL: {Instance.GetLTValue(days)}"); + days = 55; + Debug.Log($"Days: {days} => TL: {Instance.GetLTValue(days)}"); + days = 80; + Debug.Log($"Days: {days} => TL: {Instance.GetLTValue(days)}"); + days = 179; + Debug.Log($"Days: {days} => TL: {Instance.GetLTValue(days)}"); + days = 365; + Debug.Log($"Days: {days} => TL: {Instance.GetLTValue(days)}"); + } + +#endif + + #endregion + + } + + #region 云控参数 + + [Serializable] + internal class KeywordsConfig + { + public const string KEY = "keywords_config"; + public bool enable = false; + + /// + /// 获取功能是否开启 + /// + /// + public static bool Enabled() + { + var cfg = Get(); + return cfg?.enable ?? false; + } + + public static KeywordsConfig Get() + { + try + { + string raw = GetRemoteValue(KEY); + if (!string.IsNullOrEmpty(raw)) + { + return JsonUtility.FromJson(raw); + } + } + catch (Exception e) + { + Debug.LogError(e); + Crashlytics.LogException(e); + } + + return new KeywordsConfig(); // 返回默认值 + } + + + /// + /// 获取Online值 + /// + /// + /// + private static string GetRemoteValue(string key) + { + var remote = Firebase.RemoteConfig.FirebaseRemoteConfig.DefaultInstance; + if (remote != null) + { + try + { + return remote.GetValue(key).StringValue; + } + catch (Exception e) + { + Crashlytics.LogException(e); + } + } + + return ""; + } + + + } + + #endregion + +} \ No newline at end of file diff --git a/Runtime/Keywords/Runtime/KeywordsManager.cs.meta b/Runtime/Keywords/Runtime/KeywordsManager.cs.meta new file mode 100644 index 0000000..22927d6 --- /dev/null +++ b/Runtime/Keywords/Runtime/KeywordsManager.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: dcb5e6533eeb4740a34384a75eac57fc +timeCreated: 1691830454 \ No newline at end of file diff --git a/link.xml b/link.xml new file mode 100644 index 0000000..049145e --- /dev/null +++ b/link.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/link.xml.meta b/link.xml.meta new file mode 100644 index 0000000..f020e7e --- /dev/null +++ b/link.xml.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 88e0dfae65764d0382e8b5987ec546bc +timeCreated: 1703388758 \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..4a437d4 --- /dev/null +++ b/package.json @@ -0,0 +1,32 @@ +{ + "name": "com.guru.unity.sdk.core", + "displayName": "Guru SDK Core", + "version": "0.0.1", + "description": "Guru SDK core for Unity developers", + "unity": "2021.3", + "author":{ + "name": "Guru Fungames Studio" + }, + "license": "MIT", + "category": ["game","tool","development", "Guru"], + "relatedPackages": { + }, + "dependencies": { + "com.unity.purchasing": "4.10.0", + "com.unity.mobile.android-logcat": "1.3.2", + "com.unity.editorcoroutines": "1.0.0", + "com.coffee.upm-git-extension": "https://github.com/mob-sakai/UpmGitExtension.git", + "com.google.external-dependency-manager": "https://github.com/GameWorkstore/com.google.external-dependency-manager.git", + "com.google.firebase.crashlytics": "https://github.com/GameWorkstore/com.google.firebase.crashlytics.git#10.1.1", + "com.google.firebase.analytics": "https://github.com/GameWorkstore/com.google.firebase.analytics.git#10.1.1", + "com.google.firebase.app": "https://github.com/GameWorkstore/com.google.firebase.app.git#10.1.1", + "com.google.firebase.auth": "https://github.com/GameWorkstore/com.google.firebase.auth.git#10.1.1", + "com.google.firebase.firestore": "https://github.com/GameWorkstore/com.google.firebase.firestore.git#10.1.1", + "com.google.firebase.messaging": "https://github.com/GameWorkstore/com.google.firebase.messaging.git#10.1.1", + "com.google.firebase.remote-config": "https://github.com/GameWorkstore/com.google.firebase.remote-config.git#10.1.1", + "com.google.firebase.dynamic-links": "https://github.com/GameWorkstore/com.google.firebase.dynamic-links.git#10.1.1", + + "com.guru.unity.adjust": "git@git.chengdu.pundit.company:castbox/com.guru.unity.adjust.git", + "com.guru.unity.max": "git@git.chengdu.pundit.company:castbox/com.guru.unity.max.git" + } +} \ No newline at end of file diff --git a/package.json.meta b/package.json.meta new file mode 100644 index 0000000..5d0dc33 --- /dev/null +++ b/package.json.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: c87b469b5418e4cadb97b6265f516fd6 +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: