commit
						de45dd6e81
					
				|  | @ -0,0 +1,38 @@ | |||
| # macOS | ||||
| .DS_Store | ||||
| 
 | ||||
| # Xcode | ||||
| build/ | ||||
| *.pbxuser | ||||
| !default.pbxuser | ||||
| *.mode1v3 | ||||
| !default.mode1v3 | ||||
| *.mode2v3 | ||||
| !default.mode2v3 | ||||
| *.perspectivev3 | ||||
| !default.perspectivev3 | ||||
| xcuserdata/ | ||||
| *.xccheckout | ||||
| *.moved-aside | ||||
| DerivedData | ||||
| *.hmap | ||||
| *.ipa | ||||
| 
 | ||||
| # Bundler | ||||
| .bundle | ||||
| 
 | ||||
| # Add this line if you want to avoid checking in source code from Carthage dependencies. | ||||
| # Carthage/Checkouts | ||||
| 
 | ||||
| Carthage/Build | ||||
| 
 | ||||
| # We recommend against adding the Pods directory to your .gitignore. However | ||||
| # you should judge for yourself, the pros and cons are mentioned at: | ||||
| # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-ignore-the-pods-directory-in-source-control | ||||
| #  | ||||
| # Note: if you ignore the Pods directory, make sure to uncomment | ||||
| # `pod install` in .travis.yml | ||||
| # | ||||
| # Pods/ | ||||
| Example/Pods | ||||
| Podfile.lock | ||||
|  | @ -0,0 +1,150 @@ | |||
| ## v0.3.5 | ||||
| - 接口更新: | ||||
|   - 日志打包方法eventsLogsArchive废弃,使用eventsLogsDirectory获取文件夹URL | ||||
| 
 | ||||
| ## v0.3.4 | ||||
| - 配置privacy manifest文件,增加隐私项: | ||||
|   - 隐私API访问: | ||||
|     - System boot time APIs | ||||
|     - File timestamp APIs | ||||
|     - User defaults APIs | ||||
| 
 | ||||
| ## v0.3.3 | ||||
| - feature | ||||
|   - 增加获取当前user properties接口 | ||||
|     - getUserProperties() -> [String : String] | ||||
|   - 增加upload开关切换接口 | ||||
|     - setEnableUpload(isOn: Bool = true) -> Void | ||||
| 
 | ||||
| ## v0.3.2 | ||||
| - bugfix | ||||
|   - 修复一些字典访问的崩溃问题 | ||||
| 
 | ||||
| ## v0.3.1 | ||||
| - feature | ||||
|   - 重命名 | ||||
|     - registerInternalEventObserver(reportCallback: @escaping (_ eventCode: Int, _ info: String) -> Void) | ||||
| 
 | ||||
| ## v0.3.0 | ||||
| - feature | ||||
|   - 增加注册网络层错误监听接口 | ||||
|     - registerErrorReporter(reportCallback: @escaping (_ errorCode: Int, _ info: String) -> Void) | ||||
| 
 | ||||
| ## v0.2.17 | ||||
| - feature | ||||
|   - 增加清除user property接口 | ||||
|     - removeUserProperties(forNames names: [String]) | ||||
| 
 | ||||
| ## v0.2.16 | ||||
| - feature | ||||
|   - 增加fg打点,保证轮询上传的每批事件中包含一个fg事件。 | ||||
| 
 | ||||
| ## v0.2.15 | ||||
| - feature | ||||
|   - 初始化过程中延迟500ms打一个fg事件。 | ||||
| 
 | ||||
| ## v0.2.14 | ||||
| - bugfix | ||||
|   - 修复logEvent(_ name: String, parameters: [String : Any]?)parameters中int类型未解析的错误。 | ||||
| 
 | ||||
| ## v0.2.13 | ||||
| - feature | ||||
|   - 请求header中添加中台标准字段X-APP-ID,X-DEVICE-INFO,对应值由应用层从初始化方法中传入。 | ||||
|     - initializeLib(..., **saasXAPPID: String, saasXDEVICEINFO: String,** ...) | ||||
| 
 | ||||
| ## v0.2.12 | ||||
| - feature | ||||
|   - 上传服务器每个event增加eventId字段,采用uuid4算法,小写。 | ||||
| - bugfix | ||||
|   - 将放置到info内的字段从userProperty中移除。 | ||||
| 
 | ||||
| ## v0.2.11 | ||||
| - optimization | ||||
|   - fg计算逻辑调整为每间隔1s,更新一次缓存累计时长,切入后台后记录fg事件,缓存累计时长置零。 | ||||
| 
 | ||||
| ## v0.2.10 | ||||
| - optimization | ||||
|   - app切入后台上传events任务等待fg事件入库后再执行。 | ||||
| 
 | ||||
| ## v0.2.9 | ||||
| - feature | ||||
|   - 增加设置events host name接口setEventsUploadEndPoint(host: String?) | ||||
| 
 | ||||
| ## v0.2.8 | ||||
| - bugfix | ||||
|   - 修复fg时间异常问题,将计算时间差所用起始系统时间和当前系统时间均替换为cpu时间。 | ||||
|   - 修复当前服务器时间计算逻辑bug。 | ||||
| 
 | ||||
| ## v0.2.7 | ||||
| - bugfix | ||||
|   - 修复fg可能出现负值的情况 | ||||
| 
 | ||||
| ## v0.2.6 | ||||
| - bugfix | ||||
|   - 修复后台任务超时时未及时调用结束任务,造成app被系统终止的问题 | ||||
| 
 | ||||
| ## v0.2.5 | ||||
| - bugfix | ||||
|   - 修复Manager成员userProperty多线程访问引起的崩溃 | ||||
|   - 修复manager工作队列发生阻塞时,block上传任务问题 | ||||
| 
 | ||||
| ## v0.2.4 | ||||
| - bugfix | ||||
|   - podspec文件回滚对subspec的配置,lint无法通过。 | ||||
| 
 | ||||
| ## v0.2.3 | ||||
| - bugfix | ||||
|   - 修复Manager成员userProperty多线程访问引起的崩溃 | ||||
| 
 | ||||
| ## v0.2.2 | ||||
| - public接口适配objc,可供OC调用 | ||||
| 
 | ||||
| ## v0.2.1 | ||||
| 
 | ||||
| - 调整IP直联时服务器证书验证规则 | ||||
| - 上传任务适配后台运行 | ||||
| - 增加文件日志功能,提供日志文件打包导出接口 | ||||
| eventsLogsArchive(_ callback: @escaping (_ url: URL?) -> Void) | ||||
| 
 | ||||
| ## v0.2.0 | ||||
| 
 | ||||
| - 支持HTTPDNS | ||||
| 
 | ||||
| ## v0.1.1 | ||||
| 
 | ||||
| - 将原来设置成员变量方式替换为通过初始化接口传值  | ||||
| initializeLib(uploadPeriodInSecond: Double, batchLimit: Int, eventExpiredSeconds: Double, initializeTimeout: Double, loggerDebug: Bool) | ||||
| 
 | ||||
| - 间隔`uploadPeriodInSecond`秒后打包上传`batchLimit`个数据 | ||||
| 
 | ||||
| - 支持延时记录event的逻辑,初始化后等待user id/device id/firebase pseudo id等属性,超时`initializeTimeout`秒 | ||||
| 
 | ||||
| - 支持事件优先级 | ||||
| 
 | ||||
|     | ***NAME*** | ***PRIORITY*** | | ||||
|     | :--------  | :--------: | | ||||
|     | `EMERGENCE` | 0 | | ||||
|     | `HIGH` | 5 | | ||||
|     | `DEFAULT` | 10 | | ||||
|     | `LOW` | 15 | | ||||
| 
 | ||||
| - 首次初始化时自动上报first_open点,并将该点以`EMERGENCE`优先级发送 | ||||
| 
 | ||||
| ## v0.1.0 | ||||
| 
 | ||||
| - 支持App生命周期自打点`fg`,并根据前后台时间计算`duration` | ||||
| 
 | ||||
| - 首次初始化时记录first_open_time | ||||
| 
 | ||||
| - 提供下列接口 | ||||
| 
 | ||||
|     | ***Method*** | ***Description*** | | ||||
|     | :--------  | :--------: | | ||||
|     | `logEvent` | 所有event点都是通过该函数完成 | | ||||
|     | `setUserProperty` | 设置用户属性 | | ||||
|     | `setScreen` | 设置当前屏幕 | | ||||
|     | `setDeviceId` | 设置设备ID | | ||||
|     | `setUid` | 设置用户ID | | ||||
|     | `setAdjustId` | 设置AdjustId | | ||||
|     | `setAdId` | 设置Google Ad Id | | ||||
|     | `setFirebaseId` | 设置Firebase Id | | ||||
|  | @ -0,0 +1,430 @@ | |||
| // !$*UTF8*$! | ||||
| { | ||||
| 	archiveVersion = 1; | ||||
| 	classes = { | ||||
| 	}; | ||||
| 	objectVersion = 51; | ||||
| 	objects = { | ||||
| 
 | ||||
| /* Begin PBXBuildFile section */ | ||||
| 		607FACD61AFB9204008FA782 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 607FACD51AFB9204008FA782 /* AppDelegate.swift */; }; | ||||
| 		607FACD81AFB9204008FA782 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 607FACD71AFB9204008FA782 /* ViewController.swift */; }; | ||||
| 		607FACDB1AFB9204008FA782 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 607FACD91AFB9204008FA782 /* Main.storyboard */; }; | ||||
| 		607FACDD1AFB9204008FA782 /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 607FACDC1AFB9204008FA782 /* Images.xcassets */; }; | ||||
| 		607FACE01AFB9204008FA782 /* LaunchScreen.xib in Resources */ = {isa = PBXBuildFile; fileRef = 607FACDE1AFB9204008FA782 /* LaunchScreen.xib */; }; | ||||
| 		63E4E6CEDD0672A6AB498174 /* Pods_GuruAnalytics_Example.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B1F595BB712FF36838F4A125 /* Pods_GuruAnalytics_Example.framework */; }; | ||||
| /* End PBXBuildFile section */ | ||||
| 
 | ||||
| /* Begin PBXFileReference section */ | ||||
| 		267F05352936F18F00D72EFD /* CHANGELOG.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; name = CHANGELOG.md; path = ../CHANGELOG.md; sourceTree = "<group>"; }; | ||||
| 		26BF34CF292C8122001CFE57 /* MyPlayground.playground */ = {isa = PBXFileReference; lastKnownFileType = file.playground; path = MyPlayground.playground; sourceTree = "<group>"; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; | ||||
| 		37274AD5F3F5B3AAD2714B34 /* LICENSE */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text; name = LICENSE; path = ../LICENSE; sourceTree = "<group>"; }; | ||||
| 		407E9D4F744C1F686237011A /* README.md */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = net.daringfireball.markdown; name = README.md; path = ../README.md; sourceTree = "<group>"; }; | ||||
| 		5CD9DCBF7AE700FB60660DB4 /* Pods_GuruAnalytics_Tests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_GuruAnalytics_Tests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; | ||||
| 		607FACD01AFB9204008FA782 /* GuruAnalytics_Example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = GuruAnalytics_Example.app; sourceTree = BUILT_PRODUCTS_DIR; }; | ||||
| 		607FACD41AFB9204008FA782 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; }; | ||||
| 		607FACD51AFB9204008FA782 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; }; | ||||
| 		607FACD71AFB9204008FA782 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = "<group>"; }; | ||||
| 		607FACDA1AFB9204008FA782 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; }; | ||||
| 		607FACDC1AFB9204008FA782 /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Images.xcassets; sourceTree = "<group>"; }; | ||||
| 		607FACDF1AFB9204008FA782 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/LaunchScreen.xib; sourceTree = "<group>"; }; | ||||
| 		B1F595BB712FF36838F4A125 /* Pods_GuruAnalytics_Example.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_GuruAnalytics_Example.framework; sourceTree = BUILT_PRODUCTS_DIR; }; | ||||
| 		D30C5441D6D5CBD750E76657 /* Pods-GuruAnalytics_Example.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-GuruAnalytics_Example.debug.xcconfig"; path = "Target Support Files/Pods-GuruAnalytics_Example/Pods-GuruAnalytics_Example.debug.xcconfig"; sourceTree = "<group>"; }; | ||||
| 		DC74B33D46880B782C485C13 /* GuruAnalyticsLib.podspec */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text; name = GuruAnalyticsLib.podspec; path = ../GuruAnalyticsLib.podspec; sourceTree = "<group>"; xcLanguageSpecificationIdentifier = xcode.lang.ruby; }; | ||||
| 		F992AC1E7C4013032773C33F /* Pods-GuruAnalytics_Example.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-GuruAnalytics_Example.release.xcconfig"; path = "Target Support Files/Pods-GuruAnalytics_Example/Pods-GuruAnalytics_Example.release.xcconfig"; sourceTree = "<group>"; }; | ||||
| /* End PBXFileReference section */ | ||||
| 
 | ||||
| /* Begin PBXFrameworksBuildPhase section */ | ||||
| 		607FACCD1AFB9204008FA782 /* Frameworks */ = { | ||||
| 			isa = PBXFrameworksBuildPhase; | ||||
| 			buildActionMask = 2147483647; | ||||
| 			files = ( | ||||
| 				63E4E6CEDD0672A6AB498174 /* Pods_GuruAnalytics_Example.framework in Frameworks */, | ||||
| 			); | ||||
| 			runOnlyForDeploymentPostprocessing = 0; | ||||
| 		}; | ||||
| /* End PBXFrameworksBuildPhase section */ | ||||
| 
 | ||||
| /* Begin PBXGroup section */ | ||||
| 		607FACC71AFB9204008FA782 = { | ||||
| 			isa = PBXGroup; | ||||
| 			children = ( | ||||
| 				607FACF51AFB993E008FA782 /* Podspec Metadata */, | ||||
| 				607FACD21AFB9204008FA782 /* Example for GuruAnalytics */, | ||||
| 				607FACD11AFB9204008FA782 /* Products */, | ||||
| 				8ABE29700505470322F45254 /* Pods */, | ||||
| 				81BE026791FA842AE71ACD3B /* Frameworks */, | ||||
| 			); | ||||
| 			sourceTree = "<group>"; | ||||
| 		}; | ||||
| 		607FACD11AFB9204008FA782 /* Products */ = { | ||||
| 			isa = PBXGroup; | ||||
| 			children = ( | ||||
| 				607FACD01AFB9204008FA782 /* GuruAnalytics_Example.app */, | ||||
| 			); | ||||
| 			name = Products; | ||||
| 			sourceTree = "<group>"; | ||||
| 		}; | ||||
| 		607FACD21AFB9204008FA782 /* Example for GuruAnalytics */ = { | ||||
| 			isa = PBXGroup; | ||||
| 			children = ( | ||||
| 				607FACD51AFB9204008FA782 /* AppDelegate.swift */, | ||||
| 				607FACD71AFB9204008FA782 /* ViewController.swift */, | ||||
| 				607FACD91AFB9204008FA782 /* Main.storyboard */, | ||||
| 				607FACDC1AFB9204008FA782 /* Images.xcassets */, | ||||
| 				607FACDE1AFB9204008FA782 /* LaunchScreen.xib */, | ||||
| 				607FACD31AFB9204008FA782 /* Supporting Files */, | ||||
| 				26BF34CF292C8122001CFE57 /* MyPlayground.playground */, | ||||
| 			); | ||||
| 			name = "Example for GuruAnalytics"; | ||||
| 			path = GuruAnalytics; | ||||
| 			sourceTree = "<group>"; | ||||
| 		}; | ||||
| 		607FACD31AFB9204008FA782 /* Supporting Files */ = { | ||||
| 			isa = PBXGroup; | ||||
| 			children = ( | ||||
| 				607FACD41AFB9204008FA782 /* Info.plist */, | ||||
| 			); | ||||
| 			name = "Supporting Files"; | ||||
| 			sourceTree = "<group>"; | ||||
| 		}; | ||||
| 		607FACF51AFB993E008FA782 /* Podspec Metadata */ = { | ||||
| 			isa = PBXGroup; | ||||
| 			children = ( | ||||
| 				DC74B33D46880B782C485C13 /* GuruAnalyticsLib.podspec */, | ||||
| 				407E9D4F744C1F686237011A /* README.md */, | ||||
| 				267F05352936F18F00D72EFD /* CHANGELOG.md */, | ||||
| 				37274AD5F3F5B3AAD2714B34 /* LICENSE */, | ||||
| 			); | ||||
| 			name = "Podspec Metadata"; | ||||
| 			sourceTree = "<group>"; | ||||
| 		}; | ||||
| 		81BE026791FA842AE71ACD3B /* Frameworks */ = { | ||||
| 			isa = PBXGroup; | ||||
| 			children = ( | ||||
| 				B1F595BB712FF36838F4A125 /* Pods_GuruAnalytics_Example.framework */, | ||||
| 				5CD9DCBF7AE700FB60660DB4 /* Pods_GuruAnalytics_Tests.framework */, | ||||
| 			); | ||||
| 			name = Frameworks; | ||||
| 			sourceTree = "<group>"; | ||||
| 		}; | ||||
| 		8ABE29700505470322F45254 /* Pods */ = { | ||||
| 			isa = PBXGroup; | ||||
| 			children = ( | ||||
| 				D30C5441D6D5CBD750E76657 /* Pods-GuruAnalytics_Example.debug.xcconfig */, | ||||
| 				F992AC1E7C4013032773C33F /* Pods-GuruAnalytics_Example.release.xcconfig */, | ||||
| 			); | ||||
| 			path = Pods; | ||||
| 			sourceTree = "<group>"; | ||||
| 		}; | ||||
| /* End PBXGroup section */ | ||||
| 
 | ||||
| /* Begin PBXNativeTarget section */ | ||||
| 		607FACCF1AFB9204008FA782 /* GuruAnalytics_Example */ = { | ||||
| 			isa = PBXNativeTarget; | ||||
| 			buildConfigurationList = 607FACEF1AFB9204008FA782 /* Build configuration list for PBXNativeTarget "GuruAnalytics_Example" */; | ||||
| 			buildPhases = ( | ||||
| 				12A998D33B96E401B841E243 /* [CP] Check Pods Manifest.lock */, | ||||
| 				607FACCC1AFB9204008FA782 /* Sources */, | ||||
| 				607FACCD1AFB9204008FA782 /* Frameworks */, | ||||
| 				607FACCE1AFB9204008FA782 /* Resources */, | ||||
| 				86E7176C5034831947DC0310 /* [CP] Embed Pods Frameworks */, | ||||
| 			); | ||||
| 			buildRules = ( | ||||
| 			); | ||||
| 			dependencies = ( | ||||
| 			); | ||||
| 			name = GuruAnalytics_Example; | ||||
| 			productName = GuruAnalytics; | ||||
| 			productReference = 607FACD01AFB9204008FA782 /* GuruAnalytics_Example.app */; | ||||
| 			productType = "com.apple.product-type.application"; | ||||
| 		}; | ||||
| /* End PBXNativeTarget section */ | ||||
| 
 | ||||
| /* Begin PBXProject section */ | ||||
| 		607FACC81AFB9204008FA782 /* Project object */ = { | ||||
| 			isa = PBXProject; | ||||
| 			attributes = { | ||||
| 				LastSwiftUpdateCheck = 0830; | ||||
| 				LastUpgradeCheck = 0830; | ||||
| 				ORGANIZATIONNAME = CocoaPods; | ||||
| 				TargetAttributes = { | ||||
| 					607FACCF1AFB9204008FA782 = { | ||||
| 						CreatedOnToolsVersion = 6.3.1; | ||||
| 						LastSwiftMigration = 0900; | ||||
| 					}; | ||||
| 				}; | ||||
| 			}; | ||||
| 			buildConfigurationList = 607FACCB1AFB9204008FA782 /* Build configuration list for PBXProject "GuruAnalytics" */; | ||||
| 			compatibilityVersion = "Xcode 9.3"; | ||||
| 			developmentRegion = English; | ||||
| 			hasScannedForEncodings = 0; | ||||
| 			knownRegions = ( | ||||
| 				English, | ||||
| 				en, | ||||
| 				Base, | ||||
| 			); | ||||
| 			mainGroup = 607FACC71AFB9204008FA782; | ||||
| 			productRefGroup = 607FACD11AFB9204008FA782 /* Products */; | ||||
| 			projectDirPath = ""; | ||||
| 			projectRoot = ""; | ||||
| 			targets = ( | ||||
| 				607FACCF1AFB9204008FA782 /* GuruAnalytics_Example */, | ||||
| 			); | ||||
| 		}; | ||||
| /* End PBXProject section */ | ||||
| 
 | ||||
| /* Begin PBXResourcesBuildPhase section */ | ||||
| 		607FACCE1AFB9204008FA782 /* Resources */ = { | ||||
| 			isa = PBXResourcesBuildPhase; | ||||
| 			buildActionMask = 2147483647; | ||||
| 			files = ( | ||||
| 				607FACDB1AFB9204008FA782 /* Main.storyboard in Resources */, | ||||
| 				607FACE01AFB9204008FA782 /* LaunchScreen.xib in Resources */, | ||||
| 				607FACDD1AFB9204008FA782 /* Images.xcassets in Resources */, | ||||
| 			); | ||||
| 			runOnlyForDeploymentPostprocessing = 0; | ||||
| 		}; | ||||
| /* End PBXResourcesBuildPhase section */ | ||||
| 
 | ||||
| /* Begin PBXShellScriptBuildPhase section */ | ||||
| 		12A998D33B96E401B841E243 /* [CP] Check Pods Manifest.lock */ = { | ||||
| 			isa = PBXShellScriptBuildPhase; | ||||
| 			buildActionMask = 2147483647; | ||||
| 			files = ( | ||||
| 			); | ||||
| 			inputFileListPaths = ( | ||||
| 			); | ||||
| 			inputPaths = ( | ||||
| 				"${PODS_PODFILE_DIR_PATH}/Podfile.lock", | ||||
| 				"${PODS_ROOT}/Manifest.lock", | ||||
| 			); | ||||
| 			name = "[CP] Check Pods Manifest.lock"; | ||||
| 			outputFileListPaths = ( | ||||
| 			); | ||||
| 			outputPaths = ( | ||||
| 				"$(DERIVED_FILE_DIR)/Pods-GuruAnalytics_Example-checkManifestLockResult.txt", | ||||
| 			); | ||||
| 			runOnlyForDeploymentPostprocessing = 0; | ||||
| 			shellPath = /bin/sh; | ||||
| 			shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n    # print error to STDERR\n    echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n    exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; | ||||
| 			showEnvVarsInLog = 0; | ||||
| 		}; | ||||
| 		86E7176C5034831947DC0310 /* [CP] Embed Pods Frameworks */ = { | ||||
| 			isa = PBXShellScriptBuildPhase; | ||||
| 			buildActionMask = 2147483647; | ||||
| 			files = ( | ||||
| 			); | ||||
| 			inputFileListPaths = ( | ||||
| 				"${PODS_ROOT}/Target Support Files/Pods-GuruAnalytics_Example/Pods-GuruAnalytics_Example-frameworks-${CONFIGURATION}-input-files.xcfilelist", | ||||
| 			); | ||||
| 			name = "[CP] Embed Pods Frameworks"; | ||||
| 			outputFileListPaths = ( | ||||
| 				"${PODS_ROOT}/Target Support Files/Pods-GuruAnalytics_Example/Pods-GuruAnalytics_Example-frameworks-${CONFIGURATION}-output-files.xcfilelist", | ||||
| 			); | ||||
| 			runOnlyForDeploymentPostprocessing = 0; | ||||
| 			shellPath = /bin/sh; | ||||
| 			shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-GuruAnalytics_Example/Pods-GuruAnalytics_Example-frameworks.sh\"\n"; | ||||
| 			showEnvVarsInLog = 0; | ||||
| 		}; | ||||
| /* End PBXShellScriptBuildPhase section */ | ||||
| 
 | ||||
| /* Begin PBXSourcesBuildPhase section */ | ||||
| 		607FACCC1AFB9204008FA782 /* Sources */ = { | ||||
| 			isa = PBXSourcesBuildPhase; | ||||
| 			buildActionMask = 2147483647; | ||||
| 			files = ( | ||||
| 				607FACD81AFB9204008FA782 /* ViewController.swift in Sources */, | ||||
| 				607FACD61AFB9204008FA782 /* AppDelegate.swift in Sources */, | ||||
| 			); | ||||
| 			runOnlyForDeploymentPostprocessing = 0; | ||||
| 		}; | ||||
| /* End PBXSourcesBuildPhase section */ | ||||
| 
 | ||||
| /* Begin PBXVariantGroup section */ | ||||
| 		607FACD91AFB9204008FA782 /* Main.storyboard */ = { | ||||
| 			isa = PBXVariantGroup; | ||||
| 			children = ( | ||||
| 				607FACDA1AFB9204008FA782 /* Base */, | ||||
| 			); | ||||
| 			name = Main.storyboard; | ||||
| 			sourceTree = "<group>"; | ||||
| 		}; | ||||
| 		607FACDE1AFB9204008FA782 /* LaunchScreen.xib */ = { | ||||
| 			isa = PBXVariantGroup; | ||||
| 			children = ( | ||||
| 				607FACDF1AFB9204008FA782 /* Base */, | ||||
| 			); | ||||
| 			name = LaunchScreen.xib; | ||||
| 			sourceTree = "<group>"; | ||||
| 		}; | ||||
| /* End PBXVariantGroup section */ | ||||
| 
 | ||||
| /* Begin XCBuildConfiguration section */ | ||||
| 		607FACED1AFB9204008FA782 /* Debug */ = { | ||||
| 			isa = XCBuildConfiguration; | ||||
| 			buildSettings = { | ||||
| 				ALWAYS_SEARCH_USER_PATHS = NO; | ||||
| 				CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; | ||||
| 				CLANG_CXX_LIBRARY = "libc++"; | ||||
| 				CLANG_ENABLE_MODULES = YES; | ||||
| 				CLANG_ENABLE_OBJC_ARC = YES; | ||||
| 				CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; | ||||
| 				CLANG_WARN_BOOL_CONVERSION = YES; | ||||
| 				CLANG_WARN_COMMA = YES; | ||||
| 				CLANG_WARN_CONSTANT_CONVERSION = YES; | ||||
| 				CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; | ||||
| 				CLANG_WARN_EMPTY_BODY = YES; | ||||
| 				CLANG_WARN_ENUM_CONVERSION = YES; | ||||
| 				CLANG_WARN_INFINITE_RECURSION = YES; | ||||
| 				CLANG_WARN_INT_CONVERSION = YES; | ||||
| 				CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; | ||||
| 				CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; | ||||
| 				CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; | ||||
| 				CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; | ||||
| 				CLANG_WARN_STRICT_PROTOTYPES = YES; | ||||
| 				CLANG_WARN_SUSPICIOUS_MOVE = YES; | ||||
| 				CLANG_WARN_UNREACHABLE_CODE = YES; | ||||
| 				CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; | ||||
| 				"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; | ||||
| 				COPY_PHASE_STRIP = NO; | ||||
| 				DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; | ||||
| 				ENABLE_STRICT_OBJC_MSGSEND = YES; | ||||
| 				ENABLE_TESTABILITY = YES; | ||||
| 				GCC_C_LANGUAGE_STANDARD = gnu99; | ||||
| 				GCC_DYNAMIC_NO_PIC = NO; | ||||
| 				GCC_NO_COMMON_BLOCKS = YES; | ||||
| 				GCC_OPTIMIZATION_LEVEL = 0; | ||||
| 				GCC_PREPROCESSOR_DEFINITIONS = ( | ||||
| 					"DEBUG=1", | ||||
| 					"$(inherited)", | ||||
| 				); | ||||
| 				GCC_SYMBOLS_PRIVATE_EXTERN = NO; | ||||
| 				GCC_WARN_64_TO_32_BIT_CONVERSION = YES; | ||||
| 				GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; | ||||
| 				GCC_WARN_UNDECLARED_SELECTOR = YES; | ||||
| 				GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; | ||||
| 				GCC_WARN_UNUSED_FUNCTION = YES; | ||||
| 				GCC_WARN_UNUSED_VARIABLE = YES; | ||||
| 				IPHONEOS_DEPLOYMENT_TARGET = 9.3; | ||||
| 				MTL_ENABLE_DEBUG_INFO = YES; | ||||
| 				ONLY_ACTIVE_ARCH = YES; | ||||
| 				SDKROOT = iphoneos; | ||||
| 				SWIFT_OPTIMIZATION_LEVEL = "-Onone"; | ||||
| 				SWIFT_VERSION = 5.0; | ||||
| 			}; | ||||
| 			name = Debug; | ||||
| 		}; | ||||
| 		607FACEE1AFB9204008FA782 /* Release */ = { | ||||
| 			isa = XCBuildConfiguration; | ||||
| 			buildSettings = { | ||||
| 				ALWAYS_SEARCH_USER_PATHS = NO; | ||||
| 				CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; | ||||
| 				CLANG_CXX_LIBRARY = "libc++"; | ||||
| 				CLANG_ENABLE_MODULES = YES; | ||||
| 				CLANG_ENABLE_OBJC_ARC = YES; | ||||
| 				CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; | ||||
| 				CLANG_WARN_BOOL_CONVERSION = YES; | ||||
| 				CLANG_WARN_COMMA = YES; | ||||
| 				CLANG_WARN_CONSTANT_CONVERSION = YES; | ||||
| 				CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; | ||||
| 				CLANG_WARN_EMPTY_BODY = YES; | ||||
| 				CLANG_WARN_ENUM_CONVERSION = YES; | ||||
| 				CLANG_WARN_INFINITE_RECURSION = YES; | ||||
| 				CLANG_WARN_INT_CONVERSION = YES; | ||||
| 				CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; | ||||
| 				CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; | ||||
| 				CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; | ||||
| 				CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; | ||||
| 				CLANG_WARN_STRICT_PROTOTYPES = YES; | ||||
| 				CLANG_WARN_SUSPICIOUS_MOVE = YES; | ||||
| 				CLANG_WARN_UNREACHABLE_CODE = YES; | ||||
| 				CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; | ||||
| 				"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; | ||||
| 				COPY_PHASE_STRIP = NO; | ||||
| 				DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; | ||||
| 				ENABLE_NS_ASSERTIONS = NO; | ||||
| 				ENABLE_STRICT_OBJC_MSGSEND = YES; | ||||
| 				GCC_C_LANGUAGE_STANDARD = gnu99; | ||||
| 				GCC_NO_COMMON_BLOCKS = YES; | ||||
| 				GCC_WARN_64_TO_32_BIT_CONVERSION = YES; | ||||
| 				GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; | ||||
| 				GCC_WARN_UNDECLARED_SELECTOR = YES; | ||||
| 				GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; | ||||
| 				GCC_WARN_UNUSED_FUNCTION = YES; | ||||
| 				GCC_WARN_UNUSED_VARIABLE = YES; | ||||
| 				IPHONEOS_DEPLOYMENT_TARGET = 9.3; | ||||
| 				MTL_ENABLE_DEBUG_INFO = NO; | ||||
| 				SDKROOT = iphoneos; | ||||
| 				SWIFT_COMPILATION_MODE = wholemodule; | ||||
| 				SWIFT_OPTIMIZATION_LEVEL = "-O"; | ||||
| 				SWIFT_VERSION = 5.0; | ||||
| 				VALIDATE_PRODUCT = YES; | ||||
| 			}; | ||||
| 			name = Release; | ||||
| 		}; | ||||
| 		607FACF01AFB9204008FA782 /* Debug */ = { | ||||
| 			isa = XCBuildConfiguration; | ||||
| 			baseConfigurationReference = D30C5441D6D5CBD750E76657 /* Pods-GuruAnalytics_Example.debug.xcconfig */; | ||||
| 			buildSettings = { | ||||
| 				ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; | ||||
| 				DEVELOPMENT_TEAM = 43U6TB4QK3; | ||||
| 				INFOPLIST_FILE = GuruAnalytics/Info.plist; | ||||
| 				IPHONEOS_DEPLOYMENT_TARGET = 11.0; | ||||
| 				LD_RUNPATH_SEARCH_PATHS = ( | ||||
| 					"$(inherited)", | ||||
| 					"@executable_path/Frameworks", | ||||
| 				); | ||||
| 				MODULE_NAME = ExampleApp; | ||||
| 				PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.demo.$(PRODUCT_NAME:rfc1034identifier)"; | ||||
| 				PRODUCT_NAME = "$(TARGET_NAME)"; | ||||
| 				SWIFT_SWIFT3_OBJC_INFERENCE = Default; | ||||
| 				SWIFT_VERSION = 5.0; | ||||
| 			}; | ||||
| 			name = Debug; | ||||
| 		}; | ||||
| 		607FACF11AFB9204008FA782 /* Release */ = { | ||||
| 			isa = XCBuildConfiguration; | ||||
| 			baseConfigurationReference = F992AC1E7C4013032773C33F /* Pods-GuruAnalytics_Example.release.xcconfig */; | ||||
| 			buildSettings = { | ||||
| 				ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; | ||||
| 				DEVELOPMENT_TEAM = 43U6TB4QK3; | ||||
| 				INFOPLIST_FILE = GuruAnalytics/Info.plist; | ||||
| 				IPHONEOS_DEPLOYMENT_TARGET = 11.0; | ||||
| 				LD_RUNPATH_SEARCH_PATHS = ( | ||||
| 					"$(inherited)", | ||||
| 					"@executable_path/Frameworks", | ||||
| 				); | ||||
| 				MODULE_NAME = ExampleApp; | ||||
| 				PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.demo.$(PRODUCT_NAME:rfc1034identifier)"; | ||||
| 				PRODUCT_NAME = "$(TARGET_NAME)"; | ||||
| 				SWIFT_SWIFT3_OBJC_INFERENCE = Default; | ||||
| 				SWIFT_VERSION = 5.0; | ||||
| 			}; | ||||
| 			name = Release; | ||||
| 		}; | ||||
| /* End XCBuildConfiguration section */ | ||||
| 
 | ||||
| /* Begin XCConfigurationList section */ | ||||
| 		607FACCB1AFB9204008FA782 /* Build configuration list for PBXProject "GuruAnalytics" */ = { | ||||
| 			isa = XCConfigurationList; | ||||
| 			buildConfigurations = ( | ||||
| 				607FACED1AFB9204008FA782 /* Debug */, | ||||
| 				607FACEE1AFB9204008FA782 /* Release */, | ||||
| 			); | ||||
| 			defaultConfigurationIsVisible = 0; | ||||
| 			defaultConfigurationName = Release; | ||||
| 		}; | ||||
| 		607FACEF1AFB9204008FA782 /* Build configuration list for PBXNativeTarget "GuruAnalytics_Example" */ = { | ||||
| 			isa = XCConfigurationList; | ||||
| 			buildConfigurations = ( | ||||
| 				607FACF01AFB9204008FA782 /* Debug */, | ||||
| 				607FACF11AFB9204008FA782 /* Release */, | ||||
| 			); | ||||
| 			defaultConfigurationIsVisible = 0; | ||||
| 			defaultConfigurationName = Release; | ||||
| 		}; | ||||
| /* End XCConfigurationList section */ | ||||
| 	}; | ||||
| 	rootObject = 607FACC81AFB9204008FA782 /* Project object */; | ||||
| } | ||||
							
								
								
									
										7
									
								
								Example/GuruAnalytics.xcodeproj/project.xcworkspace/contents.xcworkspacedata
								
								
									generated
								
								
								
									Normal file
								
							
							
						
						
									
										7
									
								
								Example/GuruAnalytics.xcodeproj/project.xcworkspace/contents.xcworkspacedata
								
								
									generated
								
								
								
									Normal file
								
							|  | @ -0,0 +1,7 @@ | |||
| <?xml version="1.0" encoding="UTF-8"?> | ||||
| <Workspace | ||||
|    version = "1.0"> | ||||
|    <FileRef | ||||
|       location = "self:GuruAnalytics.xcodeproj"> | ||||
|    </FileRef> | ||||
| </Workspace> | ||||
|  | @ -0,0 +1,117 @@ | |||
| <?xml version="1.0" encoding="UTF-8"?> | ||||
| <Scheme | ||||
|    LastUpgradeVersion = "0900" | ||||
|    version = "1.3"> | ||||
|    <BuildAction | ||||
|       parallelizeBuildables = "YES" | ||||
|       buildImplicitDependencies = "YES"> | ||||
|       <BuildActionEntries> | ||||
|          <BuildActionEntry | ||||
|             buildForTesting = "YES" | ||||
|             buildForRunning = "YES" | ||||
|             buildForProfiling = "YES" | ||||
|             buildForArchiving = "YES" | ||||
|             buildForAnalyzing = "YES"> | ||||
|             <BuildableReference | ||||
|                BuildableIdentifier = "primary" | ||||
|                BlueprintIdentifier = "607FACCF1AFB9204008FA782" | ||||
|                BuildableName = "GuruAnalytics_Example.app" | ||||
|                BlueprintName = "GuruAnalytics_Example" | ||||
|                ReferencedContainer = "container:GuruAnalytics.xcodeproj"> | ||||
|             </BuildableReference> | ||||
|          </BuildActionEntry> | ||||
|          <BuildActionEntry | ||||
|             buildForTesting = "YES" | ||||
|             buildForRunning = "YES" | ||||
|             buildForProfiling = "NO" | ||||
|             buildForArchiving = "NO" | ||||
|             buildForAnalyzing = "YES"> | ||||
|             <BuildableReference | ||||
|                BuildableIdentifier = "primary" | ||||
|                BlueprintIdentifier = "607FACE41AFB9204008FA782" | ||||
|                BuildableName = "GuruAnalytics_Tests.xctest" | ||||
|                BlueprintName = "GuruAnalytics_Tests" | ||||
|                ReferencedContainer = "container:GuruAnalytics.xcodeproj"> | ||||
|             </BuildableReference> | ||||
|          </BuildActionEntry> | ||||
|       </BuildActionEntries> | ||||
|    </BuildAction> | ||||
|    <TestAction | ||||
|       buildConfiguration = "Debug" | ||||
|       selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" | ||||
|       selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" | ||||
|       language = "" | ||||
|       shouldUseLaunchSchemeArgsEnv = "YES"> | ||||
|       <Testables> | ||||
|          <TestableReference | ||||
|             skipped = "NO"> | ||||
|             <BuildableReference | ||||
|                BuildableIdentifier = "primary" | ||||
|                BlueprintIdentifier = "607FACE41AFB9204008FA782" | ||||
|                BuildableName = "GuruAnalytics_Tests.xctest" | ||||
|                BlueprintName = "GuruAnalytics_Tests" | ||||
|                ReferencedContainer = "container:GuruAnalytics.xcodeproj"> | ||||
|             </BuildableReference> | ||||
|          </TestableReference> | ||||
|       </Testables> | ||||
|       <MacroExpansion> | ||||
|          <BuildableReference | ||||
|             BuildableIdentifier = "primary" | ||||
|             BlueprintIdentifier = "607FACCF1AFB9204008FA782" | ||||
|             BuildableName = "GuruAnalytics_Example.app" | ||||
|             BlueprintName = "GuruAnalytics_Example" | ||||
|             ReferencedContainer = "container:GuruAnalytics.xcodeproj"> | ||||
|          </BuildableReference> | ||||
|       </MacroExpansion> | ||||
|       <AdditionalOptions> | ||||
|       </AdditionalOptions> | ||||
|    </TestAction> | ||||
|    <LaunchAction | ||||
|       buildConfiguration = "Debug" | ||||
|       selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" | ||||
|       selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" | ||||
|       language = "" | ||||
|       launchStyle = "0" | ||||
|       useCustomWorkingDirectory = "NO" | ||||
|       ignoresPersistentStateOnLaunch = "NO" | ||||
|       debugDocumentVersioning = "YES" | ||||
|       debugServiceExtension = "internal" | ||||
|       allowLocationSimulation = "YES"> | ||||
|       <BuildableProductRunnable | ||||
|          runnableDebuggingMode = "0"> | ||||
|          <BuildableReference | ||||
|             BuildableIdentifier = "primary" | ||||
|             BlueprintIdentifier = "607FACCF1AFB9204008FA782" | ||||
|             BuildableName = "GuruAnalytics_Example.app" | ||||
|             BlueprintName = "GuruAnalytics_Example" | ||||
|             ReferencedContainer = "container:GuruAnalytics.xcodeproj"> | ||||
|          </BuildableReference> | ||||
|       </BuildableProductRunnable> | ||||
|       <AdditionalOptions> | ||||
|       </AdditionalOptions> | ||||
|    </LaunchAction> | ||||
|    <ProfileAction | ||||
|       buildConfiguration = "Release" | ||||
|       shouldUseLaunchSchemeArgsEnv = "YES" | ||||
|       savedToolIdentifier = "" | ||||
|       useCustomWorkingDirectory = "NO" | ||||
|       debugDocumentVersioning = "YES"> | ||||
|       <BuildableProductRunnable | ||||
|          runnableDebuggingMode = "0"> | ||||
|          <BuildableReference | ||||
|             BuildableIdentifier = "primary" | ||||
|             BlueprintIdentifier = "607FACCF1AFB9204008FA782" | ||||
|             BuildableName = "GuruAnalytics_Example.app" | ||||
|             BlueprintName = "GuruAnalytics_Example" | ||||
|             ReferencedContainer = "container:GuruAnalytics.xcodeproj"> | ||||
|          </BuildableReference> | ||||
|       </BuildableProductRunnable> | ||||
|    </ProfileAction> | ||||
|    <AnalyzeAction | ||||
|       buildConfiguration = "Debug"> | ||||
|    </AnalyzeAction> | ||||
|    <ArchiveAction | ||||
|       buildConfiguration = "Release" | ||||
|       revealArchiveInOrganizer = "YES"> | ||||
|    </ArchiveAction> | ||||
| </Scheme> | ||||
|  | @ -0,0 +1,10 @@ | |||
| <?xml version="1.0" encoding="UTF-8"?> | ||||
| <Workspace | ||||
|    version = "1.0"> | ||||
|    <FileRef | ||||
|       location = "group:GuruAnalytics.xcodeproj"> | ||||
|    </FileRef> | ||||
|    <FileRef | ||||
|       location = "group:Pods/Pods.xcodeproj"> | ||||
|    </FileRef> | ||||
| </Workspace> | ||||
|  | @ -0,0 +1,8 @@ | |||
| <?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>IDEDidComputeMac32BitWarning</key> | ||||
| 	<true/> | ||||
| </dict> | ||||
| </plist> | ||||
|  | @ -0,0 +1,67 @@ | |||
| // | ||||
| //  AppDelegate.swift | ||||
| //  GuruAnalytics | ||||
| // | ||||
| //  Created by devSC on 11/16/2022. | ||||
| //  Copyright (c) 2022 devSC. All rights reserved. | ||||
| // | ||||
| 
 | ||||
| import UIKit | ||||
| import GuruAnalyticsLib | ||||
| import RxSwift | ||||
| 
 | ||||
| @UIApplicationMain | ||||
| class AppDelegate: UIResponder, UIApplicationDelegate { | ||||
| 
 | ||||
|     var window: UIWindow? | ||||
| 
 | ||||
| 
 | ||||
|     func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { | ||||
|         // Override point for customization after application launch. | ||||
|          | ||||
| //        _ = NotificationCenter.default.rx.notification(UIApplication.didBecomeActiveNotification).take(1) | ||||
| //            .asSingle() | ||||
| //            .delay(.seconds(2), scheduler: MainScheduler.asyncInstance) | ||||
| //            .subscribe(onSuccess: { _ in | ||||
|                 GuruAnalytics.initializeLib(uploadPeriodInSecond: 60, | ||||
|                                             batchLimit: 25, | ||||
|                                             initializeTimeout: 5, | ||||
|                                             saasXAPPID: "test_app_id", | ||||
|                                             saasXDEVICEINFO: "appIdentifier=test.app.example;appVersion=1.0;deviceType=apple;deviceCountry=CN") | ||||
|                 GuruAnalytics.setUserID("100004") | ||||
|                 GuruAnalytics.setAdjustId("xsdfal021sxasdfl") | ||||
|                 GuruAnalytics.setDeviceId(UUID().uuidString) | ||||
|                 GuruAnalytics.setScreen("home") | ||||
|         GuruAnalytics.registerInternalEventObserver { errorCode, info in | ||||
|             print("") | ||||
|         } | ||||
| //            }) | ||||
| 
 | ||||
|         return true | ||||
|     } | ||||
| 
 | ||||
|     func applicationWillResignActive(_ application: UIApplication) { | ||||
|         // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. | ||||
|         // Use this method to pause ongoing tasks, disable timers, and throttle down OpenGL ES frame rates. Games should use this method to pause the game. | ||||
|     } | ||||
| 
 | ||||
|     func applicationDidEnterBackground(_ application: UIApplication) { | ||||
|         // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. | ||||
|         // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. | ||||
|     } | ||||
| 
 | ||||
|     func applicationWillEnterForeground(_ application: UIApplication) { | ||||
|         // Called as part of the transition from the background to the inactive state; here you can undo many of the changes made on entering the background. | ||||
|     } | ||||
| 
 | ||||
|     func applicationDidBecomeActive(_ application: UIApplication) { | ||||
|         // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. | ||||
|     } | ||||
| 
 | ||||
|     func applicationWillTerminate(_ application: UIApplication) { | ||||
|         // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
| } | ||||
| 
 | ||||
|  | @ -0,0 +1,46 @@ | |||
| <?xml version="1.0" encoding="UTF-8"?> | ||||
| <document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="13771" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" colorMatched="YES"> | ||||
|     <device id="retina4_7" orientation="portrait"> | ||||
|         <adaptation id="fullscreen"/> | ||||
|     </device> | ||||
|     <dependencies> | ||||
|         <deployment identifier="iOS"/> | ||||
|         <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="13772"/> | ||||
|         <capability name="Constraints with non-1.0 multipliers" minToolsVersion="5.1"/> | ||||
|         <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/> | ||||
|     </dependencies> | ||||
|     <objects> | ||||
|         <placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner"/> | ||||
|         <placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/> | ||||
|         <view contentMode="scaleToFill" id="iN0-l3-epB"> | ||||
|             <rect key="frame" x="0.0" y="0.0" width="480" height="480"/> | ||||
|             <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> | ||||
|             <subviews> | ||||
|                 <label opaque="NO" clipsSubviews="YES" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="  Copyright (c) 2015 CocoaPods. All rights reserved." textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" minimumFontSize="9" translatesAutoresizingMaskIntoConstraints="NO" id="8ie-xW-0ye"> | ||||
|                     <rect key="frame" x="20" y="439" width="441" height="21"/> | ||||
|                     <fontDescription key="fontDescription" type="system" pointSize="17"/> | ||||
|                     <color key="textColor" cocoaTouchSystemColor="darkTextColor"/> | ||||
|                     <nil key="highlightedColor"/> | ||||
|                 </label> | ||||
|                 <label opaque="NO" clipsSubviews="YES" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="GuruAnalytics_iOS" textAlignment="center" lineBreakMode="middleTruncation" baselineAdjustment="alignBaselines" minimumFontSize="18" translatesAutoresizingMaskIntoConstraints="NO" id="kId-c2-rCX"> | ||||
|                     <rect key="frame" x="20" y="140" width="441" height="43"/> | ||||
|                     <fontDescription key="fontDescription" type="boldSystem" pointSize="36"/> | ||||
|                     <color key="textColor" cocoaTouchSystemColor="darkTextColor"/> | ||||
|                     <nil key="highlightedColor"/> | ||||
|                 </label> | ||||
|             </subviews> | ||||
|             <color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/> | ||||
|             <constraints> | ||||
|                 <constraint firstItem="kId-c2-rCX" firstAttribute="centerY" secondItem="iN0-l3-epB" secondAttribute="bottom" multiplier="1/3" constant="1" id="5cJ-9S-tgC"/> | ||||
|                 <constraint firstAttribute="centerX" secondItem="kId-c2-rCX" secondAttribute="centerX" id="Koa-jz-hwk"/> | ||||
|                 <constraint firstAttribute="bottom" secondItem="8ie-xW-0ye" secondAttribute="bottom" constant="20" id="Kzo-t9-V3l"/> | ||||
|                 <constraint firstItem="8ie-xW-0ye" firstAttribute="leading" secondItem="iN0-l3-epB" secondAttribute="leading" constant="20" symbolic="YES" id="MfP-vx-nX0"/> | ||||
|                 <constraint firstAttribute="centerX" secondItem="8ie-xW-0ye" secondAttribute="centerX" id="ZEH-qu-HZ9"/> | ||||
|                 <constraint firstItem="kId-c2-rCX" firstAttribute="leading" secondItem="iN0-l3-epB" secondAttribute="leading" constant="20" symbolic="YES" id="fvb-Df-36g"/> | ||||
|             </constraints> | ||||
|             <nil key="simulatedStatusBarMetrics"/> | ||||
|             <freeformSimulatedSizeMetrics key="simulatedDestinationMetrics"/> | ||||
|             <point key="canvasLocation" x="548" y="455"/> | ||||
|         </view> | ||||
|     </objects> | ||||
| </document> | ||||
|  | @ -0,0 +1,156 @@ | |||
| <?xml version="1.0" encoding="UTF-8"?> | ||||
| <document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="22505" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES" initialViewController="vXZ-lx-hvc"> | ||||
|     <device id="retina4_7" orientation="portrait" appearance="light"/> | ||||
|     <dependencies> | ||||
|         <deployment identifier="iOS"/> | ||||
|         <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="22504"/> | ||||
|         <capability name="System colors in document resources" minToolsVersion="11.0"/> | ||||
|         <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/> | ||||
|     </dependencies> | ||||
|     <scenes> | ||||
|         <!--View Controller--> | ||||
|         <scene sceneID="ufC-wZ-h7g"> | ||||
|             <objects> | ||||
|                 <viewController id="vXZ-lx-hvc" customClass="ViewController" customModule="GuruAnalytics_Example" customModuleProvider="target" sceneMemberID="viewController"> | ||||
|                     <layoutGuides> | ||||
|                         <viewControllerLayoutGuide type="top" id="jyV-Pf-zRb"/> | ||||
|                         <viewControllerLayoutGuide type="bottom" id="2fi-mo-0CV"/> | ||||
|                     </layoutGuides> | ||||
|                     <view key="view" contentMode="scaleToFill" id="kh9-bI-dsS"> | ||||
|                         <rect key="frame" x="0.0" y="0.0" width="375" height="667"/> | ||||
|                         <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/> | ||||
|                         <subviews> | ||||
|                             <button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="UiT-xC-g9f"> | ||||
|                                 <rect key="frame" x="150.5" y="125" width="74" height="35"/> | ||||
|                                 <constraints> | ||||
|                                     <constraint firstAttribute="height" constant="35" id="clk-M7-Fmx"/> | ||||
|                                 </constraints> | ||||
|                                 <state key="normal" title="Button"/> | ||||
|                                 <buttonConfiguration key="configuration" style="plain" title="Delete"/> | ||||
|                                 <connections> | ||||
|                                     <action selector="deleteItem:" destination="vXZ-lx-hvc" eventType="touchUpInside" id="qxv-Dq-ok1"/> | ||||
|                                 </connections> | ||||
|                             </button> | ||||
|                             <button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="MFg-e1-QYB"> | ||||
|                                 <rect key="frame" x="121.5" y="180" width="132" height="35"/> | ||||
|                                 <constraints> | ||||
|                                     <constraint firstAttribute="height" constant="35" id="hZX-Nz-pHw"/> | ||||
|                                 </constraints> | ||||
|                                 <state key="normal" title="Button"/> | ||||
|                                 <buttonConfiguration key="configuration" style="plain" title="set firebase id"/> | ||||
|                                 <connections> | ||||
|                                     <action selector="setFirebaseId:" destination="vXZ-lx-hvc" eventType="touchUpInside" id="iaO-7l-pOx"/> | ||||
|                                 </connections> | ||||
|                             </button> | ||||
|                             <button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="P7y-en-hqa"> | ||||
|                                 <rect key="frame" x="141" y="235" width="93" height="35"/> | ||||
|                                 <constraints> | ||||
|                                     <constraint firstAttribute="height" constant="35" id="q4X-qi-ks2"/> | ||||
|                                 </constraints> | ||||
|                                 <state key="normal" title="Button"/> | ||||
|                                 <buttonConfiguration key="configuration" style="plain" title="Get Logs"/> | ||||
|                                 <connections> | ||||
|                                     <action selector="getLogs:" destination="vXZ-lx-hvc" eventType="touchUpInside" id="fKR-om-I4f"/> | ||||
|                                 </connections> | ||||
|                             </button> | ||||
|                             <button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="G3I-za-UMF"> | ||||
|                                 <rect key="frame" x="132.5" y="290" width="110" height="35"/> | ||||
|                                 <constraints> | ||||
|                                     <constraint firstAttribute="height" constant="35" id="2Fo-sC-mX5"/> | ||||
|                                 </constraints> | ||||
|                                 <state key="normal" title="Button"/> | ||||
|                                 <buttonConfiguration key="configuration" style="plain" title="Start Timer"/> | ||||
|                                 <connections> | ||||
|                                     <action selector="startTimer:" destination="vXZ-lx-hvc" eventType="touchUpInside" id="aj0-1v-Trc"/> | ||||
|                                 </connections> | ||||
|                             </button> | ||||
|                             <button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="hDD-Ie-zVC"> | ||||
|                                 <rect key="frame" x="133.5" y="345" width="108" height="35"/> | ||||
|                                 <constraints> | ||||
|                                     <constraint firstAttribute="height" constant="35" id="WAl-TP-kMI"/> | ||||
|                                 </constraints> | ||||
|                                 <state key="normal" title="Button"/> | ||||
|                                 <buttonConfiguration key="configuration" style="plain" title="Stop Timer"/> | ||||
|                                 <connections> | ||||
|                                     <action selector="stopTimer:" destination="vXZ-lx-hvc" eventType="touchUpInside" id="s1z-ZZ-0bX"/> | ||||
|                                 </connections> | ||||
|                             </button> | ||||
|                             <slider opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" value="0.5" minValue="0.0" maxValue="1" translatesAutoresizingMaskIntoConstraints="NO" id="wDL-gO-FHP"> | ||||
|                                 <rect key="frame" x="24" y="400" width="327" height="31"/> | ||||
|                                 <connections> | ||||
|                                     <action selector="onSliderAction:" destination="vXZ-lx-hvc" eventType="valueChanged" id="Vhs-Q3-lRm"/> | ||||
|                                 </connections> | ||||
|                             </slider> | ||||
|                             <button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="6oL-kc-ZkC"> | ||||
|                                 <rect key="frame" x="153.5" y="70" width="68" height="35"/> | ||||
|                                 <constraints> | ||||
|                                     <constraint firstAttribute="height" constant="35" id="IMO-pR-HdA"/> | ||||
|                                 </constraints> | ||||
|                                 <state key="normal" title="Button"/> | ||||
|                                 <buttonConfiguration key="configuration" style="plain" title="Insert"/> | ||||
|                                 <connections> | ||||
|                                     <action selector="create:" destination="vXZ-lx-hvc" eventType="touchUpInside" id="Zut-Np-82g"/> | ||||
|                                 </connections> | ||||
|                             </button> | ||||
|                             <view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="IoV-jb-3en"> | ||||
|                                 <rect key="frame" x="100.5" y="450" width="174.5" height="31"/> | ||||
|                                 <subviews> | ||||
|                                     <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Enable Upload" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="imo-Fc-VhE"> | ||||
|                                         <rect key="frame" x="0.0" y="0.0" width="110.5" height="31"/> | ||||
|                                         <fontDescription key="fontDescription" type="system" pointSize="17"/> | ||||
|                                         <nil key="textColor"/> | ||||
|                                         <nil key="highlightedColor"/> | ||||
|                                     </label> | ||||
|                                     <switch opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="750" verticalHuggingPriority="750" contentHorizontalAlignment="center" contentVerticalAlignment="center" on="YES" translatesAutoresizingMaskIntoConstraints="NO" id="PzM-jf-3Zn"> | ||||
|                                         <rect key="frame" x="125.5" y="0.0" width="51" height="31"/> | ||||
|                                         <connections> | ||||
|                                             <action selector="onEnableUploadSwitcher:" destination="vXZ-lx-hvc" eventType="valueChanged" id="b3h-gD-Cxk"/> | ||||
|                                         </connections> | ||||
|                                     </switch> | ||||
|                                 </subviews> | ||||
|                                 <color key="backgroundColor" systemColor="systemBackgroundColor"/> | ||||
|                                 <constraints> | ||||
|                                     <constraint firstAttribute="trailing" secondItem="PzM-jf-3Zn" secondAttribute="trailing" id="9wc-B1-IHF"/> | ||||
|                                     <constraint firstItem="PzM-jf-3Zn" firstAttribute="top" secondItem="IoV-jb-3en" secondAttribute="top" id="BoM-nX-inP"/> | ||||
|                                     <constraint firstItem="PzM-jf-3Zn" firstAttribute="leading" secondItem="imo-Fc-VhE" secondAttribute="trailing" constant="15" id="THp-97-usz"/> | ||||
|                                     <constraint firstAttribute="bottom" secondItem="PzM-jf-3Zn" secondAttribute="bottom" id="UJN-XS-pIl"/> | ||||
|                                     <constraint firstItem="imo-Fc-VhE" firstAttribute="leading" secondItem="IoV-jb-3en" secondAttribute="leading" id="fbI-2d-yHW"/> | ||||
|                                     <constraint firstAttribute="bottom" secondItem="imo-Fc-VhE" secondAttribute="bottom" id="nLV-ho-ab5"/> | ||||
|                                     <constraint firstItem="imo-Fc-VhE" firstAttribute="top" secondItem="IoV-jb-3en" secondAttribute="top" id="wTY-dv-3Tv"/> | ||||
|                                 </constraints> | ||||
|                             </view> | ||||
|                         </subviews> | ||||
|                         <color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/> | ||||
|                         <constraints> | ||||
|                             <constraint firstItem="UiT-xC-g9f" firstAttribute="centerX" secondItem="kh9-bI-dsS" secondAttribute="centerX" id="0nd-lb-t1t"/> | ||||
|                             <constraint firstItem="G3I-za-UMF" firstAttribute="centerX" secondItem="kh9-bI-dsS" secondAttribute="centerX" id="7yj-Lv-JX9"/> | ||||
|                             <constraint firstItem="6oL-kc-ZkC" firstAttribute="top" secondItem="jyV-Pf-zRb" secondAttribute="bottom" constant="50" id="8hA-7d-tDI"/> | ||||
|                             <constraint firstItem="G3I-za-UMF" firstAttribute="top" secondItem="P7y-en-hqa" secondAttribute="bottom" constant="20" id="BGD-WM-vcu"/> | ||||
|                             <constraint firstItem="wDL-gO-FHP" firstAttribute="leading" secondItem="kh9-bI-dsS" secondAttribute="leadingMargin" constant="10" id="DGc-Ts-Qth"/> | ||||
|                             <constraint firstItem="MFg-e1-QYB" firstAttribute="centerX" secondItem="kh9-bI-dsS" secondAttribute="centerX" id="NdO-nP-Diu"/> | ||||
|                             <constraint firstItem="P7y-en-hqa" firstAttribute="centerX" secondItem="kh9-bI-dsS" secondAttribute="centerX" id="P2P-Q3-Fz3"/> | ||||
|                             <constraint firstItem="P7y-en-hqa" firstAttribute="top" secondItem="MFg-e1-QYB" secondAttribute="bottom" constant="20" id="RfT-JT-d4s"/> | ||||
|                             <constraint firstItem="IoV-jb-3en" firstAttribute="centerX" secondItem="kh9-bI-dsS" secondAttribute="centerX" id="V09-Ub-KVG"/> | ||||
|                             <constraint firstItem="hDD-Ie-zVC" firstAttribute="top" secondItem="G3I-za-UMF" secondAttribute="bottom" constant="20" id="VhL-0o-Kp4"/> | ||||
|                             <constraint firstItem="IoV-jb-3en" firstAttribute="top" secondItem="wDL-gO-FHP" secondAttribute="bottom" constant="20" id="Yxs-CC-aBR"/> | ||||
|                             <constraint firstAttribute="trailingMargin" secondItem="wDL-gO-FHP" secondAttribute="trailing" constant="10" id="ZlY-n5-meR"/> | ||||
|                             <constraint firstItem="6oL-kc-ZkC" firstAttribute="centerX" secondItem="kh9-bI-dsS" secondAttribute="centerX" id="cOt-z8-AaJ"/> | ||||
|                             <constraint firstItem="IoV-jb-3en" firstAttribute="leading" relation="greaterThanOrEqual" secondItem="kh9-bI-dsS" secondAttribute="leading" constant="20" id="cwu-l4-u6z"/> | ||||
|                             <constraint firstItem="UiT-xC-g9f" firstAttribute="top" secondItem="6oL-kc-ZkC" secondAttribute="bottom" constant="20" id="gpg-kk-w0q"/> | ||||
|                             <constraint firstItem="hDD-Ie-zVC" firstAttribute="centerX" secondItem="kh9-bI-dsS" secondAttribute="centerX" id="n2v-EB-mYi"/> | ||||
|                             <constraint firstItem="wDL-gO-FHP" firstAttribute="top" secondItem="hDD-Ie-zVC" secondAttribute="bottom" constant="20" id="nqa-66-evV"/> | ||||
|                             <constraint firstItem="MFg-e1-QYB" firstAttribute="top" secondItem="UiT-xC-g9f" secondAttribute="bottom" constant="20" id="sSY-mT-dVD"/> | ||||
|                         </constraints> | ||||
|                     </view> | ||||
|                 </viewController> | ||||
|                 <placeholder placeholderIdentifier="IBFirstResponder" id="x5A-6p-PRh" sceneMemberID="firstResponder"/> | ||||
|             </objects> | ||||
|             <point key="canvasLocation" x="136.80000000000001" y="-0.44977511244377816"/> | ||||
|         </scene> | ||||
|     </scenes> | ||||
|     <resources> | ||||
|         <systemColor name="systemBackgroundColor"> | ||||
|             <color white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> | ||||
|         </systemColor> | ||||
|     </resources> | ||||
| </document> | ||||
|  | @ -0,0 +1,53 @@ | |||
| { | ||||
|   "images" : [ | ||||
|     { | ||||
|       "idiom" : "iphone", | ||||
|       "size" : "20x20", | ||||
|       "scale" : "2x" | ||||
|     }, | ||||
|     { | ||||
|       "idiom" : "iphone", | ||||
|       "size" : "20x20", | ||||
|       "scale" : "3x" | ||||
|     }, | ||||
|     { | ||||
|       "idiom" : "iphone", | ||||
|       "size" : "29x29", | ||||
|       "scale" : "2x" | ||||
|     }, | ||||
|     { | ||||
|       "idiom" : "iphone", | ||||
|       "size" : "29x29", | ||||
|       "scale" : "3x" | ||||
|     }, | ||||
|     { | ||||
|       "idiom" : "iphone", | ||||
|       "size" : "40x40", | ||||
|       "scale" : "2x" | ||||
|     }, | ||||
|     { | ||||
|       "idiom" : "iphone", | ||||
|       "size" : "40x40", | ||||
|       "scale" : "3x" | ||||
|     }, | ||||
|     { | ||||
|       "idiom" : "iphone", | ||||
|       "size" : "60x60", | ||||
|       "scale" : "2x" | ||||
|     }, | ||||
|     { | ||||
|       "idiom" : "iphone", | ||||
|       "size" : "60x60", | ||||
|       "scale" : "3x" | ||||
|     }, | ||||
|     { | ||||
|       "idiom" : "ios-marketing", | ||||
|       "size" : "1024x1024", | ||||
|       "scale" : "1x" | ||||
|     } | ||||
|   ], | ||||
|   "info" : { | ||||
|     "version" : 1, | ||||
|     "author" : "xcode" | ||||
|   } | ||||
| } | ||||
|  | @ -0,0 +1,39 @@ | |||
| <?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>CFBundleDevelopmentRegion</key> | ||||
| 	<string>en</string> | ||||
| 	<key>CFBundleExecutable</key> | ||||
| 	<string>$(EXECUTABLE_NAME)</string> | ||||
| 	<key>CFBundleIdentifier</key> | ||||
| 	<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string> | ||||
| 	<key>CFBundleInfoDictionaryVersion</key> | ||||
| 	<string>6.0</string> | ||||
| 	<key>CFBundleName</key> | ||||
| 	<string>$(PRODUCT_NAME)</string> | ||||
| 	<key>CFBundlePackageType</key> | ||||
| 	<string>APPL</string> | ||||
| 	<key>CFBundleShortVersionString</key> | ||||
| 	<string>1.0</string> | ||||
| 	<key>CFBundleSignature</key> | ||||
| 	<string>????</string> | ||||
| 	<key>CFBundleVersion</key> | ||||
| 	<string>1</string> | ||||
| 	<key>LSRequiresIPhoneOS</key> | ||||
| 	<true/> | ||||
| 	<key>UILaunchStoryboardName</key> | ||||
| 	<string>LaunchScreen</string> | ||||
| 	<key>UIMainStoryboardFile</key> | ||||
| 	<string>Main</string> | ||||
| 	<key>UIRequiredDeviceCapabilities</key> | ||||
| 	<array> | ||||
| 		<string>armv7</string> | ||||
| 	</array> | ||||
| 	<key>UISupportedInterfaceOrientations</key> | ||||
| 	<array> | ||||
| 		<string>UIInterfaceOrientationPortrait</string> | ||||
| 		<string>UIInterfaceOrientationLandscapeLeft</string> | ||||
| 	</array> | ||||
| </dict> | ||||
| </plist> | ||||
|  | @ -0,0 +1,7 @@ | |||
| import UIKit | ||||
| 
 | ||||
| var greeting = "Hello, playground" | ||||
| 
 | ||||
| let sss = ["1", "2", "3"].map({ "'\($0)'" | ||||
| }).joined(separator: ",") | ||||
| print("value in (\(sss))") | ||||
|  | @ -0,0 +1,4 @@ | |||
| <?xml version="1.0" encoding="UTF-8" standalone="yes"?> | ||||
| <playground version='5.0' target-platform='ios' buildActiveScheme='true' importAppTypes='true'> | ||||
|     <timeline fileName='timeline.xctimeline'/> | ||||
| </playground> | ||||
|  | @ -0,0 +1,112 @@ | |||
| // | ||||
| //  ViewController.swift | ||||
| //  GuruAnalytics_iOS | ||||
| // | ||||
| //  Created by mayue on 11/07/2022. | ||||
| //  Copyright (c) 2022 mayue. All rights reserved. | ||||
| // | ||||
| 
 | ||||
| import UIKit | ||||
| import GuruAnalyticsLib | ||||
| import MessageUI | ||||
| import RxSwift | ||||
| 
 | ||||
| class ViewController: UIViewController { | ||||
| 
 | ||||
|     private var timer1: Disposable? | ||||
|     private var timer2: Disposable? | ||||
|      | ||||
|     override func viewDidLoad() { | ||||
|         super.viewDidLoad() | ||||
|         // Do any additional setup after loading the view, typically from a nib. | ||||
|     } | ||||
|      | ||||
|     @IBAction func setFirebaseId(_ sender: Any) { | ||||
|         GuruAnalytics.setFirebaseId("2312:3XSFA0211231") | ||||
|     } | ||||
|      | ||||
|     @IBAction func create(_ sender: Any) { | ||||
|         GuruAnalytics.setScreen("home") | ||||
|         GuruAnalytics.logEvent("crate_clk_" + String(Int(Date().timeIntervalSince1970)), | ||||
|                                parameters: ["category": "category_\(Int.random(in: 0...100000))", | ||||
|                                             "int_v_test": 2147483647, "double_v_test": 200.1, | ||||
|                                             "string_v_test": "400", | ||||
|                                             "long_v_test":  Int64(1)]) | ||||
|     } | ||||
|      | ||||
|     @IBAction func deleteItem(_ sender: Any) { | ||||
|          | ||||
|     } | ||||
| 
 | ||||
|     @IBAction func getLogs(_ sender: UIButton) { | ||||
|         GuruAnalytics.eventsLogsArchive({ [weak self] url in | ||||
|             guard let `self` = self, let url = url else { return } | ||||
|              | ||||
|             if MFMailComposeViewController.canSendMail() { | ||||
|                 let controller = MFMailComposeViewController() | ||||
|                  | ||||
|                 do { | ||||
|                     let data = try Data(contentsOf: url) | ||||
|                     controller.addAttachmentData(data, mimeType: "application/zip", fileName: url.lastPathComponent) | ||||
|                 } catch { | ||||
|                     NSLog("\(error)") | ||||
|                 } | ||||
|                  | ||||
|                 controller.mailComposeDelegate = self | ||||
|                 self.present(controller, animated: true, completion: nil) | ||||
|             } | ||||
|              | ||||
|         }) | ||||
|     } | ||||
|      | ||||
|     @IBAction func startTimer(_ sender: Any) { | ||||
|         timer1 = Observable<Int>.interval(.milliseconds(10), scheduler: SerialDispatchQueueScheduler(qos: .default)) | ||||
|             .subscribe(onNext: { int in | ||||
|                 if int % 2 == 0 { | ||||
|                     GuruAnalytics.setUserProperty("\(int)", forName: "SerialDispatchQueueScheduler_interval_2") | ||||
|                 } | ||||
|                 GuruAnalytics.logEvent("SerialDispatchQueueScheduler_interval", parameters: ["value": int]) | ||||
|                 if int % 3 == 0 { | ||||
|                     GuruAnalytics.setUserProperty("\(int)", forName: "SerialDispatchQueueScheduler_interval_3") | ||||
|                 } | ||||
|             }) | ||||
|                  | ||||
|         timer2 = Observable<Int>.interval(.milliseconds(20), scheduler: MainScheduler.instance) | ||||
|             .subscribe(onNext: { int in | ||||
|                 if int % 2 == 0 { | ||||
|                     GuruAnalytics.setUserProperty("\(int)", forName: "MainScheduler_interval") | ||||
|                 } | ||||
|                 GuruAnalytics.logEvent("MainScheduler_interval", parameters: ["value": int]) | ||||
|                 if int % 3 == 0 { | ||||
|                     GuruAnalytics.setUserProperty("\(int)", forName: "MainScheduler_interval") | ||||
|                 } | ||||
|             }) | ||||
| 
 | ||||
|     } | ||||
|      | ||||
|     @IBAction func stopTimer(_ sender: Any) { | ||||
|         timer1?.dispose() | ||||
|         timer1 = nil | ||||
|         timer2?.dispose() | ||||
|         timer2 = nil | ||||
|     } | ||||
|      | ||||
|     @IBAction func onSliderAction(_ sender: UISlider) { | ||||
|         UIScreen.main.brightness = CGFloat(sender.value); | ||||
|     } | ||||
|     @IBAction func onEnableUploadSwitcher(_ sender: UISwitch) { | ||||
|         GuruAnalytics.setEnableUpload(isOn: sender.isOn) | ||||
|     } | ||||
|      | ||||
|     override func didReceiveMemoryWarning() { | ||||
|         super.didReceiveMemoryWarning() | ||||
|         // Dispose of any resources that can be recreated. | ||||
|     } | ||||
| 
 | ||||
| } | ||||
| 
 | ||||
| extension ViewController: MFMailComposeViewControllerDelegate { | ||||
|     func mailComposeController(_ controller: MFMailComposeViewController, didFinishWith result: MFMailComposeResult, error: Error?) { | ||||
|         controller.presentingViewController?.dismiss(animated: true, completion: nil) | ||||
|     } | ||||
| } | ||||
|  | @ -0,0 +1,29 @@ | |||
| source 'https://cdn.cocoapods.org/' | ||||
| source 'git@github.com:castbox/GuruSpecs.git' | ||||
| 
 | ||||
| use_frameworks! | ||||
| 
 | ||||
| platform :ios, '11.0' | ||||
| 
 | ||||
| target 'GuruAnalytics_Example' do | ||||
|   pod 'GuruAnalyticsLib', :path => '../' | ||||
| #  pod 'GuruAnalyticsLib', '0.3.4' | ||||
| end | ||||
| 
 | ||||
| post_install do |installer| | ||||
|    | ||||
|   installer.pods_project.targets.each do |target| | ||||
|     target.build_configurations.each do |config| | ||||
|       config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '11.0' | ||||
|     end | ||||
|   end | ||||
|    | ||||
|   installer.generated_projects.each do |project| | ||||
|     project.targets.each do |target| | ||||
|       target.build_configurations.each do |config| | ||||
|         config.build_settings["DEVELOPMENT_TEAM"] = "FRX48476C2" | ||||
|       end | ||||
|     end | ||||
|   end | ||||
|    | ||||
| end | ||||
|  | @ -0,0 +1,41 @@ | |||
| <?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> | ||||
|  | @ -0,0 +1,158 @@ | |||
| // | ||||
| //  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 | ||||
|     } | ||||
| 
 | ||||
| } | ||||
|  | @ -0,0 +1,228 @@ | |||
| // | ||||
| //  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 | ||||
|     } | ||||
| } | ||||
|  | @ -0,0 +1,391 @@ | |||
| // | ||||
| //  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 | ||||
| } | ||||
|  | @ -0,0 +1,681 @@ | |||
| // | ||||
| //  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) | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | @ -0,0 +1,65 @@ | |||
| // | ||||
| //  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" | ||||
|     } | ||||
| } | ||||
|  | @ -0,0 +1,36 @@ | |||
| // | ||||
| //  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") | ||||
|     } | ||||
| } | ||||
|  | @ -0,0 +1,51 @@ | |||
| // | ||||
| //  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 | ||||
|     } | ||||
|      | ||||
| } | ||||
|  | @ -0,0 +1,78 @@ | |||
| // | ||||
| //  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 | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | @ -0,0 +1,419 @@ | |||
| // | ||||
| //  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 | ||||
|     } | ||||
| } | ||||
|  | @ -0,0 +1,160 @@ | |||
| // | ||||
| //  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 | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|      | ||||
| } | ||||
|  | @ -0,0 +1,54 @@ | |||
| // | ||||
| //  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 | ||||
|     } | ||||
| } | ||||
|  | @ -0,0 +1,25 @@ | |||
| // | ||||
| //  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 | ||||
| } | ||||
|  | @ -0,0 +1,28 @@ | |||
| // | ||||
| //  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) | ||||
|     } | ||||
| } | ||||
|  | @ -0,0 +1,158 @@ | |||
| // | ||||
| //  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) | ||||
|     } | ||||
| } | ||||
|  | @ -0,0 +1,32 @@ | |||
| // | ||||
| //  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" | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | @ -0,0 +1,158 @@ | |||
| // | ||||
| //  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" | ||||
|     } | ||||
| } | ||||
|  | @ -0,0 +1,53 @@ | |||
| # | ||||
| # 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.5' | ||||
|   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@git.chengdu.pundit.company:castbox/GuruAnalytics_iOS.git', :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' | ||||
|   s.dependency 'Alamofire', '~> 5.0' | ||||
|   s.dependency 'FMDB' | ||||
|   s.dependency 'GzipSwift' | ||||
|   s.dependency 'CryptoSwift' | ||||
|   s.dependency 'SwiftyBeaver' | ||||
|    | ||||
|   s.subspec 'Privacy' do |ss| | ||||
|       ss.resource_bundles = { | ||||
|             s.name => 'GuruAnalytics/Assets/PrivacyInfo.xcprivacy' | ||||
|       } | ||||
|   end | ||||
| end | ||||
|  | @ -0,0 +1,19 @@ | |||
| Copyright (c) 2022 devSC <xiaochong2154@163.com> | ||||
| 
 | ||||
| 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. | ||||
|  | @ -0,0 +1,31 @@ | |||
| # GuruAnalytics | ||||
| 
 | ||||
| [](https://travis-ci.org/devSC/GuruAnalyticsLib) | ||||
| [](https://cocoapods.org/pods/GuruAnalyticsLib) | ||||
| [](https://cocoapods.org/pods/GuruAnalyticsLib) | ||||
| [](https://cocoapods.org/pods/GuruAnalyticsLib) | ||||
| 
 | ||||
| ## [CHANGELOG](https://github.com/castbox/GuruAnalytics_iOS/blob/master/CHANGELOG.md) | ||||
| 
 | ||||
| ## Example | ||||
| 
 | ||||
| To run the example project, clone the repo, and run `pod install` from the Example directory first. | ||||
| 
 | ||||
| ## Requirements | ||||
| 
 | ||||
| ## Installation | ||||
| 
 | ||||
| GuruAnalytics is available through [CocoaPods](https://cocoapods.org). To install | ||||
| it, simply add the following line to your Podfile: | ||||
| 
 | ||||
| ```ruby | ||||
| pod 'GuruAnalyticsLib' | ||||
| ``` | ||||
| 
 | ||||
| ## Author | ||||
| 
 | ||||
| devSC, xiaochong2154@163.com | ||||
| 
 | ||||
| ## License | ||||
| 
 | ||||
| GuruAnalyticsLib is available under the MIT license. See the LICENSE file for more info. | ||||
		Loading…
	
		Reference in New Issue