Compare commits
1 Commits
| Author | SHA1 | Date |
|---|---|---|
|
|
c3b77622d9 |
|
|
@ -1,8 +1,8 @@
|
|||
{
|
||||
"name": "Guru.Editor",
|
||||
"rootNamespace": "",
|
||||
"references": [
|
||||
"Guru.Runtime",
|
||||
"Guru.Notification"
|
||||
"Guru.Runtime"
|
||||
],
|
||||
"includePlatforms": [
|
||||
"Editor"
|
||||
|
|
|
|||
|
|
@ -1,119 +0,0 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using UnityEngine;
|
||||
|
||||
#if UNITY_IOS
|
||||
|
||||
namespace Guru.Editor
|
||||
{
|
||||
using UnityEditor;
|
||||
using System;
|
||||
using System.IO;
|
||||
using UnityEditor.Callbacks;
|
||||
|
||||
|
||||
public class IOSPostBuild_Firebase_VersionFix
|
||||
{
|
||||
|
||||
|
||||
private static string[] fixedLibs = new string[]
|
||||
{
|
||||
"Firebase/Core",
|
||||
"Firebase/Firestore",
|
||||
"Firebase/Analytics",
|
||||
"Firebase/Storage",
|
||||
"Firebase/Auth",
|
||||
"Firebase/Messaging",
|
||||
"Firebase/Crashlytics",
|
||||
"Firebase/DynamicLinks",
|
||||
"Firebase/RemoteConfig",
|
||||
};
|
||||
|
||||
private static string[] additionLibs = new string[]
|
||||
{
|
||||
"FirebaseFirestoreInternal",
|
||||
};
|
||||
|
||||
private static string fixedVersion = "10.22.0";
|
||||
private static string minTargetSdk = "8.0";
|
||||
|
||||
// <iosPod name="Firebase/Core" version="10.22.0" minTargetSdk="8.0" />
|
||||
|
||||
|
||||
|
||||
// Firebase 10.20.0 fixed to 10.22.0. BUT higher version do not open this ATTRIBUTE !!
|
||||
// [PostProcessBuild(47)] // MAX POD Process Order
|
||||
public static void PostBuildFixPodDeps(BuildTarget target, string projPath)
|
||||
{
|
||||
if (target != BuildTarget.iOS) return;
|
||||
|
||||
string podfile = Path.Combine(projPath, "Podfile");
|
||||
if (!File.Exists(podfile)) return;
|
||||
|
||||
FixFirebasePodVersion(podfile);
|
||||
}
|
||||
|
||||
|
||||
private static void FixFirebasePodVersion(string podfile)
|
||||
{
|
||||
var lines = File.ReadAllLines(podfile).ToList();
|
||||
string line = "";
|
||||
int idx = 0;
|
||||
bool isDirty = false;
|
||||
List<string> needAdded = new List<string>(additionLibs);
|
||||
for (int i = 0; i < lines.Count; i++)
|
||||
{
|
||||
line = lines[i];
|
||||
|
||||
if (line.Contains("pod '"))
|
||||
{
|
||||
idx = i;
|
||||
}
|
||||
|
||||
foreach (var libName in fixedLibs)
|
||||
{
|
||||
if (line.Contains(libName))
|
||||
{
|
||||
lines[i] = FixOneFirebaseLibVersion(line, fixedVersion);
|
||||
isDirty = true;
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var libName in additionLibs)
|
||||
{
|
||||
if (line.Contains(libName))
|
||||
{
|
||||
needAdded.Remove(libName);
|
||||
lines[i] = FixOneFirebaseLibVersion(line, fixedVersion);
|
||||
isDirty = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (needAdded.Count > 0)
|
||||
{
|
||||
// pod 'Firebase/DynamicLinks', '10.20.0'
|
||||
foreach (var libName in needAdded)
|
||||
{
|
||||
idx++;
|
||||
idx = Mathf.Min(idx, lines.Count - 1);
|
||||
lines.Insert(idx, $"\tpod '{libName}', '{fixedVersion}'");
|
||||
isDirty = true;
|
||||
}
|
||||
}
|
||||
|
||||
if(isDirty) File.WriteAllLines(podfile, lines);
|
||||
}
|
||||
|
||||
private static string FixOneFirebaseLibVersion(string line, string fixedVersion)
|
||||
{
|
||||
if(!line.Contains("', '") || !line.Contains("pod")) return line;
|
||||
string fixedLine = line.Substring(0, line.IndexOf("', '") + 4) + $"{fixedVersion}'";
|
||||
return fixedLine;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
#endif
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 2aab28d86c8346e581c650a86cad060f
|
||||
timeCreated: 1717039005
|
||||
|
|
@ -77,8 +77,6 @@ namespace Guru.Editor
|
|||
|
||||
var workdir = GetWorkdir();
|
||||
var source = $"{workdir}/{SourceFileName}";
|
||||
var toDir = Directory.GetParent(IosPrivacyInfoPath);
|
||||
if (!toDir.Exists) toDir.Create();
|
||||
if (File.Exists(source))
|
||||
{
|
||||
FileUtil.ReplaceFile(source, IosPrivacyInfoPath);
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ namespace Guru
|
|||
/// </summary>
|
||||
public class IOSPostBuildSwift
|
||||
{
|
||||
[PostProcessBuild(2000)]
|
||||
[PostProcessBuild(40)]
|
||||
public static void OnPostProcessBuild(BuildTarget target, string buildPath)
|
||||
{
|
||||
if (target != BuildTarget.iOS) return;
|
||||
|
|
@ -43,7 +43,7 @@ namespace Guru
|
|||
|
||||
// 设置主项目的SWIFT构建支持
|
||||
project.SetBuildProperty(mainTargetGuid, "ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES", "YES");
|
||||
project.SetBuildProperty(frameworkTargetGuid, "ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES", "NO");
|
||||
|
||||
|
||||
project.WriteToFile(projectPath);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@
|
|||
"MaxSdk",
|
||||
"MaxSdk.Scripts",
|
||||
"Amazon",
|
||||
"Amazon.Scripts",
|
||||
"OpenWrapSDK",
|
||||
"UniWebView-CSharp",
|
||||
"UnityEngine.Purchasing",
|
||||
|
|
@ -18,9 +17,7 @@
|
|||
"Google.Play.Review",
|
||||
"Google.Play.Common",
|
||||
"Guru.LitJson",
|
||||
"Unity.Advertisement.IosSupport",
|
||||
"Unity.Notifications.Android",
|
||||
"Unity.Notifications.iOS"
|
||||
"Unity.Advertisement.IosSupport"
|
||||
],
|
||||
"includePlatforms": [],
|
||||
"excludePlatforms": [],
|
||||
|
|
|
|||
|
|
@ -1,3 +0,0 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 2b993d660d0a48b1a098f5d42611b464
|
||||
timeCreated: 1717034031
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 43aa7a523e0141d4a7f64e16e5294d2d
|
||||
timeCreated: 1717034046
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
{
|
||||
"name": "GuruAdjust.Editor",
|
||||
"includePlatforms": [
|
||||
"Editor"
|
||||
]
|
||||
}
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 82f90ccbb33b42e9ad29f5f5a861dc4a
|
||||
timeCreated: 1717137351
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 522c3aca8edd4e5bb3c57f54460df356
|
||||
timeCreated: 1717137307
|
||||
|
|
@ -1,81 +0,0 @@
|
|||
using System.IO;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Guru
|
||||
{
|
||||
public class AdjustSignatureHelper
|
||||
{
|
||||
|
||||
private static readonly string AndroidLib = "adjust-android-signature-3.13.1.aar";
|
||||
private static readonly string iOSLib = "AdjustSigSdk.a";
|
||||
|
||||
public static void DeployFiles()
|
||||
{
|
||||
var dir = GetParentDir();
|
||||
var files = $"{dir}/Files";
|
||||
if (Directory.Exists(files))
|
||||
{
|
||||
string from, to;
|
||||
bool res;
|
||||
from = $"{files}/{AndroidLib}.f";
|
||||
to = $"{Application.dataPath}/Plugins/Android/{AndroidLib}";
|
||||
res = CopyFile(from, to);
|
||||
if (res) Debug.Log($"Copy <color=#88ff00>{AndroidLib} to {to}</color> success...");
|
||||
from = $"{files}/{AndroidLib}.f.meta";
|
||||
to = $"{Application.dataPath}/Plugins/Android/{AndroidLib}.meta";
|
||||
CopyFile(from, to);
|
||||
|
||||
from = $"{files}/{iOSLib}.f";
|
||||
to = $"{Application.dataPath}/Plugins/iOS/{iOSLib}";
|
||||
res = CopyFile(from, to);
|
||||
if (res) Debug.Log($"Copy <color=#88ff00>{iOSLib} to {to}</color> success...");
|
||||
from = $"{files}/{iOSLib}.f.meta";
|
||||
to = $"{Application.dataPath}/Plugins/iOS/{iOSLib}.meta";
|
||||
CopyFile(from, to);
|
||||
|
||||
AssetDatabase.Refresh();
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.Log($"<color=red>Files not found: {files}</color>");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private static string GetParentDir()
|
||||
{
|
||||
var guids = AssetDatabase.FindAssets(nameof(AdjustSignatureHelper));
|
||||
if (guids != null && guids.Length > 0)
|
||||
{
|
||||
var path = AssetDatabase.GUIDToAssetPath(guids[0]);
|
||||
var dir = Directory.GetParent(Path.GetFullPath(path));
|
||||
return dir.FullName;
|
||||
}
|
||||
return Path.GetFullPath($"{Application.dataPath}/../Packages/com.guru.unity.sdk.core/Runtime/GuruAdjust/Editor/Signature");
|
||||
}
|
||||
|
||||
private static bool CopyFile(string source, string dest)
|
||||
{
|
||||
if (File.Exists(source))
|
||||
{
|
||||
if (!File.Exists(dest))
|
||||
{
|
||||
File.Delete(dest);
|
||||
}
|
||||
else
|
||||
{
|
||||
var destDir = Directory.GetParent(dest);
|
||||
if(!destDir.Exists) destDir.Create();
|
||||
}
|
||||
|
||||
File.Copy(source, dest, true);
|
||||
return true;
|
||||
}
|
||||
|
||||
Debug.Log($"<colo=red>File not found: {source}...</color>");
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
fileFormatVersion: 2
|
||||
guid: fcbb67d0a48d4b88bc8fd1430c4bbda4
|
||||
timeCreated: 1717137470
|
||||
|
|
@ -1,14 +0,0 @@
|
|||
namespace Guru
|
||||
{
|
||||
using UnityEditor;
|
||||
|
||||
public class AdjustSignatureMenuItem
|
||||
{
|
||||
[MenuItem("Guru/Adjust/SignatureV3/Deploy Libs")]
|
||||
private static void CopyLibsToPlugins()
|
||||
{
|
||||
AdjustSignatureHelper.DeployFiles();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 5f624e98bef44a399cf808a6aa7f5499
|
||||
timeCreated: 1717137523
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
fileFormatVersion: 2
|
||||
guid: f0f3f1cbb077474882bf4725c274efa9
|
||||
timeCreated: 1717034059
|
||||
Binary file not shown.
|
|
@ -1,80 +0,0 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 7dfc92df774d347749c60047aaa3da41
|
||||
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:
|
||||
Binary file not shown.
|
|
@ -79,7 +79,7 @@ namespace Guru
|
|||
/// <summary>
|
||||
/// 初始化平台
|
||||
/// </summary>
|
||||
public void Initialize(bool isDebug = false)
|
||||
public void Initialize()
|
||||
{
|
||||
#if UNITY_EDITOR
|
||||
Debug.Log($"<color=orange>=== Amazon will not init on Editor ===</color>");
|
||||
|
|
@ -93,9 +93,11 @@ namespace Guru
|
|||
// 初始化Amazon
|
||||
Amazon.Initialize (AmazonAppID);
|
||||
Amazon.SetAdNetworkInfo(new AdNetworkInfo(DTBAdNetwork.MAX));
|
||||
Debug.Log($"[Ads] --- Amazon init start isDebug:{isDebug}, AmazonID:{AmazonAppID}");
|
||||
Amazon.EnableTesting (isDebug); // Make sure to take this off when going live.
|
||||
Amazon.EnableLogging (isDebug);
|
||||
#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);
|
||||
|
|
|
|||
|
|
@ -50,7 +50,7 @@ namespace Guru
|
|||
* 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(bool isDebug = false)
|
||||
public void Initialize()
|
||||
{
|
||||
#if UNITY_EDITOR
|
||||
Debug.Log($"<color=orange>=== PubMatic will not init on Editor ===</color>");
|
||||
|
|
|
|||
|
|
@ -20,11 +20,16 @@ Sample Dependencies.xml:
|
|||
<androidPackage spec="com.squareup.retrofit2:converter-gson:2.7.1" />
|
||||
<androidPackage spec="com.squareup.retrofit2:adapter-rxjava2:2.7.1" />
|
||||
<androidPackage spec="com.squareup.okhttp3:okhttp:4.9.3" />
|
||||
|
||||
<!-- <androidPackage spec="com.mapzen:on-the-road:0.8.1" />-->
|
||||
<!-- <androidPackage spec="com.squareup.retrofit2:retrofit:2.7.1" />-->
|
||||
</androidPackages>
|
||||
<iosPods>
|
||||
<iosPod name="GuruAnalyticsLib" bitcodeEnabled="false" path="Packages/com.guru.unity.sdk.core/Runtime/GuruAnalytics/Plugins/iOS" />
|
||||
<iosPod name="GuruAnalyticsLib" version="0.3.3" bitcodeEnabled="false">
|
||||
<sources>
|
||||
<source>git@github.com:castbox/GuruSpecs.git</source>
|
||||
</sources>
|
||||
</iosPod>
|
||||
<iosPod name="JJException" bitcodeEnabled="false" />
|
||||
</iosPods>
|
||||
</dependencies>
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -1,5 +1,5 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 07dda3e28d25446c3bb0924d7fc21cb4
|
||||
guid: 07cf2335bd298401b8015718fca55265
|
||||
PluginImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
Binary file not shown.
Binary file not shown.
|
|
@ -1,5 +1,5 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 32eda01e213614348899eefe856392d3
|
||||
guid: b0e58ca75957d470cbc4951b34f31bf8
|
||||
PluginImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
Binary file not shown.
|
|
@ -1,32 +0,0 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 66c5f430ab9654ef4a2376e71aa04bca
|
||||
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:
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
fileFormatVersion: 2
|
||||
guid: e53e2bfca0fd949559d383674081f737
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 502f707bde2a24fadb6ec09ac5a3593f
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -1,41 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>NSPrivacyAccessedAPITypes</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>NSPrivacyAccessedAPITypeReasons</key>
|
||||
<array>
|
||||
<string>CA92.1</string>
|
||||
</array>
|
||||
<key>NSPrivacyAccessedAPIType</key>
|
||||
<string>NSPrivacyAccessedAPICategoryUserDefaults</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>NSPrivacyAccessedAPITypeReasons</key>
|
||||
<array>
|
||||
<string>C617.1</string>
|
||||
</array>
|
||||
<key>NSPrivacyAccessedAPIType</key>
|
||||
<string>NSPrivacyAccessedAPICategoryFileTimestamp</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>NSPrivacyAccessedAPITypeReasons</key>
|
||||
<array>
|
||||
<string>35F9.1</string>
|
||||
<string>8FFB.1</string>
|
||||
<string>3D61.1</string>
|
||||
</array>
|
||||
<key>NSPrivacyAccessedAPIType</key>
|
||||
<string>NSPrivacyAccessedAPICategorySystemBootTime</string>
|
||||
</dict>
|
||||
</array>
|
||||
<key>NSPrivacyTracking</key>
|
||||
<false/>
|
||||
<key>NSPrivacyTrackingDomains</key>
|
||||
<array>
|
||||
<string></string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
|
|
@ -1,85 +0,0 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 63885139be48c43ae8cd3b1c403a686f
|
||||
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: 1
|
||||
- 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: AnyCPU
|
||||
- first:
|
||||
Standalone: OSXUniversal
|
||||
second:
|
||||
enabled: 0
|
||||
settings:
|
||||
CPU: AnyCPU
|
||||
- first:
|
||||
Standalone: Win
|
||||
second:
|
||||
enabled: 0
|
||||
settings:
|
||||
CPU: AnyCPU
|
||||
- first:
|
||||
Standalone: Win64
|
||||
second:
|
||||
enabled: 0
|
||||
settings:
|
||||
CPU: AnyCPU
|
||||
- first:
|
||||
iPhone: iOS
|
||||
second:
|
||||
enabled: 0
|
||||
settings:
|
||||
AddToEmbeddedBinaries: false
|
||||
CPU: AnyCPU
|
||||
CompileFlags:
|
||||
FrameworkDependencies:
|
||||
- first:
|
||||
tvOS: tvOS
|
||||
second:
|
||||
enabled: 1
|
||||
settings: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
fileFormatVersion: 2
|
||||
guid: d30316515c87a4421bc7032194f888e1
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
fileFormatVersion: 2
|
||||
guid: e0086576c1ac64707b788bef25dc9316
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -1,158 +0,0 @@
|
|||
//
|
||||
// GuruAnalytics.swift
|
||||
// GuruAnalytics_iOS
|
||||
//
|
||||
// Created by mayue on 2022/11/4.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
public class GuruAnalytics: NSObject {
|
||||
|
||||
internal static var uploadPeriodInSecond: Double = 60
|
||||
internal static var batchLimit: Int = 25
|
||||
internal static var eventExpiredSeconds: Double = 7 * 24 * 60 * 60
|
||||
internal static var initializeTimeout: Double = 5
|
||||
internal static var saasXAPPID = ""
|
||||
internal static var saasXDEVICEINFO = ""
|
||||
internal static var loggerDebug = true
|
||||
internal static var enableUpload = true
|
||||
|
||||
/// 初始化设置
|
||||
/// - Parameters:
|
||||
/// - uploadPeriodInSecond: 批量上传周期,单位秒
|
||||
/// - batchLimit: 批量条数
|
||||
/// - eventExpiredSeconds: 数据过期时间,单位秒
|
||||
/// - initializeTimeout: 初始化后等待user id/device id/firebase pseudo id等属性超时时间,单位秒
|
||||
/// - saasXAPPID: 中台接口header中的X-APP-ID
|
||||
/// - saasXDEVICEINFO: 中台接口header中的X-DEVICE-INFO
|
||||
/// - loggerDebug: 开启控制台输出debug信息
|
||||
@objc
|
||||
public class func initializeLib(uploadPeriodInSecond: Double = 60,
|
||||
batchLimit: Int = 25,
|
||||
eventExpiredSeconds: Double = 7 * 24 * 60 * 60,
|
||||
initializeTimeout: Double = 5,
|
||||
saasXAPPID: String,
|
||||
saasXDEVICEINFO: String,
|
||||
loggerDebug: Bool = true) {
|
||||
Self.uploadPeriodInSecond = uploadPeriodInSecond
|
||||
Self.batchLimit = batchLimit
|
||||
Self.eventExpiredSeconds = eventExpiredSeconds
|
||||
Self.initializeTimeout = initializeTimeout
|
||||
Self.saasXAPPID = saasXAPPID
|
||||
Self.saasXDEVICEINFO = saasXDEVICEINFO
|
||||
Self.loggerDebug = loggerDebug
|
||||
_ = Manager.shared
|
||||
}
|
||||
|
||||
/// 记录event
|
||||
@objc
|
||||
public class func logEvent(_ name: String, parameters: [String : Any]?) {
|
||||
Manager.shared.logEvent(name, parameters: parameters)
|
||||
}
|
||||
|
||||
/// 中台ID。只在未获取到uid时可以为空
|
||||
@objc
|
||||
public class func setUserID(_ userID: String?) {
|
||||
setUserProperty(userID, forName: .uid)
|
||||
}
|
||||
|
||||
/// 设备ID(用户的设备ID,iOS取用户的IDFV或UUID,Android取androidID)
|
||||
@objc
|
||||
public class func setDeviceId(_ deviceId: String?) {
|
||||
setUserProperty(deviceId, forName: .deviceId)
|
||||
}
|
||||
|
||||
/// adjust_id。只在未获取到adjust时可以为空
|
||||
@objc
|
||||
public class func setAdjustId(_ adjustId: String?) {
|
||||
setUserProperty(adjustId, forName: .adjustId)
|
||||
}
|
||||
|
||||
/// 广告 ID/广告标识符 (IDFA)
|
||||
@objc
|
||||
public class func setAdId(_ adId: String?) {
|
||||
setUserProperty(adId, forName: .adId)
|
||||
}
|
||||
|
||||
/// 用户的pseudo_id
|
||||
@objc
|
||||
public class func setFirebaseId(_ firebaseId: String?) {
|
||||
setUserProperty(firebaseId, forName: .firebaseId)
|
||||
}
|
||||
|
||||
/// screen name
|
||||
@objc
|
||||
public class func setScreen(_ name: String) {
|
||||
Manager.shared.setScreen(name)
|
||||
}
|
||||
|
||||
/// 设置userproperty
|
||||
@objc
|
||||
public class func setUserProperty(_ value: String?, forName name: String) {
|
||||
Manager.shared.setUserProperty(value ?? "", forName: name)
|
||||
}
|
||||
|
||||
/// 移除userproperty
|
||||
@objc
|
||||
public class func removeUserProperties(forNames names: [String]) {
|
||||
Manager.shared.removeUserProperties(forNames: names)
|
||||
}
|
||||
|
||||
/// 获取events相关日志文件zip包
|
||||
/// zip解压密码:Castbox123
|
||||
@available(*, deprecated, renamed: "eventsLogsDirectory", message: "废弃,使用eventsLogsDirectory方法获取日志文件目录URL")
|
||||
@objc
|
||||
public class func eventsLogsArchive(_ callback: @escaping (_ url: URL?) -> Void) {
|
||||
Manager.shared.eventsLogsArchive(callback)
|
||||
}
|
||||
|
||||
/// 获取events相关日志文件目录
|
||||
@objc
|
||||
public class func eventsLogsDirectory(_ callback: @escaping (_ url: URL?) -> Void) {
|
||||
Manager.shared.eventsLogsDirURL(callback)
|
||||
}
|
||||
|
||||
/// 更新events上报服务器域名
|
||||
/// host: 服务器域名,例如:“abc.bbb.com”, "https://abc.bbb.com", "http://abc.bbb.com"
|
||||
@objc
|
||||
public class func setEventsUploadEndPoint(host: String?) {
|
||||
UserDefaults.eventsServerHost = host
|
||||
}
|
||||
|
||||
/// 获取events统计数据
|
||||
/// - Parameter callback: 数据回调
|
||||
/// - callback parameters:
|
||||
/// - uploadedEventsCount: 上传后端成功event条数
|
||||
/// - loggedEventsCount: 已记录event总条数
|
||||
@objc
|
||||
@available(*, deprecated, message: "used for debug, will be removed on any future released versions")
|
||||
public class func debug_eventsStatistics(_ callback: @escaping (_ uploadedEventsCount: Int, _ loggedEventsCount: Int) -> Void) {
|
||||
Manager.shared.debug_eventsStatistics(callback)
|
||||
}
|
||||
|
||||
/// 将内部事件信息上报给应用层
|
||||
/// - Parameter reportCallback: 数据回调
|
||||
/// - callback parameters:
|
||||
/// - eventCode: 事件代码
|
||||
/// - info: 事件相关信息
|
||||
@objc
|
||||
public class func registerInternalEventObserver(reportCallback: @escaping (_ eventCode: Int, _ info: String) -> Void) {
|
||||
Manager.shared.registerInternalEventObserver(reportCallback: reportCallback)
|
||||
}
|
||||
|
||||
/// 获取当前user property
|
||||
@objc
|
||||
public class func getUserProperties() -> [String : String] {
|
||||
return Manager.shared.getUserProperties()
|
||||
}
|
||||
|
||||
/// 设置上传开关,默认为true
|
||||
/// true - 开启上传
|
||||
/// false - 关闭上传
|
||||
@objc
|
||||
public class func setEnableUpload(isOn: Bool = true) -> Void {
|
||||
enableUpload = isOn
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -1,85 +0,0 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 669b744f21d994fd3b6fb7aeb95b0669
|
||||
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: 1
|
||||
- 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: AnyCPU
|
||||
- first:
|
||||
Standalone: OSXUniversal
|
||||
second:
|
||||
enabled: 0
|
||||
settings:
|
||||
CPU: AnyCPU
|
||||
- first:
|
||||
Standalone: Win
|
||||
second:
|
||||
enabled: 0
|
||||
settings:
|
||||
CPU: AnyCPU
|
||||
- first:
|
||||
Standalone: Win64
|
||||
second:
|
||||
enabled: 0
|
||||
settings:
|
||||
CPU: AnyCPU
|
||||
- first:
|
||||
iPhone: iOS
|
||||
second:
|
||||
enabled: 0
|
||||
settings:
|
||||
AddToEmbeddedBinaries: false
|
||||
CPU: AnyCPU
|
||||
CompileFlags:
|
||||
FrameworkDependencies:
|
||||
- first:
|
||||
tvOS: tvOS
|
||||
second:
|
||||
enabled: 1
|
||||
settings: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
fileFormatVersion: 2
|
||||
guid: c6cbae57da78c46c7918b2bfd24d7335
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
fileFormatVersion: 2
|
||||
guid: a9b9cc55c438041a7ae3ce46bd896d8d
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -1,228 +0,0 @@
|
|||
//
|
||||
// DBEntities.swift
|
||||
// Alamofire
|
||||
//
|
||||
// Created by mayue on 2022/11/4.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import CryptoSwift
|
||||
|
||||
internal enum Entity {
|
||||
|
||||
}
|
||||
|
||||
extension Entity {
|
||||
struct EventRecord: Codable {
|
||||
|
||||
enum Priority: Int, Codable {
|
||||
case EMERGENCE = 0
|
||||
case HIGH = 5
|
||||
case DEFAULT = 10
|
||||
case LOW = 15
|
||||
}
|
||||
|
||||
enum TransitionStatus: Int, Codable {
|
||||
case idle = 0
|
||||
case instransition = 1
|
||||
}
|
||||
|
||||
let recordId: String
|
||||
let eventName: String
|
||||
let eventJson: String
|
||||
///单位毫秒
|
||||
let timestamp: Int64
|
||||
let priority: Priority
|
||||
let transitionStatus: TransitionStatus
|
||||
|
||||
init(eventName: String, event: Entity.Event, priority: Priority = .DEFAULT, transitionStatus: TransitionStatus = .idle) {
|
||||
let now = Date()
|
||||
let eventJson = event.asString ?? ""
|
||||
if eventJson.isEmpty {
|
||||
cdPrint("[WARNING] error for convert event to json")
|
||||
}
|
||||
self.recordId = "\(eventName)\(eventJson)\(now.timeIntervalSince1970)\(Int.random(in: Int.min...Int.max))".md5()
|
||||
self.eventName = eventName
|
||||
self.eventJson = eventJson
|
||||
self.timestamp = event.timestamp
|
||||
self.priority = priority
|
||||
self.transitionStatus = transitionStatus
|
||||
}
|
||||
|
||||
init(recordId: String, eventName: String, eventJson: String, timestamp: Int64, priority: Int, transitionStatus: Int) {
|
||||
self.recordId = recordId
|
||||
self.eventName = eventName
|
||||
self.eventJson = eventJson
|
||||
self.timestamp = timestamp
|
||||
self.priority = .init(rawValue: priority) ?? .DEFAULT
|
||||
self.transitionStatus = .init(rawValue: transitionStatus) ?? .idle
|
||||
}
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case recordId
|
||||
case eventName
|
||||
case eventJson
|
||||
case timestamp
|
||||
case priority
|
||||
case transitionStatus
|
||||
}
|
||||
|
||||
static func createTableSql(with name: String) -> String {
|
||||
return """
|
||||
CREATE TABLE IF NOT EXISTS \(name)(
|
||||
\(CodingKeys.recordId.rawValue) TEXT UNIQUE NOT NULL PRIMARY KEY,
|
||||
\(CodingKeys.eventName.rawValue) TEXT NOT NULL,
|
||||
\(CodingKeys.eventJson.rawValue) TEXT NOT NULL,
|
||||
\(CodingKeys.timestamp.rawValue) INTEGER,
|
||||
\(CodingKeys.priority.rawValue) INTEGER,
|
||||
\(CodingKeys.transitionStatus.rawValue) INTEGER);
|
||||
"""
|
||||
}
|
||||
|
||||
func insertSql(to tableName: String) -> String {
|
||||
return "INSERT INTO \(tableName) VALUES ('\(recordId)', '\(eventName)', '\(eventJson)', '\(timestamp)', '\(priority.rawValue)', '\(transitionStatus.rawValue)')"
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension Entity {
|
||||
struct Event: Codable {
|
||||
///客户端中记录此事件的时间(采用世界协调时间,毫秒为单位)
|
||||
let timestamp: Int64
|
||||
let event: String
|
||||
let userInfo: UserInfo
|
||||
let param: [String: EventValue]
|
||||
let properties: [String: String]
|
||||
let eventId: String
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case timestamp
|
||||
case userInfo = "info"
|
||||
case event, param, properties
|
||||
case eventId
|
||||
}
|
||||
|
||||
init(timestamp: Int64, event: String, userInfo: UserInfo, parameters: [String : Any], properties: [String : String]) throws {
|
||||
guard let normalizedEvent = Self.normalizeKey(event),
|
||||
normalizedEvent == event else {
|
||||
cdPrint("drop event because of illegal event name: \(event)")
|
||||
cdPrint("standard: https://developers.google.com/android/reference/com/google/firebase/analytics/FirebaseAnalytics.Event")
|
||||
throw NSError(domain: "cunstrcting event error", code: 0, userInfo: [NSLocalizedDescriptionKey : "illegal event name: \(event)"])
|
||||
}
|
||||
self.eventId = UUID().uuidString.lowercased()
|
||||
self.timestamp = timestamp
|
||||
self.event = normalizedEvent
|
||||
self.userInfo = userInfo
|
||||
self.param = Self.normalizeParameters(parameters)
|
||||
self.properties = properties
|
||||
}
|
||||
|
||||
static let maxParametersCount = 25
|
||||
static let maxKeyLength = 40
|
||||
static let maxParameterStringValueLength = 128
|
||||
|
||||
static func normalizeParameters(_ parameters: [String : Any]) -> [String : EventValue] {
|
||||
var params = [String : EventValue]()
|
||||
var count = 0
|
||||
parameters.sorted(by: { $0.key < $1.key }).forEach({ key, value in
|
||||
|
||||
guard count < maxParametersCount else {
|
||||
cdPrint("too many parameters")
|
||||
cdPrint("standard: https://developers.google.com/android/reference/com/google/firebase/analytics/FirebaseAnalytics.Event")
|
||||
return
|
||||
}
|
||||
|
||||
guard let normalizedKey = normalizeKey(key),
|
||||
normalizedKey == key else {
|
||||
cdPrint("drop event parameter because of illegal key: \(key)")
|
||||
cdPrint("standard: https://developers.google.com/android/reference/com/google/firebase/analytics/FirebaseAnalytics.Event")
|
||||
return
|
||||
}
|
||||
|
||||
if let value = value as? String {
|
||||
params[normalizedKey] = Entity.EventValue(stringValue: String(value.prefix(maxParameterStringValueLength)))
|
||||
} else if let value = value as? Int {
|
||||
params[normalizedKey] = Entity.EventValue(longValue: Int64(value))
|
||||
} else if let value = value as? Int64 {
|
||||
params[normalizedKey] = Entity.EventValue(longValue: value)
|
||||
} else if let value = value as? Double {
|
||||
params[normalizedKey] = Entity.EventValue(doubleValue: value)
|
||||
} else {
|
||||
params[normalizedKey] = Entity.EventValue(stringValue: String("\(value)".prefix(maxParameterStringValueLength)))
|
||||
}
|
||||
|
||||
count += 1
|
||||
})
|
||||
|
||||
return params
|
||||
}
|
||||
|
||||
static func normalizeKey(_ key: String) -> String? {
|
||||
var mutableKey = key
|
||||
|
||||
while let first = mutableKey.first,
|
||||
!first.isLetter {
|
||||
_ = mutableKey.removeFirst()
|
||||
}
|
||||
|
||||
var normalizedKey = ""
|
||||
var count = 0
|
||||
mutableKey.forEach { c in
|
||||
guard count < maxKeyLength,
|
||||
c.isAlphabetic || c.isDigit || c == "_" else { return }
|
||||
normalizedKey.append(c)
|
||||
count += 1
|
||||
}
|
||||
|
||||
return normalizedKey.isEmpty ? nil : normalizedKey
|
||||
}
|
||||
}
|
||||
|
||||
///用户信息
|
||||
struct UserInfo: Codable {
|
||||
///中台ID。只在未获取到uid时可以为空
|
||||
let uid: String?
|
||||
///设备ID(用户的设备ID,iOS取用户的IDFV或UUID,Android取androidID)
|
||||
let deviceId: String?
|
||||
///adjust_id。只在未获取到adjust时可以为空
|
||||
let adjustId: String?
|
||||
///广告 ID/广告标识符 (IDFA)
|
||||
let adId: String?
|
||||
///用户的pseudo_id
|
||||
let firebaseId: String?
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case deviceId
|
||||
case uid
|
||||
case adjustId
|
||||
case adId
|
||||
case firebaseId
|
||||
}
|
||||
}
|
||||
|
||||
// 参数对应的值
|
||||
struct EventValue: Codable {
|
||||
let stringValue: String? // 事件参数的字符串值
|
||||
let longValue: Int64? // 事件参数的整数值
|
||||
let doubleValue: Double? // 事件参数的小数值。注意:APP序列化成JSON时,注意不要序列化成科学计数法
|
||||
|
||||
init(stringValue: String? = nil, longValue: Int64? = nil, doubleValue: Double? = nil) {
|
||||
self.stringValue = stringValue
|
||||
self.longValue = longValue
|
||||
self.doubleValue = doubleValue
|
||||
}
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case stringValue = "s"
|
||||
case longValue = "i"
|
||||
case doubleValue = "d"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Entity {
|
||||
struct SystemTimeResult: Codable {
|
||||
let data: Int64
|
||||
}
|
||||
}
|
||||
|
|
@ -1,85 +0,0 @@
|
|||
fileFormatVersion: 2
|
||||
guid: f63b0ff90afd0409781c1266dc618875
|
||||
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: 1
|
||||
- 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: AnyCPU
|
||||
- first:
|
||||
Standalone: OSXUniversal
|
||||
second:
|
||||
enabled: 0
|
||||
settings:
|
||||
CPU: AnyCPU
|
||||
- first:
|
||||
Standalone: Win
|
||||
second:
|
||||
enabled: 0
|
||||
settings:
|
||||
CPU: AnyCPU
|
||||
- first:
|
||||
Standalone: Win64
|
||||
second:
|
||||
enabled: 0
|
||||
settings:
|
||||
CPU: AnyCPU
|
||||
- first:
|
||||
iPhone: iOS
|
||||
second:
|
||||
enabled: 0
|
||||
settings:
|
||||
AddToEmbeddedBinaries: false
|
||||
CPU: AnyCPU
|
||||
CompileFlags:
|
||||
FrameworkDependencies:
|
||||
- first:
|
||||
tvOS: tvOS
|
||||
second:
|
||||
enabled: 1
|
||||
settings: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
fileFormatVersion: 2
|
||||
guid: bb7dde11f0ad6496ca231330136b7b61
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -1,391 +0,0 @@
|
|||
//
|
||||
// Database.swift
|
||||
// GuruAnalytics_iOS
|
||||
//
|
||||
// Created by mayue on 2022/11/4.
|
||||
// Copyright © 2022 Guru Network Limited. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import RxSwift
|
||||
import RxCocoa
|
||||
import FMDB
|
||||
|
||||
internal class Database {
|
||||
|
||||
typealias PropertyName = GuruAnalytics.PropertyName
|
||||
|
||||
enum TableName: String, CaseIterable {
|
||||
case event = "event"
|
||||
}
|
||||
|
||||
private let dbIOQueue = DispatchQueue.init(label: "com.guru.analytics.db.io.queue", qos: .userInitiated)
|
||||
|
||||
private let dbQueueRelay = BehaviorRelay<FMDatabaseQueue?>(value: nil)
|
||||
private let bag = DisposeBag()
|
||||
|
||||
/// 更新数据库表结构后,需要更新数据库版本
|
||||
private let currentDBVersion = DBVersionHistory.v_3
|
||||
|
||||
private var dbVersion: Database.DBVersionHistory {
|
||||
get {
|
||||
if let v = UserDefaults.defaults?.value(forKey: UserDefaults.dbVersionKey) as? String,
|
||||
let dbV = Database.DBVersionHistory.init(rawValue: v) {
|
||||
return dbV
|
||||
} else {
|
||||
return .initialVersion
|
||||
}
|
||||
|
||||
}
|
||||
set {
|
||||
UserDefaults.defaults?.set(newValue.rawValue, forKey: UserDefaults.dbVersionKey)
|
||||
}
|
||||
}
|
||||
|
||||
internal init() {
|
||||
|
||||
dbIOQueue.async { [weak self] in
|
||||
|
||||
guard let `self` = self else { return }
|
||||
|
||||
let applicationSupportPath = NSSearchPathForDirectoriesInDomains(.applicationSupportDirectory,
|
||||
.userDomainMask,
|
||||
true).last! + "/GuruAnalytics"
|
||||
|
||||
if !FileManager.default.fileExists(atPath: applicationSupportPath) {
|
||||
do {
|
||||
try FileManager.default.createDirectory(atPath: applicationSupportPath, withIntermediateDirectories: true)
|
||||
} catch {
|
||||
assertionFailure("create db path error: \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
let dbPath = applicationSupportPath + "/analytics.db"
|
||||
let queue = FMDatabaseQueue(url: URL(fileURLWithPath: dbPath))!
|
||||
cdPrint("database path: \(queue.path ?? "")")
|
||||
|
||||
self.createEventTable(in: queue)
|
||||
.filter { $0 }
|
||||
.flatMap { _ in
|
||||
self.migrateDB(in: queue).asMaybe()
|
||||
}
|
||||
.flatMap({ _ in
|
||||
self.resetAllTransitionStatus(in: queue).asMaybe()
|
||||
})
|
||||
.subscribe(onSuccess: { _ in
|
||||
self.dbQueueRelay.accept(queue)
|
||||
})
|
||||
.disposed(by: self.bag)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
internal extension Database {
|
||||
|
||||
func addEventRecords(_ events: Entity.EventRecord) -> Single<Void> {
|
||||
cdPrint(#function)
|
||||
return mapTransactionToSingle { (db) in
|
||||
try db.executeUpdate(events.insertSql(to: TableName.event.rawValue), values: nil)
|
||||
}
|
||||
.do(onSuccess: { [weak self] (_) in
|
||||
guard let `self` = self else { return }
|
||||
NotificationCenter.default.post(name: self.tableUpdateNotification(TableName.event.rawValue), object: nil)
|
||||
})
|
||||
}
|
||||
|
||||
func fetchEventRecordsToUpload(limit: Int) -> Single<[Entity.EventRecord]> {
|
||||
return mapTransactionToSingle { (db) in
|
||||
let querySQL: String =
|
||||
"""
|
||||
SELECT * FROM \(TableName.event.rawValue)
|
||||
WHERE \(Entity.EventRecord.CodingKeys.transitionStatus.rawValue) IS NULL
|
||||
OR \(Entity.EventRecord.CodingKeys.transitionStatus.rawValue) != \(Entity.EventRecord.TransitionStatus.instransition.rawValue)
|
||||
ORDER BY \(Entity.EventRecord.CodingKeys.priority.rawValue) ASC, \(Entity.EventRecord.CodingKeys.timestamp.rawValue) ASC
|
||||
LIMIT \(limit)
|
||||
"""
|
||||
cdPrint(#function + "query sql: \(querySQL)")
|
||||
let results = try db.executeQuery(querySQL, values: nil) //[ASC | DESC]
|
||||
var t: [Entity.EventRecord] = []
|
||||
while results.next() {
|
||||
guard let recordId = results.string(forColumnIndex: 0),
|
||||
let eventName = results.string(forColumnIndex: 1),
|
||||
let eventJson = results.string(forColumnIndex: 2) else {
|
||||
continue
|
||||
}
|
||||
|
||||
let priority: Int = results.columnIsNull(Entity.EventRecord.CodingKeys.priority.rawValue) ?
|
||||
Entity.EventRecord.Priority.DEFAULT.rawValue : Int(results.int(forColumn: Entity.EventRecord.CodingKeys.priority.rawValue))
|
||||
|
||||
let ts: Int = results.columnIsNull(Entity.EventRecord.CodingKeys.transitionStatus.rawValue) ?
|
||||
Entity.EventRecord.TransitionStatus.idle.rawValue : Int(results.int(forColumn: Entity.EventRecord.CodingKeys.transitionStatus.rawValue))
|
||||
|
||||
let record = Entity.EventRecord(recordId: recordId, eventName: eventName, eventJson: eventJson,
|
||||
timestamp: results.longLongInt(forColumn: Entity.EventRecord.CodingKeys.timestamp.rawValue),
|
||||
priority: priority, transitionStatus: ts)
|
||||
t.append(record)
|
||||
}
|
||||
|
||||
results.close()
|
||||
|
||||
try t.forEach { record in
|
||||
let updateSQL =
|
||||
"""
|
||||
UPDATE \(TableName.event.rawValue)
|
||||
SET \(Entity.EventRecord.CodingKeys.transitionStatus.rawValue) = \(Entity.EventRecord.TransitionStatus.instransition.rawValue)
|
||||
WHERE \(Entity.EventRecord.CodingKeys.recordId.rawValue) = '\(record.recordId)'
|
||||
"""
|
||||
try db.executeUpdate(updateSQL, values: nil)
|
||||
}
|
||||
|
||||
return t
|
||||
}
|
||||
}
|
||||
|
||||
func deleteEventRecords(_ recordIds: [String]) -> Single<Void> {
|
||||
guard !recordIds.isEmpty else {
|
||||
return .just(())
|
||||
}
|
||||
cdPrint(#function + "\(recordIds)")
|
||||
return mapTransactionToSingle { db in
|
||||
try recordIds.forEach { item in
|
||||
try db.executeUpdate("DELETE FROM \(TableName.event.rawValue) WHERE \(Entity.EventRecord.CodingKeys.recordId.rawValue) = '\(item)'", values: nil)
|
||||
}
|
||||
}
|
||||
.do(onSuccess: { [weak self] (_) in
|
||||
guard let `self` = self else { return }
|
||||
NotificationCenter.default.post(name: self.tableUpdateNotification(TableName.event.rawValue), object: nil)
|
||||
}, onError: { error in
|
||||
cdPrint("\(#function) error: \(error)")
|
||||
})
|
||||
}
|
||||
|
||||
func removeOutdatedEventRecords(earlierThan: Int64) -> Single<Void> {
|
||||
return mapTransactionToSingle { db in
|
||||
let sql = """
|
||||
DELETE FROM \(TableName.event.rawValue)
|
||||
WHERE \(Entity.EventRecord.CodingKeys.timestamp.rawValue) < \(earlierThan)
|
||||
"""
|
||||
try db.executeUpdate(sql, values: nil)
|
||||
}
|
||||
.do(onSuccess: { [weak self] (_) in
|
||||
guard let `self` = self else { return }
|
||||
NotificationCenter.default.post(name: self.tableUpdateNotification(TableName.event.rawValue), object: nil)
|
||||
}, onError: { error in
|
||||
cdPrint("\(#function) error: \(error)")
|
||||
})
|
||||
}
|
||||
|
||||
func resetTransitionStatus(for recordIds: [String]) -> Single<Void> {
|
||||
guard !recordIds.isEmpty else {
|
||||
return .just(())
|
||||
}
|
||||
cdPrint(#function + "\(recordIds)")
|
||||
return mapTransactionToSingle { db in
|
||||
try recordIds.forEach { item in
|
||||
let updateSQL =
|
||||
"""
|
||||
UPDATE \(TableName.event.rawValue)
|
||||
SET \(Entity.EventRecord.CodingKeys.transitionStatus.rawValue) = \(Entity.EventRecord.TransitionStatus.idle.rawValue)
|
||||
WHERE \(Entity.EventRecord.CodingKeys.recordId.rawValue) = '\(item)'
|
||||
"""
|
||||
try db.executeUpdate(updateSQL, values: nil)
|
||||
}
|
||||
}
|
||||
.do(onSuccess: { [weak self] (_) in
|
||||
guard let `self` = self else { return }
|
||||
NotificationCenter.default.post(name: self.tableUpdateNotification(TableName.event.rawValue), object: nil)
|
||||
}, onError: { error in
|
||||
cdPrint("\(#function) error: \(error)")
|
||||
})
|
||||
}
|
||||
|
||||
func uploadableEventRecordCount() -> Single<Int> {
|
||||
return mapTransactionToSingle { db in
|
||||
let querySQL =
|
||||
"""
|
||||
SELECT count(*) as Count FROM \(TableName.event.rawValue)
|
||||
WHERE \(Entity.EventRecord.CodingKeys.transitionStatus.rawValue) IS NULL
|
||||
OR \(Entity.EventRecord.CodingKeys.transitionStatus.rawValue) != \(Entity.EventRecord.TransitionStatus.instransition.rawValue)
|
||||
"""
|
||||
let result = try db.executeQuery(querySQL, values: nil)
|
||||
var count = 0
|
||||
while result.next() {
|
||||
count = Int(result.int(forColumn: "Count"))
|
||||
}
|
||||
result.parentDB = nil
|
||||
result.close()
|
||||
return count
|
||||
}
|
||||
}
|
||||
|
||||
func uploadableEventRecordCountOb() -> Observable<Int> {
|
||||
return NotificationCenter.default.rx.notification(tableUpdateNotification(TableName.event.rawValue))
|
||||
.startWith(Notification(name: tableUpdateNotification(TableName.event.rawValue)))
|
||||
.flatMap({ [weak self] (_) -> Observable<Int> in
|
||||
guard let `self` = self else {
|
||||
return Observable.empty()
|
||||
}
|
||||
return self.uploadableEventRecordCount().asObservable()
|
||||
})
|
||||
}
|
||||
|
||||
func hasFgEventRecord() -> Single<Bool> {
|
||||
return mapTransactionToSingle { db in
|
||||
let querySQL =
|
||||
"""
|
||||
SELECT count(*) as Count FROM \(TableName.event.rawValue)
|
||||
WHERE \(Entity.EventRecord.CodingKeys.eventName.rawValue) == '\(GuruAnalytics.fgEvent.name)'
|
||||
"""
|
||||
let result = try db.executeQuery(querySQL, values: nil)
|
||||
var count = 0
|
||||
while result.next() {
|
||||
count = Int(result.int(forColumn: "Count"))
|
||||
}
|
||||
result.parentDB = nil
|
||||
result.close()
|
||||
return count > 0
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private extension Database {
|
||||
func createEventTable(in queue: FMDatabaseQueue) -> Single<Bool> {
|
||||
return mapTransactionToSingle(queue: queue) { db in
|
||||
db.executeStatements(Entity.EventRecord.createTableSql(with: TableName.event.rawValue))
|
||||
}
|
||||
.do(onSuccess: { result in
|
||||
cdPrint("createEventTable result: \(result)")
|
||||
}, onError: { error in
|
||||
cdPrint("createEventTable error: \(error)")
|
||||
})
|
||||
}
|
||||
|
||||
func mapTransactionToSingle<T>(_ transaction: @escaping ((FMDatabase) throws -> T)) -> Single<T> {
|
||||
return dbQueueRelay.compactMap({ $0 })
|
||||
.take(1)
|
||||
.asSingle()
|
||||
.flatMap { [unowned self] queue -> Single<T> in
|
||||
return self.mapTransactionToSingle(queue: queue, transaction)
|
||||
}
|
||||
}
|
||||
|
||||
func mapTransactionToSingle<T>(queue: FMDatabaseQueue, _ transaction: @escaping ((FMDatabase) throws -> T)) -> Single<T> {
|
||||
return Single<T>.create { [weak self] (subscriber) -> Disposable in
|
||||
self?.dbIOQueue.async {
|
||||
queue.inDeferredTransaction { (db, rollback) in
|
||||
do {
|
||||
let data = try transaction(db)
|
||||
subscriber(.success(data))
|
||||
} catch {
|
||||
rollback.pointee = true
|
||||
cdPrint("inDeferredTransaction failed: \(error.localizedDescription)")
|
||||
subscriber(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
return Disposables.create()
|
||||
}
|
||||
}
|
||||
|
||||
func tableUpdateNotification(_ tableName: String) -> Notification.Name {
|
||||
return Notification.Name("Guru.Analytics.DB.Table.update-\(tableName)")
|
||||
}
|
||||
|
||||
func migrateDB(in queue: FMDatabaseQueue) -> Single<Void> {
|
||||
|
||||
return mapTransactionToSingle(queue: queue) { [weak self] db in
|
||||
|
||||
guard let `self` = self else { return }
|
||||
|
||||
while let nextVersion = self.dbVersion.nextVersion,
|
||||
self.dbVersion < self.currentDBVersion {
|
||||
switch nextVersion {
|
||||
case .v_1:
|
||||
()
|
||||
case .v_2:
|
||||
/// v_1 -> v_2
|
||||
/// event表增加priority列
|
||||
if !db.columnExists(Entity.EventRecord.CodingKeys.priority.rawValue, inTableWithName: TableName.event.rawValue) {
|
||||
db.executeStatements("""
|
||||
ALTER TABLE \(TableName.event.rawValue)
|
||||
ADD \(Entity.EventRecord.CodingKeys.priority.rawValue) Integer DEFAULT \(Entity.EventRecord.Priority.DEFAULT.rawValue)
|
||||
""")
|
||||
}
|
||||
|
||||
case .v_3:
|
||||
/// v_2 -> v_3
|
||||
/// event表增加transitionStatus列
|
||||
if !db.columnExists(Entity.EventRecord.CodingKeys.transitionStatus.rawValue, inTableWithName: TableName.event.rawValue) {
|
||||
db.executeStatements("""
|
||||
ALTER TABLE \(TableName.event.rawValue)
|
||||
ADD \(Entity.EventRecord.CodingKeys.transitionStatus.rawValue) Integer DEFAULT \(Entity.EventRecord.TransitionStatus.idle.rawValue)
|
||||
""")
|
||||
}
|
||||
|
||||
}
|
||||
self.dbVersion = nextVersion
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
.do(onError: { error in
|
||||
cdPrint("migrate db error: \(error)")
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
func resetAllTransitionStatus(in queue: FMDatabaseQueue) -> Single<Void> {
|
||||
return mapTransactionToSingle(queue: queue) { db in
|
||||
let updateSQL =
|
||||
"""
|
||||
UPDATE \(TableName.event.rawValue)
|
||||
SET \(Entity.EventRecord.CodingKeys.transitionStatus.rawValue) = \(Entity.EventRecord.TransitionStatus.idle.rawValue)
|
||||
"""
|
||||
try db.executeUpdate(updateSQL, values: nil)
|
||||
}
|
||||
.do(onSuccess: { [weak self] (_) in
|
||||
guard let `self` = self else { return }
|
||||
NotificationCenter.default.post(name: self.tableUpdateNotification(TableName.event.rawValue), object: nil)
|
||||
}, onError: { error in
|
||||
cdPrint("\(#function) error: \(error)")
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
fileprivate extension Array where Element == String {
|
||||
|
||||
var joinedStringForSQL: String {
|
||||
return self.map { "'\($0)'" }.joined(separator: ",")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private extension Database {
|
||||
|
||||
enum DBVersionHistory: String, Comparable {
|
||||
case v_1
|
||||
case v_2
|
||||
case v_3
|
||||
}
|
||||
}
|
||||
|
||||
extension Database.DBVersionHistory {
|
||||
|
||||
static func < (lhs: Database.DBVersionHistory, rhs: Database.DBVersionHistory) -> Bool {
|
||||
return lhs.versionNumber < rhs.versionNumber
|
||||
}
|
||||
|
||||
|
||||
var versionNumber: Int {
|
||||
return Int(String(self.rawValue.split(separator: "_")[1])) ?? 1
|
||||
}
|
||||
|
||||
var nextVersion: Self? {
|
||||
return .init(rawValue: "v_\(versionNumber + 1)")
|
||||
}
|
||||
|
||||
static let initialVersion: Self = .v_1
|
||||
}
|
||||
|
|
@ -1,49 +0,0 @@
|
|||
fileFormatVersion: 2
|
||||
guid: cc72ba03590e04e4db8e3d9eb675d0d2
|
||||
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: 1
|
||||
- first:
|
||||
Any:
|
||||
second:
|
||||
enabled: 0
|
||||
settings: {}
|
||||
- first:
|
||||
Editor: Editor
|
||||
second:
|
||||
enabled: 0
|
||||
settings:
|
||||
DefaultValueInitialized: true
|
||||
- first:
|
||||
iPhone: iOS
|
||||
second:
|
||||
enabled: 0
|
||||
settings: {}
|
||||
- first:
|
||||
tvOS: tvOS
|
||||
second:
|
||||
enabled: 1
|
||||
settings: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -1,681 +0,0 @@
|
|||
//
|
||||
// Manager.swift
|
||||
// GuruAnalytics_iOS
|
||||
//
|
||||
// Created by 袁仕崇 on 16/11/22.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import RxCocoa
|
||||
import RxSwift
|
||||
|
||||
internal class Manager {
|
||||
|
||||
// MARK: - temporary, will be removed soon
|
||||
@available(*, deprecated, message: "used for debug, will be removed on any future released versions")
|
||||
private var loggedEventsCount: Int = 0
|
||||
|
||||
@available(*, deprecated, message: "used for debug, will be removed on any future released versions")
|
||||
private func accumulateLoggedEventsCount(_ count: Int) {
|
||||
loggedEventsCount += count
|
||||
}
|
||||
|
||||
@available(*, deprecated, message: "used for debug, will be removed on any future released versions")
|
||||
private var uploadedEventsCount: Int = 0
|
||||
|
||||
@available(*, deprecated, message: "used for debug, will be removed on any future released versions")
|
||||
private func accumulateUploadedEventsCount(_ count: Int) {
|
||||
uploadedEventsCount += count
|
||||
}
|
||||
|
||||
@available(*, deprecated, message: "used for debug, will be removed on any future released versions")
|
||||
internal func debug_eventsStatistics(_ callback: @escaping (_ uploadedEventsCount: Int, _ loggedEventsCount: Int) -> Void) {
|
||||
callback(uploadedEventsCount, loggedEventsCount)
|
||||
}
|
||||
|
||||
// MARK: - internal members
|
||||
|
||||
internal static let shared = Manager()
|
||||
|
||||
/// 时间维度,默认每1分钟后批量上传1次
|
||||
private var scheduleInterval: TimeInterval = GuruAnalytics.uploadPeriodInSecond
|
||||
|
||||
/// 数量维度,默认满25条批量上传1次
|
||||
private var numberOfCountPerConsume: Int = GuruAnalytics.batchLimit
|
||||
|
||||
/// event过期时间,默认7天
|
||||
private var eventExpiredIntervel: TimeInterval = GuruAnalytics.eventExpiredSeconds
|
||||
|
||||
private var initializeTimeout: Double = GuruAnalytics.initializeTimeout
|
||||
|
||||
/// 根据时差计算的当前服务端时间
|
||||
internal var serverNowMs: Int64 { serverInitialMs + (Date.absoluteTimeMs - serverSyncedAtAbsoluteMs)}
|
||||
|
||||
// MARK: - private members
|
||||
|
||||
private typealias PropertyName = GuruAnalytics.PropertyName
|
||||
|
||||
private let bag = DisposeBag()
|
||||
|
||||
private let db = Database()
|
||||
|
||||
private let ntwkMgr = NetworkManager()
|
||||
|
||||
/// 生成background 任务时,将 key 和当前任务的 disposeable 一一对应
|
||||
private var taskKeyDisposableMap: [Int: Disposable] = [:]
|
||||
|
||||
/// 从数据库中一次性拉取最多条数
|
||||
private var maxEventFetchingCount: Int = 100
|
||||
|
||||
/// 工作队列
|
||||
private let workQueue = DispatchQueue.init(label: "com.guru.analytics.manager.work.queue", qos: .userInitiated)
|
||||
///网络服务队列
|
||||
private lazy var rxNetworkScheduler = SerialDispatchQueueScheduler(qos: .default, internalSerialQueueName: "com.guru.analytics.manager.rx.network.queue")
|
||||
private lazy var rxConsumeScheduler = SerialDispatchQueueScheduler(qos: .default, internalSerialQueueName: "com.guru.analytics.manager.rx.consume.queue")
|
||||
|
||||
private lazy var rxWorkScheduler = SerialDispatchQueueScheduler.init(queue: workQueue, internalSerialQueueName: "com.guru.analytics.manager.rx.work.queue")
|
||||
private let bgWorkQueue = DispatchQueue.init(label: "com.guru.analytics.manager.background.work.queue", qos: .background)
|
||||
private lazy var rxBgWorkScheduler = SerialDispatchQueueScheduler.init(queue: bgWorkQueue, internalSerialQueueName: "com.guru.analytics.manager.background.work.queue")
|
||||
|
||||
/// 过期event记录已清除
|
||||
private let outdatedEventsCleared = BehaviorSubject(value: false)
|
||||
|
||||
/// 服务端时间
|
||||
private var serverInitialMs = Date().msSince1970 {
|
||||
didSet {
|
||||
serverSyncedAtAbsoluteMs = Date.absoluteTimeMs
|
||||
}
|
||||
}
|
||||
private var serverSyncedAtAbsoluteMs = Date.absoluteTimeMs
|
||||
private let startAt = Date()
|
||||
/// 服务器时间已同步信号
|
||||
private let _serverTimeSynced = BehaviorRelay(value: false)
|
||||
private var serverNowMsSingle: Single<Int64> {
|
||||
|
||||
guard _serverTimeSynced.value == false else {
|
||||
return .just(serverNowMs)
|
||||
}
|
||||
return _serverTimeSynced.observe(on: rxNetworkScheduler)
|
||||
.filter { $0 }
|
||||
.take(1).asSingle()
|
||||
.timeout(.seconds(10), scheduler: rxNetworkScheduler)
|
||||
.catchAndReturn(false)
|
||||
.map({ [weak self] _ in
|
||||
return self?.serverNowMs ?? 0
|
||||
})
|
||||
}
|
||||
|
||||
/// 统计fg起始时间
|
||||
private var fgStartAtAbsoluteMs = Date.absoluteTimeMs
|
||||
private var fgAccumulateTimer: Disposable? = nil
|
||||
|
||||
/// 内存中user property 信息
|
||||
private var userProperty: Observable<[String : String]> {
|
||||
let p = userPropertyUpdated.startWith(()).observe(on: rxWorkScheduler).flatMap { [weak self] _ -> Observable<[String : String]> in
|
||||
guard let `self` = self else { return .just([:]) }
|
||||
return .create({ subscriber in
|
||||
subscriber.onNext(self._userProperty)
|
||||
subscriber.onCompleted()
|
||||
// debugPrint("userProperty thread queueName: \(Thread.current.queueName)")
|
||||
return Disposables.create()
|
||||
})
|
||||
}
|
||||
let latency = self.initializeTimeout - Date().timeIntervalSince(self.startAt)
|
||||
let intLatency = Int(latency)
|
||||
|
||||
guard latency > 0 else {
|
||||
return p
|
||||
}
|
||||
|
||||
return p.filter({ property in
|
||||
/// 需要等待以下userproperty已设置
|
||||
/// PropertyName.deviceId
|
||||
/// PropertyName.uid
|
||||
/// PropertyName.firebaseId
|
||||
guard let deviceId = property[PropertyName.deviceId.rawValue], !deviceId.isEmpty,
|
||||
let uid = property[PropertyName.uid.rawValue], !uid.isEmpty,
|
||||
let firebaseId = property[PropertyName.firebaseId.rawValue], !firebaseId.isEmpty else {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
.timeout(.milliseconds(intLatency), scheduler: rxNetworkScheduler)
|
||||
.catch { _ in
|
||||
return p
|
||||
}
|
||||
}
|
||||
private var _userProperty: [String : String] = [:] {
|
||||
didSet {
|
||||
userPropertyUpdated.onNext(())
|
||||
}
|
||||
}
|
||||
private var userPropertyUpdated = PublishSubject<Void>()
|
||||
|
||||
/// 同步服务器时间触发器
|
||||
private let syncServerTrigger = PublishSubject<Void>()
|
||||
|
||||
/// 轮询上传event任务
|
||||
private var pollingUploadTask: Disposable?
|
||||
|
||||
/// 重置轮询上传触发器
|
||||
private let reschedulePollingTrigger = BehaviorSubject(value: ())
|
||||
|
||||
/// 记录events相关的logger
|
||||
private lazy var eventsLogger: LoggerManager = {
|
||||
let l = LoggerManager(logCategoryName: "eventLogs")
|
||||
return l
|
||||
}()
|
||||
|
||||
/// 将错误上报给上层的
|
||||
private typealias InternalEventReporter = ((_ eventCode: Int, _ info: String) -> Void)
|
||||
private var internalEventReporter: InternalEventReporter?
|
||||
|
||||
private init() {
|
||||
|
||||
// first open
|
||||
logFirstOpenIfNeeded()
|
||||
|
||||
// 监听事件
|
||||
setupOberving()
|
||||
|
||||
// 检查旧数据
|
||||
clearOutdatedEventsIfNeeded()
|
||||
|
||||
// 设置轮询上传任务
|
||||
setupPollingUpload()
|
||||
|
||||
// 先打一个fg
|
||||
logFirstFgEvent()
|
||||
|
||||
ntwkMgr.networkErrorReporter = self
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - internal functions
|
||||
internal extension Manager {
|
||||
|
||||
func logEvent(_ eventName: String, parameters: [String : Any]?, priority: Entity.EventRecord.Priority = .DEFAULT) {
|
||||
_ = _logEvent(eventName, parameters: parameters, priority: priority)
|
||||
.subscribe()
|
||||
.disposed(by: bag)
|
||||
}
|
||||
|
||||
func setUserProperty(_ value: String, forName name: String) {
|
||||
eventsLogger.verbose(#function + "name: \(name) value: \(value)")
|
||||
workQueue.async { [weak self] in
|
||||
self?._userProperty[name] = value
|
||||
}
|
||||
}
|
||||
|
||||
func removeUserProperties(forNames names: [String]) {
|
||||
eventsLogger.verbose(#function + "names: \(names)")
|
||||
workQueue.async { [weak self] in
|
||||
guard let `self` = self else { return }
|
||||
var temp = self._userProperty
|
||||
for name in names {
|
||||
temp.removeValue(forKey: name)
|
||||
}
|
||||
self._userProperty = temp
|
||||
}
|
||||
}
|
||||
|
||||
func setScreen(_ name: String) {
|
||||
setUserProperty(name, forName: PropertyName.screen.rawValue)
|
||||
}
|
||||
|
||||
private func constructEvent(_ eventName: String,
|
||||
parameters: [String : Any]?,
|
||||
timestamp: Int64,
|
||||
priority: Entity.EventRecord.Priority) -> Single<Entity.EventRecord> {
|
||||
|
||||
return userProperty.take(1).observe(on: rxWorkScheduler).asSingle().flatMap { p in
|
||||
.create { subscriber in
|
||||
do {
|
||||
debugPrint("userProperty thread queueName: \(Thread.current.queueName) count: \(p.count)")
|
||||
var userProperty = p
|
||||
var eventParam = parameters ?? [:]
|
||||
|
||||
// append screen
|
||||
if let screen = userProperty.removeValue(forKey: PropertyName.screen.rawValue) {
|
||||
eventParam[PropertyName.screen.rawValue] = screen
|
||||
}
|
||||
|
||||
let userInfo = Entity.UserInfo(
|
||||
uid: userProperty.removeValue(forKey: PropertyName.uid.rawValue),
|
||||
deviceId: userProperty.removeValue(forKey: PropertyName.deviceId.rawValue),
|
||||
adjustId: userProperty.removeValue(forKey: PropertyName.adjustId.rawValue),
|
||||
adId: userProperty.removeValue(forKey: PropertyName.adId.rawValue),
|
||||
firebaseId: userProperty.removeValue(forKey: PropertyName.firebaseId.rawValue)
|
||||
)
|
||||
|
||||
let event = try Entity.Event(timestamp: timestamp,
|
||||
event: eventName,
|
||||
userInfo: userInfo,
|
||||
parameters: eventParam,
|
||||
properties: userProperty)
|
||||
let eventRecord = Entity.EventRecord(eventName: event.event, event: event, priority: priority)
|
||||
subscriber(.success(eventRecord))
|
||||
} catch {
|
||||
subscriber(.failure(error))
|
||||
}
|
||||
return Disposables.create()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func eventsLogsArchive(_ callback: @escaping (URL?) -> Void) {
|
||||
eventsLogger.logFilesZipArchive()
|
||||
.subscribe(onSuccess: { url in
|
||||
callback(url)
|
||||
}, onFailure: { error in
|
||||
callback(nil)
|
||||
cdPrint("events logs archive error: \(error)")
|
||||
})
|
||||
.disposed(by: bag)
|
||||
}
|
||||
|
||||
func eventsLogsDirURL(_ callback: @escaping (URL?) -> Void) {
|
||||
eventsLogger.logFilesDirURL()
|
||||
.subscribe(onSuccess: { url in
|
||||
callback(url)
|
||||
}, onFailure: { error in
|
||||
callback(nil)
|
||||
cdPrint("events logs archive error: \(error)")
|
||||
})
|
||||
.disposed(by: bag)
|
||||
}
|
||||
|
||||
func registerInternalEventObserver(reportCallback: @escaping (_ eventCode: Int, _ info: String) -> Void) {
|
||||
self.internalEventReporter = reportCallback
|
||||
}
|
||||
|
||||
func getUserProperties() -> [String : String] {
|
||||
return _userProperty
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - private functions
|
||||
private extension Manager {
|
||||
|
||||
func setupOberving() {
|
||||
|
||||
syncServerTrigger
|
||||
.debounce(.seconds(1), scheduler: rxConsumeScheduler)
|
||||
.subscribe(onNext: { [weak self] _ in
|
||||
self?.syncServerTime()
|
||||
})
|
||||
.disposed(by: bag)
|
||||
|
||||
var activeNoti = NotificationCenter.default.rx.notification(UIApplication.didBecomeActiveNotification)
|
||||
|
||||
if UIApplication.shared.applicationState == .active {
|
||||
activeNoti = activeNoti.startWith(.init(name: UIApplication.didBecomeActiveNotification))
|
||||
}
|
||||
|
||||
activeNoti
|
||||
.subscribe(onNext: { [weak self] _ in
|
||||
self?.syncServerTrigger.onNext(())
|
||||
// fg计时器
|
||||
self?.setupFgAccumulateTimer()
|
||||
})
|
||||
.disposed(by: bag)
|
||||
|
||||
NotificationCenter.default.rx.notification(UIApplication.didEnterBackgroundNotification)
|
||||
.subscribe(onNext: { [weak self] _ in
|
||||
guard let `self` = self else { return }
|
||||
//这里log fg和上传events任务并行关系改为前后依赖关系
|
||||
_ = self.logForegroundDuration()
|
||||
.catchAndReturn(())
|
||||
.map { self.consumeEvents() }
|
||||
.subscribe()
|
||||
self._serverTimeSynced.accept(false)
|
||||
self.invalidFgAccumulateTimer()
|
||||
})
|
||||
.disposed(by: bag)
|
||||
}
|
||||
|
||||
func syncServerTime() {
|
||||
//有网时立即同步,无网时等待有网后同步
|
||||
ntwkMgr.reachableObservable.filter { $0 }.map { _ in }.take(1).asSingle()
|
||||
.flatMap { [weak self] _ -> Single<Int64> in
|
||||
guard let `self` = self else { return Observable.empty().asSingle()}
|
||||
return self.ntwkMgr.syncServerTime()
|
||||
}
|
||||
.observe(on: rxNetworkScheduler)
|
||||
.subscribe(onSuccess: { [weak self] ms in
|
||||
self?.serverInitialMs = ms
|
||||
self?._serverTimeSynced.accept(true)
|
||||
})
|
||||
.disposed(by: bag)
|
||||
}
|
||||
|
||||
func logForegroundDuration() -> Single<Void> {
|
||||
return _logEvent(GuruAnalytics.fgEvent.name, parameters: [GuruAnalytics.fgEvent.paramKeyType.duration.rawValue : fgDurationMs()])
|
||||
.observe(on: MainScheduler.asyncInstance)
|
||||
.do(onSuccess: { _ in
|
||||
UserDefaults.fgAccumulatedDuration = 0
|
||||
})
|
||||
}
|
||||
|
||||
func clearOutdatedEventsIfNeeded() {
|
||||
|
||||
/// 1. 删除过期的数据
|
||||
serverNowMsSingle
|
||||
.flatMap({ [weak self] serverNowMs -> Single<Void> in
|
||||
guard let `self` = self else { return .just(()) }
|
||||
let earlierThan: Int64 = serverNowMs - self.eventExpiredIntervel.int64Ms
|
||||
return self.db.removeOutdatedEventRecords(earlierThan: earlierThan)
|
||||
})
|
||||
.catch({ error in
|
||||
cdPrint("remove outdated records error: \(error)")
|
||||
return .just(())
|
||||
})
|
||||
.subscribe(onSuccess: { [weak self] _ in
|
||||
self?.outdatedEventsCleared.onNext(true)
|
||||
})
|
||||
.disposed(by: bag)
|
||||
}
|
||||
|
||||
func logFirstOpenIfNeeded() {
|
||||
|
||||
if let t = UserDefaults.defaults?.value(forKey: UserDefaults.firstOpenTimeKey),
|
||||
let firstOpenTimeMs = t as? Int64 {
|
||||
setUserProperty("\(firstOpenTimeMs)", forName: PropertyName.firstOpenTime.rawValue)
|
||||
} else {
|
||||
/// log first open event
|
||||
logEvent(GuruAnalytics.firstOpenEvent.name, parameters: nil, priority: .EMERGENCE)
|
||||
|
||||
/// save first open time
|
||||
/// set to userProperty
|
||||
let firstOpenAt = Date()
|
||||
|
||||
let saveFirstOpenTime = { [weak self] (ms: Int64) -> Void in
|
||||
UserDefaults.defaults?.set(ms, forKey: UserDefaults.firstOpenTimeKey)
|
||||
self?.setUserProperty("\(ms)", forName: PropertyName.firstOpenTime.rawValue)
|
||||
}
|
||||
|
||||
serverNowMsSingle
|
||||
.subscribe(onSuccess: { _ in
|
||||
let latency = Date().timeIntervalSince(firstOpenAt)
|
||||
let adjustedFirstOpenTimeMs = self.serverInitialMs - latency.int64Ms
|
||||
saveFirstOpenTime(adjustedFirstOpenTimeMs)
|
||||
}, onFailure: { error in
|
||||
cdPrint("waiting for server time syncing error: \(error)")
|
||||
saveFirstOpenTime(firstOpenAt.timeIntervalSince1970.int64Ms)
|
||||
})
|
||||
.disposed(by: bag)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func _logEvent(_ eventName: String, parameters: [String : Any]?, priority: Entity.EventRecord.Priority = .DEFAULT) -> Single<Void> {
|
||||
eventsLogger.verbose(#function + " eventName: \(eventName)" + " params: \(parameters?.jsonString() ?? "")")
|
||||
return { [weak self] () -> Single<Void> in
|
||||
guard let `self` = self else { return Observable<Void>.empty().asSingle() }
|
||||
return self.serverNowMsSingle
|
||||
.flatMap { self.constructEvent(eventName, parameters: parameters, timestamp: $0, priority: priority) }
|
||||
.flatMap { self.db.addEventRecords($0) }
|
||||
.do(onSuccess: { _ in
|
||||
self.accumulateLoggedEventsCount(1)
|
||||
self.eventsLogger.verbose("log event success")
|
||||
}, onError: { error in
|
||||
self.eventsLogger.error("log event error: \(error)")
|
||||
})
|
||||
}()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - 轮询上传相关
|
||||
private extension Manager {
|
||||
|
||||
typealias TaskCallback = (() -> Void)
|
||||
typealias Task = ((@escaping TaskCallback, Int) -> Void)
|
||||
|
||||
func performBackgroundTask(task: @escaping Task) -> Single<Void> {
|
||||
return Single.create { [weak self] subscriber in
|
||||
var backgroundTaskID: UIBackgroundTaskIdentifier?
|
||||
|
||||
let stopTaskHandler = {
|
||||
///结束任务时需要找到对应的 dispose 取消当前任务
|
||||
guard let taskId = backgroundTaskID,
|
||||
let disposable = self?.taskKeyDisposableMap[taskId.rawValue] else {
|
||||
return
|
||||
}
|
||||
cdPrint("[performBackgroundTask] performBackgroundTask expired: \(backgroundTaskID?.rawValue ?? -1)")
|
||||
disposable.dispose()
|
||||
}
|
||||
|
||||
// Request the task assertion and save the ID.
|
||||
backgroundTaskID = UIApplication.shared.beginBackgroundTask (withName: "com.guru.analytics.manager.background.task", expirationHandler: {
|
||||
// End the task if time expires.
|
||||
self?.eventsLogger.verbose("performBackgroundTask expirationHandler: \(backgroundTaskID?.rawValue ?? -1)")
|
||||
stopTaskHandler()
|
||||
})
|
||||
|
||||
self?.eventsLogger.verbose("performBackgroundTask start: \(backgroundTaskID?.rawValue ?? -1)")
|
||||
if let taskID = backgroundTaskID {
|
||||
task({
|
||||
self?.eventsLogger.verbose("performBackgroundTask finish: \(taskID.rawValue)")
|
||||
subscriber(.success(()))
|
||||
}, taskID.rawValue)
|
||||
}
|
||||
|
||||
return Disposables.create {
|
||||
if var taskID = backgroundTaskID {
|
||||
self?.eventsLogger.verbose("performBackgroundTask dispose: \(taskID.rawValue)")
|
||||
UIApplication.shared.endBackgroundTask(taskID)
|
||||
taskID = .invalid
|
||||
backgroundTaskID = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
.subscribe(on: rxBgWorkScheduler)
|
||||
}
|
||||
|
||||
/// 上传数据库中的event
|
||||
func consumeEvents() {
|
||||
guard GuruAnalytics.enableUpload else {
|
||||
return
|
||||
}
|
||||
self.eventsLogger.verbose("consumeEvents start")
|
||||
performBackgroundTask { [weak self] callback, taskId in
|
||||
|
||||
guard let `self` = self else { return }
|
||||
cdPrint("consumeEvents start background task")
|
||||
// 等待清理过期记录完成
|
||||
let disposable = outdatedEventsCleared
|
||||
.filter { $0 }
|
||||
.take(1)
|
||||
.observe(on: rxBgWorkScheduler)
|
||||
.asSingle()
|
||||
.flatMap { _ -> Single<[Entity.EventRecord]> in
|
||||
self.eventsLogger.verbose("consumeEvents fetchEventRecordsToUpload")
|
||||
///step1: 拉取数据库记录
|
||||
return self.db.fetchEventRecordsToUpload(limit: self.maxEventFetchingCount)
|
||||
}
|
||||
.map { records -> [[Entity.EventRecord]] in
|
||||
/// step2: 将event数组分割成若干批次,numberOfCountPerConsume个一批
|
||||
/// self.eventsLogger.verbose("consumeEvents fetchEventRecordsToUpload")
|
||||
self.eventsLogger.verbose("consumeEvents fetchEventRecordsToUpload result: \(records.count)")
|
||||
return records.chunked(into: self.numberOfCountPerConsume)
|
||||
}
|
||||
.flatMap({ batches -> Single<[[Entity.EventRecord]]> in
|
||||
|
||||
guard batches.count > 0 else { return .just([]) }
|
||||
|
||||
/// 监听网络信号
|
||||
return self.ntwkMgr.reachableObservable.filter { $0 }
|
||||
.take(1).asSingle()
|
||||
.map { _ in batches }
|
||||
})
|
||||
.map { batches -> [Single<[String]>] in
|
||||
/// step3: 转为批次上传任务
|
||||
self.eventsLogger.verbose("consumeEvents uploadEvents")
|
||||
return batches.map { records in
|
||||
return self.ntwkMgr.uploadEvents(records)
|
||||
.do(onSuccess: { t in
|
||||
self.eventsLogger.verbose("consumeEvents upload events succeed: \(t.eventsJson)")
|
||||
})
|
||||
.catch({ error in
|
||||
self.eventsLogger.error("consumeEvents upload events error: \(error)")
|
||||
// 上传失败,移除对应的缓存ID
|
||||
let recordIds = records.map { $0.recordId }
|
||||
return self.db.resetTransitionStatus(for: recordIds)
|
||||
.map { _ in ([], "") }
|
||||
})
|
||||
.map { $0.recordIDs }
|
||||
}
|
||||
}
|
||||
.flatMap { uploadBatches -> Single<[String]> in
|
||||
guard uploadBatches.count > 0 else { return .just([]) }
|
||||
/// 合并上传结果
|
||||
return Observable.from(uploadBatches)
|
||||
.merge()
|
||||
.toArray().map { batches -> [String] in batches.flatMap { $0 } }
|
||||
}
|
||||
.flatMap { recordIDs -> Single<Void> in
|
||||
self.accumulateUploadedEventsCount(recordIDs.count)
|
||||
/// step4: 删除数据库中对应记录
|
||||
return self.db.deleteEventRecords(recordIDs)
|
||||
.catch { error in
|
||||
cdPrint("consumeEvents delete events from DB error: \(error)")
|
||||
return .just(())
|
||||
}
|
||||
}
|
||||
.observe(on: self.rxBgWorkScheduler)
|
||||
.subscribe(onFailure: { error in
|
||||
cdPrint("consumeEvents error: \(error)")
|
||||
}, onDisposed: { [weak self] in
|
||||
self?.taskKeyDisposableMap.removeValue(forKey: taskId)
|
||||
cdPrint("consumeEvents onDisposed")
|
||||
callback()
|
||||
})
|
||||
|
||||
taskKeyDisposableMap[taskId] = disposable
|
||||
}
|
||||
.subscribe()
|
||||
.disposed(by: bag)
|
||||
|
||||
}
|
||||
|
||||
func startPollingUpload() {
|
||||
pollingUploadTask?.dispose()
|
||||
pollingUploadTask = nil
|
||||
|
||||
// 每scheduleInterval时间间隔启动一次,立即启动一次
|
||||
let timer = Observable<Int>.timer(.seconds(0), period: .milliseconds(Int(scheduleInterval.int64Ms)),
|
||||
scheduler: rxConsumeScheduler)
|
||||
.do(onNext: { _ in
|
||||
cdPrint("consumeEvents timer")
|
||||
})
|
||||
|
||||
// 每满numberOfCountPerConsume个数启动一次,立即启动一次
|
||||
let counter = db.uploadableEventRecordCountOb()
|
||||
.distinctUntilChanged()
|
||||
.compactMap({ [weak self] count -> Int? in
|
||||
cdPrint("consumeEvents uploadableEventRecordCountOb count: \(count) numberOfCountPerConsume: \(self?.numberOfCountPerConsume)")
|
||||
guard let `self` = self,
|
||||
count >= self.numberOfCountPerConsume else { return nil }
|
||||
return count
|
||||
})
|
||||
.map { _ in }
|
||||
.startWith(())
|
||||
|
||||
pollingUploadTask = Observable.combineLatest(timer, counter)
|
||||
.throttle(.seconds(1), scheduler: rxConsumeScheduler)
|
||||
.flatMap({ [weak self] t -> Single<(Int, Void)> in
|
||||
guard let `self` = self else { return .just(t) }
|
||||
return Observable.combineLatest(self.db.hasFgEventRecord().asObservable(), self.db.uploadableEventRecordCount().asObservable())
|
||||
.take(1).asSingle()
|
||||
.flatMap({ (hasFgEventInDb, eventsCount) -> Single<(Int, Void)> in
|
||||
guard !hasFgEventInDb, eventsCount > 0 else {
|
||||
return .just(t)
|
||||
}
|
||||
return self.logForegroundDuration().catchAndReturn(()).map({ _ in t })
|
||||
})
|
||||
})
|
||||
.subscribe(onNext: { [weak self] (timer, counter) in
|
||||
self?.consumeEvents()
|
||||
})
|
||||
}
|
||||
|
||||
func setupPollingUpload() {
|
||||
reschedulePollingTrigger
|
||||
.debounce(.seconds(1), scheduler: rxConsumeScheduler)
|
||||
.subscribe(onNext: { [weak self] _ in
|
||||
self?.startPollingUpload()
|
||||
})
|
||||
.disposed(by: bag)
|
||||
}
|
||||
|
||||
func logFirstFgEvent() {
|
||||
_ = Single.just(()).delay(.milliseconds(500), scheduler: MainScheduler.asyncInstance)
|
||||
.flatMap({ [weak self] _ in
|
||||
self?.logForegroundDuration() ?? .just(())
|
||||
})
|
||||
.subscribe()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - fg相关
|
||||
private extension Manager {
|
||||
|
||||
func setupFgAccumulateTimer() {
|
||||
invalidFgAccumulateTimer()
|
||||
fgStartAtAbsoluteMs = Date.absoluteTimeMs
|
||||
fgAccumulateTimer = Observable<Int>.timer(.seconds(0), period: .seconds(1), scheduler: MainScheduler.asyncInstance)
|
||||
.subscribe(onNext: { [weak self] _ in
|
||||
guard let `self` = self else { return }
|
||||
UserDefaults.fgAccumulatedDuration = self.fgDurationMs()
|
||||
}, onDisposed: {
|
||||
cdPrint("fg accumulate timer disposed")
|
||||
})
|
||||
}
|
||||
|
||||
func invalidFgAccumulateTimer() {
|
||||
fgAccumulateTimer?.dispose()
|
||||
fgAccumulateTimer = nil
|
||||
}
|
||||
|
||||
/// 前台停留时长
|
||||
func fgDurationMs() -> Int64 {
|
||||
let slice = Date.absoluteTimeMs - fgStartAtAbsoluteMs
|
||||
fgStartAtAbsoluteMs = Date.absoluteTimeMs
|
||||
// cdPrint("accumulate fg duration: \(slice)")
|
||||
let totalDuration = UserDefaults.fgAccumulatedDuration + slice
|
||||
// cdPrint("total fg duration: \(totalDuration)")
|
||||
return totalDuration
|
||||
}
|
||||
}
|
||||
|
||||
extension Manager: GuruAnalyticsNetworkErrorReportDelegate {
|
||||
func reportError(networkError: GuruAnalyticsNetworkError) {
|
||||
|
||||
enum UserInfoKey: String, Encodable {
|
||||
case httpCode = "h_c"
|
||||
case errorCode = "e_c"
|
||||
case url, msg
|
||||
}
|
||||
|
||||
let errorCode = networkError.internalErrorCategory.rawValue
|
||||
let userInfo = (networkError.originError as NSError).userInfo
|
||||
var info: [UserInfoKey : String] = [
|
||||
.url : (userInfo[NSURLErrorFailingURLStringErrorKey] as? String) ?? "",
|
||||
.msg : networkError.originError.localizedDescription,
|
||||
]
|
||||
|
||||
if let httpCode = networkError.httpStatusCode {
|
||||
info[.httpCode] = "\(httpCode)"
|
||||
} else {
|
||||
info[.errorCode] = "\((networkError.originError as NSError).code)"
|
||||
}
|
||||
|
||||
info = info.compactMapValues { $0.isEmpty ? nil : $0 }
|
||||
|
||||
let jsonString = info.asString ?? ""
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
self?.internalEventReporter?(errorCode, jsonString)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,49 +0,0 @@
|
|||
fileFormatVersion: 2
|
||||
guid: dab7243d39b964205a4fa4446e95403b
|
||||
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: 1
|
||||
- first:
|
||||
Any:
|
||||
second:
|
||||
enabled: 0
|
||||
settings: {}
|
||||
- first:
|
||||
Editor: Editor
|
||||
second:
|
||||
enabled: 0
|
||||
settings:
|
||||
DefaultValueInitialized: true
|
||||
- first:
|
||||
iPhone: iOS
|
||||
second:
|
||||
enabled: 0
|
||||
settings: {}
|
||||
- first:
|
||||
tvOS: tvOS
|
||||
second:
|
||||
enabled: 1
|
||||
settings: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -1,65 +0,0 @@
|
|||
//
|
||||
// UserDefaults.swift
|
||||
// GuruAnalytics
|
||||
//
|
||||
// Created by mayue on 2022/11/21.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
internal enum UserDefaults {
|
||||
|
||||
static let defaults = Foundation.UserDefaults(suiteName: "com.guru.guru_analytics_lib")
|
||||
|
||||
static var eventsServerHost: String? {
|
||||
|
||||
get {
|
||||
return defaults?.value(forKey: eventsServerHostKey) as? String
|
||||
}
|
||||
|
||||
set {
|
||||
var host = newValue
|
||||
let h_sch = "http://"
|
||||
let hs_sch = "https://"
|
||||
host?.deletePrefix(h_sch)
|
||||
host?.deletePrefix(hs_sch)
|
||||
host?.trimmed(in: .whitespacesAndNewlines.union(.init(charactersIn: "/")))
|
||||
defaults?.set(host, forKey: eventsServerHostKey)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
static var fgAccumulatedDuration: Int64 {
|
||||
get {
|
||||
return defaults?.value(forKey: fgDurationKey) as? Int64 ?? 0
|
||||
}
|
||||
|
||||
set {
|
||||
defaults?.set(newValue, forKey: fgDurationKey)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension UserDefaults {
|
||||
|
||||
static var firstOpenTimeKey: String {
|
||||
return "app.first.open.timestamp"
|
||||
}
|
||||
|
||||
static var dbVersionKey: String {
|
||||
return "db.version"
|
||||
}
|
||||
|
||||
static var hostsMapKey: String {
|
||||
return "hosts.map"
|
||||
}
|
||||
|
||||
static var eventsServerHostKey: String {
|
||||
return "events.server.host"
|
||||
}
|
||||
|
||||
static var fgDurationKey: String {
|
||||
return "fg.duration.ms"
|
||||
}
|
||||
}
|
||||
|
|
@ -1,49 +0,0 @@
|
|||
fileFormatVersion: 2
|
||||
guid: e022f21a2d3a94edb87e7479aab2cfb9
|
||||
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: 1
|
||||
- first:
|
||||
Any:
|
||||
second:
|
||||
enabled: 0
|
||||
settings: {}
|
||||
- first:
|
||||
Editor: Editor
|
||||
second:
|
||||
enabled: 0
|
||||
settings:
|
||||
DefaultValueInitialized: true
|
||||
- first:
|
||||
iPhone: iOS
|
||||
second:
|
||||
enabled: 0
|
||||
settings: {}
|
||||
- first:
|
||||
tvOS: tvOS
|
||||
second:
|
||||
enabled: 1
|
||||
settings: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
fileFormatVersion: 2
|
||||
guid: b2af081c64bfe4af7b232b8132d01544
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -1,36 +0,0 @@
|
|||
//
|
||||
// GuruAnalyticsErrorHandleDelegate.swift
|
||||
// Alamofire
|
||||
//
|
||||
// Created by mayue on 2023/10/27.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
internal enum GuruAnalyticsNetworkLayerErrorCategory: Int {
|
||||
case unknown = -100
|
||||
case serverAPIError = 101
|
||||
case responseParsingError = 102
|
||||
case googleDNSServiceError = 106
|
||||
}
|
||||
|
||||
@objc internal protocol GuruAnalyticsNetworkErrorReportDelegate {
|
||||
func reportError(networkError: GuruAnalyticsNetworkError) -> Void
|
||||
}
|
||||
|
||||
internal class GuruAnalyticsNetworkError: NSError {
|
||||
private(set) var httpStatusCode: Int?
|
||||
private(set) var originError: Error
|
||||
private(set) var internalErrorCategory: GuruAnalyticsNetworkLayerErrorCategory
|
||||
|
||||
init(httpStatusCode: Int? = nil, internalErrorCategory: GuruAnalyticsNetworkLayerErrorCategory, originError: Error) {
|
||||
self.httpStatusCode = httpStatusCode
|
||||
self.originError = originError
|
||||
self.internalErrorCategory = internalErrorCategory
|
||||
super.init(domain: "com.guru.analytics.network.layer", code: internalErrorCategory.rawValue, userInfo: (originError as NSError).userInfo)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
}
|
||||
|
|
@ -1,85 +0,0 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 77961084d08bf4074afb847d866d89e0
|
||||
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: 1
|
||||
- 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: AnyCPU
|
||||
- first:
|
||||
Standalone: OSXUniversal
|
||||
second:
|
||||
enabled: 0
|
||||
settings:
|
||||
CPU: AnyCPU
|
||||
- first:
|
||||
Standalone: Win
|
||||
second:
|
||||
enabled: 0
|
||||
settings:
|
||||
CPU: AnyCPU
|
||||
- first:
|
||||
Standalone: Win64
|
||||
second:
|
||||
enabled: 0
|
||||
settings:
|
||||
CPU: AnyCPU
|
||||
- first:
|
||||
iPhone: iOS
|
||||
second:
|
||||
enabled: 0
|
||||
settings:
|
||||
AddToEmbeddedBinaries: false
|
||||
CPU: AnyCPU
|
||||
CompileFlags:
|
||||
FrameworkDependencies:
|
||||
- first:
|
||||
tvOS: tvOS
|
||||
second:
|
||||
enabled: 1
|
||||
settings: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -1,51 +0,0 @@
|
|||
//
|
||||
// GuruAnalytics+Internal.swift
|
||||
// Pods
|
||||
//
|
||||
// Created by mayue on 2022/11/18.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
internal extension GuruAnalytics {
|
||||
|
||||
///built-in user property keys
|
||||
enum PropertyName: String {
|
||||
case deviceId
|
||||
case uid
|
||||
case adjustId
|
||||
case adId
|
||||
case firebaseId
|
||||
case screen = "screen_name"
|
||||
case firstOpenTime = "first_open_time"
|
||||
}
|
||||
|
||||
///built-in events
|
||||
static let fgEvent: EventProto = {
|
||||
var e = EventProto(paramKeyType: FgEventParametersKeys.self, name: "fg")
|
||||
return e
|
||||
}()
|
||||
|
||||
static let firstOpenEvent: EventProto = {
|
||||
var e = EventProto(paramKeyType: FgEventParametersKeys.self, name: "first_open")
|
||||
return e
|
||||
}()
|
||||
|
||||
class func setUserProperty(_ value: String?, forName name: PropertyName) {
|
||||
setUserProperty(value, forName: name.rawValue)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
internal extension GuruAnalytics {
|
||||
|
||||
struct EventProto<ParametersKeys> {
|
||||
var paramKeyType: ParametersKeys.Type
|
||||
var name: String
|
||||
}
|
||||
|
||||
enum FgEventParametersKeys: String {
|
||||
case duration
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -1,85 +0,0 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 2623be1999d104830a332ddf24de490a
|
||||
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: 1
|
||||
- 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: AnyCPU
|
||||
- first:
|
||||
Standalone: OSXUniversal
|
||||
second:
|
||||
enabled: 0
|
||||
settings:
|
||||
CPU: AnyCPU
|
||||
- first:
|
||||
Standalone: Win
|
||||
second:
|
||||
enabled: 0
|
||||
settings:
|
||||
CPU: AnyCPU
|
||||
- first:
|
||||
Standalone: Win64
|
||||
second:
|
||||
enabled: 0
|
||||
settings:
|
||||
CPU: AnyCPU
|
||||
- first:
|
||||
iPhone: iOS
|
||||
second:
|
||||
enabled: 0
|
||||
settings:
|
||||
AddToEmbeddedBinaries: false
|
||||
CPU: AnyCPU
|
||||
CompileFlags:
|
||||
FrameworkDependencies:
|
||||
- first:
|
||||
tvOS: tvOS
|
||||
second:
|
||||
enabled: 1
|
||||
settings: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
fileFormatVersion: 2
|
||||
guid: ef02d71253914417f9d007629ed0eb1d
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -1,78 +0,0 @@
|
|||
//
|
||||
// APIService.swift
|
||||
// GuruAnalytics_iOS
|
||||
//
|
||||
// Created by mayue on 2022/11/8.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Alamofire
|
||||
|
||||
internal enum APIService {}
|
||||
|
||||
extension APIService {
|
||||
enum Backend: CaseIterable {
|
||||
case event
|
||||
case systemTime
|
||||
}
|
||||
}
|
||||
|
||||
extension APIService.Backend {
|
||||
|
||||
var scheme: String {
|
||||
return "https"
|
||||
}
|
||||
|
||||
var host: String {
|
||||
switch self {
|
||||
case .systemTime:
|
||||
return "saas.castbox.fm"
|
||||
case .event:
|
||||
return UserDefaults.eventsServerHost ?? "collect.saas.castbox.fm"
|
||||
}
|
||||
}
|
||||
|
||||
var urlComponents: URLComponents {
|
||||
var urlC = URLComponents()
|
||||
urlC.host = self.host
|
||||
urlC.scheme = self.scheme
|
||||
urlC.path = self.path
|
||||
return urlC
|
||||
}
|
||||
|
||||
var path: String {
|
||||
switch self {
|
||||
case .event:
|
||||
return "/event"
|
||||
case .systemTime:
|
||||
return "/tool/api/v1/system/time"
|
||||
}
|
||||
}
|
||||
|
||||
var method: HTTPMethod {
|
||||
switch self {
|
||||
case .event:
|
||||
return .post
|
||||
case .systemTime:
|
||||
return .get
|
||||
}
|
||||
}
|
||||
|
||||
var headers: HTTPHeaders {
|
||||
HTTPHeaders(
|
||||
["Content-Type": "application/json",
|
||||
"Content-Encoding": "gzip",
|
||||
"x_event_type": "event"]
|
||||
)
|
||||
}
|
||||
|
||||
var version: Int {
|
||||
/// 接口版本
|
||||
switch self {
|
||||
case .event:
|
||||
return 10
|
||||
case .systemTime:
|
||||
return 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,49 +0,0 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 09cfaaabf31cb4a73ab8d83aa65eb2de
|
||||
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: 1
|
||||
- first:
|
||||
Any:
|
||||
second:
|
||||
enabled: 0
|
||||
settings: {}
|
||||
- first:
|
||||
Editor: Editor
|
||||
second:
|
||||
enabled: 0
|
||||
settings:
|
||||
DefaultValueInitialized: true
|
||||
- first:
|
||||
iPhone: iOS
|
||||
second:
|
||||
enabled: 0
|
||||
settings: {}
|
||||
- first:
|
||||
tvOS: tvOS
|
||||
second:
|
||||
enabled: 1
|
||||
settings: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -1,419 +0,0 @@
|
|||
//
|
||||
// Network.swift
|
||||
// GuruAnalytics_iOS
|
||||
//
|
||||
// Created by mayue on 2022/11/3.
|
||||
// Copyright © 2022 Guru Network Limited. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Alamofire
|
||||
import RxSwift
|
||||
import RxRelay
|
||||
import Gzip
|
||||
|
||||
internal class NetworkManager {
|
||||
|
||||
private static let ipErrorUserInfoKey = "failed_ip"
|
||||
|
||||
internal var isReachable: Bool {
|
||||
return _reachableObservable.value
|
||||
}
|
||||
|
||||
internal var reachableObservable: Observable<Bool> {
|
||||
return _reachableObservable.asObservable()
|
||||
}
|
||||
|
||||
private let _reachableObservable = BehaviorRelay(value: false)
|
||||
|
||||
private let reachablity = NetworkReachabilityManager()
|
||||
|
||||
private let networkQueue = DispatchQueue.init(label: "com.guru.analytics.network.queue", qos: .userInitiated)
|
||||
private lazy var rxWorkScheduler = SerialDispatchQueueScheduler.init(queue: networkQueue, internalSerialQueueName: "com.guru.analytics.network.rx.work.queue")
|
||||
|
||||
private lazy var session: Session = {
|
||||
let trustManager = CertificatePinnerServerTrustManager()
|
||||
trustManager.evaluator.hostWhiteList = hostsMap
|
||||
return Session(serverTrustManager: trustManager)
|
||||
}()
|
||||
|
||||
private var hostsMap: [String : [String]] {
|
||||
get {
|
||||
return UserDefaults.defaults?.value(forKey: UserDefaults.hostsMapKey) as? [String : [String]] ?? [:]
|
||||
}
|
||||
|
||||
set {
|
||||
UserDefaults.defaults?.set(newValue, forKey: UserDefaults.hostsMapKey)
|
||||
(session.serverTrustManager as? CertificatePinnerServerTrustManager)?.evaluator.hostWhiteList = newValue
|
||||
checkHostMap(newValue)
|
||||
}
|
||||
}
|
||||
|
||||
internal weak var networkErrorReporter: GuruAnalyticsNetworkErrorReportDelegate?
|
||||
|
||||
internal init() {
|
||||
|
||||
reachablity?.startListening(onQueue: networkQueue, onUpdatePerforming: { [weak self] status in
|
||||
var reachable: Bool
|
||||
switch status {
|
||||
case .reachable(_):
|
||||
reachable = true
|
||||
case .notReachable, .unknown:
|
||||
reachable = false
|
||||
}
|
||||
self?._reachableObservable.accept(reachable)
|
||||
})
|
||||
|
||||
APIService.Backend.allCases.forEach({ service in
|
||||
_ = lookupHostRemote(service.host).subscribe()
|
||||
})
|
||||
}
|
||||
|
||||
/// 上报event请求
|
||||
/// - Parameter events: event record数组
|
||||
/// - Returns: 上报成功的event record ID数组
|
||||
internal func uploadEvents(_ events: [Entity.EventRecord]) -> Single<(recordIDs: [String], eventsJson: String)> {
|
||||
guard !events.isEmpty else {
|
||||
return .just(([], ""))
|
||||
}
|
||||
|
||||
let service = APIService.Backend.event
|
||||
|
||||
return lookupHostLocal(service.host)
|
||||
.flatMap { ip in
|
||||
|
||||
Single.create { [weak self] subscriber in
|
||||
guard let `self` = self else {
|
||||
subscriber(.failure(
|
||||
NSError(domain: "networkManager", code: 0, userInfo: [NSLocalizedDescriptionKey : "manager is released"])
|
||||
))
|
||||
return Disposables.create()
|
||||
}
|
||||
var postJson = [String : Any]()
|
||||
postJson["version"] = service.version
|
||||
postJson["deviceInfo"] = Constants.deviceInfo
|
||||
let eventJsonArray = events.compactMap { $0.eventJson.jsonObject() }
|
||||
postJson["events"] = eventJsonArray
|
||||
|
||||
do {
|
||||
let jsonData = try JSONSerialization.data(withJSONObject: postJson)
|
||||
let jsonString = String(data: jsonData, encoding: .utf8) ?? ""
|
||||
let gzippedJsonData = try jsonData.gzipped()
|
||||
let httpBody = gzippedJsonData
|
||||
|
||||
var urlRequest: URLRequest
|
||||
var urlC = service.urlComponents
|
||||
let session: Session
|
||||
|
||||
if let ip = ip {
|
||||
session = self.session
|
||||
urlC.host = ip
|
||||
urlRequest = try URLRequest(url: urlC, method: service.method, headers: service.headers)
|
||||
urlRequest.setValue(service.host, forHTTPHeaderField: "host")
|
||||
|
||||
} else {
|
||||
session = AF
|
||||
urlRequest = try URLRequest(url: urlC, method: service.method, headers: service.headers)
|
||||
}
|
||||
urlRequest.setValue(GuruAnalytics.saasXAPPID, forHTTPHeaderField: "X-APP-ID")
|
||||
urlRequest.setValue(GuruAnalytics.saasXDEVICEINFO, forHTTPHeaderField: "X-DEVICE-INFO")
|
||||
urlRequest.httpBody = httpBody
|
||||
|
||||
var emptyResponseCodes = DataResponseSerializer.defaultEmptyResponseCodes
|
||||
emptyResponseCodes.insert(200)
|
||||
|
||||
let request = session.request(urlRequest).validate(statusCode: [200])
|
||||
.responseData(
|
||||
queue: self.networkQueue,
|
||||
emptyResponseCodes: emptyResponseCodes,
|
||||
completionHandler: { response in
|
||||
cdPrint("\(#function): request: \(urlRequest) \nheader:\(urlRequest.headers) \nhttpbody: \(jsonString) \nresponse: \(response)")
|
||||
switch response.result {
|
||||
case .failure(let error):
|
||||
subscriber(.failure(self.mapError(error, for: ip)))
|
||||
cdPrint("\(#function) error: \(error)")
|
||||
case .success:
|
||||
subscriber(.success((events.map { $0.recordId }, jsonString)))
|
||||
}
|
||||
})
|
||||
|
||||
return Disposables.create {
|
||||
request.cancel()
|
||||
}
|
||||
} catch {
|
||||
cdPrint("construct request failed: \(error)")
|
||||
subscriber(.failure(error))
|
||||
return Disposables.create()
|
||||
}
|
||||
}
|
||||
}
|
||||
.do(onError: { [weak self] error in
|
||||
self?.reportError(error: error, internalErrorCategory: .serverAPIError)
|
||||
})
|
||||
.catch { [weak self] error in
|
||||
|
||||
guard let `self` = self else { throw error }
|
||||
|
||||
return try self.errorCatcher(error, for: service.host) {
|
||||
self.uploadEvents(events)
|
||||
}
|
||||
}
|
||||
.subscribe(on: rxWorkScheduler)
|
||||
}
|
||||
|
||||
/// 同步服务器时间请求
|
||||
/// - Returns: 毫秒整数
|
||||
internal func syncServerTime() -> Single<Int64> {
|
||||
let service = APIService.Backend.systemTime
|
||||
|
||||
return lookupHostLocal(service.host)
|
||||
.flatMap { ip in
|
||||
|
||||
Single.create { [weak self] subscriber in
|
||||
|
||||
guard let `self` = self else {
|
||||
subscriber(.failure(
|
||||
NSError(domain: "networkManager", code: 0, userInfo: [NSLocalizedDescriptionKey : "manager is released"])
|
||||
))
|
||||
return Disposables.create()
|
||||
}
|
||||
|
||||
do {
|
||||
let start = Date()
|
||||
var urlC = service.urlComponents
|
||||
let session: Session
|
||||
var urlReq: URLRequest
|
||||
|
||||
if let ip = ip {
|
||||
session = self.session
|
||||
urlC.host = ip
|
||||
urlReq = try URLRequest(url: urlC, method: service.method, headers: service.headers)
|
||||
urlReq.setValue(service.host, forHTTPHeaderField: "host")
|
||||
} else {
|
||||
session = AF
|
||||
urlReq = try URLRequest(url: urlC, method: service.method, headers: service.headers)
|
||||
}
|
||||
urlReq.setValue(GuruAnalytics.saasXAPPID, forHTTPHeaderField: "X-APP-ID")
|
||||
urlReq.setValue(GuruAnalytics.saasXDEVICEINFO, forHTTPHeaderField: "X-DEVICE-INFO")
|
||||
|
||||
let request = session.request(urlReq).validate(statusCode: [200])
|
||||
.responseDecodable(of: Entity.SystemTimeResult.self,
|
||||
queue: self.networkQueue,
|
||||
completionHandler: { response in
|
||||
cdPrint("\(#function): request: \(urlReq) \nheaders:\(urlReq.headers) \nresponse: \(response)")
|
||||
switch response.result {
|
||||
case .success(let data):
|
||||
let timespan = Date().timeIntervalSince(start).int64Ms
|
||||
let systemTime = data.data - timespan / 2
|
||||
subscriber(.success(systemTime))
|
||||
case .failure(let error):
|
||||
cdPrint("\(#function) error: \(error)")
|
||||
subscriber(.failure(self.mapError(error, for: ip)))
|
||||
}
|
||||
})
|
||||
|
||||
return Disposables.create {
|
||||
request.cancel()
|
||||
}
|
||||
|
||||
} catch {
|
||||
cdPrint("construct request failed: \(error)")
|
||||
subscriber(.failure(error))
|
||||
return Disposables.create()
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
.do(onError: { [weak self] error in
|
||||
self?.reportError(error: error, internalErrorCategory: .serverAPIError)
|
||||
})
|
||||
.catch { [weak self] error in
|
||||
|
||||
guard let `self` = self else { throw error }
|
||||
|
||||
return try self.errorCatcher(error, for: service.host) {
|
||||
self.syncServerTime()
|
||||
}
|
||||
}
|
||||
.subscribe(on: rxWorkScheduler)
|
||||
}
|
||||
|
||||
private func _lookupHostRemote(_ host: String) -> Single<[IpAdress]> {
|
||||
return Single.create { subscriber in
|
||||
|
||||
do {
|
||||
var urlC = URLComponents()
|
||||
urlC.scheme = "https"
|
||||
urlC.host = "dns.google"
|
||||
urlC.path = "/resolve"
|
||||
urlC.queryItems = [.init(name: "name", value: "\(host)")]
|
||||
|
||||
let urlReq = try URLRequest(url: urlC, method: .get)
|
||||
|
||||
let request = AF.request(urlReq)
|
||||
.validate(statusCode: [200])
|
||||
.responseData(completionHandler: { response in
|
||||
switch response.result {
|
||||
case .success(let data):
|
||||
|
||||
do {
|
||||
guard let dict = try JSONSerialization.jsonObject(with: data, options: .allowFragments) as? [String : Any],
|
||||
let answerDictArr = dict["Answer"] as? [[String : Any]] else {
|
||||
let customError = NSError(domain: "com.guru.analytics.network.layer", code: 0,
|
||||
userInfo: [NSLocalizedDescriptionKey : "dns.google service returned unexpected data"])
|
||||
subscriber(.failure(customError))
|
||||
return
|
||||
}
|
||||
|
||||
let ips = try JSONDecoder().decodeAnyData([IpAdress].self, from: answerDictArr)
|
||||
subscriber(.success(ips))
|
||||
cdPrint("\(#function) success request: \(urlReq) \nresponse: \(ips)")
|
||||
} catch {
|
||||
subscriber(.failure(error))
|
||||
}
|
||||
|
||||
case .failure(let error):
|
||||
cdPrint("\(#function) error: \(error) request: \(urlReq)")
|
||||
subscriber(.failure(error))
|
||||
}
|
||||
})
|
||||
return Disposables.create {
|
||||
request.cancel()
|
||||
}
|
||||
} catch {
|
||||
cdPrint("construct request failed: \(error)")
|
||||
subscriber(.failure(error))
|
||||
return Disposables.create()
|
||||
}
|
||||
}
|
||||
.subscribe(on: rxWorkScheduler)
|
||||
}
|
||||
|
||||
private func lookupHostRemote(_ host: String) -> Single<[String]> {
|
||||
return _lookupHostRemote(host)
|
||||
.map { ipList -> [String] in
|
||||
ipList.compactMap { ip in
|
||||
guard ip.type == 1 else { return nil }
|
||||
return ip.data
|
||||
}
|
||||
}
|
||||
.do(onSuccess: { [weak self] ipList in
|
||||
self?.hostsMap[host] = ipList
|
||||
}, onError: { [weak self] error in
|
||||
self?.reportError(error: error, internalErrorCategory: .googleDNSServiceError)
|
||||
})
|
||||
}
|
||||
|
||||
private func lookupHostLocal(_ host: String) -> Single<String?> {
|
||||
return Single.create { [weak self] subscriber in
|
||||
|
||||
guard let `self` = self else {
|
||||
subscriber(.failure(
|
||||
NSError(domain: "networkManager", code: 0, userInfo: [NSLocalizedDescriptionKey : "manager is released"])
|
||||
))
|
||||
return Disposables.create()
|
||||
}
|
||||
|
||||
subscriber(.success(self.hostsMap[host]?.first))
|
||||
|
||||
return Disposables.create()
|
||||
}
|
||||
.subscribe(on: rxWorkScheduler)
|
||||
}
|
||||
|
||||
private func mapError(_ error: AFError, for ip: String?) -> Error {
|
||||
|
||||
guard let ip = ip else { return error }
|
||||
|
||||
var e = (error.underlyingError ?? error) as NSError
|
||||
var userInfo = e.userInfo
|
||||
userInfo[Self.ipErrorUserInfoKey] = ip
|
||||
e = NSError(domain: e.domain, code: e.code, userInfo: userInfo)
|
||||
return e
|
||||
}
|
||||
|
||||
private func errorCatcher<T>(_ error: Error, for host: String, returnValue: (() -> Single<T>) ) throws -> Single<T> {
|
||||
|
||||
let e = error as NSError
|
||||
guard let ip = e.userInfo[Self.ipErrorUserInfoKey] as? String else {
|
||||
throw error
|
||||
}
|
||||
//FIX: https://console.firebase.google.com/u/1/project/ball-sort-dd4d0/crashlytics/app/ios:ball.sort.puzzle.color.sorting.bubble.games/issues/c1f6d36aeb7c105a32015504776adff5?time=last-ninety-days&sessionEventKey=27d699688a594f96a7b17003a3c49c84_1900062047348716162
|
||||
if var hosts = hostsMap[host] {
|
||||
hosts.removeAll(where: { $0 == ip })
|
||||
hostsMap[host] = hosts
|
||||
}
|
||||
return returnValue()
|
||||
}
|
||||
|
||||
private func checkHostMap(_ hostMap: [String : [String]]) {
|
||||
|
||||
hostMap.forEach { key, value in
|
||||
guard value.count <= 0 else { return }
|
||||
_ = lookupHostRemote(key).subscribe()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private func reportError(error: Error, internalErrorCategory: GuruAnalyticsNetworkLayerErrorCategory) {
|
||||
let customError: GuruAnalyticsNetworkError
|
||||
if let aferror = error.asAFError {
|
||||
|
||||
if case let AFError.responseValidationFailed(reason) = aferror,
|
||||
case let AFError.ResponseValidationFailureReason.unacceptableStatusCode(httpStatusCode) = reason {
|
||||
customError = GuruAnalyticsNetworkError(httpStatusCode: httpStatusCode, internalErrorCategory: internalErrorCategory, originError: aferror.underlyingError ?? error)
|
||||
} else {
|
||||
customError = GuruAnalyticsNetworkError(internalErrorCategory: internalErrorCategory, originError: aferror.underlyingError ?? error)
|
||||
}
|
||||
|
||||
} else {
|
||||
customError = GuruAnalyticsNetworkError(internalErrorCategory: internalErrorCategory, originError: error)
|
||||
}
|
||||
|
||||
networkErrorReporter?.reportError(networkError: customError)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
internal final class CertificatePinnerTrustEvaluator: ServerTrustEvaluating {
|
||||
|
||||
private let dftEvaluator = DefaultTrustEvaluator()
|
||||
|
||||
init() {}
|
||||
|
||||
var hostWhiteList: [String : [String]] = [:]
|
||||
|
||||
func evaluate(_ trust: SecTrust, forHost host: String) throws {
|
||||
|
||||
let originHostName: String = hostWhiteList.first { _, value in
|
||||
value.contains { $0 == host }
|
||||
}?.key ?? host
|
||||
|
||||
try dftEvaluator.evaluate(trust, forHost: originHostName)
|
||||
|
||||
cdPrint(#function + " \(trust) forHost: \(host) originHostName: \(originHostName)")
|
||||
}
|
||||
}
|
||||
|
||||
internal class CertificatePinnerServerTrustManager: ServerTrustManager {
|
||||
|
||||
let evaluator = CertificatePinnerTrustEvaluator()
|
||||
|
||||
init() {
|
||||
super.init(allHostsMustBeEvaluated: true, evaluators: [:])
|
||||
}
|
||||
|
||||
override func serverTrustEvaluator(forHost host: String) throws -> ServerTrustEvaluating? {
|
||||
|
||||
return evaluator
|
||||
}
|
||||
}
|
||||
|
||||
extension NetworkManager {
|
||||
struct IpAdress: Codable {
|
||||
let name: String
|
||||
let type: Int
|
||||
let TTL: Int
|
||||
let data: String
|
||||
}
|
||||
}
|
||||
|
|
@ -1,49 +0,0 @@
|
|||
fileFormatVersion: 2
|
||||
guid: f197036e5438741cea961bc00c23c775
|
||||
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: 1
|
||||
- first:
|
||||
Any:
|
||||
second:
|
||||
enabled: 0
|
||||
settings: {}
|
||||
- first:
|
||||
Editor: Editor
|
||||
second:
|
||||
enabled: 0
|
||||
settings:
|
||||
DefaultValueInitialized: true
|
||||
- first:
|
||||
iPhone: iOS
|
||||
second:
|
||||
enabled: 0
|
||||
settings: {}
|
||||
- first:
|
||||
tvOS: tvOS
|
||||
second:
|
||||
enabled: 1
|
||||
settings: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 8675c92422b8d45f49ced2e0f1602f32
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -1,160 +0,0 @@
|
|||
//
|
||||
// Constants.swift
|
||||
// AgoraChatRoom
|
||||
//
|
||||
// Created by LXH on 2019/11/27.
|
||||
// Copyright © 2019 CavanSu. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
internal struct Constants {
|
||||
|
||||
private static let appVersion: String = {
|
||||
guard let infoDict = Bundle.main.infoDictionary,
|
||||
let currentVersion = infoDict["CFBundleShortVersionString"] as? String else {
|
||||
return ""
|
||||
}
|
||||
return currentVersion
|
||||
}()
|
||||
|
||||
private static let appBundleIdentifier: String = {
|
||||
guard let infoDictionary = Bundle.main.infoDictionary,
|
||||
let shortVersion = infoDictionary["CFBundleIdentifier"] as? String else {
|
||||
return ""
|
||||
}
|
||||
return shortVersion
|
||||
}()
|
||||
|
||||
private static let preferredLocale: Locale = {
|
||||
guard let preferredIdentifier = Locale.preferredLanguages.first else {
|
||||
return Locale.current
|
||||
}
|
||||
return Locale(identifier: preferredIdentifier)
|
||||
}()
|
||||
|
||||
private static let countryCode: String = {
|
||||
return preferredLocale.regionCode?.uppercased() ?? ""
|
||||
}()
|
||||
|
||||
private static let timeZone: String = {
|
||||
return TimeZone.current.identifier
|
||||
}()
|
||||
|
||||
private static let languageCode: String = {
|
||||
return preferredLocale.languageCode ?? ""
|
||||
}()
|
||||
|
||||
private static let localeCode: String = {
|
||||
return preferredLocale.identifier
|
||||
}()
|
||||
|
||||
private static let modelName: String = {
|
||||
return platform().deviceType.rawValue
|
||||
}()
|
||||
|
||||
private static let model: String = {
|
||||
return hardwareString()
|
||||
}()
|
||||
|
||||
private static let systemVersion: String = {
|
||||
return UIDevice.current.systemVersion
|
||||
}()
|
||||
|
||||
private static let screenSize: (w: CGFloat, h: CGFloat) = {
|
||||
return (UIScreen.main.bounds.width, UIScreen.main.bounds.height)
|
||||
}()
|
||||
|
||||
/// 时区偏移毫秒数
|
||||
private static let tzOffset: Int64 = {
|
||||
return Int64(TimeZone.current.secondsFromGMT(for: Date())) * 1000
|
||||
}()
|
||||
|
||||
static var deviceInfo: [String : Any] {
|
||||
return [
|
||||
"country": countryCode,
|
||||
"platform": "IOS",
|
||||
"appId" : appBundleIdentifier,
|
||||
"version" : appVersion,
|
||||
"tzOffset": tzOffset,
|
||||
"deviceType" : modelName,
|
||||
"brand": "Apple",
|
||||
"model": model,
|
||||
"screenH": Int(screenSize.h),
|
||||
"screenW": Int(screenSize.w),
|
||||
"osVersion": systemVersion,
|
||||
"language" : languageCode
|
||||
]
|
||||
}
|
||||
|
||||
/// This method returns the hardware type
|
||||
///
|
||||
///
|
||||
/// - returns: raw `String` of device type, e.g. iPhone5,1
|
||||
///
|
||||
private static func hardwareString() -> String {
|
||||
var name: [Int32] = [CTL_HW, HW_MACHINE]
|
||||
var size: Int = 2
|
||||
sysctl(&name, 2, nil, &size, nil, 0)
|
||||
var hw_machine = [CChar](repeating: 0, count: Int(size))
|
||||
sysctl(&name, 2, &hw_machine, &size, nil, 0)
|
||||
|
||||
var hardware: String = String(cString: hw_machine)
|
||||
|
||||
// Check for simulator
|
||||
if hardware == "x86_64" || hardware == "i386" || hardware == "arm64" {
|
||||
if let deviceID = ProcessInfo.processInfo.environment["SIMULATOR_MODEL_IDENTIFIER"] {
|
||||
hardware = deviceID
|
||||
}
|
||||
}
|
||||
|
||||
return hardware
|
||||
}
|
||||
|
||||
/// This method returns the Platform enum depending upon harware string
|
||||
///
|
||||
///
|
||||
/// - returns: `Platform` type of the device
|
||||
///
|
||||
static func platform() -> Platform {
|
||||
|
||||
let hardware = hardwareString()
|
||||
|
||||
if (hardware.hasPrefix("iPhone")) { return .iPhone }
|
||||
if (hardware.hasPrefix("iPod")) { return .iPodTouch }
|
||||
if (hardware.hasPrefix("iPad")) { return .iPad }
|
||||
if (hardware.hasPrefix("Watch")) { return .appleWatch }
|
||||
if (hardware.hasPrefix("AppleTV")) { return .appleTV }
|
||||
|
||||
return .unknown
|
||||
}
|
||||
|
||||
enum Platform {
|
||||
case iPhone
|
||||
case iPodTouch
|
||||
case iPad
|
||||
case appleWatch
|
||||
case appleTV
|
||||
case unknown
|
||||
|
||||
enum DeviceType: String {
|
||||
case mobile, tablet, desktop, smartTV, watch, other
|
||||
}
|
||||
|
||||
var deviceType: DeviceType {
|
||||
switch self {
|
||||
case .iPad:
|
||||
return .tablet
|
||||
case .iPhone, .iPodTouch:
|
||||
return .mobile
|
||||
case .appleTV:
|
||||
return .smartTV
|
||||
case .appleWatch:
|
||||
return .watch
|
||||
case .unknown:
|
||||
return .other
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -1,49 +0,0 @@
|
|||
fileFormatVersion: 2
|
||||
guid: b7b38f09cfd4b47dc8ffd0e05fd14523
|
||||
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: 1
|
||||
- first:
|
||||
Any:
|
||||
second:
|
||||
enabled: 0
|
||||
settings: {}
|
||||
- first:
|
||||
Editor: Editor
|
||||
second:
|
||||
enabled: 0
|
||||
settings:
|
||||
DefaultValueInitialized: true
|
||||
- first:
|
||||
iPhone: iOS
|
||||
second:
|
||||
enabled: 0
|
||||
settings: {}
|
||||
- first:
|
||||
tvOS: tvOS
|
||||
second:
|
||||
enabled: 1
|
||||
settings: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -1,54 +0,0 @@
|
|||
//
|
||||
// EncodableExtension.swift
|
||||
// Runner
|
||||
//
|
||||
// Created by 袁仕崇 on 2020/5/19.
|
||||
// Copyright © 2020 Guru. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
internal extension Encodable {
|
||||
func asDictionary() throws -> [String: Any] {
|
||||
let data = try JSONEncoder().encode(self)
|
||||
guard let dictionary = try JSONSerialization.jsonObject(with: data, options: .allowFragments) as? [String: Any] else {
|
||||
throw NSError()
|
||||
}
|
||||
return dictionary
|
||||
}
|
||||
|
||||
var dictionary: [String: Any]? {
|
||||
guard let data = try? JSONEncoder().encode(self) else { return nil }
|
||||
return (try? JSONSerialization.jsonObject(with: data, options: .allowFragments)).flatMap { $0 as? [String: Any] }
|
||||
}
|
||||
|
||||
var asString: String? {
|
||||
guard let data = try? JSONEncoder().encode(self) else { return nil }
|
||||
return String(data: data, encoding: .utf8)
|
||||
}
|
||||
}
|
||||
|
||||
internal extension String {
|
||||
|
||||
func jsonObject() -> [String: Any]? {
|
||||
|
||||
guard let data = data(using: .utf8) else {
|
||||
return nil
|
||||
}
|
||||
guard let jsonData = try? JSONSerialization.jsonObject(with: data, options: .fragmentsAllowed) as? [String: Any] else {
|
||||
return nil
|
||||
}
|
||||
return jsonData
|
||||
}
|
||||
|
||||
func jsonArrayObject() -> [[String: Any]]? {
|
||||
|
||||
guard let data = data(using: .utf8) else {
|
||||
return nil
|
||||
}
|
||||
guard let jsonData = try? JSONSerialization.jsonObject(with: data, options: .fragmentsAllowed) as? [[String: Any]] else {
|
||||
return nil
|
||||
}
|
||||
return jsonData
|
||||
}
|
||||
}
|
||||
|
|
@ -1,49 +0,0 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 8225294b372d84b579cecb97daca5100
|
||||
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: 1
|
||||
- first:
|
||||
Any:
|
||||
second:
|
||||
enabled: 0
|
||||
settings: {}
|
||||
- first:
|
||||
Editor: Editor
|
||||
second:
|
||||
enabled: 0
|
||||
settings:
|
||||
DefaultValueInitialized: true
|
||||
- first:
|
||||
iPhone: iOS
|
||||
second:
|
||||
enabled: 0
|
||||
settings: {}
|
||||
- first:
|
||||
tvOS: tvOS
|
||||
second:
|
||||
enabled: 1
|
||||
settings: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -1,25 +0,0 @@
|
|||
//
|
||||
// Helper.swift
|
||||
// GuruAnalytics_iOS
|
||||
//
|
||||
// Created by mayue on 2022/11/4.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
internal func cdPrint(_ items: Any..., context: String? = nil, separator: String = " ", terminator: String = "\n") {
|
||||
#if DEBUG
|
||||
guard GuruAnalytics.loggerDebug else { return }
|
||||
let date = Date()
|
||||
let df = DateFormatter()
|
||||
df.dateFormat = "HH:mm:ss.SSSS"
|
||||
let dateString = df.string(from: date)
|
||||
|
||||
print("\(dateString) [GuruAnalytics] Thread: \(Thread.current.queueName) \(context ?? "") ", terminator: "")
|
||||
for item in items {
|
||||
print(item, terminator: " ")
|
||||
}
|
||||
print("", terminator: terminator)
|
||||
#else
|
||||
#endif
|
||||
}
|
||||
|
|
@ -1,49 +0,0 @@
|
|||
fileFormatVersion: 2
|
||||
guid: bf62f6d630bd84c75834df5c964dee4f
|
||||
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: 1
|
||||
- first:
|
||||
Any:
|
||||
second:
|
||||
enabled: 0
|
||||
settings: {}
|
||||
- first:
|
||||
Editor: Editor
|
||||
second:
|
||||
enabled: 0
|
||||
settings:
|
||||
DefaultValueInitialized: true
|
||||
- first:
|
||||
iPhone: iOS
|
||||
second:
|
||||
enabled: 0
|
||||
settings: {}
|
||||
- first:
|
||||
tvOS: tvOS
|
||||
second:
|
||||
enabled: 1
|
||||
settings: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
//
|
||||
// JSONDecoder.Extension.swift
|
||||
// Moya-Cuddle
|
||||
//
|
||||
// Created by Wilson-Yuan on 2019/12/25.
|
||||
// Copyright © 2019 Guru. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
internal extension JSONDecoder {
|
||||
func decodeAnyData<T>(_ type: T.Type, from data: Any) throws -> T where T: Decodable {
|
||||
var unwrappedData = Data()
|
||||
if let data = data as? Data {
|
||||
unwrappedData = data
|
||||
}
|
||||
else if let data = data as? [String: Any] {
|
||||
unwrappedData = try JSONSerialization.data(withJSONObject: data, options: .prettyPrinted)
|
||||
}
|
||||
else if let data = data as? [[String: Any]] {
|
||||
unwrappedData = try JSONSerialization.data(withJSONObject: data, options: .prettyPrinted)
|
||||
}
|
||||
else {
|
||||
fatalError("error format of data ")
|
||||
}
|
||||
return try decode(type, from: unwrappedData)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,49 +0,0 @@
|
|||
fileFormatVersion: 2
|
||||
guid: a45044c2a092c455881ad4f39fdeec2b
|
||||
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: 1
|
||||
- first:
|
||||
Any:
|
||||
second:
|
||||
enabled: 0
|
||||
settings: {}
|
||||
- first:
|
||||
Editor: Editor
|
||||
second:
|
||||
enabled: 0
|
||||
settings:
|
||||
DefaultValueInitialized: true
|
||||
- first:
|
||||
iPhone: iOS
|
||||
second:
|
||||
enabled: 0
|
||||
settings: {}
|
||||
- first:
|
||||
tvOS: tvOS
|
||||
second:
|
||||
enabled: 1
|
||||
settings: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -1,158 +0,0 @@
|
|||
//
|
||||
// Logger.swift
|
||||
// GuruAnalyticsLib
|
||||
//
|
||||
// Created by mayue on 2022/12/21.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import SwiftyBeaver
|
||||
import CryptoSwift
|
||||
import RxSwift
|
||||
|
||||
internal class LoggerManager {
|
||||
|
||||
private static let password: String = "Castbox123"
|
||||
|
||||
private lazy var logger: SwiftyBeaver.Type = {
|
||||
let logger = SwiftyBeaver.self
|
||||
logger.addDestination(consoleOutputDestination)
|
||||
logger.addDestination(fileOutputDestination)
|
||||
return logger
|
||||
}()
|
||||
|
||||
private lazy var logFileDir: URL = {
|
||||
let baseDir = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask).first!
|
||||
return baseDir.appendingPathComponent("GuruAnalytics/Logs/\(logCategoryName)/", isDirectory: true)
|
||||
}()
|
||||
|
||||
private lazy var consoleOutputDestination: ConsoleDestination = {
|
||||
let d = ConsoleDestination()
|
||||
return d
|
||||
}()
|
||||
|
||||
private lazy var fileOutputDestination: FileDestination = {
|
||||
let file = FileDestination()
|
||||
let dateFormatter = DateFormatter()
|
||||
dateFormatter.dateFormat = "yyyy-MM-dd"
|
||||
let dateString = dateFormatter.string(from: Date())
|
||||
file.logFileURL = logFileDir.appendingPathComponent("\(dateString).log", isDirectory: false)
|
||||
file.asynchronously = true
|
||||
return file
|
||||
}()
|
||||
|
||||
private let logCategoryName: String
|
||||
|
||||
internal init(logCategoryName: String) {
|
||||
self.logCategoryName = logCategoryName
|
||||
}
|
||||
}
|
||||
|
||||
internal extension LoggerManager {
|
||||
|
||||
func logFilesZipArchive() -> Single<URL?> {
|
||||
|
||||
return Single.create { subscriber in
|
||||
subscriber(.success(nil))
|
||||
return Disposables.create()
|
||||
}
|
||||
.observe(on: MainScheduler.asyncInstance)
|
||||
}
|
||||
|
||||
func logFilesDirURL() -> Single<URL?> {
|
||||
|
||||
return Single.create { subscriber in
|
||||
|
||||
DispatchQueue.global().async { [weak self] in
|
||||
guard let `self` = self else {
|
||||
subscriber(.failure(
|
||||
NSError(domain: "loggerManager", code: 0, userInfo: [NSLocalizedDescriptionKey : "manager is released"])
|
||||
))
|
||||
return
|
||||
}
|
||||
|
||||
do {
|
||||
let filePaths = try FileManager.default.contentsOfDirectory(at: self.logFileDir,
|
||||
includingPropertiesForKeys: nil,
|
||||
options: [.skipsHiddenFiles])
|
||||
.filter { $0.pathExtension == "log" }
|
||||
.map { $0.path }
|
||||
|
||||
guard filePaths.count > 0 else {
|
||||
subscriber(.success(nil))
|
||||
return
|
||||
}
|
||||
subscriber(.success(self.logFileDir))
|
||||
} catch {
|
||||
subscriber(.failure(error))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return Disposables.create()
|
||||
}
|
||||
.observe(on: MainScheduler.asyncInstance)
|
||||
}
|
||||
|
||||
func clearAllLogFiles() {
|
||||
|
||||
DispatchQueue.global().async { [weak self] in
|
||||
guard let `self` = self else { return }
|
||||
if let files = try? FileManager.default.contentsOfDirectory(at: self.logFileDir, includingPropertiesForKeys: [], options: [.skipsHiddenFiles]) {
|
||||
files.forEach { url in
|
||||
do {
|
||||
try FileManager.default.removeItem(at: url)
|
||||
} catch {
|
||||
cdPrint("remove file: \(url.path) \n error: \(error)")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func verbose(_ message: Any,
|
||||
_ file: String = #file,
|
||||
_ function: String = #function,
|
||||
line: Int = #line,
|
||||
context: Any? = nil) {
|
||||
guard GuruAnalytics.loggerDebug else { return }
|
||||
logger.verbose(message, file, function, line: line, context: context)
|
||||
}
|
||||
|
||||
func debug(_ message: Any,
|
||||
_ file: String = #file,
|
||||
_ function: String = #function,
|
||||
line: Int = #line,
|
||||
context: Any? = nil) {
|
||||
guard GuruAnalytics.loggerDebug else { return }
|
||||
logger.debug(message, file, function, line: line, context: context)
|
||||
}
|
||||
|
||||
func info(_ message: Any,
|
||||
_ file: String = #file,
|
||||
_ function: String = #function,
|
||||
line: Int = #line,
|
||||
context: Any? = nil) {
|
||||
guard GuruAnalytics.loggerDebug else { return }
|
||||
logger.info(message, file, function, line: line, context: context)
|
||||
}
|
||||
|
||||
func warning(_ message: Any,
|
||||
_ file: String = #file,
|
||||
_ function: String = #function,
|
||||
line: Int = #line,
|
||||
context: Any? = nil) {
|
||||
guard GuruAnalytics.loggerDebug else { return }
|
||||
logger.warning(message, file, function, line: line, context: context)
|
||||
}
|
||||
|
||||
func error(_ message: Any,
|
||||
_ file: String = #file,
|
||||
_ function: String = #function,
|
||||
line: Int = #line,
|
||||
context: Any? = nil) {
|
||||
guard GuruAnalytics.loggerDebug else { return }
|
||||
logger.error(message, file, function, line: line, context: context)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,49 +0,0 @@
|
|||
fileFormatVersion: 2
|
||||
guid: c0c260bf3d64e48688db3309a7ea46c3
|
||||
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: 1
|
||||
- first:
|
||||
Any:
|
||||
second:
|
||||
enabled: 0
|
||||
settings: {}
|
||||
- first:
|
||||
Editor: Editor
|
||||
second:
|
||||
enabled: 0
|
||||
settings:
|
||||
DefaultValueInitialized: true
|
||||
- first:
|
||||
iPhone: iOS
|
||||
second:
|
||||
enabled: 0
|
||||
settings: {}
|
||||
- first:
|
||||
tvOS: tvOS
|
||||
second:
|
||||
enabled: 1
|
||||
settings: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -1,32 +0,0 @@
|
|||
//
|
||||
// ThreadExtension.swift
|
||||
// GuruAnalyticsLib
|
||||
//
|
||||
// Created by 袁仕崇 on 17/02/23.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
extension Thread {
|
||||
var threadName: String {
|
||||
if isMainThread {
|
||||
return "main"
|
||||
} else if let threadName = Thread.current.name, !threadName.isEmpty {
|
||||
return threadName
|
||||
} else {
|
||||
return description
|
||||
}
|
||||
}
|
||||
|
||||
var queueName: String {
|
||||
if let queueName = String(validatingUTF8: __dispatch_queue_get_label(nil)) {
|
||||
return queueName
|
||||
} else if let operationQueueName = OperationQueue.current?.name, !operationQueueName.isEmpty {
|
||||
return operationQueueName
|
||||
} else if let dispatchQueueName = OperationQueue.current?.underlyingQueue?.label, !dispatchQueueName.isEmpty {
|
||||
return dispatchQueueName
|
||||
} else {
|
||||
return "n/a"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,49 +0,0 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 7ae00a082cf8149ea808b748124345a4
|
||||
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: 1
|
||||
- first:
|
||||
Any:
|
||||
second:
|
||||
enabled: 0
|
||||
settings: {}
|
||||
- first:
|
||||
Editor: Editor
|
||||
second:
|
||||
enabled: 0
|
||||
settings:
|
||||
DefaultValueInitialized: true
|
||||
- first:
|
||||
iPhone: iOS
|
||||
second:
|
||||
enabled: 0
|
||||
settings: {}
|
||||
- first:
|
||||
tvOS: tvOS
|
||||
second:
|
||||
enabled: 1
|
||||
settings: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -1,158 +0,0 @@
|
|||
//
|
||||
// Utilities.swift
|
||||
// GuruAnalytics_iOS
|
||||
//
|
||||
// Created by mayue on 2022/11/4.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import RxSwift
|
||||
|
||||
internal extension TimeInterval {
|
||||
|
||||
var int64Ms: Int64 {
|
||||
return Int64(self * 1000)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
internal extension Date {
|
||||
|
||||
var msSince1970: Int64 {
|
||||
timeIntervalSince1970.int64Ms
|
||||
}
|
||||
|
||||
static var absoluteTimeMs: Int64 {
|
||||
return CACurrentMediaTime().int64Ms
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
internal extension Dictionary {
|
||||
|
||||
func jsonString(prettify: Bool = false) -> String? {
|
||||
guard JSONSerialization.isValidJSONObject(self) else { return nil }
|
||||
let options = (prettify == true) ? JSONSerialization.WritingOptions.prettyPrinted : JSONSerialization.WritingOptions()
|
||||
guard let jsonData = try? JSONSerialization.data(withJSONObject: self, options: options) else { return nil }
|
||||
return String(data: jsonData, encoding: .utf8)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
internal extension String {
|
||||
func convertToDictionary() -> [String: Any]? {
|
||||
if let data = data(using: .utf8) {
|
||||
return (try? JSONSerialization.jsonObject(with: data, options: [])) as? [String: Any]
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
mutating func deletePrefix(_ prefix: String) {
|
||||
guard hasPrefix(prefix) else { return }
|
||||
if #available(iOS 16.0, *) {
|
||||
trimPrefix(prefix)
|
||||
} else {
|
||||
removeFirst(prefix.count)
|
||||
}
|
||||
}
|
||||
|
||||
mutating func trimmed(in set: CharacterSet) {
|
||||
self = trimmingCharacters(in: set)
|
||||
}
|
||||
}
|
||||
|
||||
internal extension Array {
|
||||
func chunked(into size: Int) -> [[Element]] {
|
||||
return stride(from: 0, to: count, by: size).map {
|
||||
Array(self[$0 ..< Swift.min($0 + size, count)])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal class SafeValue<T> {
|
||||
|
||||
private var _value: T
|
||||
|
||||
private let queue = DispatchQueue(label: "com.guru.analytics.safe.value.reader.writer.queue", attributes: .concurrent)
|
||||
private let group = DispatchGroup()
|
||||
|
||||
internal init(_ value: T) {
|
||||
_value = value
|
||||
}
|
||||
|
||||
internal func setValue(_ value: T) {
|
||||
queue.async(group: group, execute: .init(flags: .barrier, block: { [weak self] in
|
||||
self?._value = value
|
||||
}))
|
||||
}
|
||||
|
||||
internal func getValue(_ valueBlock: @escaping ((T) -> Void)) {
|
||||
queue.async(group: group, execute: .init(block: { [weak self] in
|
||||
guard let `self` = self else { return }
|
||||
valueBlock(self._value)
|
||||
}))
|
||||
}
|
||||
|
||||
internal var singleValue: Single<T> {
|
||||
return Single.create { [weak self] subscriber in
|
||||
|
||||
self?.getValue { value in
|
||||
subscriber(.success(value))
|
||||
}
|
||||
|
||||
return Disposables.create()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal extension SafeValue where T == Dictionary<String, String> {
|
||||
|
||||
func mergeValue(_ value: T) -> Single<Void> {
|
||||
return .create { [weak self] subscriber in
|
||||
guard let `self` = self else {
|
||||
subscriber(.failure(
|
||||
NSError(domain: "safevalue", code: 0, userInfo: [NSLocalizedDescriptionKey : "safevalue object is released"])
|
||||
))
|
||||
return Disposables.create()
|
||||
}
|
||||
self.getValue { currentValue in
|
||||
let newValue = currentValue.merging(value) { _, new in new }
|
||||
self.setValue(newValue)
|
||||
subscriber(.success(()))
|
||||
}
|
||||
|
||||
return Disposables.create()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal extension SafeValue where T == Array<String> {
|
||||
|
||||
func appendValue(_ value: T) {
|
||||
getValue { [weak self] v in
|
||||
var currentValue = v
|
||||
currentValue.append(contentsOf: value)
|
||||
self?.setValue(currentValue)
|
||||
}
|
||||
}
|
||||
|
||||
func removeAll(where shouldBeRemoved: @escaping (Array<String>.Element) -> Bool) {
|
||||
getValue { [weak self] v in
|
||||
var currentValue = v
|
||||
currentValue.removeAll(where: shouldBeRemoved)
|
||||
self?.setValue(currentValue)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
internal extension Character {
|
||||
|
||||
var isAlphabetic: Bool {
|
||||
return (self >= "a" && self <= "z") || (self >= "A" && self <= "Z")
|
||||
}
|
||||
|
||||
var isDigit: Bool {
|
||||
return self >= "0" && self <= "9"
|
||||
}
|
||||
}
|
||||
|
|
@ -1,49 +0,0 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 1f70e4103a1d748d4af94d840c55a281
|
||||
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: 1
|
||||
- first:
|
||||
Any:
|
||||
second:
|
||||
enabled: 0
|
||||
settings: {}
|
||||
- first:
|
||||
Editor: Editor
|
||||
second:
|
||||
enabled: 0
|
||||
settings:
|
||||
DefaultValueInitialized: true
|
||||
- first:
|
||||
iPhone: iOS
|
||||
second:
|
||||
enabled: 0
|
||||
settings: {}
|
||||
- first:
|
||||
tvOS: tvOS
|
||||
second:
|
||||
enabled: 1
|
||||
settings: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -1,54 +0,0 @@
|
|||
#
|
||||
# Be sure to run `pod lib lint GuruAnalytics.podspec' to ensure this is a
|
||||
# valid spec before submitting.
|
||||
#
|
||||
# Any lines starting with a # are optional, but their use is encouraged
|
||||
# To learn more about a Podspec see https://guides.cocoapods.org/syntax/podspec.html
|
||||
#
|
||||
|
||||
Pod::Spec.new do |s|
|
||||
s.name = 'GuruAnalyticsLib'
|
||||
s.version = '0.3.6'
|
||||
s.summary = 'A short description of GuruAnalytics.'
|
||||
|
||||
# This description is used to generate tags and improve search results.
|
||||
# * Think: What does it do? Why did you write it? What is the focus?
|
||||
# * Try to keep it short, snappy and to the point.
|
||||
# * Write the description between the DESC delimiters below.
|
||||
# * Finally, don't worry about the indent, CocoaPods strips it!
|
||||
|
||||
s.description = <<-DESC
|
||||
TODO: Add long description of the pod here.
|
||||
DESC
|
||||
|
||||
s.homepage = 'https://github.com/castbox/GuruAnalytics_iOS'
|
||||
# s.screenshots = 'www.example.com/screenshots_1', 'www.example.com/screenshots_2'
|
||||
s.license = { :type => 'MIT', :file => 'LICENSE' }
|
||||
s.author = { 'devSC' => 'xiaochong2154@163.com' }
|
||||
# s.source = { :git => 'git@github.com:castbox/GuruAnalytics_iOS.git', :tag => s.version.to_s }
|
||||
s.source = { :tag => s.version.to_s }
|
||||
# s.social_media_url = 'https://twitter.com/<TWITTER_USERNAME>'
|
||||
|
||||
s.ios.deployment_target = '11.0'
|
||||
s.swift_version = '5'
|
||||
s.source_files = 'GuruAnalytics/Classes/**/*'
|
||||
# s.resource_bundles = {
|
||||
# 'GuruAnalytics' => ['GuruAnalytics/Assets/*.png']
|
||||
# }
|
||||
|
||||
# s.public_header_files = 'Pod/Classes/**/*.h'
|
||||
# s.frameworks = 'UIKit', 'MapKit'
|
||||
# s.dependency 'AFNetworking', '~> 2.3'
|
||||
s.dependency 'RxCocoa', '~> 6.7.0'
|
||||
s.dependency 'Alamofire', '~> 5.9'
|
||||
s.dependency 'FMDB', '~> 2.0'
|
||||
s.dependency 'GzipSwift', '~> 5.0'
|
||||
s.dependency 'CryptoSwift', '~> 1.0'
|
||||
s.dependency 'SwiftyBeaver', '~> 1.0'
|
||||
|
||||
s.subspec 'Privacy' do |ss|
|
||||
ss.resource_bundles = {
|
||||
s.name => 'GuruAnalytics/Assets/PrivacyInfo.xcprivacy'
|
||||
}
|
||||
end
|
||||
end
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 09c4804bc0744da2bcc1f3fa6479b8b6
|
||||
timeCreated: 1717115698
|
||||
|
|
@ -2,42 +2,10 @@
|
|||
|
||||
GuruAnalyticsLib 的 Unity 插件库
|
||||
|
||||
## 研发注意:
|
||||
- **Android**
|
||||
- 插件库内的 .aar 通过 [guru_analytics](https://github.com/castbox/guru_analytics) 项目直接构建 ( 命令 `gradle publishToMavenLocal` )
|
||||
- 构建后请改名为 `guru-analytics-{version}.aar`
|
||||
- 请将 .aar 文件放置于 `./Runtime/GuruAnalytics/Plugins/Android` 目录下
|
||||
- **iOS**
|
||||
- 插件库内的文件 通过 [GuruAnalytics_iOS](https://github.com/castbox/GuruAnalytics_iOS) 项目
|
||||
- (1) 请将 repo 内的两个文件夹 `Assets` 和 `Classses` 拷贝至 `./Runtime/GuruAnalytics/Plugins/iOS/GuruAnalytics` 目录下:
|
||||
- (2) 请将部署到 Unity 内所有的 `.swift` 文件的 meta 属性内, 取消 iOS 文件属性. (因为打包时会按照 POD 导入)
|
||||
- 注意及时更新 `GuruAnalyticsLib.podspec`文件内的更新内容
|
||||
```ruby
|
||||
# 将 source 内的 git 属性删除, 只保留 tag 属性
|
||||
# s.source = { :git => 'git@github.com:castbox/GuruAnalytics_iOS.git', :tag => s.version.to_s }
|
||||
s.source = { :tag => s.version.to_s }
|
||||
```
|
||||
---
|
||||
|
||||
## Change Logs
|
||||
|
||||
### 1.12.0
|
||||
- Android 端对齐 `1.1.1`
|
||||
> Hash: bdb41ae118dcf438e8efe4f27d0ec856bc3147b0
|
||||
- iOS 端对齐 `0.3.6`
|
||||
> Hash: 0cd5ce7aa64e12caa7413c938a3164687b973843
|
||||
- Pod 库改为 本地文件引用 (配合外部发行项目)
|
||||
|
||||
|
||||
|
||||
### 1.11.0
|
||||
- Android 端对齐 `1.0.3`
|
||||
> Hash: 1978686dbcba38b7b0421d8b6b2bef111356366b
|
||||
- iOS 端对齐 `0.3.6`
|
||||
> Hash: 0cd5ce7aa64e12caa7413c938a3164687b973843
|
||||
- Pod 库改为 本地文件引用 (配合外部发行项目)
|
||||
|
||||
|
||||
### 1.9.0
|
||||
- Android 端对齐 0.3.1+.
|
||||
> Hash: 0457eba963a9049fb6a16708b921573ef36c99b1
|
||||
|
|
|
|||
|
|
@ -14,11 +14,10 @@ namespace Guru
|
|||
public class GuruAnalytics
|
||||
{
|
||||
// Plugin Version
|
||||
public const string Version = "1.10.5";
|
||||
public const string Version = "1.10.4";
|
||||
|
||||
public static readonly string Tag = "[ANU]";
|
||||
private static readonly string ActionName = "logger_error";
|
||||
internal const int EventPriorityDefault = 10;
|
||||
|
||||
private static IAnalyticsAgent _agent;
|
||||
|
||||
|
|
@ -86,8 +85,6 @@ namespace Guru
|
|||
public static void Init(string appId, string deviceInfo, bool isDebug = false,
|
||||
bool enableErrorLog = false, bool syncProperties = false)
|
||||
{
|
||||
Debug.Log($"{Tag} --- Guru Analytics [{Version}] initialing...");
|
||||
|
||||
_autoSyncProperties = syncProperties;
|
||||
_enableErrorLog = enableErrorLog;
|
||||
Agent?.Init(appId, deviceInfo, isDebug);
|
||||
|
|
@ -201,8 +198,7 @@ namespace Guru
|
|||
/// </summary>
|
||||
/// <param name="eventName">事件名称</param>
|
||||
/// <param name="data">INT类型的值</param>
|
||||
/// <param name="priority"></param>
|
||||
public static void LogEvent(string eventName, Dictionary<string, dynamic> data = null, int priority = -1)
|
||||
public static void LogEvent(string eventName, Dictionary<string, dynamic> data = null)
|
||||
{
|
||||
if(_autoSyncProperties)
|
||||
UpdateAllUserProperties(); // 每次打点更新用户属性
|
||||
|
|
@ -212,9 +208,8 @@ namespace Guru
|
|||
{
|
||||
raw = BuildParamsJson(data);
|
||||
}
|
||||
if (priority < 0) priority = EventPriorityDefault;
|
||||
Debug.Log($"{Tag} event:{eventName} | raw: {raw} | priority: {priority}");
|
||||
Agent?.LogEvent(eventName, raw, priority);
|
||||
Debug.Log($"{Tag} event:{eventName} | raw: {raw}");
|
||||
Agent?.LogEvent(eventName, raw);
|
||||
}
|
||||
|
||||
private static string BuildParamsString(Dictionary<string, dynamic> data)
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ namespace Guru
|
|||
void SetUid(string uid);
|
||||
bool IsDebug { get; }
|
||||
bool EnableErrorLog { get; set; }
|
||||
void LogEvent(string eventName, string parameters, int priority = -1);
|
||||
void LogEvent(string eventName, string parameters);
|
||||
void ReportEventSuccessRate(); // 上报任务成功率
|
||||
void SetTch02Value(double value); // 设置太极02数值
|
||||
void InitCallback(string objName, string method); // 设置回调对象参数
|
||||
|
|
|
|||
|
|
@ -80,8 +80,7 @@ namespace Guru
|
|||
{
|
||||
_isDebug = isDebug;
|
||||
string bundleId = Application.identifier;
|
||||
// public static void init(String appId, String deviceInfo, String bundleId, boolean isDebug, boolean useWorker, boolean useCronet, String baseUrl)
|
||||
CallStatic("init", appId, deviceInfo, bundleId, isDebug, UseWorker, UseCronet, BaseUrl); // 调用接口
|
||||
CallStatic("init", appId, deviceInfo, bundleId, UseWorker, isDebug, UseCronet, BaseUrl); // 调用接口
|
||||
}
|
||||
|
||||
public void SetScreen(string screenName)
|
||||
|
|
@ -125,10 +124,7 @@ namespace Guru
|
|||
}
|
||||
|
||||
public bool IsDebug => CallStatic<bool>("isDebug");
|
||||
public void LogEvent(string eventName, string parameters, int priority = -1)
|
||||
{
|
||||
CallStatic("logEvent", eventName, parameters, priority);
|
||||
}
|
||||
public void LogEvent(string eventName, string parameters) => CallStatic("logEvent", eventName, parameters);
|
||||
public void ReportEventSuccessRate() => CallStatic("reportEventRate");
|
||||
public void SetTch02Value(double value) => CallStatic("setTch02Value", value);
|
||||
public void InitCallback(string objName, string method) => CallStatic("initCallback", objName, method);
|
||||
|
|
|
|||
|
|
@ -122,7 +122,7 @@ namespace Guru
|
|||
|
||||
public bool IsDebug => _isDebug;
|
||||
|
||||
public void LogEvent(string eventName, string data, int priority = -1)
|
||||
public void LogEvent(string eventName, string data)
|
||||
{
|
||||
#if UNITY_IOS
|
||||
unityLogEvent(eventName, data);
|
||||
|
|
|
|||
|
|
@ -87,7 +87,7 @@ namespace Guru
|
|||
public bool IsDebug => _isDebug;
|
||||
|
||||
|
||||
public void LogEvent(string eventName, string parameters, int priority = -1)
|
||||
public void LogEvent(string eventName, string parameters)
|
||||
{
|
||||
if (_isShowLog)
|
||||
{
|
||||
|
|
@ -127,7 +127,7 @@ namespace Guru
|
|||
}
|
||||
}
|
||||
|
||||
Debug.Log($"{TAG} LogEvent: event:<color=orange>{eventName} ({priority})</color> Properties:\n{sb.ToString()}");
|
||||
Debug.Log($"{TAG} LogEvent: event:<color=orange>{eventName}</color> Properties:\n{sb.ToString()}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,6 +10,10 @@ Sample Dependencies.xml:
|
|||
<androidPackage spec="com.mapzen:on-the-road:0.8.1" />
|
||||
</androidPackages>
|
||||
<iosPods>
|
||||
<iosPod name="GuruConsent" bitcodeEnabled="false" path="Packages/com.guru.unity.sdk.core/Runtime/GuruConsent/Plugins/iOS" />
|
||||
<iosPod name="GuruConsent" bitcodeEnabled="false">
|
||||
<sources>
|
||||
<source>git@github.com:castbox/GuruSpecs.git</source>
|
||||
</sources>
|
||||
</iosPod>>
|
||||
</iosPods>
|
||||
</dependencies>
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -1,3 +0,0 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 78bd1ca99d374a1f9f31be5cbf38b03c
|
||||
timeCreated: 1717118600
|
||||
|
|
@ -1,40 +0,0 @@
|
|||
#
|
||||
# Be sure to run `pod lib lint CastboxNetwork.podspec' to ensure this is a
|
||||
# valid spec before submitting.
|
||||
#
|
||||
# Any lines starting with a # are optional, but their use is encouraged
|
||||
# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html
|
||||
#
|
||||
|
||||
Pod::Spec.new do |s|
|
||||
s.name = 'GuruConsent'
|
||||
s.version = '1.4.6'
|
||||
s.summary = 'Google GDPR'
|
||||
s.description = 'Google GDPR'
|
||||
|
||||
s.homepage = 'https://github.com/castbox/GuruConsent-iOS'
|
||||
s.license = { :type => 'MIT', :file => 'LICENSE' }
|
||||
s.author = { 'LEE' => 'xiang.li@castbox.fm' }
|
||||
# s.source = { :git => 'git@github.com:castbox/GuruConsent-iOS.git', :tag => s.version }
|
||||
s.source = { :tag => s.version }
|
||||
|
||||
s.frameworks = 'UIKit', 'AppTrackingTransparency'
|
||||
s.swift_version = '5.0'
|
||||
s.ios.deployment_target = '12.0'
|
||||
|
||||
s.source_files = "GuruConsent/Sources/**/*.swift"
|
||||
|
||||
s.requires_arc = true
|
||||
|
||||
s.static_framework = true
|
||||
|
||||
s.default_subspec = 'Privacy'
|
||||
|
||||
s.dependency 'GoogleUserMessagingPlatform', '2.3.0'
|
||||
|
||||
s.subspec 'Privacy' do |ss|
|
||||
ss.resource_bundles = {
|
||||
s.name => 'GuruConsent/Resources/PrivacyInfo.xcprivacy'
|
||||
}
|
||||
end
|
||||
end
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 4d451434a71243edb55e61e8fd0040a0
|
||||
timeCreated: 1717035350
|
||||
|
|
@ -1,21 +0,0 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2022 CastBox Dev Team
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 0819b6a57c25e4ef0bd2f83c97a353fd
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -1,282 +0,0 @@
|
|||
# GuruConsent-iOS
|
||||
|
||||

|
||||
|
||||
## 特性
|
||||
|
||||
- [x] 支持系统ATT权限引导弹窗.
|
||||
- [x] 支持多国语言显示.
|
||||
- [x] 支持调试EEA地理设置等.
|
||||
- [x] 支持结果状态回调.
|
||||
|
||||
## 准备
|
||||
|
||||
将应用 ID 添加到 Info.plist 中:
|
||||
|
||||
```
|
||||
<key>GADApplicationIdentifier</key>
|
||||
<string>YOUR-APP-ID</string>
|
||||
```
|
||||
|
||||
将ATT跟踪权限添加到 Info.plist 中:
|
||||
|
||||
```
|
||||
<key>NSUserTrackingUsageDescription</key>
|
||||
<string>This identifier will be used to deliver personalized ads to you.</string>
|
||||
```
|
||||
|
||||
## 安装
|
||||
|
||||
GuruConsent 仅支持CocoaPods.
|
||||
|
||||
**CocoaPods - Podfile**
|
||||
|
||||
```ruby
|
||||
source 'git@github.com:castbox/GuruSpecs.git'
|
||||
|
||||
pod 'GuruConsent'
|
||||
```
|
||||
|
||||
## 使用
|
||||
|
||||
首先导入framework:
|
||||
|
||||
Swift:
|
||||
|
||||
```swift
|
||||
import GuruConsent
|
||||
```
|
||||
|
||||
Objective-C:
|
||||
|
||||
```objc
|
||||
#import <GuruConsent/GuruConsent-Swift.h>
|
||||
```
|
||||
|
||||
下面是一些简单示例. 支持所有设备和模拟器:
|
||||
|
||||
### 方式一: 自动
|
||||
|
||||
如果满足显示条件 自动显示弹窗 具有一定的延迟效果 但具体显示时机不定, 受网络影响.
|
||||
|
||||
__建议在应用启动后 延后一些调用开始 请务必确保首次启动后已授权网络请求权限再调用__
|
||||
|
||||
Swift:
|
||||
|
||||
```swift
|
||||
// 开始请求
|
||||
GuruConsent.start(from: controller) { result in
|
||||
switch result {
|
||||
case .success(let status):
|
||||
if #available(iOS 14, *) {
|
||||
print("ATT 结果: \(ATTrackingManager.trackingAuthorizationStatus)")
|
||||
}
|
||||
print("GDPR 结果: \(status)")
|
||||
|
||||
case .failure(let error):
|
||||
print("失败: \(error)")
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Objective-C:
|
||||
|
||||
```objc
|
||||
// 开始请求
|
||||
[GuruConsent startFrom:self success:^(enum GuruConsentGDPRStatus status) {
|
||||
|
||||
if (@available(iOS 14, *)) {
|
||||
NSLog(@"ATT 结果: %lu", (unsigned long)ATTrackingManager.trackingAuthorizationStatus);
|
||||
}
|
||||
|
||||
switch (status) {
|
||||
case GuruConsentGDPRStatusUnknown:
|
||||
|
||||
break;
|
||||
|
||||
case GuruConsentGDPRStatusRequired:
|
||||
|
||||
break;
|
||||
|
||||
case GuruConsentGDPRStatusNotRequired:
|
||||
|
||||
break;
|
||||
|
||||
case GuruConsentGDPRStatusObtained:
|
||||
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
NSLog(@"GDPR 结果: %ld", (long)status);
|
||||
|
||||
} failure:^(NSError * _Nonnull error) {
|
||||
NSLog(@"失败: %@", error);
|
||||
}];
|
||||
```
|
||||
|
||||
|
||||
### 方式二: 手动
|
||||
|
||||
先调用准备, 准备完成后在合适的时机手动调用弹窗显示.
|
||||
|
||||
__建议在应用启动后 延后一些调用准备 请务必确保首次启动后已授权网络请求权限再调用__
|
||||
|
||||
Swift:
|
||||
|
||||
```swift
|
||||
// 准备
|
||||
GuruConsent.prepare { result in
|
||||
switch result {
|
||||
case .success(let status):
|
||||
print("GDPR 结果: \(status)")
|
||||
|
||||
case .failure(let error):
|
||||
print("失败: \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 显示 请确保status为.required 否则无法显示
|
||||
GuruConsent.present(from: self) { result in
|
||||
switch result {
|
||||
case .success(let status):
|
||||
if #available(iOS 14, *) {
|
||||
print("ATT 结果: \(ATTrackingManager.trackingAuthorizationStatus)")
|
||||
}
|
||||
print("GDPR 结果: \(status)")
|
||||
|
||||
case .failure(let error):
|
||||
print("失败: \(error)")
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Objective-C:
|
||||
|
||||
```objc
|
||||
// 准备
|
||||
[GuruConsent prepareWithSuccess:^(enum GuruConsentGDPRStatus status) {
|
||||
|
||||
switch (status) {
|
||||
case GuruConsentGDPRStatusUnknown:
|
||||
|
||||
break;
|
||||
|
||||
case GuruConsentGDPRStatusRequired:
|
||||
|
||||
break;
|
||||
|
||||
case GuruConsentGDPRStatusNotRequired:
|
||||
|
||||
break;
|
||||
|
||||
case GuruConsentGDPRStatusObtained:
|
||||
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
NSLog(@"GDPR 结果: %ld", (long)status);
|
||||
|
||||
} failure:^(NSError * _Nonnull error) {
|
||||
NSLog(@"失败: %@", error);
|
||||
}];
|
||||
|
||||
|
||||
// 显示 请确保status为.required 否则无法显示
|
||||
[GuruConsent presentFrom:self success:^(enum GuruConsentGDPRStatus status) {
|
||||
|
||||
if (@available(iOS 14, *)) {
|
||||
NSLog(@"ATT 结果: %lu", (unsigned long)ATTrackingManager.trackingAuthorizationStatus);
|
||||
}
|
||||
|
||||
switch (status) {
|
||||
case GuruConsentGDPRStatusUnknown:
|
||||
|
||||
break;
|
||||
|
||||
case GuruConsentGDPRStatusRequired:
|
||||
|
||||
break;
|
||||
|
||||
case GuruConsentGDPRStatusNotRequired:
|
||||
|
||||
break;
|
||||
|
||||
case GuruConsentGDPRStatusObtained:
|
||||
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
NSLog(@"GDPR 结果: %ld", (long)status);
|
||||
|
||||
} failure:^(NSError * _Nonnull error) {
|
||||
NSLog(@"%@", error);
|
||||
}];
|
||||
```
|
||||
|
||||
|
||||
### 调试设置
|
||||
|
||||
`testDeviceIdentifiers`获取方式: 当传入空置, 运行调用`GuruConsent.start(from:)` Xcode控制台会输出如下:
|
||||
|
||||
```
|
||||
<UMP SDK> To enable debug mode for this device, set: UMPDebugSettings.testDeviceIdentifiers = @[ @"8C5E8576-5090-4C41-8FC4-A5A80FF77D9E" ];
|
||||
```
|
||||
将控制台的`8C5E8576-5090-4C41-8FC4-A5A80FF77D9E` 复制粘贴到代码中, 再次运行即可进行调试.
|
||||
|
||||
Swift:
|
||||
|
||||
```swift
|
||||
// 设置调试配置
|
||||
let debug = GuruConsent.DebugSettings()
|
||||
debug.testDeviceIdentifiers = ["8C5E8576-5090-4C41-8FC4-A5A80FF77D9E"]
|
||||
debug.geography = .EEA
|
||||
GuruConsent.debug = debug
|
||||
```
|
||||
|
||||
Objective-C:
|
||||
|
||||
```objc
|
||||
// 设置调试配置
|
||||
GuruConsentDebugSettings *debug = [[GuruConsentDebugSettings alloc] init];
|
||||
debug.testDeviceIdentifiers = @[@"8C5E8576-5090-4C41-8FC4-A5A80FF77D9E"];
|
||||
debug.geography = GuruConsentDebugSettingsGeographyEEA;
|
||||
GuruConsent.debug = debug;
|
||||
```
|
||||
|
||||
重置状态
|
||||
|
||||
```swift
|
||||
GuruConsent.reset()
|
||||
```
|
||||
|
||||
## 运行
|
||||
|
||||
### 未授权过ATT权限 (非EEA地区):
|
||||

|
||||
|
||||
点击`Continue`按钮弹出ATT授权弹窗
|
||||
|
||||

|
||||
|
||||
### EEA地区:
|
||||

|
||||
|
||||
未授权过ATT权限 点击同意等按钮弹出ATT授权弹窗
|
||||
|
||||

|
||||
|
||||
|
||||
## 参考
|
||||
|
||||
[官方文档](https://developers.google.com/admob/ump/ios/quick-start)
|
||||
|
||||
## 协议
|
||||
|
||||
GuruConsent 使用 MIT 协议. 有关更多信息,请参阅 [LICENSE](LICENSE) 文件.
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 2591a1ab95ce94235adc38c94f28f029
|
||||
TextScriptImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 30771cebf05b44ad19739c67642b61ce
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -1,60 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>NSPrivacyCollectedDataTypes</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>NSPrivacyCollectedDataType</key>
|
||||
<string>NSPrivacyCollectedDataTypeCoarseLocation</string>
|
||||
<key>NSPrivacyCollectedDataTypeLinked</key>
|
||||
<false/>
|
||||
<key>NSPrivacyCollectedDataTypeTracking</key>
|
||||
<false/>
|
||||
<key>NSPrivacyCollectedDataTypePurposes</key>
|
||||
<array>
|
||||
<string>NSPrivacyCollectedDataTypePurposeAppFunctionality</string>
|
||||
</array>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>NSPrivacyCollectedDataType</key>
|
||||
<string>NSPrivacyCollectedDataTypePerformanceData</string>
|
||||
<key>NSPrivacyCollectedDataTypeLinked</key>
|
||||
<false/>
|
||||
<key>NSPrivacyCollectedDataTypeTracking</key>
|
||||
<false/>
|
||||
<key>NSPrivacyCollectedDataTypePurposes</key>
|
||||
<array>
|
||||
<string>NSPrivacyCollectedDataTypePurposeAppFunctionality</string>
|
||||
</array>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>NSPrivacyCollectedDataType</key>
|
||||
<string>NSPrivacyCollectedDataTypeProductInteraction</string>
|
||||
<key>NSPrivacyCollectedDataTypeLinked</key>
|
||||
<false/>
|
||||
<key>NSPrivacyCollectedDataTypeTracking</key>
|
||||
<false/>
|
||||
<key>NSPrivacyCollectedDataTypePurposes</key>
|
||||
<array>
|
||||
<string>NSPrivacyCollectedDataTypePurposeAppFunctionality</string>
|
||||
</array>
|
||||
</dict>
|
||||
</array>
|
||||
<key>NSPrivacyAccessedAPITypes</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>NSPrivacyAccessedAPITypeReasons</key>
|
||||
<array>
|
||||
<string>CA92.1</string>
|
||||
</array>
|
||||
<key>NSPrivacyAccessedAPIType</key>
|
||||
<string>NSPrivacyAccessedAPICategoryUserDefaults</string>
|
||||
</dict>
|
||||
</array>
|
||||
<key>NSPrivacyTrackingDomains</key>
|
||||
<array/>
|
||||
<key>NSPrivacyTracking</key>
|
||||
<false/>
|
||||
</dict>
|
||||
</plist>
|
||||
|
|
@ -1,85 +0,0 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 71f33675e1c99450686a549ba5f04505
|
||||
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: 1
|
||||
- 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: AnyCPU
|
||||
- first:
|
||||
Standalone: OSXUniversal
|
||||
second:
|
||||
enabled: 0
|
||||
settings:
|
||||
CPU: AnyCPU
|
||||
- first:
|
||||
Standalone: Win
|
||||
second:
|
||||
enabled: 0
|
||||
settings:
|
||||
CPU: AnyCPU
|
||||
- first:
|
||||
Standalone: Win64
|
||||
second:
|
||||
enabled: 0
|
||||
settings:
|
||||
CPU: AnyCPU
|
||||
- first:
|
||||
iPhone: iOS
|
||||
second:
|
||||
enabled: 0
|
||||
settings:
|
||||
AddToEmbeddedBinaries: false
|
||||
CPU: AnyCPU
|
||||
CompileFlags:
|
||||
FrameworkDependencies:
|
||||
- first:
|
||||
tvOS: tvOS
|
||||
second:
|
||||
enabled: 1
|
||||
settings: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 9c2ff42f8bf73454290c2c13d6de943d
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -1,121 +0,0 @@
|
|||
//
|
||||
// GuruConsent.GDPR.swift
|
||||
// GuruConsent
|
||||
//
|
||||
// Created by 李响 on 2022/11/11.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import UserMessagingPlatform
|
||||
|
||||
public extension GuruConsent {
|
||||
|
||||
/// 当前状态
|
||||
@objc
|
||||
public static var status: GDPRStatus {
|
||||
return .init(
|
||||
rawValue: UMPConsentInformation.sharedInstance.consentStatus.rawValue
|
||||
) ?? .unknown
|
||||
}
|
||||
|
||||
/// 是否可以请求广告 (status为.obtained或.notRequired)
|
||||
@objc
|
||||
public static var canRequestAds: Bool {
|
||||
return UMPConsentInformation.sharedInstance.canRequestAds
|
||||
}
|
||||
|
||||
/// 隐私选项状态
|
||||
/// 初始状态为.unknown 如果status为.notRequired 该状态也是.notRequired
|
||||
/// status为.obtained(弹窗同意后) 会变成.required, 代表可以让用户修改之前同意的隐私选项
|
||||
/// 调用`func openPrivacyOptions(from: with:)`方法可以再次打开弹窗
|
||||
/// 弹窗第二次打开后 用户可以重新勾选隐私选项并同意, 操作完成后状态会变为.unknown
|
||||
@objc
|
||||
public static var privacyOptionsRequirementStatus: GDPRPrivacyOptionsRequirementStatus {
|
||||
return .init(
|
||||
rawValue: UMPConsentInformation.sharedInstance.privacyOptionsRequirementStatus.rawValue
|
||||
) ?? .unknown
|
||||
}
|
||||
|
||||
private static var form: UMPConsentForm?
|
||||
|
||||
internal static func request(with completion: @escaping ((Swift.Result<GDPRFormStatus, Error>) -> Void)) {
|
||||
let parameters = UMPRequestParameters()
|
||||
// 设置未满同意年龄的标签。此处false表示用户达到年龄
|
||||
parameters.tagForUnderAgeOfConsent = tagForUnderAgeOfConsent
|
||||
// 设置调试设置
|
||||
if let debug = GuruConsent.debug {
|
||||
let debugSettings = UMPDebugSettings()
|
||||
debugSettings.testDeviceIdentifiers = debug.testDeviceIdentifiers
|
||||
debugSettings.geography = .init(rawValue: debug.geography.rawValue) ?? .disabled
|
||||
parameters.debugSettings = debugSettings
|
||||
}
|
||||
|
||||
// 请求最新同意信息
|
||||
UMPConsentInformation.sharedInstance.requestConsentInfoUpdate(
|
||||
with: parameters,
|
||||
completionHandler: { error in
|
||||
if let error = error {
|
||||
// 请求同意信息失败
|
||||
completion(.failure(error))
|
||||
|
||||
} else {
|
||||
let status = UMPConsentInformation.sharedInstance.formStatus
|
||||
completion(.success(.init(rawValue: status.rawValue) ?? .unknown))
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
static func loadForm(with completion: @escaping ((Swift.Result<GDPRStatus, Error>) -> Void)) {
|
||||
// 加载表单
|
||||
UMPConsentForm.load { form, error in
|
||||
if let error = error {
|
||||
// 表单加载失败
|
||||
completion(.failure(error))
|
||||
|
||||
} else {
|
||||
self.form = form
|
||||
let status = UMPConsentInformation.sharedInstance.consentStatus
|
||||
completion(.success(.init(rawValue: status.rawValue) ?? .unknown))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static func openForm(from controller: UIViewController, with completion: @escaping ((Swift.Result<GDPRStatus, Error>) -> Void)) {
|
||||
guard let form = form else {
|
||||
completion(.failure(NSError(domain: "Form Empty.", code: -1)))
|
||||
return
|
||||
}
|
||||
// 打开弹窗
|
||||
form.present(
|
||||
from: controller,
|
||||
completionHandler: { error in
|
||||
if let error = error {
|
||||
// 弹窗失败
|
||||
completion(.failure(error))
|
||||
|
||||
} else {
|
||||
// 是否已同意
|
||||
completion(.success(status))
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
/// 重置
|
||||
@objc
|
||||
public static func reset() {
|
||||
UMPConsentInformation.sharedInstance.reset()
|
||||
}
|
||||
|
||||
/// 打开隐私选项弹窗 (privacyOptionsRequirementStatus必须为.required)
|
||||
/// - Parameters:
|
||||
/// - controller: 视图控制器
|
||||
/// - completion: 完成回调
|
||||
@objc
|
||||
public static func openPrivacyOptions(from controller: UIViewController, with completion: @escaping ((Error?) -> Void)) {
|
||||
UMPConsentForm.presentPrivacyOptionsForm(from: controller) { error in
|
||||
completion(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,49 +0,0 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 604558f3813844f3eaa13f84fde7e522
|
||||
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: 1
|
||||
- first:
|
||||
Any:
|
||||
second:
|
||||
enabled: 0
|
||||
settings: {}
|
||||
- first:
|
||||
Editor: Editor
|
||||
second:
|
||||
enabled: 0
|
||||
settings:
|
||||
DefaultValueInitialized: true
|
||||
- first:
|
||||
iPhone: iOS
|
||||
second:
|
||||
enabled: 0
|
||||
settings: {}
|
||||
- first:
|
||||
tvOS: tvOS
|
||||
second:
|
||||
enabled: 1
|
||||
settings: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -1,204 +0,0 @@
|
|||
//
|
||||
// GuruConsent.swift
|
||||
// GuruConsent
|
||||
//
|
||||
// Created by 李响 on 2022/11/11.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
@objc
|
||||
public class GuruConsent: NSObject {
|
||||
|
||||
/// CDPR状态
|
||||
@objc(GuruConsentGDPRStatus)
|
||||
public enum GDPRStatus: Int {
|
||||
case unknown ///< Unknown consent status.
|
||||
case required ///< User consent required but not yet obtained.
|
||||
case notRequired ///< Consent not required.
|
||||
case obtained ///< User consent obtained, personalized vs non-personalized undefined.
|
||||
}
|
||||
|
||||
/// CDPR表单状态
|
||||
internal enum GDPRFormStatus: Int {
|
||||
case unknown
|
||||
case available
|
||||
case unavailable
|
||||
}
|
||||
|
||||
/// GDPR隐私选项所需状态
|
||||
@objc(GuruConsentGDPRPrivacyOptionsRequirementStatus)
|
||||
public enum GDPRPrivacyOptionsRequirementStatus: Int {
|
||||
case unknown ///< Requirement unknown.
|
||||
case required ///< A way must be provided for the user to modify their privacy options.
|
||||
case notRequired ///< User does not need to modify their privacy options. Either consent is not required, or the consent type does not require modification.
|
||||
}
|
||||
|
||||
/// 调试设置
|
||||
@objc(GuruConsentDebugSettings)
|
||||
public class DebugSettings: NSObject {
|
||||
|
||||
@objc(GuruConsentDebugSettingsGeography)
|
||||
public enum Geography: Int {
|
||||
case disabled
|
||||
case EEA
|
||||
case notEEA
|
||||
}
|
||||
|
||||
/// 测试设备ID
|
||||
@objc
|
||||
public var testDeviceIdentifiers: [String]
|
||||
/// 地理位置
|
||||
@objc
|
||||
public var geography: Geography
|
||||
|
||||
@objc
|
||||
public override init() {
|
||||
testDeviceIdentifiers = []
|
||||
geography = .disabled
|
||||
}
|
||||
}
|
||||
|
||||
/// 调试设置
|
||||
@objc
|
||||
public static var debug: DebugSettings?
|
||||
|
||||
/// 设置未满同意年龄的标签 默认为false 表示用户达到年龄
|
||||
@objc
|
||||
public static var tagForUnderAgeOfConsent: Bool = false
|
||||
|
||||
/// 是否已同意 (status为.obtained)
|
||||
@objc
|
||||
public static var isObtained: Bool {
|
||||
return status == .obtained
|
||||
}
|
||||
|
||||
/// 开始 OC
|
||||
/// ATT未授权过(非EEA地区) 会弹出ATT引导弹窗 在点击继续按钮时弹出ATT权限弹窗
|
||||
/// ATT未授权过(EEA地区) 会弹出GDPR弹窗 在点击同意时弹出ATT权限弹窗
|
||||
/// - Parameters:
|
||||
/// - controller: 视图控制器
|
||||
/// - completion: 完成回调
|
||||
@objc
|
||||
public static func start(from controller: UIViewController, success: @escaping ((GDPRStatus) -> Void), failure: @escaping (Error) -> Void) {
|
||||
start(from: controller) { result in
|
||||
switch result {
|
||||
case .success(let value):
|
||||
success(value)
|
||||
|
||||
case .failure(let error):
|
||||
failure(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 开始 Swift
|
||||
/// ATT未授权过(非EEA地区) 会弹出ATT引导弹窗 在点击继续按钮时弹出ATT权限弹窗
|
||||
/// ATT未授权过(EEA地区) 会弹出GDPR弹窗 在点击同意时弹出ATT权限弹窗
|
||||
/// - Parameters:
|
||||
/// - controller: 视图控制器
|
||||
/// - completion: 完成回调
|
||||
public static func start(from controller: UIViewController, with completion: @escaping ((Swift.Result<GDPRStatus, Error>) -> Void)) {
|
||||
request { result in
|
||||
switch result {
|
||||
case .success(let value):
|
||||
if value == .available {
|
||||
// 加载表单
|
||||
loadForm { result in
|
||||
switch result {
|
||||
case .success(let value):
|
||||
switch value {
|
||||
case .required:
|
||||
// 打开表单
|
||||
openForm(from: controller, with: completion)
|
||||
|
||||
default:
|
||||
completion(.success(value))
|
||||
}
|
||||
|
||||
case .failure(let error):
|
||||
// 表单加载失败
|
||||
completion(.failure(error))
|
||||
}
|
||||
}
|
||||
|
||||
} else {
|
||||
// 无表单需要加载
|
||||
completion(.success(.notRequired))
|
||||
}
|
||||
|
||||
case .failure(let error):
|
||||
// 请求同意信息失败
|
||||
completion(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// ----------------------------------------
|
||||
|
||||
@objc
|
||||
public static func prepare(success: @escaping ((GDPRStatus) -> Void), failure: @escaping (Error) -> Void) {
|
||||
prepare { result in
|
||||
switch result {
|
||||
case .success(let value):
|
||||
success(value)
|
||||
|
||||
case .failure(let error):
|
||||
failure(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@objc
|
||||
public static func present(from controller: UIViewController, success: @escaping ((GDPRStatus) -> Void), failure: @escaping (Error) -> Void) {
|
||||
present(from: controller) { result in
|
||||
switch result {
|
||||
case .success(let value):
|
||||
success(value)
|
||||
|
||||
case .failure(let error):
|
||||
failure(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 预加载 (需在加载成功后 手动调用打开表单)
|
||||
/// - Parameter completion: 完成回调
|
||||
public static func prepare(with completion: @escaping ((Swift.Result<GDPRStatus, Error>) -> Void)) {
|
||||
request { result in
|
||||
switch result {
|
||||
case .success(let value):
|
||||
if value == .available {
|
||||
// 加载表单
|
||||
loadForm { result in
|
||||
switch result {
|
||||
case .success(let value):
|
||||
// 表单加载成功
|
||||
completion(.success(value))
|
||||
|
||||
case .failure(let error):
|
||||
// 表单加载失败
|
||||
completion(.failure(error))
|
||||
}
|
||||
}
|
||||
|
||||
} else {
|
||||
// 无表单需要加载
|
||||
completion(.success(.notRequired))
|
||||
}
|
||||
|
||||
case .failure(let error):
|
||||
// 请求同意信息失败
|
||||
completion(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 打开表单 请确保status为.required 否则无效
|
||||
/// - Parameters:
|
||||
/// - controller: 视图控制器
|
||||
/// - completion: 完成回调
|
||||
public static func present(from controller: UIViewController, with completion: @escaping ((Swift.Result<GDPRStatus, Error>) -> Void)) {
|
||||
openForm(from: controller, with: completion)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,49 +0,0 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 0b11b5b1fa30a432eaf1b02602717dc9
|
||||
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: 1
|
||||
- first:
|
||||
Any:
|
||||
second:
|
||||
enabled: 0
|
||||
settings: {}
|
||||
- first:
|
||||
Editor: Editor
|
||||
second:
|
||||
enabled: 0
|
||||
settings:
|
||||
DefaultValueInitialized: true
|
||||
- first:
|
||||
iPhone: iOS
|
||||
second:
|
||||
enabled: 0
|
||||
settings: {}
|
||||
- first:
|
||||
tvOS: tvOS
|
||||
second:
|
||||
enabled: 1
|
||||
settings: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue