commit
						1e66521d9c
					
				
										
											Binary file not shown.
										
									
								
							|  | @ -0,0 +1,12 @@ | |||
| 
 | ||||
| 
 | ||||
| #import <Foundation/Foundation.h> | ||||
| #import <AppLovinSDK/AppLovinSDK.h> | ||||
| 
 | ||||
| NS_ASSUME_NONNULL_BEGIN | ||||
| 
 | ||||
| @interface ALMCMediationAdapter : ALMediationAdapter | ||||
| 
 | ||||
| @end | ||||
| 
 | ||||
| NS_ASSUME_NONNULL_END | ||||
										
											Binary file not shown.
										
									
								
							|  | @ -0,0 +1,6 @@ | |||
| framework module ALMCMediationAdapter { | ||||
|   umbrella header "ALMCMediationAdapter.h" | ||||
|   export * | ||||
| 
 | ||||
|   module * { export * } | ||||
| } | ||||
|  | @ -0,0 +1,796 @@ | |||
| // !$*UTF8*$! | ||||
| { | ||||
| 	archiveVersion = 1; | ||||
| 	classes = { | ||||
| 	}; | ||||
| 	objectVersion = 46; | ||||
| 	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 */; }; | ||||
| 		607FACEC1AFB9204008FA782 /* Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 607FACEB1AFB9204008FA782 /* Tests.swift */; }; | ||||
| 		644E309B2D899D6A00050E04 /* HappyPathAdsSdk.swift in Sources */ = {isa = PBXBuildFile; fileRef = 644E309A2D899D5B00050E04 /* HappyPathAdsSdk.swift */; }; | ||||
| 		64B020AA2D93CCB200B0F238 /* FusionAdsExample.debug.xcconfig in Resources */ = {isa = PBXBuildFile; fileRef = 64B020A92D93CCB200B0F238 /* FusionAdsExample.debug.xcconfig */; }; | ||||
| 		7F380C61845DF1BC864FB1F3 /* Pods_FusionAds_Example.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1647017313EA85A491798026 /* Pods_FusionAds_Example.framework */; }; | ||||
| 		C551C6C178AA8D1465FBF62D /* Pods_FusionAds_Tests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 94F874AE62963EE9EDA6A4F3 /* Pods_FusionAds_Tests.framework */; }; | ||||
| /* End PBXBuildFile section */ | ||||
| 
 | ||||
| /* Begin PBXContainerItemProxy section */ | ||||
| 		607FACE61AFB9204008FA782 /* PBXContainerItemProxy */ = { | ||||
| 			isa = PBXContainerItemProxy; | ||||
| 			containerPortal = 607FACC81AFB9204008FA782 /* Project object */; | ||||
| 			proxyType = 1; | ||||
| 			remoteGlobalIDString = 607FACCF1AFB9204008FA782; | ||||
| 			remoteInfo = FusionAds; | ||||
| 		}; | ||||
| /* End PBXContainerItemProxy section */ | ||||
| 
 | ||||
| /* Begin PBXFileReference section */ | ||||
| 		1647017313EA85A491798026 /* Pods_FusionAds_Example.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_FusionAds_Example.framework; sourceTree = BUILT_PRODUCTS_DIR; }; | ||||
| 		217D84A11B0D93B5D7BAEB44 /* Pods-FusionAds_Example.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-FusionAds_Example.release.xcconfig"; path = "Target Support Files/Pods-FusionAds_Example/Pods-FusionAds_Example.release.xcconfig"; sourceTree = "<group>"; }; | ||||
| 		3DBB4B509E1C6B4B597B85ED /* Pods-FusionAds_Tests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-FusionAds_Tests.debug.xcconfig"; path = "Target Support Files/Pods-FusionAds_Tests/Pods-FusionAds_Tests.debug.xcconfig"; sourceTree = "<group>"; }; | ||||
| 		562BB989E504293AF3C37E19 /* README.md */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = net.daringfireball.markdown; name = README.md; path = ../README.md; sourceTree = "<group>"; }; | ||||
| 		607FACD01AFB9204008FA782 /* FusionAds_Example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = FusionAds_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>"; }; | ||||
| 		607FACE51AFB9204008FA782 /* FusionAds_Tests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = FusionAds_Tests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; | ||||
| 		607FACEA1AFB9204008FA782 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; }; | ||||
| 		607FACEB1AFB9204008FA782 /* Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Tests.swift; sourceTree = "<group>"; }; | ||||
| 		644E309A2D899D5B00050E04 /* HappyPathAdsSdk.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HappyPathAdsSdk.swift; sourceTree = "<group>"; }; | ||||
| 		64B020A92D93CCB200B0F238 /* FusionAdsExample.debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = FusionAdsExample.debug.xcconfig; sourceTree = SOURCE_ROOT; }; | ||||
| 		94F874AE62963EE9EDA6A4F3 /* Pods_FusionAds_Tests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_FusionAds_Tests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; | ||||
| 		C3C7B70C80D4C136889B6E9F /* LICENSE */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text; name = LICENSE; path = ../LICENSE; sourceTree = "<group>"; }; | ||||
| 		CB2B8B14F76462E07EAC9E63 /* Pods-FusionAds_Example.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-FusionAds_Example.debug.xcconfig"; path = "Target Support Files/Pods-FusionAds_Example/Pods-FusionAds_Example.debug.xcconfig"; sourceTree = "<group>"; }; | ||||
| 		D642D8B715B7E7D56126D64A /* FusionAds.podspec */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text; name = FusionAds.podspec; path = ../FusionAds.podspec; sourceTree = "<group>"; xcLanguageSpecificationIdentifier = xcode.lang.ruby; }; | ||||
| 		FA9307EAC19AE275617F247A /* Pods-FusionAds_Tests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-FusionAds_Tests.release.xcconfig"; path = "Target Support Files/Pods-FusionAds_Tests/Pods-FusionAds_Tests.release.xcconfig"; sourceTree = "<group>"; }; | ||||
| /* End PBXFileReference section */ | ||||
| 
 | ||||
| /* Begin PBXFrameworksBuildPhase section */ | ||||
| 		607FACCD1AFB9204008FA782 /* Frameworks */ = { | ||||
| 			isa = PBXFrameworksBuildPhase; | ||||
| 			buildActionMask = 2147483647; | ||||
| 			files = ( | ||||
| 				7F380C61845DF1BC864FB1F3 /* Pods_FusionAds_Example.framework in Frameworks */, | ||||
| 			); | ||||
| 			runOnlyForDeploymentPostprocessing = 0; | ||||
| 		}; | ||||
| 		607FACE21AFB9204008FA782 /* Frameworks */ = { | ||||
| 			isa = PBXFrameworksBuildPhase; | ||||
| 			buildActionMask = 2147483647; | ||||
| 			files = ( | ||||
| 				C551C6C178AA8D1465FBF62D /* Pods_FusionAds_Tests.framework in Frameworks */, | ||||
| 			); | ||||
| 			runOnlyForDeploymentPostprocessing = 0; | ||||
| 		}; | ||||
| /* End PBXFrameworksBuildPhase section */ | ||||
| 
 | ||||
| /* Begin PBXGroup section */ | ||||
| 		031A8DA505F309DDA1866DDA /* Pods */ = { | ||||
| 			isa = PBXGroup; | ||||
| 			children = ( | ||||
| 				CB2B8B14F76462E07EAC9E63 /* Pods-FusionAds_Example.debug.xcconfig */, | ||||
| 				217D84A11B0D93B5D7BAEB44 /* Pods-FusionAds_Example.release.xcconfig */, | ||||
| 				3DBB4B509E1C6B4B597B85ED /* Pods-FusionAds_Tests.debug.xcconfig */, | ||||
| 				FA9307EAC19AE275617F247A /* Pods-FusionAds_Tests.release.xcconfig */, | ||||
| 			); | ||||
| 			path = Pods; | ||||
| 			sourceTree = "<group>"; | ||||
| 		}; | ||||
| 		607FACC71AFB9204008FA782 = { | ||||
| 			isa = PBXGroup; | ||||
| 			children = ( | ||||
| 				607FACF51AFB993E008FA782 /* Podspec Metadata */, | ||||
| 				607FACD21AFB9204008FA782 /* Example for FusionAds */, | ||||
| 				607FACE81AFB9204008FA782 /* Tests */, | ||||
| 				607FACD11AFB9204008FA782 /* Products */, | ||||
| 				031A8DA505F309DDA1866DDA /* Pods */, | ||||
| 				F055A86E03635AF331B617A6 /* Frameworks */, | ||||
| 			); | ||||
| 			sourceTree = "<group>"; | ||||
| 		}; | ||||
| 		607FACD11AFB9204008FA782 /* Products */ = { | ||||
| 			isa = PBXGroup; | ||||
| 			children = ( | ||||
| 				607FACD01AFB9204008FA782 /* FusionAds_Example.app */, | ||||
| 				607FACE51AFB9204008FA782 /* FusionAds_Tests.xctest */, | ||||
| 			); | ||||
| 			name = Products; | ||||
| 			sourceTree = "<group>"; | ||||
| 		}; | ||||
| 		607FACD21AFB9204008FA782 /* Example for FusionAds */ = { | ||||
| 			isa = PBXGroup; | ||||
| 			children = ( | ||||
| 				607FACD51AFB9204008FA782 /* AppDelegate.swift */, | ||||
| 				644E309A2D899D5B00050E04 /* HappyPathAdsSdk.swift */, | ||||
| 				607FACD71AFB9204008FA782 /* ViewController.swift */, | ||||
| 				607FACD91AFB9204008FA782 /* Main.storyboard */, | ||||
| 				607FACDC1AFB9204008FA782 /* Images.xcassets */, | ||||
| 				607FACDE1AFB9204008FA782 /* LaunchScreen.xib */, | ||||
| 				607FACD31AFB9204008FA782 /* Supporting Files */, | ||||
| 			); | ||||
| 			name = "Example for FusionAds"; | ||||
| 			path = FusionAds; | ||||
| 			sourceTree = "<group>"; | ||||
| 		}; | ||||
| 		607FACD31AFB9204008FA782 /* Supporting Files */ = { | ||||
| 			isa = PBXGroup; | ||||
| 			children = ( | ||||
| 				64B020A92D93CCB200B0F238 /* FusionAdsExample.debug.xcconfig */, | ||||
| 				607FACD41AFB9204008FA782 /* Info.plist */, | ||||
| 			); | ||||
| 			name = "Supporting Files"; | ||||
| 			sourceTree = "<group>"; | ||||
| 		}; | ||||
| 		607FACE81AFB9204008FA782 /* Tests */ = { | ||||
| 			isa = PBXGroup; | ||||
| 			children = ( | ||||
| 				607FACEB1AFB9204008FA782 /* Tests.swift */, | ||||
| 				607FACE91AFB9204008FA782 /* Supporting Files */, | ||||
| 			); | ||||
| 			path = Tests; | ||||
| 			sourceTree = "<group>"; | ||||
| 		}; | ||||
| 		607FACE91AFB9204008FA782 /* Supporting Files */ = { | ||||
| 			isa = PBXGroup; | ||||
| 			children = ( | ||||
| 				607FACEA1AFB9204008FA782 /* Info.plist */, | ||||
| 			); | ||||
| 			name = "Supporting Files"; | ||||
| 			sourceTree = "<group>"; | ||||
| 		}; | ||||
| 		607FACF51AFB993E008FA782 /* Podspec Metadata */ = { | ||||
| 			isa = PBXGroup; | ||||
| 			children = ( | ||||
| 				D642D8B715B7E7D56126D64A /* FusionAds.podspec */, | ||||
| 				562BB989E504293AF3C37E19 /* README.md */, | ||||
| 				C3C7B70C80D4C136889B6E9F /* LICENSE */, | ||||
| 			); | ||||
| 			name = "Podspec Metadata"; | ||||
| 			sourceTree = "<group>"; | ||||
| 		}; | ||||
| 		F055A86E03635AF331B617A6 /* Frameworks */ = { | ||||
| 			isa = PBXGroup; | ||||
| 			children = ( | ||||
| 				1647017313EA85A491798026 /* Pods_FusionAds_Example.framework */, | ||||
| 				94F874AE62963EE9EDA6A4F3 /* Pods_FusionAds_Tests.framework */, | ||||
| 			); | ||||
| 			name = Frameworks; | ||||
| 			sourceTree = "<group>"; | ||||
| 		}; | ||||
| /* End PBXGroup section */ | ||||
| 
 | ||||
| /* Begin PBXNativeTarget section */ | ||||
| 		607FACCF1AFB9204008FA782 /* FusionAds_Example */ = { | ||||
| 			isa = PBXNativeTarget; | ||||
| 			buildConfigurationList = 607FACEF1AFB9204008FA782 /* Build configuration list for PBXNativeTarget "FusionAds_Example" */; | ||||
| 			buildPhases = ( | ||||
| 				85691E62AF861653F33F7D28 /* [CP] Check Pods Manifest.lock */, | ||||
| 				607FACCC1AFB9204008FA782 /* Sources */, | ||||
| 				607FACCD1AFB9204008FA782 /* Frameworks */, | ||||
| 				607FACCE1AFB9204008FA782 /* Resources */, | ||||
| 				2750E8ADDDEFDCE68C501BDC /* [CP] Embed Pods Frameworks */, | ||||
| 				CBEDD8405524A33EC2AD32E7 /* [CP] Copy Pods Resources */, | ||||
| 			); | ||||
| 			buildRules = ( | ||||
| 			); | ||||
| 			dependencies = ( | ||||
| 			); | ||||
| 			name = FusionAds_Example; | ||||
| 			productName = FusionAds; | ||||
| 			productReference = 607FACD01AFB9204008FA782 /* FusionAds_Example.app */; | ||||
| 			productType = "com.apple.product-type.application"; | ||||
| 		}; | ||||
| 		607FACE41AFB9204008FA782 /* FusionAds_Tests */ = { | ||||
| 			isa = PBXNativeTarget; | ||||
| 			buildConfigurationList = 607FACF21AFB9204008FA782 /* Build configuration list for PBXNativeTarget "FusionAds_Tests" */; | ||||
| 			buildPhases = ( | ||||
| 				91D0DD3A884742465EABB0C2 /* [CP] Check Pods Manifest.lock */, | ||||
| 				607FACE11AFB9204008FA782 /* Sources */, | ||||
| 				607FACE21AFB9204008FA782 /* Frameworks */, | ||||
| 				607FACE31AFB9204008FA782 /* Resources */, | ||||
| 			); | ||||
| 			buildRules = ( | ||||
| 			); | ||||
| 			dependencies = ( | ||||
| 				607FACE71AFB9204008FA782 /* PBXTargetDependency */, | ||||
| 			); | ||||
| 			name = FusionAds_Tests; | ||||
| 			productName = Tests; | ||||
| 			productReference = 607FACE51AFB9204008FA782 /* FusionAds_Tests.xctest */; | ||||
| 			productType = "com.apple.product-type.bundle.unit-test"; | ||||
| 		}; | ||||
| /* End PBXNativeTarget section */ | ||||
| 
 | ||||
| /* Begin PBXProject section */ | ||||
| 		607FACC81AFB9204008FA782 /* Project object */ = { | ||||
| 			isa = PBXProject; | ||||
| 			attributes = { | ||||
| 				LastSwiftUpdateCheck = 0830; | ||||
| 				LastUpgradeCheck = 0830; | ||||
| 				ORGANIZATIONNAME = CocoaPods; | ||||
| 				TargetAttributes = { | ||||
| 					607FACCF1AFB9204008FA782 = { | ||||
| 						CreatedOnToolsVersion = 6.3.1; | ||||
| 						DevelopmentTeam = WP698B8M33; | ||||
| 						LastSwiftMigration = 0900; | ||||
| 					}; | ||||
| 					607FACE41AFB9204008FA782 = { | ||||
| 						CreatedOnToolsVersion = 6.3.1; | ||||
| 						DevelopmentTeam = SUAYNYNC5C; | ||||
| 						LastSwiftMigration = 0900; | ||||
| 						TestTargetID = 607FACCF1AFB9204008FA782; | ||||
| 					}; | ||||
| 				}; | ||||
| 			}; | ||||
| 			buildConfigurationList = 607FACCB1AFB9204008FA782 /* Build configuration list for PBXProject "FusionAds" */; | ||||
| 			compatibilityVersion = "Xcode 3.2"; | ||||
| 			developmentRegion = English; | ||||
| 			hasScannedForEncodings = 0; | ||||
| 			knownRegions = ( | ||||
| 				English, | ||||
| 				en, | ||||
| 				Base, | ||||
| 			); | ||||
| 			mainGroup = 607FACC71AFB9204008FA782; | ||||
| 			productRefGroup = 607FACD11AFB9204008FA782 /* Products */; | ||||
| 			projectDirPath = ""; | ||||
| 			projectRoot = ""; | ||||
| 			targets = ( | ||||
| 				607FACCF1AFB9204008FA782 /* FusionAds_Example */, | ||||
| 				607FACE41AFB9204008FA782 /* FusionAds_Tests */, | ||||
| 			); | ||||
| 		}; | ||||
| /* 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 */, | ||||
| 				64B020AA2D93CCB200B0F238 /* FusionAdsExample.debug.xcconfig in Resources */, | ||||
| 			); | ||||
| 			runOnlyForDeploymentPostprocessing = 0; | ||||
| 		}; | ||||
| 		607FACE31AFB9204008FA782 /* Resources */ = { | ||||
| 			isa = PBXResourcesBuildPhase; | ||||
| 			buildActionMask = 2147483647; | ||||
| 			files = ( | ||||
| 			); | ||||
| 			runOnlyForDeploymentPostprocessing = 0; | ||||
| 		}; | ||||
| /* End PBXResourcesBuildPhase section */ | ||||
| 
 | ||||
| /* Begin PBXShellScriptBuildPhase section */ | ||||
| 		2750E8ADDDEFDCE68C501BDC /* [CP] Embed Pods Frameworks */ = { | ||||
| 			isa = PBXShellScriptBuildPhase; | ||||
| 			buildActionMask = 2147483647; | ||||
| 			files = ( | ||||
| 			); | ||||
| 			inputPaths = ( | ||||
| 				"${PODS_ROOT}/Target Support Files/Pods-FusionAds_Example/Pods-FusionAds_Example-frameworks.sh", | ||||
| 				"${PODS_XCFRAMEWORKS_BUILD_DIR}/ATOM-Standalone/ATOM.framework/ATOM", | ||||
| 				"${PODS_XCFRAMEWORKS_BUILD_DIR}/AmazonPublisherServicesSDK/DTBiOSSDK.framework/DTBiOSSDK", | ||||
| 				"${PODS_XCFRAMEWORKS_BUILD_DIR}/AppLovinSDK/AppLovinSDK.framework/AppLovinSDK", | ||||
| 				"${PODS_XCFRAMEWORKS_BUILD_DIR}/HyBid/Core/OMSDK_Pubnativenet.framework/OMSDK_Pubnativenet", | ||||
| 				"${PODS_XCFRAMEWORKS_BUILD_DIR}/InMobiSDK/InMobiSDK.framework/InMobiSDK", | ||||
| 				"${PODS_XCFRAMEWORKS_BUILD_DIR}/MobileFuseSDK/MobileFuseSDK.framework/MobileFuseSDK", | ||||
| 				"${PODS_XCFRAMEWORKS_BUILD_DIR}/MolocoSDKiOS/MolocoSDK.framework/MolocoSDK", | ||||
| 				"${PODS_XCFRAMEWORKS_BUILD_DIR}/OMSDK_Appodeal/OMSDK_Appodeal.framework/OMSDK_Appodeal", | ||||
| 				"${PODS_XCFRAMEWORKS_BUILD_DIR}/OguryAds/OMID/OMSDK_Ogury.framework/OMSDK_Ogury", | ||||
| 				"${PODS_XCFRAMEWORKS_BUILD_DIR}/OpenWrapSDK/OpenWrap/OMSDK_Pubmatic.framework/OMSDK_Pubmatic", | ||||
| 				"${PODS_XCFRAMEWORKS_BUILD_DIR}/smaato-ios-sdk/InApp/SmaatoSDKInAppBidding.framework/SmaatoSDKInAppBidding", | ||||
| 				"${PODS_XCFRAMEWORKS_BUILD_DIR}/smaato-ios-sdk/Modules/Banner/SmaatoSDKBanner.framework/SmaatoSDKBanner", | ||||
| 				"${PODS_XCFRAMEWORKS_BUILD_DIR}/smaato-ios-sdk/Modules/Core/SmaatoSDKCore.framework/SmaatoSDKCore", | ||||
| 				"${PODS_XCFRAMEWORKS_BUILD_DIR}/smaato-ios-sdk/Modules/Interstitial/SmaatoSDKInterstitial.framework/SmaatoSDKInterstitial", | ||||
| 				"${PODS_XCFRAMEWORKS_BUILD_DIR}/smaato-ios-sdk/Modules/Native/SmaatoSDKNative.framework/SmaatoSDKNative", | ||||
| 				"${PODS_XCFRAMEWORKS_BUILD_DIR}/smaato-ios-sdk/Modules/OpenMeasurement/SmaatoSDKOpenMeasurement.framework/SmaatoSDKOpenMeasurement", | ||||
| 				"${PODS_XCFRAMEWORKS_BUILD_DIR}/smaato-ios-sdk/Modules/OpenMeasurement/OMSDK_Smaato.framework/OMSDK_Smaato", | ||||
| 				"${PODS_XCFRAMEWORKS_BUILD_DIR}/smaato-ios-sdk/Modules/Outstream/SmaatoSDKOutstream.framework/SmaatoSDKOutstream", | ||||
| 				"${PODS_XCFRAMEWORKS_BUILD_DIR}/smaato-ios-sdk/Modules/RewardedAds/SmaatoSDKRewardedAds.framework/SmaatoSDKRewardedAds", | ||||
| 				"${PODS_XCFRAMEWORKS_BUILD_DIR}/smaato-ios-sdk/Modules/RichMedia/SmaatoSDKRichMedia.framework/SmaatoSDKRichMedia", | ||||
| 				"${PODS_XCFRAMEWORKS_BUILD_DIR}/smaato-ios-sdk/Modules/Video/SmaatoSDKVideo.framework/SmaatoSDKVideo", | ||||
| 			); | ||||
| 			name = "[CP] Embed Pods Frameworks"; | ||||
| 			outputPaths = ( | ||||
| 				"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/ATOM.framework", | ||||
| 				"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/DTBiOSSDK.framework", | ||||
| 				"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/AppLovinSDK.framework", | ||||
| 				"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/OMSDK_Pubnativenet.framework", | ||||
| 				"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/InMobiSDK.framework", | ||||
| 				"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/MobileFuseSDK.framework", | ||||
| 				"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/MolocoSDK.framework", | ||||
| 				"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/OMSDK_Appodeal.framework", | ||||
| 				"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/OMSDK_Ogury.framework", | ||||
| 				"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/OMSDK_Pubmatic.framework", | ||||
| 				"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SmaatoSDKInAppBidding.framework", | ||||
| 				"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SmaatoSDKBanner.framework", | ||||
| 				"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SmaatoSDKCore.framework", | ||||
| 				"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SmaatoSDKInterstitial.framework", | ||||
| 				"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SmaatoSDKNative.framework", | ||||
| 				"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SmaatoSDKOpenMeasurement.framework", | ||||
| 				"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/OMSDK_Smaato.framework", | ||||
| 				"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SmaatoSDKOutstream.framework", | ||||
| 				"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SmaatoSDKRewardedAds.framework", | ||||
| 				"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SmaatoSDKRichMedia.framework", | ||||
| 				"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SmaatoSDKVideo.framework", | ||||
| 			); | ||||
| 			runOnlyForDeploymentPostprocessing = 0; | ||||
| 			shellPath = /bin/sh; | ||||
| 			shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-FusionAds_Example/Pods-FusionAds_Example-frameworks.sh\"\n"; | ||||
| 			showEnvVarsInLog = 0; | ||||
| 		}; | ||||
| 		85691E62AF861653F33F7D28 /* [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-FusionAds_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; | ||||
| 		}; | ||||
| 		91D0DD3A884742465EABB0C2 /* [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-FusionAds_Tests-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; | ||||
| 		}; | ||||
| 		CBEDD8405524A33EC2AD32E7 /* [CP] Copy Pods Resources */ = { | ||||
| 			isa = PBXShellScriptBuildPhase; | ||||
| 			buildActionMask = 2147483647; | ||||
| 			files = ( | ||||
| 			); | ||||
| 			inputPaths = ( | ||||
| 				"${PODS_ROOT}/Target Support Files/Pods-FusionAds_Example/Pods-FusionAds_Example-resources.sh", | ||||
| 				"${PODS_ROOT}/Ads-Global/SDK/PAGAdSDK.bundle", | ||||
| 				"${PODS_CONFIGURATION_BUILD_DIR}/Ads-Global/AdsGlobalSDK.bundle", | ||||
| 				"${PODS_CONFIGURATION_BUILD_DIR}/BidMachine/BidMachine.bundle", | ||||
| 				"${PODS_ROOT}/ChartboostSDK/ChartboostSDKResources.bundle", | ||||
| 				"${PODS_CONFIGURATION_BUILD_DIR}/FBAudienceNetwork/FBAudienceNetwork.bundle", | ||||
| 				"${PODS_CONFIGURATION_BUILD_DIR}/Fyber_Marketplace_SDK/Fyber_Marketplace_SDK.bundle", | ||||
| 				"${PODS_CONFIGURATION_BUILD_DIR}/Google-Mobile-Ads-SDK/GoogleMobileAdsResources.bundle", | ||||
| 				"${PODS_CONFIGURATION_BUILD_DIR}/GoogleUserMessagingPlatform/UserMessagingPlatformResources.bundle", | ||||
| 				"${PODS_CONFIGURATION_BUILD_DIR}/GuruConsent/GuruConsent.bundle", | ||||
| 				"${PODS_CONFIGURATION_BUILD_DIR}/HyBid/HyBidResources.bundle", | ||||
| 				"${PODS_ROOT}/HyBid/PubnativeLite/PubnativeLite/Resources/close.png", | ||||
| 				"${PODS_ROOT}/HyBid/PubnativeLite/PubnativeLite/Resources/close@2x.png", | ||||
| 				"${PODS_ROOT}/HyBid/PubnativeLite/PubnativeLite/Resources/close@3x.png", | ||||
| 				"${BUILT_PRODUCTS_DIR}/HyBid/HyBid.framework/HyBidCustomCTAView.nib", | ||||
| 				"${PODS_ROOT}/HyBid/PubnativeLite/PubnativeLite/Resources/HyBidLocalizable.xcstrings", | ||||
| 				"${BUILT_PRODUCTS_DIR}/HyBid/HyBid.framework/HyBidMRAIDCloseCardView.nib", | ||||
| 				"${BUILT_PRODUCTS_DIR}/HyBid/HyBid.framework/InternalWebBrowser.storyboardc", | ||||
| 				"${PODS_ROOT}/HyBid/PubnativeLite/PubnativeLite/Resources/PNLiteExternalLink.png", | ||||
| 				"${PODS_ROOT}/HyBid/PubnativeLite/PubnativeLite/Resources/PNLiteExternalLink1.png", | ||||
| 				"${PODS_ROOT}/HyBid/PubnativeLite/PubnativeLite/Resources/PNLiteExternalLink1@2x.png", | ||||
| 				"${PODS_ROOT}/HyBid/PubnativeLite/PubnativeLite/Resources/PNLiteExternalLink1@3x.png", | ||||
| 				"${PODS_ROOT}/HyBid/PubnativeLite/PubnativeLite/Resources/PNLiteExternalLink@2x.png", | ||||
| 				"${PODS_ROOT}/HyBid/PubnativeLite/PubnativeLite/Resources/PNLiteFullScreen.png", | ||||
| 				"${PODS_ROOT}/HyBid/PubnativeLite/PubnativeLite/Resources/PNLiteFullScreen@2x.png", | ||||
| 				"${PODS_ROOT}/HyBid/PubnativeLite/PubnativeLite/Resources/PNLitePlay.png", | ||||
| 				"${PODS_ROOT}/HyBid/PubnativeLite/PubnativeLite/Resources/PNLitePlay@2x.png", | ||||
| 				"${PODS_ROOT}/HyBid/PubnativeLite/PubnativeLite/Resources/PNLiteSkip.png", | ||||
| 				"${BUILT_PRODUCTS_DIR}/HyBid/HyBid.framework/PNLiteVASTPlayerFullScreenViewController.nib", | ||||
| 				"${BUILT_PRODUCTS_DIR}/HyBid/HyBid.framework/PNLiteVASTPlayerInterstitialViewController.nib", | ||||
| 				"${BUILT_PRODUCTS_DIR}/HyBid/HyBid.framework/PNLiteVASTPlayerRewardedViewController.nib", | ||||
| 				"${BUILT_PRODUCTS_DIR}/HyBid/HyBid.framework/PNLiteVASTPlayerViewController.nib", | ||||
| 				"${PODS_ROOT}/HyBid/PubnativeLite/PubnativeLite/Resources/skip.png", | ||||
| 				"${PODS_ROOT}/HyBid/PubnativeLite/PubnativeLite/Resources/skip@2x.png", | ||||
| 				"${PODS_ROOT}/HyBid/PubnativeLite/PubnativeLite/Resources/skip@3x.png", | ||||
| 				"${PODS_ROOT}/HyBid/PubnativeLite/PubnativeLite/Resources/sound-off.png", | ||||
| 				"${PODS_ROOT}/HyBid/PubnativeLite/PubnativeLite/Resources/sound-off@2x.png", | ||||
| 				"${PODS_ROOT}/HyBid/PubnativeLite/PubnativeLite/Resources/sound-off@3x.png", | ||||
| 				"${PODS_ROOT}/HyBid/PubnativeLite/PubnativeLite/Resources/sound-on.png", | ||||
| 				"${PODS_ROOT}/HyBid/PubnativeLite/PubnativeLite/Resources/sound-on@2x.png", | ||||
| 				"${PODS_ROOT}/HyBid/PubnativeLite/PubnativeLite/Resources/sound-on@3x.png", | ||||
| 				"${PODS_ROOT}/HyBid/PubnativeLite/PubnativeLite/Resources/VerveContentInfo.png", | ||||
| 				"${PODS_ROOT}/HyBid/PubnativeLite/PubnativeLite/OMSDK-1.5.2/omsdk.js", | ||||
| 				"${PODS_ROOT}/HyBid/PubnativeLite/PubnativeLite/Core/MRAID/hybidmraidscaling.js", | ||||
| 				"${PODS_ROOT}/HyBid/PubnativeLite/PubnativeLite/Core/MRAID/hybidscaling.js", | ||||
| 				"${PODS_ROOT}/HyBid/PubnativeLite/PubnativeLite/Core/MRAID/navigation_geolocation.js", | ||||
| 				"${PODS_CONFIGURATION_BUILD_DIR}/IronSourceAdMobAdapter/ISAdMobResources.bundle", | ||||
| 				"${PODS_CONFIGURATION_BUILD_DIR}/IronSourceSDK/IronSourcePrivacyInfo.bundle", | ||||
| 				"${PODS_CONFIGURATION_BUILD_DIR}/MintegralAdSDK/MTGSDK.bundle", | ||||
| 				"${PODS_CONFIGURATION_BUILD_DIR}/MolocoSDKiOS/MolocoSDK.bundle", | ||||
| 				"${PODS_CONFIGURATION_BUILD_DIR}/OpenWrapSDK/OpenWrapSDKPrivacyManifest.bundle", | ||||
| 				"${PODS_ROOT}/OpenWrapSDK/OpenWrapSDK/POBResources.bundle", | ||||
| 				"${PODS_ROOT}/OpenWrapSDK/OpenWrapSDK/POBNativeResources.bundle", | ||||
| 				"${PODS_CONFIGURATION_BUILD_DIR}/StackModules/StackModules.bundle", | ||||
| 				"${PODS_CONFIGURATION_BUILD_DIR}/StackModules/StackProductPresentation.bundle", | ||||
| 				"${PODS_CONFIGURATION_BUILD_DIR}/StackModules/StackRendering.bundle", | ||||
| 				"${PODS_ROOT}/TPExchange/TPExchange/TradPlusADX.bundle", | ||||
| 				"${PODS_ROOT}/TradPlusAdSDK/TradPlusAdSDK/TradPlusAds.bundle", | ||||
| 				"${PODS_CONFIGURATION_BUILD_DIR}/UnityAds/UnityAdsResources.bundle", | ||||
| 				"${PODS_CONFIGURATION_BUILD_DIR}/VungleAds/VungleAds.bundle", | ||||
| 				"${PODS_CONFIGURATION_BUILD_DIR}/smaato-ios-sdk/SmaatoResources.bundle", | ||||
| 			); | ||||
| 			name = "[CP] Copy Pods Resources"; | ||||
| 			outputPaths = ( | ||||
| 				"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/PAGAdSDK.bundle", | ||||
| 				"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/AdsGlobalSDK.bundle", | ||||
| 				"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/BidMachine.bundle", | ||||
| 				"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ChartboostSDKResources.bundle", | ||||
| 				"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/FBAudienceNetwork.bundle", | ||||
| 				"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/Fyber_Marketplace_SDK.bundle", | ||||
| 				"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/GoogleMobileAdsResources.bundle", | ||||
| 				"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/UserMessagingPlatformResources.bundle", | ||||
| 				"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/GuruConsent.bundle", | ||||
| 				"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/HyBidResources.bundle", | ||||
| 				"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/close.png", | ||||
| 				"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/close@2x.png", | ||||
| 				"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/close@3x.png", | ||||
| 				"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/HyBidCustomCTAView.nib", | ||||
| 				"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/HyBidLocalizable.xcstrings", | ||||
| 				"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/HyBidMRAIDCloseCardView.nib", | ||||
| 				"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/InternalWebBrowser.storyboardc", | ||||
| 				"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/PNLiteExternalLink.png", | ||||
| 				"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/PNLiteExternalLink1.png", | ||||
| 				"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/PNLiteExternalLink1@2x.png", | ||||
| 				"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/PNLiteExternalLink1@3x.png", | ||||
| 				"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/PNLiteExternalLink@2x.png", | ||||
| 				"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/PNLiteFullScreen.png", | ||||
| 				"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/PNLiteFullScreen@2x.png", | ||||
| 				"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/PNLitePlay.png", | ||||
| 				"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/PNLitePlay@2x.png", | ||||
| 				"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/PNLiteSkip.png", | ||||
| 				"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/PNLiteVASTPlayerFullScreenViewController.nib", | ||||
| 				"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/PNLiteVASTPlayerInterstitialViewController.nib", | ||||
| 				"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/PNLiteVASTPlayerRewardedViewController.nib", | ||||
| 				"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/PNLiteVASTPlayerViewController.nib", | ||||
| 				"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/skip.png", | ||||
| 				"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/skip@2x.png", | ||||
| 				"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/skip@3x.png", | ||||
| 				"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/sound-off.png", | ||||
| 				"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/sound-off@2x.png", | ||||
| 				"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/sound-off@3x.png", | ||||
| 				"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/sound-on.png", | ||||
| 				"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/sound-on@2x.png", | ||||
| 				"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/sound-on@3x.png", | ||||
| 				"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/VerveContentInfo.png", | ||||
| 				"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/omsdk.js", | ||||
| 				"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/hybidmraidscaling.js", | ||||
| 				"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/hybidscaling.js", | ||||
| 				"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/navigation_geolocation.js", | ||||
| 				"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ISAdMobResources.bundle", | ||||
| 				"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/IronSourcePrivacyInfo.bundle", | ||||
| 				"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/MTGSDK.bundle", | ||||
| 				"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/MolocoSDK.bundle", | ||||
| 				"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/OpenWrapSDKPrivacyManifest.bundle", | ||||
| 				"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/POBResources.bundle", | ||||
| 				"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/POBNativeResources.bundle", | ||||
| 				"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/StackModules.bundle", | ||||
| 				"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/StackProductPresentation.bundle", | ||||
| 				"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/StackRendering.bundle", | ||||
| 				"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/TradPlusADX.bundle", | ||||
| 				"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/TradPlusAds.bundle", | ||||
| 				"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/UnityAdsResources.bundle", | ||||
| 				"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/VungleAds.bundle", | ||||
| 				"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/SmaatoResources.bundle", | ||||
| 			); | ||||
| 			runOnlyForDeploymentPostprocessing = 0; | ||||
| 			shellPath = /bin/sh; | ||||
| 			shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-FusionAds_Example/Pods-FusionAds_Example-resources.sh\"\n"; | ||||
| 			showEnvVarsInLog = 0; | ||||
| 		}; | ||||
| /* End PBXShellScriptBuildPhase section */ | ||||
| 
 | ||||
| /* Begin PBXSourcesBuildPhase section */ | ||||
| 		607FACCC1AFB9204008FA782 /* Sources */ = { | ||||
| 			isa = PBXSourcesBuildPhase; | ||||
| 			buildActionMask = 2147483647; | ||||
| 			files = ( | ||||
| 				644E309B2D899D6A00050E04 /* HappyPathAdsSdk.swift in Sources */, | ||||
| 				607FACD81AFB9204008FA782 /* ViewController.swift in Sources */, | ||||
| 				607FACD61AFB9204008FA782 /* AppDelegate.swift in Sources */, | ||||
| 			); | ||||
| 			runOnlyForDeploymentPostprocessing = 0; | ||||
| 		}; | ||||
| 		607FACE11AFB9204008FA782 /* Sources */ = { | ||||
| 			isa = PBXSourcesBuildPhase; | ||||
| 			buildActionMask = 2147483647; | ||||
| 			files = ( | ||||
| 				607FACEC1AFB9204008FA782 /* Tests.swift in Sources */, | ||||
| 			); | ||||
| 			runOnlyForDeploymentPostprocessing = 0; | ||||
| 		}; | ||||
| /* End PBXSourcesBuildPhase section */ | ||||
| 
 | ||||
| /* Begin PBXTargetDependency section */ | ||||
| 		607FACE71AFB9204008FA782 /* PBXTargetDependency */ = { | ||||
| 			isa = PBXTargetDependency; | ||||
| 			target = 607FACCF1AFB9204008FA782 /* FusionAds_Example */; | ||||
| 			targetProxy = 607FACE61AFB9204008FA782 /* PBXContainerItemProxy */; | ||||
| 		}; | ||||
| /* End PBXTargetDependency 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 = { | ||||
| 				ALLOW_TARGET_PLATFORM_SPECIALIZATION = NO; | ||||
| 				ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; | ||||
| 				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_ON_DEMAND_RESOURCES = NO; | ||||
| 				ENABLE_STRICT_OBJC_MSGSEND = YES; | ||||
| 				ENABLE_TESTABILITY = YES; | ||||
| 				EXCLUDED_ARCHS = ""; | ||||
| 				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; | ||||
| 				SUPPORTED_PLATFORMS = "iphonesimulator iphoneos"; | ||||
| 				SUPPORTS_MACCATALYST = YES; | ||||
| 				SWIFT_OPTIMIZATION_LEVEL = "-Onone"; | ||||
| 			}; | ||||
| 			name = Debug; | ||||
| 		}; | ||||
| 		607FACEE1AFB9204008FA782 /* Release */ = { | ||||
| 			isa = XCBuildConfiguration; | ||||
| 			buildSettings = { | ||||
| 				ALLOW_TARGET_PLATFORM_SPECIALIZATION = NO; | ||||
| 				ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; | ||||
| 				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_ON_DEMAND_RESOURCES = 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; | ||||
| 				SUPPORTED_PLATFORMS = "iphonesimulator iphoneos"; | ||||
| 				SUPPORTS_MACCATALYST = YES; | ||||
| 				SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; | ||||
| 				VALIDATE_PRODUCT = YES; | ||||
| 			}; | ||||
| 			name = Release; | ||||
| 		}; | ||||
| 		607FACF01AFB9204008FA782 /* Debug */ = { | ||||
| 			isa = XCBuildConfiguration; | ||||
| 			baseConfigurationReference = 64B020A92D93CCB200B0F238 /* FusionAdsExample.debug.xcconfig */; | ||||
| 			buildSettings = { | ||||
| 				ALLOW_TARGET_PLATFORM_SPECIALIZATION = NO; | ||||
| 				ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; | ||||
| 				DEVELOPMENT_TEAM = WP698B8M33; | ||||
| 				EXCLUDED_ARCHS = ""; | ||||
| 				INFOPLIST_FILE = FusionAds/Info.plist; | ||||
| 				IPHONEOS_DEPLOYMENT_TARGET = 13.0; | ||||
| 				LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; | ||||
| 				MODULE_NAME = ExampleApp; | ||||
| 				ONLY_ACTIVE_ARCH = YES; | ||||
| 				PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.demo.$(PRODUCT_NAME:rfc1034identifier)"; | ||||
| 				PRODUCT_NAME = "$(TARGET_NAME)"; | ||||
| 				SUPPORTED_PLATFORMS = "iphonesimulator iphoneos"; | ||||
| 				SUPPORTS_MACCATALYST = YES; | ||||
| 				SWIFT_SWIFT3_OBJC_INFERENCE = Default; | ||||
| 				SWIFT_VERSION = 5.0; | ||||
| 			}; | ||||
| 			name = Debug; | ||||
| 		}; | ||||
| 		607FACF11AFB9204008FA782 /* Release */ = { | ||||
| 			isa = XCBuildConfiguration; | ||||
| 			baseConfigurationReference = 217D84A11B0D93B5D7BAEB44 /* Pods-FusionAds_Example.release.xcconfig */; | ||||
| 			buildSettings = { | ||||
| 				ALLOW_TARGET_PLATFORM_SPECIALIZATION = NO; | ||||
| 				ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; | ||||
| 				DEVELOPMENT_TEAM = SUAYNYNC5C; | ||||
| 				EXCLUDED_ARCHS = ""; | ||||
| 				"EXCLUDED_ARCHS[sdk=*]" = ""; | ||||
| 				INFOPLIST_FILE = FusionAds/Info.plist; | ||||
| 				IPHONEOS_DEPLOYMENT_TARGET = 13.0; | ||||
| 				LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; | ||||
| 				MODULE_NAME = ExampleApp; | ||||
| 				ONLY_ACTIVE_ARCH = NO; | ||||
| 				PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.demo.$(PRODUCT_NAME:rfc1034identifier)"; | ||||
| 				PRODUCT_NAME = "$(TARGET_NAME)"; | ||||
| 				SUPPORTED_PLATFORMS = "iphonesimulator iphoneos"; | ||||
| 				SUPPORTS_MACCATALYST = YES; | ||||
| 				SWIFT_SWIFT3_OBJC_INFERENCE = Default; | ||||
| 				SWIFT_VERSION = 5.0; | ||||
| 			}; | ||||
| 			name = Release; | ||||
| 		}; | ||||
| 		607FACF31AFB9204008FA782 /* Debug */ = { | ||||
| 			isa = XCBuildConfiguration; | ||||
| 			baseConfigurationReference = 3DBB4B509E1C6B4B597B85ED /* Pods-FusionAds_Tests.debug.xcconfig */; | ||||
| 			buildSettings = { | ||||
| 				DEVELOPMENT_TEAM = SUAYNYNC5C; | ||||
| 				FRAMEWORK_SEARCH_PATHS = ( | ||||
| 					"$(PLATFORM_DIR)/Developer/Library/Frameworks", | ||||
| 					"$(inherited)", | ||||
| 				); | ||||
| 				GCC_PREPROCESSOR_DEFINITIONS = ( | ||||
| 					"DEBUG=1", | ||||
| 					"$(inherited)", | ||||
| 				); | ||||
| 				INFOPLIST_FILE = Tests/Info.plist; | ||||
| 				IPHONEOS_DEPLOYMENT_TARGET = 13.0; | ||||
| 				LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; | ||||
| 				PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.$(PRODUCT_NAME:rfc1034identifier)"; | ||||
| 				PRODUCT_NAME = "$(TARGET_NAME)"; | ||||
| 				SWIFT_SWIFT3_OBJC_INFERENCE = Default; | ||||
| 				SWIFT_VERSION = 4.0; | ||||
| 				TEST_HOST = "$(BUILT_PRODUCTS_DIR)/FusionAds_Example.app/FusionAds_Example"; | ||||
| 			}; | ||||
| 			name = Debug; | ||||
| 		}; | ||||
| 		607FACF41AFB9204008FA782 /* Release */ = { | ||||
| 			isa = XCBuildConfiguration; | ||||
| 			baseConfigurationReference = FA9307EAC19AE275617F247A /* Pods-FusionAds_Tests.release.xcconfig */; | ||||
| 			buildSettings = { | ||||
| 				DEVELOPMENT_TEAM = SUAYNYNC5C; | ||||
| 				FRAMEWORK_SEARCH_PATHS = ( | ||||
| 					"$(PLATFORM_DIR)/Developer/Library/Frameworks", | ||||
| 					"$(inherited)", | ||||
| 				); | ||||
| 				INFOPLIST_FILE = Tests/Info.plist; | ||||
| 				IPHONEOS_DEPLOYMENT_TARGET = 13.0; | ||||
| 				LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; | ||||
| 				PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.$(PRODUCT_NAME:rfc1034identifier)"; | ||||
| 				PRODUCT_NAME = "$(TARGET_NAME)"; | ||||
| 				SWIFT_SWIFT3_OBJC_INFERENCE = Default; | ||||
| 				SWIFT_VERSION = 4.0; | ||||
| 				TEST_HOST = "$(BUILT_PRODUCTS_DIR)/FusionAds_Example.app/FusionAds_Example"; | ||||
| 			}; | ||||
| 			name = Release; | ||||
| 		}; | ||||
| /* End XCBuildConfiguration section */ | ||||
| 
 | ||||
| /* Begin XCConfigurationList section */ | ||||
| 		607FACCB1AFB9204008FA782 /* Build configuration list for PBXProject "FusionAds" */ = { | ||||
| 			isa = XCConfigurationList; | ||||
| 			buildConfigurations = ( | ||||
| 				607FACED1AFB9204008FA782 /* Debug */, | ||||
| 				607FACEE1AFB9204008FA782 /* Release */, | ||||
| 			); | ||||
| 			defaultConfigurationIsVisible = 0; | ||||
| 			defaultConfigurationName = Release; | ||||
| 		}; | ||||
| 		607FACEF1AFB9204008FA782 /* Build configuration list for PBXNativeTarget "FusionAds_Example" */ = { | ||||
| 			isa = XCConfigurationList; | ||||
| 			buildConfigurations = ( | ||||
| 				607FACF01AFB9204008FA782 /* Debug */, | ||||
| 				607FACF11AFB9204008FA782 /* Release */, | ||||
| 			); | ||||
| 			defaultConfigurationIsVisible = 0; | ||||
| 			defaultConfigurationName = Release; | ||||
| 		}; | ||||
| 		607FACF21AFB9204008FA782 /* Build configuration list for PBXNativeTarget "FusionAds_Tests" */ = { | ||||
| 			isa = XCConfigurationList; | ||||
| 			buildConfigurations = ( | ||||
| 				607FACF31AFB9204008FA782 /* Debug */, | ||||
| 				607FACF41AFB9204008FA782 /* Release */, | ||||
| 			); | ||||
| 			defaultConfigurationIsVisible = 0; | ||||
| 			defaultConfigurationName = Release; | ||||
| 		}; | ||||
| /* End XCConfigurationList section */ | ||||
| 	}; | ||||
| 	rootObject = 607FACC81AFB9204008FA782 /* Project object */; | ||||
| } | ||||
|  | @ -0,0 +1,7 @@ | |||
| <?xml version="1.0" encoding="UTF-8"?> | ||||
| <Workspace | ||||
|    version = "1.0"> | ||||
|    <FileRef | ||||
|       location = "self:FusionAds.xcodeproj"> | ||||
|    </FileRef> | ||||
| </Workspace> | ||||
|  | @ -0,0 +1,111 @@ | |||
| <?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 = "FusionAds_Example.app" | ||||
|                BlueprintName = "FusionAds_Example" | ||||
|                ReferencedContainer = "container:FusionAds.xcodeproj"> | ||||
|             </BuildableReference> | ||||
|          </BuildActionEntry> | ||||
|          <BuildActionEntry | ||||
|             buildForTesting = "YES" | ||||
|             buildForRunning = "YES" | ||||
|             buildForProfiling = "NO" | ||||
|             buildForArchiving = "NO" | ||||
|             buildForAnalyzing = "YES"> | ||||
|             <BuildableReference | ||||
|                BuildableIdentifier = "primary" | ||||
|                BlueprintIdentifier = "607FACE41AFB9204008FA782" | ||||
|                BuildableName = "FusionAds_Tests.xctest" | ||||
|                BlueprintName = "FusionAds_Tests" | ||||
|                ReferencedContainer = "container:FusionAds.xcodeproj"> | ||||
|             </BuildableReference> | ||||
|          </BuildActionEntry> | ||||
|       </BuildActionEntries> | ||||
|    </BuildAction> | ||||
|    <TestAction | ||||
|       buildConfiguration = "Debug" | ||||
|       selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" | ||||
|       selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" | ||||
|       shouldUseLaunchSchemeArgsEnv = "YES"> | ||||
|       <MacroExpansion> | ||||
|          <BuildableReference | ||||
|             BuildableIdentifier = "primary" | ||||
|             BlueprintIdentifier = "607FACCF1AFB9204008FA782" | ||||
|             BuildableName = "FusionAds_Example.app" | ||||
|             BlueprintName = "FusionAds_Example" | ||||
|             ReferencedContainer = "container:FusionAds.xcodeproj"> | ||||
|          </BuildableReference> | ||||
|       </MacroExpansion> | ||||
|       <Testables> | ||||
|          <TestableReference | ||||
|             skipped = "NO"> | ||||
|             <BuildableReference | ||||
|                BuildableIdentifier = "primary" | ||||
|                BlueprintIdentifier = "607FACE41AFB9204008FA782" | ||||
|                BuildableName = "FusionAds_Tests.xctest" | ||||
|                BlueprintName = "FusionAds_Tests" | ||||
|                ReferencedContainer = "container:FusionAds.xcodeproj"> | ||||
|             </BuildableReference> | ||||
|          </TestableReference> | ||||
|       </Testables> | ||||
|    </TestAction> | ||||
|    <LaunchAction | ||||
|       buildConfiguration = "Debug" | ||||
|       selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" | ||||
|       selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" | ||||
|       launchStyle = "0" | ||||
|       useCustomWorkingDirectory = "NO" | ||||
|       ignoresPersistentStateOnLaunch = "NO" | ||||
|       debugDocumentVersioning = "YES" | ||||
|       debugServiceExtension = "internal" | ||||
|       allowLocationSimulation = "YES"> | ||||
|       <BuildableProductRunnable | ||||
|          runnableDebuggingMode = "0"> | ||||
|          <BuildableReference | ||||
|             BuildableIdentifier = "primary" | ||||
|             BlueprintIdentifier = "607FACCF1AFB9204008FA782" | ||||
|             BuildableName = "FusionAds_Example.app" | ||||
|             BlueprintName = "FusionAds_Example" | ||||
|             ReferencedContainer = "container:FusionAds.xcodeproj"> | ||||
|          </BuildableReference> | ||||
|       </BuildableProductRunnable> | ||||
|    </LaunchAction> | ||||
|    <ProfileAction | ||||
|       buildConfiguration = "Release" | ||||
|       shouldUseLaunchSchemeArgsEnv = "YES" | ||||
|       savedToolIdentifier = "" | ||||
|       useCustomWorkingDirectory = "NO" | ||||
|       debugDocumentVersioning = "YES"> | ||||
|       <BuildableProductRunnable | ||||
|          runnableDebuggingMode = "0"> | ||||
|          <BuildableReference | ||||
|             BuildableIdentifier = "primary" | ||||
|             BlueprintIdentifier = "607FACCF1AFB9204008FA782" | ||||
|             BuildableName = "FusionAds_Example.app" | ||||
|             BlueprintName = "FusionAds_Example" | ||||
|             ReferencedContainer = "container:FusionAds.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:FusionAds.xcodeproj"> | ||||
|    </FileRef> | ||||
|    <FileRef | ||||
|       location = "group:Pods/Pods.xcodeproj"> | ||||
|    </FileRef> | ||||
| </Workspace> | ||||
|  | @ -0,0 +1,45 @@ | |||
| // | ||||
| //  AppDelegate.swift | ||||
| //  FusionAds | ||||
| // | ||||
| //  Created by Haoyi on 02/28/2025. | ||||
| //  Copyright (c) 2025 Haoyi. All rights reserved. | ||||
| // | ||||
| 
 | ||||
| import UIKit | ||||
| 
 | ||||
| @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. | ||||
|         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="FusionAds" 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,30 @@ | |||
| <?xml version="1.0" encoding="UTF-8"?> | ||||
| <document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="13771" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES" initialViewController="vXZ-lx-hvc"> | ||||
|     <device id="retina4_7" orientation="portrait"> | ||||
|         <adaptation id="fullscreen"/> | ||||
|     </device> | ||||
|     <dependencies> | ||||
|         <deployment identifier="iOS"/> | ||||
|         <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="13772"/> | ||||
|         <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="FusionAds_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"/> | ||||
|                         <color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/> | ||||
|                     </view> | ||||
|                 </viewController> | ||||
|                 <placeholder placeholderIdentifier="IBFirstResponder" id="x5A-6p-PRh" sceneMemberID="firstResponder"/> | ||||
|             </objects> | ||||
|         </scene> | ||||
|     </scenes> | ||||
| </document> | ||||
|  | @ -0,0 +1,286 @@ | |||
| // | ||||
| //  HappyPathAdsSdk.swift | ||||
| //  FusionAds | ||||
| // | ||||
| //  Created by 250102 on 2025/3/18. | ||||
| //  Copyright © 2025 CocoaPods. All rights reserved. | ||||
| // | ||||
| import FusionAds | ||||
| 
 | ||||
| let queue = DispatchQueue.main | ||||
| 
 | ||||
| /// 只许成功,不许失败 的假广告sdk | ||||
| class HappyPathAdsSdk: GuruAdsSdk { | ||||
|      | ||||
|     static let shared = HappyPathAdsSdk() | ||||
|      | ||||
|     public private(set) var ads: [String: Any] = [:] | ||||
|      | ||||
|     private init() { | ||||
|          | ||||
|     } | ||||
|      | ||||
|     public static func obtain(viewController: UIViewController) -> GuruAdsSdk { | ||||
|         return shared | ||||
|     } | ||||
|      | ||||
|     public var adPlatform: FusionAds.AdPlatform { | ||||
|         return AdPlatform.test | ||||
|     } | ||||
|      | ||||
|     public var adsProfile: FusionAds.AdsProfile? = nil | ||||
|      | ||||
|     func initialize(adsProfile: FusionAds.AdsProfile) async -> Bool { | ||||
|         self.adsProfile = adsProfile | ||||
|         return true | ||||
|     } | ||||
|      | ||||
|     func obtainInterstitialAd(adConfig: FusionAds.AdConfig) -> FusionAds.GuruInterstitialAd { | ||||
|          | ||||
|         let ad = ads[adConfig.adUnitId] as? HappyPathInterstitialAd | ||||
|         if(ad != nil) { | ||||
|             return ad! | ||||
|         } | ||||
|          | ||||
|         let newAd = HappyPathInterstitialAd(engineId: adConfig.engineId, adUnitId: adConfig.adUnitId, adPlatform: adPlatform) | ||||
|         ads[adConfig.adUnitId] = newAd | ||||
|         return newAd | ||||
|     } | ||||
|      | ||||
|     func obtainRewardedAd(adConfig: FusionAds.AdConfig) -> FusionAds.GuruRewardedAd { | ||||
|         let ad = ads[adConfig.adUnitId] as? HappyPathRewardedAd | ||||
|         if(ad != nil) { | ||||
|             return ad! | ||||
|         } | ||||
|          | ||||
|         let newAd = HappyPathRewardedAd(engineId: adConfig.engineId, adUnitId: adConfig.adUnitId, adPlatform: adPlatform) | ||||
|         ads[adConfig.adUnitId] = newAd | ||||
|         return newAd | ||||
|     } | ||||
|      | ||||
|     func obtainBannerAd(adConfig: FusionAds.AdConfig) -> FusionAds.GuruBannerAd { | ||||
|         let ad = ads[adConfig.adUnitId] as? HappyPathBannerAd | ||||
|         if(ad != nil) { | ||||
|             return ad! | ||||
|         } | ||||
|          | ||||
|         let newAd = HappyPathBannerAd(engineId: adConfig.engineId, adUnitId: adConfig.adUnitId, adPlatform: adPlatform) | ||||
|         ads[adConfig.adUnitId] = newAd | ||||
|         return newAd | ||||
|     } | ||||
|      | ||||
|     func obtainMRecAd(adConfig: FusionAds.AdConfig) -> FusionAds.GuruMRecAd { | ||||
|         let ad = ads[adConfig.adUnitId] as? HappyPathMRecAd | ||||
|         if(ad != nil) { | ||||
|             return ad! | ||||
|         } | ||||
|          | ||||
|         let newAd = HappyPathMRecAd(engineId: adConfig.engineId, adUnitId: adConfig.adUnitId, adPlatform: adPlatform) | ||||
|         ads[adConfig.adUnitId] = newAd | ||||
|         return newAd | ||||
|     } | ||||
|      | ||||
|     func processCrossAction(action: FusionAds.AdsCrossAction) -> Bool { | ||||
|         return false | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| class HappyPathInterstitialAd : GuruInterstitialAd { | ||||
|      | ||||
|     private var ad: HappyPathOriginalAd | ||||
|      | ||||
|     public private(set) var state: HappyPathAdState = HappyPathAdState() | ||||
| 
 | ||||
|     override init(engineId: Int, adUnitId: String, adPlatform: AdPlatform) { | ||||
|         ad = HappyPathOriginalAd(adPlatform: adPlatform, adUnitId: adUnitId) | ||||
|         super.init(engineId: engineId, adUnitId: adUnitId, adPlatform: adPlatform) | ||||
|     } | ||||
|      | ||||
|     override public func load() -> Bool { | ||||
|         state.isLoaded = true | ||||
|         let fusionAd = HappyPathFusionAd(engineId: engineId, adType: AdType.Interstitial, originAd: ad) | ||||
|         queue.async { | ||||
|             [weak self] in | ||||
|             self?.listener?.onAdLoaded(ad: fusionAd) | ||||
|         } | ||||
|         return true; | ||||
|     } | ||||
|      | ||||
|     override public func show(_ request: InterstitialShowRequest? = nil) -> Bool { | ||||
|         state.isShown = true | ||||
|         ad.placement = request?.placement | ||||
|         let fusionAd = HappyPathFusionAd(engineId: engineId, adType: AdType.Interstitial, originAd: ad) | ||||
|         queue.async { | ||||
|             [weak self] in | ||||
|             self?.listener?.onAdDisplayed(ad: fusionAd) | ||||
|         } | ||||
|         return true; | ||||
|     } | ||||
|      | ||||
|     override public func destroy() -> Bool { | ||||
|         state.isDestroyed = true | ||||
|         return true; | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| class HappyPathRewardedAd : GuruRewardedAd { | ||||
|     private var ad: HappyPathOriginalAd | ||||
|     public private(set) var state: HappyPathAdState = HappyPathAdState() | ||||
|     override init(engineId: Int, adUnitId: String, adPlatform: AdPlatform) { | ||||
|         ad = HappyPathOriginalAd(adPlatform: adPlatform, adUnitId: adUnitId) | ||||
|         super.init(engineId: engineId, adUnitId: adUnitId, adPlatform: adPlatform) | ||||
|     } | ||||
|      | ||||
|     override public func load() -> Bool { | ||||
|         let fusionAd = HappyPathFusionAd(engineId: engineId, adType: AdType.Rewarded, originAd: ad) | ||||
|         state.isLoaded = true | ||||
|         queue.async { | ||||
|             [weak self] in | ||||
|             self?.listener?.onAdLoaded(ad: fusionAd) | ||||
|         } | ||||
|         return true; | ||||
|     } | ||||
|      | ||||
|     override public func show(_ request: RewardedShowRequest? = nil) -> Bool { | ||||
|         state.isShown = true | ||||
|         ad.placement = request?.placement | ||||
|         let fusionAd = HappyPathFusionAd(engineId: engineId, adType: AdType.Rewarded, originAd: ad) | ||||
|          | ||||
|         queue.async { | ||||
|             [weak self] in | ||||
|             self?.listener?.onAdDisplayed(ad: fusionAd) | ||||
|         } | ||||
|         return true; | ||||
|     } | ||||
|      | ||||
|     override public func destroy() -> Bool { | ||||
|         state.isDestroyed = true | ||||
|         return true; | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| class HappyPathBannerAd : GuruBannerAd { | ||||
|     private var ad: HappyPathOriginalAd | ||||
|     public private(set) var state: HappyPathAdState = HappyPathAdState() | ||||
|      | ||||
|     public private(set) var lastShowRequest: BannerShowRequest? = nil | ||||
|      | ||||
|     override init(engineId: Int, adUnitId: String, adPlatform: AdPlatform) { | ||||
|         ad = HappyPathOriginalAd(adPlatform: adPlatform, adUnitId: adUnitId) | ||||
|         super.init(engineId: engineId, adUnitId: adUnitId, adPlatform: adPlatform) | ||||
|     } | ||||
|      | ||||
|     override public func load() -> Bool { | ||||
|         state.isLoaded = true | ||||
|         let fusionAd = HappyPathFusionAd(engineId: engineId, adType: AdType.Banner, originAd: ad) | ||||
|         queue.async { | ||||
|             [weak self] in | ||||
|             self?.listener?.onAdLoaded(ad: fusionAd) | ||||
|         } | ||||
|         return true; | ||||
|     } | ||||
|      | ||||
|     override public func show(_ request: BannerShowRequest? = nil) -> Bool { | ||||
|         state.isShown = true | ||||
|         lastShowRequest = request | ||||
|         ad.placement = request?.placement | ||||
|         let fusionAd = HappyPathFusionAd(engineId: engineId, adType: AdType.Banner, originAd: ad) | ||||
|         return true; | ||||
|     } | ||||
|      | ||||
|     override func hide() -> Bool { | ||||
|         state.isHidden = true | ||||
|         return true | ||||
|     } | ||||
|      | ||||
|     override func destroy() -> Bool { | ||||
|         state.isDestroyed = true | ||||
|         return true | ||||
|     } | ||||
|      | ||||
|     override func updateOrientation(orientation: ScreenOrientation) -> Bool { | ||||
|         state.orientation = orientation | ||||
|         return true | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| class HappyPathMRecAd : GuruMRecAd { | ||||
|     private var ad: HappyPathOriginalAd | ||||
|     public private(set) var state: HappyPathAdState = HappyPathAdState() | ||||
|      | ||||
|     public private(set) var lastShowRequest: MRecShowRequest? | ||||
|     override init(engineId: Int, adUnitId: String, adPlatform: AdPlatform) { | ||||
|         ad = HappyPathOriginalAd(adPlatform: adPlatform, adUnitId: adUnitId) | ||||
|         super.init(engineId: engineId, adUnitId: adUnitId, adPlatform: adPlatform) | ||||
|     } | ||||
|      | ||||
|     override public func load() -> Bool { | ||||
|         state.isLoaded = true | ||||
|         let fusionAd = HappyPathFusionAd(engineId: engineId, adType: AdType.MRec, originAd: ad) | ||||
|         queue.async { | ||||
|             [weak self] in | ||||
|             self?.listener?.onAdDisplayed(ad: fusionAd) | ||||
|         } | ||||
|         return true; | ||||
|     } | ||||
|      | ||||
|     override public func show(request: MRecShowRequest? = nil) -> Bool { | ||||
|         state.isShown = true | ||||
|         lastShowRequest = request | ||||
|         ad.placement = request?.placement | ||||
|         let fusionAd = HappyPathFusionAd(engineId: engineId, adType: AdType.MRec, originAd: ad) | ||||
|         return true; | ||||
|     } | ||||
|      | ||||
|     override func hide() -> Bool { | ||||
|         state.isHidden = true | ||||
|         return true | ||||
|     } | ||||
|      | ||||
|     override func destroy() -> Bool { | ||||
|         state.isDestroyed = true | ||||
|         return true | ||||
|     } | ||||
|      | ||||
|     override func updateOrientation(orientation: ScreenOrientation) -> Bool { | ||||
|         state.orientation = orientation | ||||
|         return true | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| struct HappyPathAdState { | ||||
|     var isLoaded = false | ||||
|     var isShown = false | ||||
|     var isHidden = false | ||||
|     var isDestroyed = false | ||||
|     var orientation: ScreenOrientation? = nil | ||||
| } | ||||
| 
 | ||||
| struct HappyPathOriginalAd { | ||||
|     let adPlatform: AdPlatform | ||||
|     let adUnitId: String | ||||
|     var revenue: Double = 0 | ||||
|     var waterfallName: String? = nil | ||||
|     var placement: String? = nil | ||||
|     var networkName: String? = nil | ||||
|     var networkPlacement: String? = nil | ||||
|     var creativeId: String? = nil | ||||
|     var adReviewCreativeId: String? = nil | ||||
| } | ||||
| 
 | ||||
| class HappyPathFusionAd : FusionAd { | ||||
|     private let originAd: HappyPathOriginalAd | ||||
|     init(engineId: Int, adType: AdType, originAd: HappyPathOriginalAd) { | ||||
|         self.originAd = originAd | ||||
|         super.init(engineId: engineId, adType: adType) | ||||
|     } | ||||
|     override var adPlatform: AdPlatform { return originAd.adPlatform } | ||||
|     override var adUnitId: String? { return originAd.adUnitId } | ||||
|     override var revenue: Double { return originAd.revenue } | ||||
|     override var waterfallName: String? { return originAd.waterfallName } | ||||
|     override var placement: String? { return originAd.placement } | ||||
|     override var networkName: String? { return originAd.networkName } | ||||
|     override var networkPlacement: String? { return originAd.networkPlacement } | ||||
|     override var creativeId: String? { return originAd.creativeId } | ||||
|     override var adReviewCreativeId: String? { return originAd.adReviewCreativeId } | ||||
| } | ||||
|  | @ -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,247 @@ | |||
| <?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>NSAppTransportSecurity</key> | ||||
| 	<dict> | ||||
| 		<key>NSAllowsArbitraryLoads</key> | ||||
| 		<true/> | ||||
| 	</dict> | ||||
| 	<key>NSAdvertisingAttributionReportEndpoint</key> | ||||
| 	<string>https://postbacks-is.com</string> | ||||
| 	<key>SKAdNetworkItems</key> | ||||
| 	<array> | ||||
| 		<dict> | ||||
| 			<key>SKAdNetworkIdentifier</key> | ||||
| 			<string>cstr6suwn9.skadnetwork</string> | ||||
| 		</dict> | ||||
| 		<dict> | ||||
| 			<key>SKAdNetworkIdentifier</key> | ||||
| 			<string>4fzdc2evr5.skadnetwork</string> | ||||
| 		</dict> | ||||
| 		<dict> | ||||
| 			<key>SKAdNetworkIdentifier</key> | ||||
| 			<string>2fnua5tdw4.skadnetwork</string> | ||||
| 		</dict> | ||||
| 		<dict> | ||||
| 			<key>SKAdNetworkIdentifier</key> | ||||
| 			<string>ydx93a7ass.skadnetwork</string> | ||||
| 		</dict> | ||||
| 		<dict> | ||||
| 			<key>SKAdNetworkIdentifier</key> | ||||
| 			<string>p78axxw29g.skadnetwork</string> | ||||
| 		</dict> | ||||
| 		<dict> | ||||
| 			<key>SKAdNetworkIdentifier</key> | ||||
| 			<string>v72qych5uu.skadnetwork</string> | ||||
| 		</dict> | ||||
| 		<dict> | ||||
| 			<key>SKAdNetworkIdentifier</key> | ||||
| 			<string>ludvb6z3bs.skadnetwork</string> | ||||
| 		</dict> | ||||
| 		<dict> | ||||
| 			<key>SKAdNetworkIdentifier</key> | ||||
| 			<string>cp8zw746q7.skadnetwork</string> | ||||
| 		</dict> | ||||
| 		<dict> | ||||
| 			<key>SKAdNetworkIdentifier</key> | ||||
| 			<string>3sh42y64q3.skadnetwork</string> | ||||
| 		</dict> | ||||
| 		<dict> | ||||
| 			<key>SKAdNetworkIdentifier</key> | ||||
| 			<string>c6k4g5qg8m.skadnetwork</string> | ||||
| 		</dict> | ||||
| 		<dict> | ||||
| 			<key>SKAdNetworkIdentifier</key> | ||||
| 			<string>s39g8k73mm.skadnetwork</string> | ||||
| 		</dict> | ||||
| 		<dict> | ||||
| 			<key>SKAdNetworkIdentifier</key> | ||||
| 			<string>3qy4746246.skadnetwork</string> | ||||
| 		</dict> | ||||
| 		<dict> | ||||
| 			<key>SKAdNetworkIdentifier</key> | ||||
| 			<string>f38h382jlk.skadnetwork</string> | ||||
| 		</dict> | ||||
| 		<dict> | ||||
| 			<key>SKAdNetworkIdentifier</key> | ||||
| 			<string>hs6bdukanm.skadnetwork</string> | ||||
| 		</dict> | ||||
| 		<dict> | ||||
| 			<key>SKAdNetworkIdentifier</key> | ||||
| 			<string>mlmmfzh3r3.skadnetwork</string> | ||||
| 		</dict> | ||||
| 		<dict> | ||||
| 			<key>SKAdNetworkIdentifier</key> | ||||
| 			<string>v4nxqhlyqp.skadnetwork</string> | ||||
| 		</dict> | ||||
| 		<dict> | ||||
| 			<key>SKAdNetworkIdentifier</key> | ||||
| 			<string>wzmmz9fp6w.skadnetwork</string> | ||||
| 		</dict> | ||||
| 		<dict> | ||||
| 			<key>SKAdNetworkIdentifier</key> | ||||
| 			<string>su67r6k2v3.skadnetwork</string> | ||||
| 		</dict> | ||||
| 		<dict> | ||||
| 			<key>SKAdNetworkIdentifier</key> | ||||
| 			<string>yclnxrl5pm.skadnetwork</string> | ||||
| 		</dict> | ||||
| 		<dict> | ||||
| 			<key>SKAdNetworkIdentifier</key> | ||||
| 			<string>t38b2kh725.skadnetwork</string> | ||||
| 		</dict> | ||||
| 		<dict> | ||||
| 			<key>SKAdNetworkIdentifier</key> | ||||
| 			<string>7ug5zh24hu.skadnetwork</string> | ||||
| 		</dict> | ||||
| 		<dict> | ||||
| 			<key>SKAdNetworkIdentifier</key> | ||||
| 			<string>gta9lk7p23.skadnetwork</string> | ||||
| 		</dict> | ||||
| 		<dict> | ||||
| 			<key>SKAdNetworkIdentifier</key> | ||||
| 			<string>vutu7akeur.skadnetwork</string> | ||||
| 		</dict> | ||||
| 		<dict> | ||||
| 			<key>SKAdNetworkIdentifier</key> | ||||
| 			<string>y5ghdn5j9k.skadnetwork</string> | ||||
| 		</dict> | ||||
| 		<dict> | ||||
| 			<key>SKAdNetworkIdentifier</key> | ||||
| 			<string>v9wttpbfk9.skadnetwork</string> | ||||
| 		</dict> | ||||
| 		<dict> | ||||
| 			<key>SKAdNetworkIdentifier</key> | ||||
| 			<string>n38lu8286q.skadnetwork</string> | ||||
| 		</dict> | ||||
| 		<dict> | ||||
| 			<key>SKAdNetworkIdentifier</key> | ||||
| 			<string>47vhws6wlr.skadnetwork</string> | ||||
| 		</dict> | ||||
| 		<dict> | ||||
| 			<key>SKAdNetworkIdentifier</key> | ||||
| 			<string>kbd757ywx3.skadnetwork</string> | ||||
| 		</dict> | ||||
| 		<dict> | ||||
| 			<key>SKAdNetworkIdentifier</key> | ||||
| 			<string>9t245vhmpl.skadnetwork</string> | ||||
| 		</dict> | ||||
| 		<dict> | ||||
| 			<key>SKAdNetworkIdentifier</key> | ||||
| 			<string>a2p9lx4jpn.skadnetwork</string> | ||||
| 		</dict> | ||||
| 		<dict> | ||||
| 			<key>SKAdNetworkIdentifier</key> | ||||
| 			<string>22mmun2rn5.skadnetwork</string> | ||||
| 		</dict> | ||||
| 		<dict> | ||||
| 			<key>SKAdNetworkIdentifier</key> | ||||
| 			<string>44jx6755aq.skadnetwork</string> | ||||
| 		</dict> | ||||
| 		<dict> | ||||
| 			<key>SKAdNetworkIdentifier</key> | ||||
| 			<string>k674qkevps.skadnetwork</string> | ||||
| 		</dict> | ||||
| 		<dict> | ||||
| 			<key>SKAdNetworkIdentifier</key> | ||||
| 			<string>4468km3ulz.skadnetwork</string> | ||||
| 		</dict> | ||||
| 		<dict> | ||||
| 			<key>SKAdNetworkIdentifier</key> | ||||
| 			<string>2u9pt9hc89.skadnetwork</string> | ||||
| 		</dict> | ||||
| 		<dict> | ||||
| 			<key>SKAdNetworkIdentifier</key> | ||||
| 			<string>8s468mfl3y.skadnetwork</string> | ||||
| 		</dict> | ||||
| 		<dict> | ||||
| 			<key>SKAdNetworkIdentifier</key> | ||||
| 			<string>klf5c3l5u5.skadnetwork</string> | ||||
| 		</dict> | ||||
| 		<dict> | ||||
| 			<key>SKAdNetworkIdentifier</key> | ||||
| 			<string>ppxm28t8ap.skadnetwork</string> | ||||
| 		</dict> | ||||
| 		<dict> | ||||
| 			<key>SKAdNetworkIdentifier</key> | ||||
| 			<string>kbmxgpxpgc.skadnetwork</string> | ||||
| 		</dict> | ||||
| 		<dict> | ||||
| 			<key>SKAdNetworkIdentifier</key> | ||||
| 			<string>uw77j35x4d.skadnetwork</string> | ||||
| 		</dict> | ||||
| 		<dict> | ||||
| 			<key>SKAdNetworkIdentifier</key> | ||||
| 			<string>578prtvx9j.skadnetwork</string> | ||||
| 		</dict> | ||||
| 		<dict> | ||||
| 			<key>SKAdNetworkIdentifier</key> | ||||
| 			<string>4dzt52r2t5.skadnetwork</string> | ||||
| 		</dict> | ||||
| 		<dict> | ||||
| 			<key>SKAdNetworkIdentifier</key> | ||||
| 			<string>tl55sbb4fm.skadnetwork</string> | ||||
| 		</dict> | ||||
| 		<dict> | ||||
| 			<key>SKAdNetworkIdentifier</key> | ||||
| 			<string>c3frkrj4fj.skadnetwork</string> | ||||
| 		</dict> | ||||
| 		<dict> | ||||
| 			<key>SKAdNetworkIdentifier</key> | ||||
| 			<string>e5fvkxwrpn.skadnetwork</string> | ||||
| 		</dict> | ||||
| 		<dict> | ||||
| 			<key>SKAdNetworkIdentifier</key> | ||||
| 			<string>8c4e2ghe7u.skadnetwork</string> | ||||
| 		</dict> | ||||
| 		<dict> | ||||
| 			<key>SKAdNetworkIdentifier</key> | ||||
| 			<string>3rd42ekr43.skadnetwork</string> | ||||
| 		</dict> | ||||
| 		<dict> | ||||
| 			<key>SKAdNetworkIdentifier</key> | ||||
| 			<string>97r2b46745.skadnetwork</string> | ||||
| 		</dict> | ||||
| 		<dict> | ||||
| 			<key>SKAdNetworkIdentifier</key> | ||||
| 			<string>3qcr597p9d.skadnetwork</string> | ||||
| 		</dict> | ||||
| 	</array> | ||||
| 	<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> | ||||
| 	<key>GADApplicationIdentifier</key> | ||||
| 	<string>ca-app-pub-2436733915645843~7466230005</string> | ||||
| </dict> | ||||
| </plist> | ||||
|  | @ -0,0 +1,254 @@ | |||
| // | ||||
| //  ViewController.swift | ||||
| //  FusionAds | ||||
| // | ||||
| //  Created by Haoyi on 02/28/2025. | ||||
| //  Copyright (c) 2025 Haoyi. All rights reserved. | ||||
| // | ||||
| 
 | ||||
| import UIKit | ||||
| import FusionAds | ||||
| 
 | ||||
| class ViewController: UIViewController { | ||||
| 
 | ||||
|     override func viewDidLoad() { | ||||
|         super.viewDidLoad() | ||||
|          | ||||
|         Logger.setLogLevel(Logger.LogLevel.verbose) | ||||
|          | ||||
|         FusionAdsSdk.registerExternalAdPlatformSdkMapping(platform: AdPlatform.test, initializer: HappyPathAdsSdk.obtain) | ||||
|          | ||||
| //        testEngine() | ||||
| //        testMaxEngine() | ||||
| //        testIronSourceEngine() | ||||
| //        testAdMobEngine() | ||||
|         testMaxOfWithSameValue() | ||||
|         do { | ||||
|             try testAdsProfileJson() | ||||
|             try testAdEngineConfigJson() | ||||
|             testLogging() | ||||
|         } catch {} | ||||
|     } | ||||
|      | ||||
|     func testAdEngineConfigJson() throws { | ||||
|         let config = AdEngineConfig(strategy: 0, faUnitId: "default", config: [ | ||||
|             "test_key": "test_value", | ||||
|             "test_int_key": 1, | ||||
|             "test_bool_key": false, | ||||
|             "test_nested_array": ["1", "2", "3"], | ||||
|             "test_nested_dict": ["test":"value"] | ||||
|         ]) | ||||
|          | ||||
|         let json = try! jsonEncode(config) | ||||
|         let configOut = try! jsonDecode(AdEngineConfig.self, from: json) | ||||
|         print(json) | ||||
|         print(configOut) | ||||
|     } | ||||
|      | ||||
|     private func jsonEncode(_ value: any Codable) throws -> String { | ||||
|         let encoder = JSONEncoder() | ||||
|         let data = try! encoder.encode(value) | ||||
|         return String.init(data: data, encoding: .utf8)! | ||||
|     } | ||||
|      | ||||
|     private func jsonDecode<T>(_ type: T.Type, from json: String) throws -> T where T : Decodable { | ||||
|         let jsonDecoder = JSONDecoder() | ||||
|         return try! jsonDecoder.decode(type, from: json.data(using: .utf8)!) | ||||
|     } | ||||
|      | ||||
|     func testAdsProfileJson() throws { | ||||
| 
 | ||||
|         let jsonEncoder = JSONEncoder() | ||||
|         let adsProfile = AdsProfile(adPlatforms: [AdPlatform.test], maxSdkKey: "") | ||||
|         let data = try! jsonEncoder.encode(adsProfile) | ||||
|         print(String.init(data: data, encoding: .utf8)!) | ||||
|          | ||||
|         let jsonData = #"{"ad_platforms":["TEST"],"max_sdk_key":"","amazon_app_id":null,"pubmatic_store_url":null,"uid2_token":null,"user_id":"","tp_creative_key":"","debug_mode":false,"segments":{"segments":[]}}"#.data(using: .utf8)! | ||||
|         let jsonDecoder = JSONDecoder() | ||||
|         let adsProfileFromJson = try! jsonDecoder.decode(AdsProfile.self, from: jsonData) | ||||
|         print(adsProfileFromJson) | ||||
|         print(adsProfile) | ||||
|     } | ||||
|      | ||||
|     @MainActor func testEngine() { | ||||
|         Task { | ||||
|             let sdk = FusionAdsSdk(controller: self) | ||||
|             let result = await sdk.initialize(adsProfile: AdsProfile(adPlatforms:[AdPlatform.max], maxSdkKey: "max_sdk_key")) | ||||
|             assert(result, "sdk initialized success!") | ||||
|              | ||||
|             let engineConfigExtras = [ | ||||
|                 "ad_platform": "test", | ||||
|                 "ad_unit_id" : "112233", | ||||
|                 "ad_amz_slot_id": "test_slot" | ||||
|             ] | ||||
|              | ||||
|             let engine = sdk.createInterstitialAdEngine(adEngineConfig: AdEngineConfig(strategy: 0, faUnitId: "default", config: engineConfigExtras)) | ||||
|              | ||||
|             assert(engine != nil, "engine is Nil") | ||||
|             engine!.initialize() | ||||
|             try await Task.sleep(nanoseconds: 1000_000_000) | ||||
|             assert(engine!.load(), "load failed!") | ||||
|             try await Task.sleep(nanoseconds: 1000_000_000) | ||||
|             assert(engine!.show(InterstitialShowRequest(placement: "1234_mock")), "show failed!") | ||||
|             try await Task.sleep(nanoseconds: 1000_000_000) | ||||
|             assert(engine!.destroy(), "destroy failed!") | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     func testMaxEngine() { | ||||
|         testPlatformEngine(adPlatform: AdPlatform.max, adUnitId: "87a262be42a9b334") | ||||
|     } | ||||
|      | ||||
|     func testIronSourceEngine() { | ||||
|         testPlatformEngine(adPlatform: AdPlatform.ironSource, adUnitId: "wmgt0712uuux8ju4") | ||||
|     } | ||||
|      | ||||
|     func testAdMobEngine() { | ||||
|         testPlatformEngine(adPlatform: AdPlatform.adMob, adUnitId: "ca-app-pub-3940256099942544/4411468910") | ||||
|     } | ||||
|      | ||||
|     var logger: FileLogHandler? = nil | ||||
|     var rotationLogger: FileLogHandler? = nil | ||||
|     var levelLogger: FileLogHandler? = nil | ||||
|     var manualRotationLogger: FileLogHandler? = nil | ||||
|      | ||||
|     func testLogging() { | ||||
|         print("开始测试 FileLogHandler...") | ||||
|         let fileManager = FileManager.default | ||||
|          // 创建测试目录 | ||||
|         let documentsDir = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first! | ||||
|          let testLogDir = documentsDir.appendingPathComponent("Logs/guru_app", isDirectory: true) | ||||
|           | ||||
|          // 测试案例1: 基本日志记录 | ||||
|          print("测试1: 基本日志记录") | ||||
|          let logger = FileLogHandler(directory: "guru_app", filename: "test.log") | ||||
|         logger.log("test", "这是一条调试消息", level: .debug) | ||||
|         logger.log("test", "这是一条信息消息", level: .info) | ||||
|         logger.log("test", "这是一条警告消息", level: .warning) | ||||
|         logger.log("test", "这是一条错误消息", level: .error) | ||||
|         logger.log("test", "这是一条致命消息", level: .fatal) | ||||
|          logger.log("test", "自定义日志级别消息", level: .info) | ||||
|         self.logger = logger | ||||
|          print("基本日志写入完成,日志文件位置: \(testLogDir.appendingPathComponent("test.log").path)") | ||||
|          | ||||
|          | ||||
|          // 测试案例3: 日志轮转 | ||||
|          print("测试3: 测试日志轮转") | ||||
|          let rotationConfig = FileLogHandler.RotationConfig( | ||||
|              maxFileSize: 1024, // 1KB,设置小一点以便测试轮转 | ||||
|              maxBackupCount: 3  // 保留3个备份 | ||||
|          ) | ||||
|          let rotationLogger = FileLogHandler( | ||||
|              directory: "guru_app", | ||||
|              filename: "rotation_test.log", | ||||
|              rotationConfig: rotationConfig | ||||
|          ) | ||||
|         self.rotationLogger = rotationLogger | ||||
|           | ||||
|          // 写入足够多的数据以触发轮转 | ||||
|          print("写入数据以触发日志轮转...") | ||||
|          for i in 1...100 { | ||||
|              rotationLogger.log("test", "这是日志条目 #\(i): " + String(repeating: "测试数据 ", count: 10), level: .info) | ||||
|          } | ||||
|           | ||||
|          // 验证轮转后的文件 | ||||
|          if let files = try? fileManager.contentsOfDirectory(atPath: testLogDir.path) { | ||||
|              let rotationFiles = files.filter { $0.starts(with: "rotation_test.log") } | ||||
|              print("找到的轮转日志文件: \(rotationFiles)") | ||||
|          } | ||||
|           | ||||
|          // 测试案例4: 自定义级别过滤 | ||||
|          print("测试4: 日志级别过滤") | ||||
|          let levelLogger = FileLogHandler( | ||||
|             directory: "guru_app", | ||||
|             filename: "level_test.log", | ||||
|              minimumLogLevel: .warning | ||||
|          ) | ||||
|         self.levelLogger = levelLogger | ||||
|           | ||||
|         levelLogger.log("level_test", "这是一条调试消息", level: .debug) | ||||
|         levelLogger.log("level_test", "这是一条信息消息", level: .info) | ||||
|         levelLogger.log("level_test", "这是一条警告消息", level: .warning) | ||||
|         levelLogger.log("level_test", "这是一条错误消息", level: .error) | ||||
|         levelLogger.log("level_test", "这是一条致命消息", level: .fatal) | ||||
|         levelLogger.log("level_test", "自定义日志级别消息", level: .info) | ||||
|          print("级别过滤测试完成") | ||||
|           | ||||
|          // 测试案例5: 手动轮转 | ||||
|          print("测试5: 手动轮转") | ||||
|          let manualRotationLogger = FileLogHandler(directory: "guru_app", filename: "manual_rotation.log") | ||||
|          manualRotationLogger.log("test", "轮转前的日志条目") | ||||
|          manualRotationLogger.rotateLogFile() | ||||
|          manualRotationLogger.log("test", "轮转后的日志条目") | ||||
|          print("手动轮转测试完成") | ||||
|         self.manualRotationLogger = manualRotationLogger | ||||
|           | ||||
|          // 测试案例6: 轮转后继续写入 | ||||
|          print("测试6: 轮转后继续写入") | ||||
|          for i in 1...5 { | ||||
|              rotationLogger.log("test", "轮转后的日志条目 #\(i)") | ||||
|          } | ||||
|          print("轮转后写入测试完成") | ||||
|           | ||||
|          // 读取日志文件内容进行验证 | ||||
|          func readLogFile(at path: String) -> String? { | ||||
|              return try? String(contentsOfFile: path, encoding: .utf8) | ||||
|          } | ||||
|          if let logContent = readLogFile(at: testLogDir.appendingPathComponent("test.log").path) { | ||||
|              print("基本日志文件的前200个字符:") | ||||
|              print(String(logContent.prefix(200))) | ||||
|          } | ||||
|           | ||||
|          print("FileLogHandler 测试完成") | ||||
|      } | ||||
|      | ||||
|      | ||||
|     func testPlatformEngine(adPlatform: AdPlatform, adUnitId: String) { | ||||
|         Task {@MainActor in | ||||
|             let sdk = FusionAdsSdk(controller: self) | ||||
|             let result = await sdk.initialize(adsProfile: AdsProfile(adPlatforms:[adPlatform], maxSdkKey: "V5I0i-vOTXkc_HEyqLg0lNm_ivlzp1wPVF3Vs7Jk4ix6WMAwEKGHMpugavZp_7xgss186Frvss23NGSWZXSago", ironSourceSdkKey: "8545d445")) | ||||
|             assert(result, "sdk initialized success!") | ||||
|              | ||||
|             let engineConfigExtras = [ | ||||
|                 "ad_platform": adPlatform.name, | ||||
|                 "ad_unit_id" : adUnitId, | ||||
|             ] | ||||
|              | ||||
|             let engine = sdk.createInterstitialAdEngine(adEngineConfig: AdEngineConfig(strategy: 0, faUnitId: "default", config: engineConfigExtras)) | ||||
|              | ||||
|             assert(engine != nil, "engine is Nil") | ||||
|             engine!.initialize() | ||||
|             try await Task.sleep(nanoseconds: 1000_000_000) | ||||
|             assert(engine!.load(), "load failed!") | ||||
|             try await Task.sleep(nanoseconds: 20_000_000_000) | ||||
|             assert(engine!.currentStateIdentifier == InterstitialAdEngine.AdState.Identifier.LOADED, "not loaded after 20s, current state is \(engine!.currentStateIdentifier.name)") | ||||
|             assert(engine!.show(InterstitialShowRequest(placement: "1234_mock")), "show failed!") | ||||
|             try await Task.sleep(nanoseconds: 5000_000_000) | ||||
|             assert(engine!.destroy(), "destroy failed!") | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     func testMaxOfWithSameValue() { | ||||
|         let list = [TestValue("A", 1), TestValue("B", 2), TestValue("C", 3), TestValue("D", 3)] | ||||
|          | ||||
|         let value = list.max(by: { $0.value <= $1.value }) | ||||
|          | ||||
|         assert(value?.name == "D") | ||||
|         print("Yes!") | ||||
|     } | ||||
| 
 | ||||
|     override func didReceiveMemoryWarning() { | ||||
|         super.didReceiveMemoryWarning() | ||||
|         // Dispose of any resources that can be recreated. | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| private struct TestValue { | ||||
|     let name: String; | ||||
|     let value: Int; | ||||
|      | ||||
|     init(_ name: String, _ value: Int) { | ||||
|         self.name = name | ||||
|         self.value = value | ||||
|     } | ||||
| } | ||||
|  | @ -0,0 +1,7 @@ | |||
| #include? "Pods/Target Support Files/Pods-FusionAds_Example/Pods-FusionAds_Example.debug.xcconfig" | ||||
| 
 | ||||
| COCOAPODS_PARALLEL_CODE_SIGN=true | ||||
| 
 | ||||
| EXCLUDED_ARCHS[sdk=iphonesimulator*]=i386 arm64 | ||||
| EXCLUDED_ARCHS[sdk=iphoneos*]=armv7 | ||||
| 
 | ||||
|  | @ -0,0 +1,46 @@ | |||
| 
 | ||||
| platform :ios, '13.0' | ||||
| 
 | ||||
| #source 'https://cdn.cocoapods.org/' | ||||
| source 'https://github.com/CocoaPods/Specs.git' | ||||
| source 'git@github.com:castbox/GuruSpecs.git' | ||||
| 
 | ||||
| target 'FusionAds_Example' do | ||||
|   use_frameworks! :linkage => :static | ||||
|   use_modular_headers! | ||||
|   pod 'FusionAds', :path => '../' | ||||
|    | ||||
|   target 'FusionAds_Tests' do | ||||
|     inherit! :search_paths | ||||
| 
 | ||||
|     pod 'Quick', '~> 7.6.2' | ||||
|     pod 'Nimble', '~> 13.7.1' | ||||
| #    pod 'FBSnapshotTestCase' , '~> 2.1.4' | ||||
| #    pod 'Nimble-Snapshots' , '~> 9.4.0' | ||||
|   end | ||||
| end | ||||
| 
 | ||||
| post_install do |installer| | ||||
|   installer.pods_project.targets.each do |target| | ||||
|     target.build_configurations.each do |config| | ||||
|       config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '13.0' | ||||
|       config.build_settings['ENABLE_BITCODE'] = 'NO' | ||||
|       config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] ||= [ | ||||
|         '$(inherited)', | ||||
|         'AUDIO_SESSION_MICROPHONE=0', | ||||
|         ## dart: PermissionGroup.photos | ||||
|         'PERMISSION_PHOTOS=1', | ||||
|         ## dart: PermissionGroup.notification | ||||
|         'PERMISSION_NOTIFICATIONS=1' | ||||
|       ] | ||||
|       config.build_settings['ONLY_ACTIVE_ARCH'] = 'NO' if config.type == :debug | ||||
| 
 | ||||
|       config.build_settings['CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER'] = 'NO' | ||||
| 
 | ||||
|       # Override legacy Xcode 11 style VALID_ARCHS[sdk=iphonesimulator*]=x86_64 and prefer Xcode 12 EXCLUDED_ARCHS. | ||||
|       config.build_settings['VALID_ARCHS[sdk=iphonesimulator*]'] = '$(ARCHS_STANDARD)' | ||||
|       config.build_settings['EXCLUDED_ARCHS[sdk=iphonesimulator*]'] = '$(inherited) i386' | ||||
|       config.build_settings['EXCLUDED_ARCHS[sdk=iphoneos*]'] = '$(inherited) armv7' | ||||
|     end | ||||
|   end | ||||
| end | ||||
|  | @ -0,0 +1,518 @@ | |||
| PODS: | ||||
|   - Ads-Global (6.5.0.8): | ||||
|     - Ads-Global/BUAdSDK (= 6.5.0.8) | ||||
|   - Ads-Global/BUAdSDK (6.5.0.8) | ||||
|   - AmazonPublisherServicesSDK (5.0.1) | ||||
|   - AppLovinMediationAmazonAdMarketplaceAdapter (5.0.1.0): | ||||
|     - AppLovinSDK | ||||
|   - AppLovinMediationBidMachineAdapter (3.2.0.0.0): | ||||
|     - AppLovinSDK (>= 13.0.0) | ||||
|     - BidMachine (= 3.2.0) | ||||
|   - AppLovinMediationByteDanceAdapter (6.5.0.8.0): | ||||
|     - Ads-Global (= 6.5.0.8) | ||||
|     - AppLovinSDK (>= 13.0.0) | ||||
|   - AppLovinMediationChartboostAdapter (9.8.0.0): | ||||
|     - AppLovinSDK (>= 13.0.0) | ||||
|     - ChartboostSDK (= 9.8.0) | ||||
|   - AppLovinMediationFacebookAdapter (6.16.0.1): | ||||
|     - AppLovinSDK (>= 13.0.0) | ||||
|     - FBAudienceNetwork (= 6.16.0) | ||||
|   - AppLovinMediationFyberAdapter (8.3.5.0): | ||||
|     - AppLovinSDK (>= 13.0.0) | ||||
|     - Fyber_Marketplace_SDK (= 8.3.5) | ||||
|   - AppLovinMediationGoogleAdapter (11.13.0.1): | ||||
|     - AppLovinSDK (>= 13.0.0) | ||||
|     - Google-Mobile-Ads-SDK (= 11.13.0) | ||||
|   - AppLovinMediationGoogleAdManagerAdapter (11.13.0.1): | ||||
|     - AppLovinSDK (>= 13.0.0) | ||||
|     - Google-Mobile-Ads-SDK (= 11.13.0) | ||||
|   - AppLovinMediationInMobiAdapter (10.8.0.0): | ||||
|     - AppLovinSDK (>= 13.0.0) | ||||
|     - InMobiSDK (= 10.8.0) | ||||
|   - AppLovinMediationMintegralAdapter (7.7.5.0.0): | ||||
|     - AppLovinSDK (>= 13.0.0) | ||||
|     - MintegralAdSDK (= 7.7.5) | ||||
|     - MintegralAdSDK/BidSplashAd (= 7.7.5) | ||||
|   - AppLovinMediationMobileFuseAdapter (1.9.0.0): | ||||
|     - AppLovinSDK (>= 13.0.0) | ||||
|     - MobileFuseSDK (= 1.9.0) | ||||
|   - AppLovinMediationMolocoAdapter (3.7.0.0): | ||||
|     - AppLovinSDK (>= 13.0.0) | ||||
|     - MolocoSDKiOS (= 3.7.0) | ||||
|   - AppLovinMediationOguryPresageAdapter (5.0.2.0): | ||||
|     - AppLovinSDK | ||||
|     - OgurySdk (= 5.0.2) | ||||
|   - AppLovinMediationPubMaticAdapter (4.4.0.0): | ||||
|     - AppLovinSDK (>= 13.0.0) | ||||
|     - OpenWrapSDK (= 4.4.0) | ||||
|   - AppLovinMediationSmaatoAdapter (22.9.2.0): | ||||
|     - AppLovinSDK (>= 13.0.0) | ||||
|     - smaato-ios-sdk (= 22.9.2) | ||||
|     - smaato-ios-sdk/InApp (= 22.9.2) | ||||
|   - AppLovinMediationUnityAdsAdapter (4.13.2.0): | ||||
|     - AppLovinSDK (>= 13.0.0) | ||||
|     - UnityAds (= 4.13.2) | ||||
|   - AppLovinMediationVerveAdapter (3.2.0.0): | ||||
|     - AppLovinSDK (>= 13.0.0) | ||||
|     - HyBid (= 3.2.0) | ||||
|   - AppLovinMediationVungleAdapter (7.4.4.0): | ||||
|     - AppLovinSDK (>= 13.0.0) | ||||
|     - VungleAds (= 7.4.4) | ||||
|   - AppLovinSDK (13.1.0) | ||||
|   - ATOM-Standalone (3.5.0) | ||||
|   - BidMachine (3.2.0): | ||||
|     - BidMachine/Static (= 3.2.0) | ||||
|   - BidMachine/Static (3.2.0): | ||||
|     - StackModules/Static (~> 3.1.0) | ||||
|   - ChartboostSDK (9.8.0) | ||||
|   - CwlCatchException (2.2.1): | ||||
|     - CwlCatchExceptionSupport (~> 2.2.1) | ||||
|   - CwlCatchExceptionSupport (2.2.1) | ||||
|   - CwlMachBadInstructionHandler (2.2.2) | ||||
|   - CwlPosixPreconditionTesting (2.2.2) | ||||
|   - CwlPreconditionTesting (2.2.2): | ||||
|     - CwlCatchException (~> 2.2.1) | ||||
|     - CwlMachBadInstructionHandler (~> 2.2.2) | ||||
|     - CwlPosixPreconditionTesting (~> 2.2.2) | ||||
|   - FBAudienceNetwork (6.16.0) | ||||
|   - FusionAds (1.3.0): | ||||
|     - FusionAds/Main (= 1.3.0) | ||||
|   - FusionAds/AdMobAdapters (1.3.0): | ||||
|     - Google-Mobile-Ads-SDK (= 11.13.0) | ||||
|     - GoogleMobileAdsMediationChartboost (= 9.8.0.0) | ||||
|     - GoogleMobileAdsMediationFacebook (= 6.16.0.0) | ||||
|     - GoogleMobileAdsMediationFyber (= 8.3.5.0) | ||||
|     - GoogleMobileAdsMediationInMobi (= 10.8.0.0) | ||||
|     - GoogleMobileAdsMediationMintegral (= 7.7.5.0) | ||||
|     - GoogleMobileAdsMediationVungle (= 7.4.4.0) | ||||
|   - FusionAds/IronSourceAdapters (1.3.0): | ||||
|     - IronSourceAdMobAdapter (= 4.3.60.0) | ||||
|     - IronSourceAppLovinAdapter (= 4.3.52.0) | ||||
|     - IronSourceAPSAdapter (= 4.3.18.0) | ||||
|     - IronSourceBidMachineAdapter (= 4.3.14.0) | ||||
|     - IronSourceFacebookAdapter (= 4.3.47.0) | ||||
|     - IronSourceFyberAdapter (= 4.3.41.0) | ||||
|     - IronSourceInMobiAdapter (= 4.3.26.0) | ||||
|     - IronSourceMintegralAdapter (= 4.3.31.0) | ||||
|     - IronSourceMolocoAdapter (= 4.3.11.0) | ||||
|     - IronSourcePangleAdapter (= 4.3.40.0) | ||||
|     - IronSourceSDK (= 8.7.0.0) | ||||
|     - IronSourceSmaatoAdapter (= 4.3.16.0) | ||||
|     - IronSourceUnityAdsAdapter (= 4.3.48.0) | ||||
|     - IronSourceVungleAdapter (= 4.3.42.0) | ||||
|   - FusionAds/Main (1.3.0): | ||||
|     - FusionAds/AdMobAdapters | ||||
|     - FusionAds/IronSourceAdapters | ||||
|     - FusionAds/MaxAdapters | ||||
|   - FusionAds/MaxAdapters (1.3.0): | ||||
|     - AmazonPublisherServicesSDK (= 5.0.1) | ||||
|     - AppLovinMediationAmazonAdMarketplaceAdapter (= 5.0.1.0) | ||||
|     - AppLovinMediationBidMachineAdapter (= 3.2.0.0.0) | ||||
|     - AppLovinMediationByteDanceAdapter (= 6.5.0.8.0) | ||||
|     - AppLovinMediationChartboostAdapter (= 9.8.0.0) | ||||
|     - AppLovinMediationFacebookAdapter (= 6.16.0.1) | ||||
|     - AppLovinMediationFyberAdapter (= 8.3.5.0) | ||||
|     - AppLovinMediationGoogleAdapter (= 11.13.0.1) | ||||
|     - AppLovinMediationGoogleAdManagerAdapter (= 11.13.0.1) | ||||
|     - AppLovinMediationInMobiAdapter (= 10.8.0.0) | ||||
|     - AppLovinMediationMintegralAdapter (= 7.7.5.0.0) | ||||
|     - AppLovinMediationMobileFuseAdapter (= 1.9.0.0) | ||||
|     - AppLovinMediationMolocoAdapter (= 3.7.0.0) | ||||
|     - AppLovinMediationOguryPresageAdapter (= 5.0.2.0) | ||||
|     - AppLovinMediationPubMaticAdapter (= 4.4.0.0) | ||||
|     - AppLovinMediationSmaatoAdapter (= 22.9.2.0) | ||||
|     - AppLovinMediationUnityAdsAdapter (= 4.13.2.0) | ||||
|     - AppLovinMediationVerveAdapter (= 3.2.0.0) | ||||
|     - AppLovinMediationVungleAdapter (= 7.4.4.0) | ||||
|     - AppLovinSDK (= 13.1.0) | ||||
|     - GuruConsent (= 1.4.6) | ||||
|     - TradPlusAdSDK (= 10.9.0) | ||||
|   - Fyber_Marketplace_SDK (8.3.5) | ||||
|   - Google-Mobile-Ads-SDK (11.13.0): | ||||
|     - GoogleUserMessagingPlatform (>= 1.1) | ||||
|   - GoogleMobileAdsMediationChartboost (9.8.0.0): | ||||
|     - ChartboostSDK (= 9.8.0) | ||||
|     - Google-Mobile-Ads-SDK (~> 11.0) | ||||
|   - GoogleMobileAdsMediationFacebook (6.16.0.0): | ||||
|     - FBAudienceNetwork (= 6.16.0) | ||||
|     - Google-Mobile-Ads-SDK (~> 11.0) | ||||
|   - GoogleMobileAdsMediationFyber (8.3.5.0): | ||||
|     - Fyber_Marketplace_SDK (= 8.3.5) | ||||
|     - Google-Mobile-Ads-SDK (~> 11.0) | ||||
|   - GoogleMobileAdsMediationInMobi (10.8.0.0): | ||||
|     - Google-Mobile-Ads-SDK (~> 11.0) | ||||
|     - InMobiSDK (= 10.8.0) | ||||
|   - GoogleMobileAdsMediationMintegral (7.7.5.0): | ||||
|     - Google-Mobile-Ads-SDK (~> 11.0) | ||||
|     - MintegralAdSDK/All (= 7.7.5) | ||||
|   - GoogleMobileAdsMediationVungle (7.4.4.0): | ||||
|     - Google-Mobile-Ads-SDK (~> 11.0) | ||||
|     - VungleAds (= 7.4.4) | ||||
|   - GoogleUserMessagingPlatform (2.3.0) | ||||
|   - GuruConsent (1.4.6): | ||||
|     - GoogleUserMessagingPlatform (= 2.3.0) | ||||
|     - GuruConsent/Privacy (= 1.4.6) | ||||
|   - GuruConsent/Privacy (1.4.6): | ||||
|     - GoogleUserMessagingPlatform (= 2.3.0) | ||||
|   - HyBid (3.2.0): | ||||
|     - HyBid/ATOM (= 3.2.0) | ||||
|     - HyBid/Banner (= 3.2.0) | ||||
|     - HyBid/Core (= 3.2.0) | ||||
|     - HyBid/FullScreen (= 3.2.0) | ||||
|     - HyBid/Native (= 3.2.0) | ||||
|     - HyBid/RewardedVideo (= 3.2.0) | ||||
|   - HyBid/ATOM (3.2.0): | ||||
|     - ATOM-Standalone (~> 3.5.0) | ||||
|     - HyBid/Core | ||||
|   - HyBid/Banner (3.2.0): | ||||
|     - HyBid/Core | ||||
|   - HyBid/Core (3.2.0) | ||||
|   - HyBid/FullScreen (3.2.0): | ||||
|     - HyBid/Core | ||||
|   - HyBid/Native (3.2.0): | ||||
|     - HyBid/Core | ||||
|   - HyBid/RewardedVideo (3.2.0): | ||||
|     - HyBid/Core | ||||
|   - InMobiSDK (10.8.0) | ||||
|   - IronSourceAdMobAdapter (4.3.60.0): | ||||
|     - Google-Mobile-Ads-SDK (= 11.13.0) | ||||
|     - IronSourceSDK (~> 8.4) | ||||
|   - IronSourceAppLovinAdapter (4.3.52.0): | ||||
|     - AppLovinSDK (= 13.1.0) | ||||
|     - IronSourceSDK (~> 8.4) | ||||
|   - IronSourceAPSAdapter (4.3.18.0): | ||||
|     - AmazonPublisherServicesSDK (= 5.0.1) | ||||
|     - IronSourceSDK (~> 8.4) | ||||
|   - IronSourceBidMachineAdapter (4.3.14.0): | ||||
|     - BidMachine (= 3.2.0) | ||||
|     - IronSourceSDK (~> 8.4) | ||||
|   - IronSourceFacebookAdapter (4.3.47.0): | ||||
|     - FBAudienceNetwork (= 6.16.0) | ||||
|     - IronSourceSDK (~> 8.5) | ||||
|   - IronSourceFyberAdapter (4.3.41.0): | ||||
|     - Fyber_Marketplace_SDK (= 8.3.5) | ||||
|     - IronSourceSDK (~> 8.4) | ||||
|   - IronSourceInMobiAdapter (4.3.26.0): | ||||
|     - InMobiSDK (= 10.8.0) | ||||
|     - IronSourceSDK (~> 8.4) | ||||
|   - IronSourceMintegralAdapter (4.3.31.0): | ||||
|     - IronSourceSDK (~> 8.4) | ||||
|     - MintegralAdSDK (= 7.7.5) | ||||
|   - IronSourceMolocoAdapter (4.3.11.0): | ||||
|     - IronSourceSDK (~> 8.4) | ||||
|     - MolocoSDKiOS (= 3.7.0) | ||||
|   - IronSourcePangleAdapter (4.3.40.0): | ||||
|     - Ads-Global (= 6.5.0.8) | ||||
|     - IronSourceSDK (~> 8.4) | ||||
|   - IronSourceSDK (8.7.0.0) | ||||
|   - IronSourceSmaatoAdapter (4.3.16.0): | ||||
|     - IronSourceSDK (~> 8.4) | ||||
|     - smaato-ios-sdk (= 22.9.2) | ||||
|     - smaato-ios-sdk/InApp (= 22.9.2) | ||||
|   - IronSourceUnityAdsAdapter (4.3.48.0): | ||||
|     - IronSourceSDK (~> 8.4) | ||||
|     - UnityAds (= 4.13.2) | ||||
|   - IronSourceVungleAdapter (4.3.42.0): | ||||
|     - IronSourceSDK (~> 8.4) | ||||
|     - VungleAds (= 7.4.4) | ||||
|   - MintegralAdSDK (7.7.5): | ||||
|     - MintegralAdSDK/BannerAd (= 7.7.5) | ||||
|     - MintegralAdSDK/BidBannerAd (= 7.7.5) | ||||
|     - MintegralAdSDK/BidInterstitialVideoAd (= 7.7.5) | ||||
|     - MintegralAdSDK/BidNativeAd (= 7.7.5) | ||||
|     - MintegralAdSDK/BidNewInterstitialAd (= 7.7.5) | ||||
|     - MintegralAdSDK/BidRewardVideoAd (= 7.7.5) | ||||
|     - MintegralAdSDK/InterstitialVideoAd (= 7.7.5) | ||||
|     - MintegralAdSDK/NativeAd (= 7.7.5) | ||||
|     - MintegralAdSDK/NewInterstitialAd (= 7.7.5) | ||||
|     - MintegralAdSDK/RewardVideoAd (= 7.7.5) | ||||
|   - MintegralAdSDK/All (7.7.5): | ||||
|     - MintegralAdSDK/BannerAd | ||||
|     - MintegralAdSDK/BidNativeAd | ||||
|     - MintegralAdSDK/InterstitialVideoAd | ||||
|     - MintegralAdSDK/NativeAd | ||||
|     - MintegralAdSDK/NativeAdvancedAd | ||||
|     - MintegralAdSDK/NewInterstitialAd | ||||
|     - MintegralAdSDK/RewardVideoAd | ||||
|     - MintegralAdSDK/SplashAd | ||||
|   - MintegralAdSDK/BannerAd (7.7.5): | ||||
|     - MintegralAdSDK/NativeAd | ||||
|   - MintegralAdSDK/BidBannerAd (7.7.5): | ||||
|     - MintegralAdSDK/BannerAd | ||||
|     - MintegralAdSDK/BidNativeAd | ||||
|   - MintegralAdSDK/BidInterstitialVideoAd (7.7.5): | ||||
|     - MintegralAdSDK/BidNativeAd | ||||
|     - MintegralAdSDK/InterstitialVideoAd | ||||
|   - MintegralAdSDK/BidNativeAd (7.7.5): | ||||
|     - MintegralAdSDK/NativeAd | ||||
|   - MintegralAdSDK/BidNewInterstitialAd (7.7.5): | ||||
|     - MintegralAdSDK/BidNativeAd | ||||
|     - MintegralAdSDK/NewInterstitialAd | ||||
|   - MintegralAdSDK/BidRewardVideoAd (7.7.5): | ||||
|     - MintegralAdSDK/BidNativeAd | ||||
|     - MintegralAdSDK/RewardVideoAd | ||||
|   - MintegralAdSDK/BidSplashAd (7.7.5): | ||||
|     - MintegralAdSDK/BidNativeAd | ||||
|     - MintegralAdSDK/SplashAd | ||||
|   - MintegralAdSDK/InterstitialVideoAd (7.7.5): | ||||
|     - MintegralAdSDK/NativeAd | ||||
|   - MintegralAdSDK/NativeAd (7.7.5) | ||||
|   - MintegralAdSDK/NativeAdvancedAd (7.7.5): | ||||
|     - MintegralAdSDK/NativeAd | ||||
|   - MintegralAdSDK/NewInterstitialAd (7.7.5): | ||||
|     - MintegralAdSDK/InterstitialVideoAd | ||||
|     - MintegralAdSDK/NativeAd | ||||
|   - MintegralAdSDK/RewardVideoAd (7.7.5): | ||||
|     - MintegralAdSDK/NativeAd | ||||
|   - MintegralAdSDK/SplashAd (7.7.5): | ||||
|     - MintegralAdSDK/NativeAd | ||||
|   - MobileFuseSDK (1.9.0) | ||||
|   - MolocoSDKiOS (3.7.0) | ||||
|   - Nimble (13.7.1): | ||||
|     - CwlPreconditionTesting (~> 2.2.0) | ||||
|   - OguryAds (4.0.2): | ||||
|     - OguryAds/OMID (= 4.0.2) | ||||
|     - OguryCore (= 2.0.0) | ||||
|   - OguryAds/OMID (4.0.2): | ||||
|     - OguryCore (= 2.0.0) | ||||
|   - OguryCore (2.0.0) | ||||
|   - OgurySdk (5.0.2): | ||||
|     - OguryCore (= 2.0.0) | ||||
|     - OgurySdk/OguryAds (= 5.0.2) | ||||
|   - OgurySdk/OguryAds (5.0.2): | ||||
|     - OguryAds (= 4.0.2) | ||||
|     - OguryCore (= 2.0.0) | ||||
|   - OMSDK_Appodeal (1.5.4) | ||||
|   - OpenWrapSDK (4.4.0): | ||||
|     - OpenWrapSDK/OpenWrap (= 4.4.0) | ||||
|   - OpenWrapSDK/OpenWrap (4.4.0) | ||||
|   - Quick (7.6.2) | ||||
|   - smaato-ios-sdk (22.9.2): | ||||
|     - smaato-ios-sdk/Full (= 22.9.2) | ||||
|   - smaato-ios-sdk/Banner (22.9.2): | ||||
|     - smaato-ios-sdk/Modules/Banner | ||||
|     - smaato-ios-sdk/Modules/Core | ||||
|     - smaato-ios-sdk/Modules/RichMedia | ||||
|   - smaato-ios-sdk/Full (22.9.2): | ||||
|     - smaato-ios-sdk/Banner | ||||
|     - smaato-ios-sdk/Interstitial | ||||
|     - smaato-ios-sdk/Native | ||||
|     - smaato-ios-sdk/Outstream | ||||
|     - smaato-ios-sdk/RewardedAds | ||||
|   - smaato-ios-sdk/InApp (22.9.2): | ||||
|     - smaato-ios-sdk/Modules/Core | ||||
|   - smaato-ios-sdk/Interstitial (22.9.2): | ||||
|     - smaato-ios-sdk/Modules/Interstitial | ||||
|     - smaato-ios-sdk/Modules/RichMedia | ||||
|     - smaato-ios-sdk/Modules/Video | ||||
|   - smaato-ios-sdk/Modules/Banner (22.9.2): | ||||
|     - smaato-ios-sdk/Modules/Core | ||||
|   - smaato-ios-sdk/Modules/Core (22.9.2) | ||||
|   - smaato-ios-sdk/Modules/Interstitial (22.9.2): | ||||
|     - smaato-ios-sdk/Modules/Core | ||||
|   - smaato-ios-sdk/Modules/Native (22.9.2): | ||||
|     - smaato-ios-sdk/Modules/Core | ||||
|   - smaato-ios-sdk/Modules/OpenMeasurement (22.9.2): | ||||
|     - smaato-ios-sdk/Modules/Core | ||||
|   - smaato-ios-sdk/Modules/Outstream (22.9.2): | ||||
|     - smaato-ios-sdk/Modules/Banner | ||||
|     - smaato-ios-sdk/Modules/Core | ||||
|     - smaato-ios-sdk/Modules/RichMedia | ||||
|   - smaato-ios-sdk/Modules/RewardedAds (22.9.2): | ||||
|     - smaato-ios-sdk/Modules/Core | ||||
|   - smaato-ios-sdk/Modules/RichMedia (22.9.2): | ||||
|     - smaato-ios-sdk/Modules/Core | ||||
|     - smaato-ios-sdk/Modules/OpenMeasurement | ||||
|   - smaato-ios-sdk/Modules/Video (22.9.2): | ||||
|     - smaato-ios-sdk/Modules/Core | ||||
|     - smaato-ios-sdk/Modules/OpenMeasurement | ||||
|   - smaato-ios-sdk/Native (22.9.2): | ||||
|     - smaato-ios-sdk/Modules/Core | ||||
|     - smaato-ios-sdk/Modules/Native | ||||
|   - smaato-ios-sdk/Outstream (22.9.2): | ||||
|     - smaato-ios-sdk/Modules/Outstream | ||||
|   - smaato-ios-sdk/RewardedAds (22.9.2): | ||||
|     - smaato-ios-sdk/Modules/RewardedAds | ||||
|     - smaato-ios-sdk/Modules/Video | ||||
|   - StackModules/Core-Static (3.1.0) | ||||
|   - StackModules/ProductPresentation-Static (3.1.0): | ||||
|     - StackModules/Core-Static | ||||
|   - StackModules/Rendering-Static (3.1.0): | ||||
|     - OMSDK_Appodeal (~> 1.5.2) | ||||
|     - StackModules/Core-Static | ||||
|     - StackModules/ProductPresentation-Static | ||||
|   - StackModules/Static (3.1.0): | ||||
|     - StackModules/Core-Static | ||||
|     - StackModules/ProductPresentation-Static | ||||
|     - StackModules/Rendering-Static | ||||
|   - TPExchange (10.9.0) | ||||
|   - TradPlusAdSDK (10.9.0): | ||||
|     - TradPlusAdSDK/TradPlusAds (= 10.9.0) | ||||
|   - TradPlusAdSDK/TradPlusAds (10.9.0): | ||||
|     - TPExchange (= 10.9.0) | ||||
|   - UnityAds (4.13.2) | ||||
|   - VungleAds (7.4.4) | ||||
| 
 | ||||
| DEPENDENCIES: | ||||
|   - FusionAds (from `../`) | ||||
|   - Nimble (~> 13.7.1) | ||||
|   - Quick (~> 7.6.2) | ||||
| 
 | ||||
| SPEC REPOS: | ||||
|   "git@github.com:castbox/GuruSpecs.git": | ||||
|     - GuruConsent | ||||
|   https://github.com/CocoaPods/Specs.git: | ||||
|     - Ads-Global | ||||
|     - AmazonPublisherServicesSDK | ||||
|     - AppLovinMediationAmazonAdMarketplaceAdapter | ||||
|     - AppLovinMediationBidMachineAdapter | ||||
|     - AppLovinMediationByteDanceAdapter | ||||
|     - AppLovinMediationChartboostAdapter | ||||
|     - AppLovinMediationFacebookAdapter | ||||
|     - AppLovinMediationFyberAdapter | ||||
|     - AppLovinMediationGoogleAdapter | ||||
|     - AppLovinMediationGoogleAdManagerAdapter | ||||
|     - AppLovinMediationInMobiAdapter | ||||
|     - AppLovinMediationMintegralAdapter | ||||
|     - AppLovinMediationMobileFuseAdapter | ||||
|     - AppLovinMediationMolocoAdapter | ||||
|     - AppLovinMediationOguryPresageAdapter | ||||
|     - AppLovinMediationPubMaticAdapter | ||||
|     - AppLovinMediationSmaatoAdapter | ||||
|     - AppLovinMediationUnityAdsAdapter | ||||
|     - AppLovinMediationVerveAdapter | ||||
|     - AppLovinMediationVungleAdapter | ||||
|     - AppLovinSDK | ||||
|     - ATOM-Standalone | ||||
|     - BidMachine | ||||
|     - ChartboostSDK | ||||
|     - CwlCatchException | ||||
|     - CwlCatchExceptionSupport | ||||
|     - CwlMachBadInstructionHandler | ||||
|     - CwlPosixPreconditionTesting | ||||
|     - CwlPreconditionTesting | ||||
|     - FBAudienceNetwork | ||||
|     - Fyber_Marketplace_SDK | ||||
|     - Google-Mobile-Ads-SDK | ||||
|     - GoogleMobileAdsMediationChartboost | ||||
|     - GoogleMobileAdsMediationFacebook | ||||
|     - GoogleMobileAdsMediationFyber | ||||
|     - GoogleMobileAdsMediationInMobi | ||||
|     - GoogleMobileAdsMediationMintegral | ||||
|     - GoogleMobileAdsMediationVungle | ||||
|     - GoogleUserMessagingPlatform | ||||
|     - HyBid | ||||
|     - InMobiSDK | ||||
|     - IronSourceAdMobAdapter | ||||
|     - IronSourceAppLovinAdapter | ||||
|     - IronSourceAPSAdapter | ||||
|     - IronSourceBidMachineAdapter | ||||
|     - IronSourceFacebookAdapter | ||||
|     - IronSourceFyberAdapter | ||||
|     - IronSourceInMobiAdapter | ||||
|     - IronSourceMintegralAdapter | ||||
|     - IronSourceMolocoAdapter | ||||
|     - IronSourcePangleAdapter | ||||
|     - IronSourceSDK | ||||
|     - IronSourceSmaatoAdapter | ||||
|     - IronSourceUnityAdsAdapter | ||||
|     - IronSourceVungleAdapter | ||||
|     - MintegralAdSDK | ||||
|     - MobileFuseSDK | ||||
|     - MolocoSDKiOS | ||||
|     - Nimble | ||||
|     - OguryAds | ||||
|     - OguryCore | ||||
|     - OgurySdk | ||||
|     - OMSDK_Appodeal | ||||
|     - OpenWrapSDK | ||||
|     - Quick | ||||
|     - smaato-ios-sdk | ||||
|     - StackModules | ||||
|     - TPExchange | ||||
|     - TradPlusAdSDK | ||||
|     - UnityAds | ||||
|     - VungleAds | ||||
| 
 | ||||
| EXTERNAL SOURCES: | ||||
|   FusionAds: | ||||
|     :path: "../" | ||||
| 
 | ||||
| SPEC CHECKSUMS: | ||||
|   Ads-Global: a5f72fa6a7de699d8190c22fbcb3cd39a42440f0 | ||||
|   AmazonPublisherServicesSDK: 2c897ab0edf7d6f7bf2bcc6c16d63530dc7ec9c1 | ||||
|   AppLovinMediationAmazonAdMarketplaceAdapter: 66c8897a2e120510f0fab0a645e989d6a860cf24 | ||||
|   AppLovinMediationBidMachineAdapter: d7f9c6e5c391215ad5f5ec14275f6373999bb5c6 | ||||
|   AppLovinMediationByteDanceAdapter: a08b5c8d4bfcff434079a5c35fc1be14bd9d3b95 | ||||
|   AppLovinMediationChartboostAdapter: cff01475136bfb065c993b2f0a1e865842ab9f71 | ||||
|   AppLovinMediationFacebookAdapter: f75ccd71f9d4f5425a7278f547f966551f0cd493 | ||||
|   AppLovinMediationFyberAdapter: 59595bef15b724090c56751a1b1799df179dc1e1 | ||||
|   AppLovinMediationGoogleAdapter: 9b56c6a93704215bc2fc366f77e7b0684444b5b5 | ||||
|   AppLovinMediationGoogleAdManagerAdapter: 72943b93d0f4de003cdd89d35fb288ddaf4e8231 | ||||
|   AppLovinMediationInMobiAdapter: 3c85806a4d9a96a58e894eff1b652fa38d4823d8 | ||||
|   AppLovinMediationMintegralAdapter: 3116fbb59153ae3270ee252c878f7c5a8d6c0a6c | ||||
|   AppLovinMediationMobileFuseAdapter: 8b9c72c8cb55b3bdf44134bd285d54fa9ad983b0 | ||||
|   AppLovinMediationMolocoAdapter: db6759753c21b880324611643f31c3af91009078 | ||||
|   AppLovinMediationOguryPresageAdapter: e7f82ffd005d550b588b7ac4edc3e502e409548c | ||||
|   AppLovinMediationPubMaticAdapter: c5755d31ac44f785a18470f8d4fd0d24b599344b | ||||
|   AppLovinMediationSmaatoAdapter: 4610a78ce03df904f805d7d9ee9b78742cc95d50 | ||||
|   AppLovinMediationUnityAdsAdapter: cf75181e59274a29740309dd4f64c37b299e1d4c | ||||
|   AppLovinMediationVerveAdapter: a328d229a9b8d8e7ccd0d738a814467e1f47ca83 | ||||
|   AppLovinMediationVungleAdapter: a288c70db5ebb967c2e4b0cb3f2fc1ffc6ac9fae | ||||
|   AppLovinSDK: 539a0178d8bef4d68b2551a93526e0c1bba06cb2 | ||||
|   ATOM-Standalone: c6f8d00c510cdeec33aa95cdf1f31d78b62d0aa7 | ||||
|   BidMachine: b5d8236e691a0c71fbb95a72e86f3ec189f1a8a9 | ||||
|   ChartboostSDK: f8422957daa675fa4a0bb9f159144b3b59ef5c27 | ||||
|   CwlCatchException: 7acc161b299a6de7f0a46a6ed741eae2c8b4d75a | ||||
|   CwlCatchExceptionSupport: 54ccab8d8c78907b57f99717fb19d4cc3bce02dc | ||||
|   CwlMachBadInstructionHandler: dae4fdd124d45c9910ac240287cc7b898f4502a1 | ||||
|   CwlPosixPreconditionTesting: ecd095aa2129e740b44301c34571e8d85906fb88 | ||||
|   CwlPreconditionTesting: 67a0047dd4de4382b93442c0e3f25207f984f35a | ||||
|   FBAudienceNetwork: d1670939884e3a2e0ad98dca98d7e0c841417228 | ||||
|   FusionAds: ed5b00827e7d467f8e6e01919e3926255bfd492d | ||||
|   Fyber_Marketplace_SDK: 8b042ecd850accf338a052ca23a4e105f71154ec | ||||
|   Google-Mobile-Ads-SDK: 14f57f2dc33532a24db288897e26494640810407 | ||||
|   GoogleMobileAdsMediationChartboost: b6815e7b1ce697b08d752cef1cb24248f3598f75 | ||||
|   GoogleMobileAdsMediationFacebook: 234b9cecc2f9c751567a71d035974c00e6803777 | ||||
|   GoogleMobileAdsMediationFyber: d4d0945274d92cc63f03e58de0c3c4862a1aacb8 | ||||
|   GoogleMobileAdsMediationInMobi: 1c09a0189c485bba2b3edc9ab0e062b9a716929f | ||||
|   GoogleMobileAdsMediationMintegral: fa41384b29c36699bf703ab75ca6407e59d29b9b | ||||
|   GoogleMobileAdsMediationVungle: d090c301d492c458986c447972808c8d9efc1439 | ||||
|   GoogleUserMessagingPlatform: b1ad7bb1ee3ce64749d4fec24f667b36e45c396c | ||||
|   GuruConsent: 281da01094a2ba49ac81ad71bbbdf9ce39d405a1 | ||||
|   HyBid: 575a647ce48fb4ed40dab1a1ce203b1ae0cdb294 | ||||
|   InMobiSDK: 0cfb1a1eb5112fa0236ae1fab549e31fb96c9566 | ||||
|   IronSourceAdMobAdapter: eab2e36036324f865f113c9ccdd8c919f888e335 | ||||
|   IronSourceAppLovinAdapter: d6a1cac7a5e8143cdab3edd301bdc166957bba93 | ||||
|   IronSourceAPSAdapter: 2aa3ccf44a220b2a38fae57817c3c73a945b6b22 | ||||
|   IronSourceBidMachineAdapter: a514e6b5310917ada357de40b320637ab11cb029 | ||||
|   IronSourceFacebookAdapter: b4ce8c7bf7db054336d0a18c4a15ad0796286563 | ||||
|   IronSourceFyberAdapter: dc02376c7ba49165cbc9728ceb52862445184b3e | ||||
|   IronSourceInMobiAdapter: adbdf1204568af02545e994c1967518504f03923 | ||||
|   IronSourceMintegralAdapter: 4c9857174622b5c7b6739aaf3c2264459a4eb152 | ||||
|   IronSourceMolocoAdapter: 41b152f48bc0fbe2aa71b9d93f1925ee97470518 | ||||
|   IronSourcePangleAdapter: 76dfb30134fa902d55c20c1b4e70fbd84e6c044f | ||||
|   IronSourceSDK: 0b5a640179ef91acc637332608183a05db1757df | ||||
|   IronSourceSmaatoAdapter: c33112852eb8830b6d2292b816df98d6cefdbb98 | ||||
|   IronSourceUnityAdsAdapter: 1fcdfd61d67f11699dbc2da153d52db2a743e9de | ||||
|   IronSourceVungleAdapter: 623d3aa35a4d2495117d7b329e8178a725d35406 | ||||
|   MintegralAdSDK: 88cc19314a1424a5d7f976c834030879017ed5f9 | ||||
|   MobileFuseSDK: e5ea2384bc85fe8c1ae13748659d2799829a8de3 | ||||
|   MolocoSDKiOS: 57d989984793ed15c7eed4aab36dd352cd5a2529 | ||||
|   Nimble: 317d713c30c3336dd8571da1889f7ec3afc626e8 | ||||
|   OguryAds: d9887cf3c756572175b22efadd08506d4b2f20fe | ||||
|   OguryCore: 1c7c2009f958b7026d5bca89e4c97dfa3fa9ef87 | ||||
|   OgurySdk: 05f1a6433d0c291a62ea46b190cf0ab1b03ddee3 | ||||
|   OMSDK_Appodeal: 9ed58fc8cabfbe31637cd267b35c1f86d227c49d | ||||
|   OpenWrapSDK: 70a433086186431c0eb2029ad049bc2175bfd3b5 | ||||
|   Quick: b8bec97cd4b9f21da0472d45580f763b801fc353 | ||||
|   smaato-ios-sdk: cd2dee212b2e160feb6443b5ce7b8f5eeedfda27 | ||||
|   StackModules: f43fdc3fc047c0e208a6a44d604c97b2d1651bd7 | ||||
|   TPExchange: ab140a1f77d531cd49f9534e7ce9d018fb313966 | ||||
|   TradPlusAdSDK: d217c79660ef243cee51879c73d55c243cbc587e | ||||
|   UnityAds: 3588f23a66cd6d4956c2c54d50d10242b24e60af | ||||
|   VungleAds: 7ceb822f20a6619d8d9ed6a616dacd8cc5b05036 | ||||
| 
 | ||||
| PODFILE CHECKSUM: 473f877be67a7223e79289b525f3bb558d259638 | ||||
| 
 | ||||
| COCOAPODS: 1.16.2 | ||||
|  | @ -0,0 +1,26 @@ | |||
| <?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>BNDL</string> | ||||
| 	<key>CFBundleShortVersionString</key> | ||||
| 	<string>1.0</string> | ||||
| 	<key>CFBundleSignature</key> | ||||
| 	<string>????</string> | ||||
| 	<key>CFBundleVersion</key> | ||||
| 	<string>1</string> | ||||
| 	<key>GADApplicationIdentifier</key> | ||||
| 	<string>ca-app-pub-2436733915645843~7466230005</string> | ||||
| </dict> | ||||
| </plist> | ||||
|  | @ -0,0 +1,2 @@ | |||
| // https://github.com/Quick/Quick | ||||
| 
 | ||||
|  | @ -0,0 +1,209 @@ | |||
| # | ||||
| # Be sure to run `pod lib lint FusionAds.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             = 'FusionAds' | ||||
|   s.version          = '1.3.0' | ||||
|   s.summary          = 'A short description of FusionAds.' | ||||
| 
 | ||||
| # 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/FusionAds-iOS' | ||||
|   # s.screenshots     = 'www.example.com/screenshots_1', 'www.example.com/screenshots_2' | ||||
|   s.license          = { :type => 'MIT', :file => 'LICENSE' } | ||||
|   s.author           = { 'Haoyi' => 'haoyi.zhang@castbox.fm' } | ||||
|   s.source           = { :git => 'git@git.chengdu.pundit.company:castbox/FusionAds-iOS.git', :tag => s.version.to_s } | ||||
|   # s.social_media_url = 'https://twitter.com/<TWITTER_USERNAME>' | ||||
| 
 | ||||
|   s.platform = :ios, "13.0" | ||||
|   s.ios.deployment_target = '13.0' | ||||
|    | ||||
|   # s.resource_bundles = { | ||||
|   #   'FusionAds' => ['FusionAds/Assets/*.png'] | ||||
|   # } | ||||
| 
 | ||||
|   # s.public_header_files = 'Pod/Classes/**/*.h' | ||||
|   # s.frameworks = 'UIKit', 'MapKit' | ||||
|   # s.dependency 'AFNetworking', '~> 2.3' | ||||
|    | ||||
|   # ======= MAX start ======= | ||||
|    | ||||
|   s.subspec 'MaxAdapters' do |sub| | ||||
|     sub.dependency 'AppLovinSDK', '13.1.0' | ||||
|       | ||||
|    # IronSource | ||||
|    # commented for avoiding collision from IronSource collision | ||||
|   # s.dependency 'AppLovinMediationIronSourceAdapter', '8.7.0.0.0' | ||||
|     | ||||
|    # Google | ||||
|    sub.dependency 'AppLovinMediationGoogleAdapter', '11.13.0.1' | ||||
|    sub.dependency 'AppLovinMediationGoogleAdManagerAdapter', '11.13.0.1' | ||||
|     | ||||
|    # APS | ||||
|    sub.dependency 'AmazonPublisherServicesSDK', '5.0.1' | ||||
|    sub.dependency 'AppLovinMediationAmazonAdMarketplaceAdapter', '5.0.1.0' | ||||
|     | ||||
|    # BidMachine | ||||
|    sub.dependency 'AppLovinMediationBidMachineAdapter', '3.2.0.0.0' | ||||
|     | ||||
|    # Mintegral | ||||
|    sub.dependency 'AppLovinMediationMintegralAdapter', '7.7.5.0.0' | ||||
|     | ||||
|    # DT Exchange | ||||
|    sub.dependency 'AppLovinMediationFyberAdapter', '8.3.5.0' | ||||
|     | ||||
|    # Facebook | ||||
|    sub.dependency 'AppLovinMediationFacebookAdapter', '6.16.0.1' | ||||
|     | ||||
|    # InMobi | ||||
|    sub.dependency 'AppLovinMediationInMobiAdapter', '10.8.0.0' | ||||
| 
 | ||||
|    # Vungle (Liftoff Monetize) | ||||
|    sub.dependency 'AppLovinMediationVungleAdapter', '7.4.4.0' | ||||
| 
 | ||||
|    # Pangle ByteDance | ||||
|    sub.dependency 'AppLovinMediationByteDanceAdapter', '6.5.0.8.0' | ||||
|     | ||||
|    # Unity Ads | ||||
|    sub.dependency 'AppLovinMediationUnityAdsAdapter', '4.13.2.0' | ||||
|     | ||||
|    # Moloc | ||||
|    sub.dependency 'AppLovinMediationMolocoAdapter', '3.7.0.0' | ||||
| 
 | ||||
|    # Smaato | ||||
|    sub.dependency 'AppLovinMediationSmaatoAdapter', '22.9.2.0' | ||||
|     | ||||
|    # Chartboost | ||||
|    sub.dependency 'AppLovinMediationChartboostAdapter', '9.8.0.0' | ||||
|     | ||||
|    # Verve | ||||
|    sub.dependency 'AppLovinMediationVerveAdapter', '3.2.0.0' | ||||
| 
 | ||||
|    # Mobile Fuse | ||||
|    sub.dependency 'AppLovinMediationMobileFuseAdapter', '1.9.0.0' | ||||
|     | ||||
|    # Ogury | ||||
|    sub.dependency 'AppLovinMediationOguryPresageAdapter', '5.0.2.0' | ||||
|     | ||||
|    # PubMatic | ||||
|   #   s.dependency 'OpenWrapSDK', '3.3.0' | ||||
|   #   s.dependency 'AppLovinPubMaticAdapter', '1.1.0' | ||||
|   sub.dependency 'AppLovinMediationPubMaticAdapter', '4.4.0.0' | ||||
| 
 | ||||
|   sub.dependency 'GuruConsent', '1.4.6' | ||||
|   sub.dependency 'TradPlusAdSDK', '10.9.0' | ||||
|     | ||||
|    sub.vendored_frameworks = 'ALMCMediationAdapter.framework' | ||||
|   end | ||||
|   | ||||
|  # ======= MAX End ======= | ||||
|   | ||||
|  # ======= IronSource Start ======= | ||||
|  s.subspec 'IronSourceAdapters' do |sub| | ||||
|    sub.dependency 'IronSourceSDK','8.7.0.0' | ||||
|     | ||||
|    # IronSourceAdQualitySDK 7.24.3 版本会在banner展示时偶发crash,类似如下崩溃堆栈,故先移除 | ||||
|    # SMLComparisonExp compareObject:toObject: | ||||
|   # s.dependency 'IronSourceAdQualitySDK','7.24.3' | ||||
| 
 | ||||
|    # SDK 11.13.0 | ||||
|    sub.dependency 'IronSourceAdMobAdapter','4.3.60.0' | ||||
| 
 | ||||
|    # SDK 13.1.0 | ||||
|    sub.dependency 'IronSourceAppLovinAdapter','4.3.52.0' | ||||
| 
 | ||||
|    # SDK 5.0.1 | ||||
|    sub.dependency 'IronSourceAPSAdapter','4.3.18.0' | ||||
| 
 | ||||
|    # SDK 3.2.0 | ||||
|    sub.dependency 'IronSourceBidMachineAdapter','4.3.14.0' | ||||
| 
 | ||||
|    # SDK 7.7.7 | ||||
|    sub.dependency 'IronSourceMintegralAdapter', '4.3.31.0' | ||||
| 
 | ||||
|    # SDK 8.3.5 (DT Exchange) | ||||
|    sub.dependency 'IronSourceFyberAdapter','4.3.41.0' | ||||
| 
 | ||||
|    # SDK 6.17.0 | ||||
|    sub.dependency 'IronSourceFacebookAdapter','4.3.47.0' | ||||
| 
 | ||||
|    # SDK 10.8.0 | ||||
|    sub.dependency 'IronSourceInMobiAdapter','4.3.26.0' | ||||
| 
 | ||||
|    # SDK 7.4.4 (Liftoff Monetize) | ||||
|    sub.dependency 'IronSourceVungleAdapter','4.3.42.0' | ||||
| 
 | ||||
|    # SDK 6.5.0.8 | ||||
|    sub.dependency 'IronSourcePangleAdapter','4.3.40.0' | ||||
| 
 | ||||
|    # SDK 4.13.2 | ||||
|    sub.dependency 'IronSourceUnityAdsAdapter','4.3.48.0' | ||||
| 
 | ||||
|    # SDK 3.7.0 | ||||
|    sub.dependency 'IronSourceMolocoAdapter', '4.3.11.0' | ||||
|   #   s.dependency 'MolocoCustomAdapterIronSource', '3.1.1.0' | ||||
| 
 | ||||
|    # SDK 22.9.2 | ||||
|    sub.dependency 'IronSourceSmaatoAdapter','4.3.16.0' | ||||
|  end | ||||
| 
 | ||||
|   | ||||
|  # ======= IronSource End ======= | ||||
|   | ||||
|  # ======= AdMob Start ======= | ||||
|   | ||||
|  s.subspec 'AdMobAdapters' do |sub| | ||||
|     | ||||
|    sub.dependency 'Google-Mobile-Ads-SDK', '11.13.0' | ||||
|     | ||||
|    # https://developers.google.com/admob/ios/mediation/chartboost#chartboost-ios-mediation-adapter-changelog | ||||
|    sub.dependency 'GoogleMobileAdsMediationChartboost', '9.8.0.0' | ||||
|     | ||||
|    # https://developers.google.com/admob/ios/mediation/dt-exchange#dt-exchange-ios-mediation-adapter-changelog | ||||
|    sub.dependency 'GoogleMobileAdsMediationFyber', '8.3.5.0' | ||||
|     | ||||
|    # https://developers.google.com/admob/ios/mediation/inmobi#inmobi-ios-mediation-adapter-changelog | ||||
|    sub.dependency 'GoogleMobileAdsMediationInMobi', '10.8.0.0' | ||||
|     | ||||
|    # commented to avoiding runtime issues with ironsource sdk | ||||
|   # # https://developers.google.com/admob/ios/mediation/ironsource#ironsource-ios-mediation-adapter-changelog | ||||
|   # sub.dependency 'GoogleMobileAdsMediationIronSource', '8.6.1.0.0' | ||||
|     | ||||
|    # https://developers.google.com/admob/ios/mediation/liftoff-monetize#liftoff-monetize-ios-mediation-adapter-changelog | ||||
|    sub.dependency 'GoogleMobileAdsMediationVungle', '7.4.4.0' | ||||
|     | ||||
|    # https://developers.google.com/admob/ios/mediation/meta#meta-audience-network-ios-mediation-adapter-changelog | ||||
|    sub.dependency 'GoogleMobileAdsMediationFacebook', '6.16.0.0' | ||||
|     | ||||
|    # https://developers.google.com/admob/ios/mediation/mintegral#mintegral-ios-mediation-adapter-changelog | ||||
|    sub.dependency 'GoogleMobileAdsMediationMintegral', '7.7.5.0' | ||||
|     | ||||
|  end | ||||
| 
 | ||||
|  s.subspec 'Main' do |sub| | ||||
|    sub.source_files = 'FusionAds/Classes/**/*' | ||||
|    sub.dependency 'FusionAds/MaxAdapters' | ||||
|    sub.dependency 'FusionAds/IronSourceAdapters' | ||||
|    sub.dependency 'FusionAds/AdMobAdapters' | ||||
|  end | ||||
|   | ||||
|  s.default_subspecs = "Main" | ||||
|   | ||||
|  # ======= AdMob End ======= | ||||
|   | ||||
|  s.static_framework = true | ||||
| # valid_archs = ['x86_64','arm64'] | ||||
| end | ||||
|  | @ -0,0 +1,186 @@ | |||
| // FusionAdsSdk.swift | ||||
| // Main entry point for the FusionAds iOS SDK | ||||
| // Corresponds to GuruAdsSdk.kt in Android implementation | ||||
| 
 | ||||
| import Foundation | ||||
| import UIKit | ||||
| import AppLovinSDK | ||||
| import DTBiOSSDK | ||||
| import GuruConsent | ||||
| import OpenWrapSDK | ||||
| import InMobiSDK | ||||
| import TradPlusAds | ||||
| import MobileFuseSDK | ||||
| import IronSource | ||||
| import GoogleMobileAds | ||||
| import ChartboostSDK | ||||
| import IASDKCore | ||||
| import InMobiAdapter | ||||
| import VungleAdsSDK | ||||
| import MTGSDK | ||||
| 
 | ||||
| 
 | ||||
| #if DEBUG | ||||
| extension FusionAdsSdk { | ||||
|     public static func registerExternalAdPlatformSdkMapping( | ||||
|         platform: AdPlatform, | ||||
|         initializer: @escaping GuruAdsInitializer | ||||
|     ) { | ||||
|         externalAdPlatformSdkMapping[platform] = initializer | ||||
|     } | ||||
| } | ||||
| #endif | ||||
| 
 | ||||
| public class FusionAdsSdk { | ||||
|      | ||||
|     private static let adPlatformSdkMapping: [AdPlatform:GuruAdsInitializer] = [ | ||||
|         AdPlatform.max : GuruMaxAdsSdk.obtain, | ||||
|         AdPlatform.ironSource : GuruIronSourceAdsSdk.obtain, | ||||
|         AdPlatform.adMob : GuruAdMobAdsSdk.obtain | ||||
|     ] | ||||
|      | ||||
|     private static var externalAdPlatformSdkMapping: [AdPlatform:GuruAdsInitializer] = [:] | ||||
|      | ||||
|     private let adsEngineManager: AdsEngineManager; | ||||
|     private let viewController: UIViewController; | ||||
|      | ||||
|     public init(controller: UIViewController) { | ||||
|         viewController = controller | ||||
|         adsEngineManager = AdsEngineManager(viewController: viewController) | ||||
|     } | ||||
|      | ||||
|     // Initialize the SDK with configuration profile | ||||
|     public func initialize(adsProfile: AdsProfile, analyticsEventHandler: AnalyticsEventHandler? = nil) async -> Bool { | ||||
|          | ||||
|         let initializers = Self.externalAdPlatformSdkMapping.merging(Self.adPlatformSdkMapping) { | ||||
|             (current, _) in current | ||||
|         } | ||||
|         if(analyticsEventHandler != nil) { | ||||
|             AnalyticsEvents.shared.registerHandler(handler: analyticsEventHandler!) | ||||
|         } | ||||
|         for adPlatform in adsProfile.adPlatforms { | ||||
|             let initializer = initializers[adPlatform] | ||||
|             let sdk = initializer?(viewController) | ||||
|             if sdk != nil { | ||||
|                 await sdk!.initialize(adsProfile: adsProfile) | ||||
|                 adsEngineManager.addAdsSdk(sdk!) | ||||
|             } | ||||
|         } | ||||
|         return true; | ||||
|     } | ||||
|      | ||||
|     // Create interstitial ad engine | ||||
|     public func createInterstitialAdEngine(adEngineConfig: AdEngineConfig) -> InterstitialAdEngine? { | ||||
|         return adsEngineManager.createInterstitialAdEngine(adEngineConfig: adEngineConfig) | ||||
|     } | ||||
|      | ||||
|     // Create rewarded ad engine | ||||
|     public func createRewardedAdEngine(adEngineConfig: AdEngineConfig) -> RewardedAdEngine? { | ||||
|         return adsEngineManager.createRewardedAdEngine(adEngineConfig: adEngineConfig) | ||||
|     } | ||||
|      | ||||
|     // Create banner ad engine | ||||
|     public func createBannerAdEngine(adEngineConfig: AdEngineConfig) -> BannerAdEngine? { | ||||
|         return adsEngineManager.createBannerAdEngine(adEngineConfig: adEngineConfig) | ||||
|     } | ||||
|      | ||||
|     // Create mrec ad engine | ||||
|     public func createMRecAdEngine(adEngineConfig: AdEngineConfig) -> MRecAdEngine? { | ||||
|         return adsEngineManager.createMRecAdEngine(adEngineConfig: adEngineConfig) | ||||
|     } | ||||
|      | ||||
|     public func getInterstitialAdEngine(id: Int) -> InterstitialAdEngine? { | ||||
|         return adsEngineManager.getInterstitialAdEngine(id: id) | ||||
|     } | ||||
|      | ||||
|     public func getRewardedAdEngine(id: Int) -> RewardedAdEngine? { | ||||
|         return adsEngineManager.getRewardedAdEngine(id: id) | ||||
|     } | ||||
|      | ||||
|     public func getBannerAdEngine(id: Int) -> BannerAdEngine? { | ||||
|         return adsEngineManager.getBannerAdEngine(id: id) | ||||
|     } | ||||
|      | ||||
|     public func getMRecAdEngine(id: Int) -> MRecAdEngine? { | ||||
|         return adsEngineManager.getMRecAdEngine(id: id) | ||||
|     } | ||||
|      | ||||
|     public func openDebugger(adPlatform: AdPlatform) -> Bool { | ||||
|         if (adPlatform == AdPlatform.max) { | ||||
|             ALSdk.shared().showMediationDebugger() | ||||
|         } else if (adPlatform == AdPlatform.ironSource) { | ||||
|             IronSource.launchTestSuite(viewController) | ||||
|         } else if (adPlatform == AdPlatform.adMob) { | ||||
|             GADMobileAds.sharedInstance().presentAdInspector(from: viewController) | ||||
|         } | ||||
|         return true; | ||||
|     } | ||||
|      | ||||
|     public func isTablet() throws -> Bool { | ||||
|         return UIDevice.current.userInterfaceIdiom == .pad | ||||
|     } | ||||
|      | ||||
|     public func afterAcceptPrivacy() -> Bool { | ||||
|         ALPrivacySettings.setHasUserConsent(true) | ||||
|          | ||||
|         // ironsource | ||||
|         IronSource.setConsent(true) | ||||
|          | ||||
|         // chartboost | ||||
|         let dataUseConsent = CHBDataUseConsent.GDPR(CHBDataUseConsent.GDPR.Consent.nonBehavioral) | ||||
|         Chartboost.addDataUseConsent(dataUseConsent) | ||||
|          | ||||
|         // fyber | ||||
|         IASDKCore.sharedInstance().gdprConsent = IAGDPRConsentType.given | ||||
|         IASDKCore.sharedInstance().gdprConsentString = "myGdprConsentString" | ||||
|          | ||||
|         // inmobi | ||||
|         var consentObject = Dictionary<String, String>() | ||||
|         consentObject["gdpr"] = "1" | ||||
|         consentObject[IMCommonConstants.IM_GDPR_CONSENT_AVAILABLE] = "true" | ||||
|         GADMInMobiConsent.updateGDPRConsent(consentObject) | ||||
|          | ||||
|         // vungle | ||||
|         VunglePrivacySettings.setGDPRStatus(true) | ||||
|         VunglePrivacySettings.setGDPRMessageVersion("v1.0.0") | ||||
|          | ||||
|         // mintegral | ||||
|         MTGSDK.sharedInstance().consentStatus = true | ||||
| 
 | ||||
|         return true | ||||
|     } | ||||
|      | ||||
|     public func updateOrientation(orientation: ScreenOrientation) { | ||||
|         adsEngineManager.dispatchOrientationUpdate(orientation: orientation) | ||||
|     } | ||||
|      | ||||
|     public func updateUid2Token(uid2Token: String) { | ||||
|         if (AdsHelper.setUid2Token(uid2Token)) { | ||||
|            NSLog("setUid2Token: \(uid2Token) success!!!") | ||||
| 
 | ||||
|            // PubMatic UID2 | ||||
|            var userId = POBExternalUserId(source: "uidapi.com", andId: uid2Token) | ||||
|            userId.atype = 1 | ||||
|            OpenWrapSDK.addExternalUserId(userId) | ||||
| 
 | ||||
|            // Inmobi UID2 | ||||
|            IMSdk.setPublisherProvidedUnifiedId(["uidapi.com": uid2Token]) | ||||
| 
 | ||||
|            // TradPlus | ||||
|            TradPlus.sharedInstance().uid2Info = { | ||||
|                let info = TradPlusUID2Info() | ||||
|                info.uid2Token = uid2Token | ||||
|                return info | ||||
|            }() | ||||
| 
 | ||||
|            // MobileFuse | ||||
|            MobileFuseTargetingData.setExtendedUserId(uid2Token, forPartner: "uidapi.com") | ||||
|        } | ||||
|     } | ||||
|      | ||||
|     // for testing only | ||||
|     public func clearCachedEngines() { | ||||
|         adsEngineManager.clearCachedEngines() | ||||
|     } | ||||
| } | ||||
| 
 | ||||
|  | @ -0,0 +1,55 @@ | |||
| // | ||||
| //  AdMobFusionAd.swift | ||||
| //  Pods | ||||
| // | ||||
| //  Created by 250102 on 2025/5/9. | ||||
| // | ||||
| import GoogleMobileAds | ||||
| 
 | ||||
| class AdMobFusionAd : FusionAd { | ||||
|     let adValue: GADAdValue? | ||||
|     let adInfo: GADResponseInfo? | ||||
|     let adUnitIdValue: String? | ||||
|     public init(engineId: Int, adType: AdType, adUnitId:String?, ad: GADResponseInfo?, adValue: GADAdValue?) { | ||||
|         adInfo = ad | ||||
|         self.adUnitIdValue = adUnitId | ||||
|         self.adValue = adValue | ||||
|         super.init(engineId: engineId, adType: adType) | ||||
|     } | ||||
|      | ||||
|     override public var adPlatform: AdPlatform { | ||||
|         return AdPlatform.adMob | ||||
|     } | ||||
|      | ||||
|     override public var adUnitId: String? { | ||||
|         return self.adUnitIdValue | ||||
|     } | ||||
|      | ||||
|     override public var revenue: Double { | ||||
|         return (adValue?.value.doubleValue ?? 0.0) / 1000000.0 | ||||
|     } | ||||
|      | ||||
|     override public var waterfallName: String? { | ||||
|         return nil | ||||
|     } | ||||
|      | ||||
|     override public var placement: String? { | ||||
|         return nil | ||||
|     } | ||||
|      | ||||
|     override public var networkName: String? { | ||||
|         return adInfo?.loadedAdNetworkResponseInfo?.adNetworkClassName | ||||
|     } | ||||
|      | ||||
|     override public var networkPlacement: String? { | ||||
|         return nil | ||||
|     } | ||||
|      | ||||
|     override public var creativeId: String? { | ||||
|         return nil | ||||
|     } | ||||
|      | ||||
|     override public var adReviewCreativeId: String? { | ||||
|         return nil | ||||
|     } | ||||
| } | ||||
|  | @ -0,0 +1,21 @@ | |||
| // | ||||
| //  AdMobFusionReward.swift | ||||
| //  Pods | ||||
| // | ||||
| //  Created by 250102 on 2025/5/9. | ||||
| // | ||||
| import GoogleMobileAds | ||||
| 
 | ||||
| class AdMobFusionReward : FusionReward { | ||||
|      | ||||
|     let reward: GADAdReward | ||||
|      | ||||
|     public init(_ reward: GADAdReward) { | ||||
|         self.reward = reward | ||||
|     } | ||||
|      | ||||
|     var rewardAmount: Int { | ||||
|         return reward.amount.intValue | ||||
|     } | ||||
|      | ||||
| } | ||||
|  | @ -0,0 +1,130 @@ | |||
| // | ||||
| //  GuruAdmobAdsSdk.swift | ||||
| //  Pods | ||||
| // | ||||
| //  Created by 250102 on 2025/5/9. | ||||
| // | ||||
| import GoogleMobileAds | ||||
| import FBAudienceNetwork | ||||
| 
 | ||||
| class GuruAdMobAdsSdk : GuruAdsSdk { | ||||
|     private static var instance: GuruAdMobAdsSdk? | ||||
| 
 | ||||
|      | ||||
|     public var adPlatform: AdPlatform { | ||||
|         return AdPlatform.adMob | ||||
|     } | ||||
|      | ||||
|     private var initialized = false | ||||
|      | ||||
|     private static var interstitialAds: [String: GuruInterstitialAd] = [:] | ||||
|     private static var rewardedAds: [String: GuruRewardedAd] = [:] | ||||
|     private static var bannerAds: [String: GuruBannerAd] = [:] | ||||
|     private static var mrecAds: [String: GuruMRecAd] = [:] | ||||
|      | ||||
|     private func logDebug(_ message: String) { | ||||
|         Logger.d(tag: "GuruAdMobAdsSdk", message: message) | ||||
|     } | ||||
|      | ||||
|     private func logInfo(_ message: String) { | ||||
|         Logger.i(tag: "GuruAdMobAdsSdk", message: message) | ||||
|     } | ||||
|      | ||||
|     private func logWarn(_ message: String) { | ||||
|         Logger.w(tag: "GuruAdMobAdsSdk", message: message) | ||||
|     } | ||||
|      | ||||
|     private func logError(_ message: String) { | ||||
|         Logger.e(tag: "GuruAdMobAdsSdk", message: message) | ||||
|     } | ||||
|      | ||||
|     private let viewController: UIViewController | ||||
|      | ||||
|     private init(viewController: UIViewController) { | ||||
|         self.viewController = viewController | ||||
|     } | ||||
|      | ||||
|     public static func obtain(viewController: UIViewController) -> GuruAdsSdk { | ||||
|         if let existingInstance = instance { | ||||
|             return existingInstance | ||||
|         } | ||||
|          | ||||
|         let newInstance = GuruAdMobAdsSdk(viewController: viewController) | ||||
|         instance = newInstance | ||||
|         return newInstance | ||||
|     } | ||||
|      | ||||
|     public func initialize(adsProfile: AdsProfile) async -> Bool { | ||||
|         guard !initialized else { | ||||
|             logWarn("SDK already initialized! Ignoring.") | ||||
|             return false | ||||
|         } | ||||
|          | ||||
|         initialized = true | ||||
|          | ||||
|         if(adsProfile.debugMode) { | ||||
|              | ||||
|         } | ||||
|         return await withCheckedContinuation(isolation: MainActor.shared) { continuation in | ||||
|             FBAdSettings.setAdvertiserTrackingEnabled(true) | ||||
|             GADMobileAds.sharedInstance().start(completionHandler: { status in | ||||
|                 var initedAtLeastOne = false | ||||
|                 status.adapterStatusesByClassName.forEach {key, value in | ||||
|                     if(value.state == GADAdapterInitializationState.ready) { | ||||
|                         self.logInfo("adapter \(key) ready, latency \(value.latency)") | ||||
|                         initedAtLeastOne = true | ||||
|                     } else { | ||||
|                         self.logWarn("adapter \(key) not ready, latency \(value.latency)") | ||||
|                     } | ||||
|                 } | ||||
|                 continuation.resume(returning: initedAtLeastOne) | ||||
|             }) | ||||
|         } | ||||
| 
 | ||||
|     } | ||||
|      | ||||
|     public func obtainInterstitialAd(adConfig: AdConfig) -> GuruInterstitialAd { | ||||
|         if let existingAd = Self.interstitialAds[adConfig.cacheKey] { | ||||
|             return existingAd | ||||
|         } | ||||
|          | ||||
|         let newAd = GuruAdMobInterstitialAd(viewController: viewController, adConfig: adConfig) | ||||
|         Self.interstitialAds[adConfig.cacheKey] = newAd | ||||
|         return newAd | ||||
|     } | ||||
|      | ||||
|     public func obtainRewardedAd(adConfig: AdConfig) -> GuruRewardedAd { | ||||
|         if let existingAd = Self.rewardedAds[adConfig.cacheKey] { | ||||
|             return existingAd | ||||
|         } | ||||
|          | ||||
|         let newAd = GuruAdMobRewardedAd(viewController: viewController, adConfig: adConfig) | ||||
|         Self.rewardedAds[adConfig.cacheKey] = newAd | ||||
|         return newAd | ||||
|     } | ||||
|      | ||||
|     public func obtainBannerAd(adConfig: AdConfig) -> GuruBannerAd { | ||||
|         if let existingAd = Self.bannerAds[adConfig.cacheKey] { | ||||
|             return existingAd | ||||
|         } | ||||
|          | ||||
|         let newAd = GuruAdMobBannerAd(viewController: viewController, adConfig: adConfig) | ||||
|         Self.bannerAds[adConfig.cacheKey] = newAd | ||||
|         return newAd | ||||
|     } | ||||
|      | ||||
|     public func obtainMRecAd(adConfig: AdConfig) -> GuruMRecAd { | ||||
|         if let existingAd = Self.mrecAds[adConfig.cacheKey] { | ||||
|             return existingAd | ||||
|         } | ||||
|          | ||||
|         let newAd = GuruAdMobMRecAd(viewController: viewController, adConfig: adConfig) | ||||
|         Self.mrecAds[adConfig.cacheKey] = newAd | ||||
|         return newAd | ||||
|     } | ||||
|      | ||||
|     public func processCrossAction(action: AdsCrossAction) -> Bool { | ||||
|         return false | ||||
|     } | ||||
|      | ||||
| } | ||||
|  | @ -0,0 +1,116 @@ | |||
| // | ||||
| //  GuruAdMobBannerAd.swift | ||||
| //  Pods | ||||
| // | ||||
| //  Created by 250102 on 2025/5/9. | ||||
| // | ||||
| 
 | ||||
| import GoogleMobileAds | ||||
| 
 | ||||
| class GuruAdMobBannerView : GuruBannerView { | ||||
|     func view() -> UIView { | ||||
|         return bannerAdView | ||||
|     } | ||||
|      | ||||
|     func setPlacementName(placement: String) { | ||||
|     } | ||||
|      | ||||
|     func onShow() { | ||||
|         bannerAdView.isHidden = false | ||||
|         bannerAdView.isAutoloadEnabled = true | ||||
|     } | ||||
|      | ||||
|     func onHide() { | ||||
|         bannerAdView.isHidden = true | ||||
|         bannerAdView.isAutoloadEnabled = false | ||||
|     } | ||||
|      | ||||
|     private var bannerAdView: GADBannerView | ||||
|      | ||||
|     public init(_ banner: GADBannerView) { | ||||
|         self.bannerAdView = banner | ||||
|     } | ||||
|      | ||||
| } | ||||
| 
 | ||||
| class GuruAdMobBannerAd : GuruBannerAd, GADBannerViewDelegate { | ||||
|     private let viewController: UIViewController | ||||
|     private let adConfig: AdConfig | ||||
|     private var bannerAd: GADBannerView? | ||||
|     private var coordinator: GuruBannerCoordinator<GuruAdMobBannerView>? | ||||
|     private var currentUid2Token: String = "" | ||||
|      | ||||
|     public init(viewController: UIViewController, adConfig: AdConfig) { | ||||
|         self.viewController = viewController | ||||
|         self.adConfig = adConfig | ||||
|         super.init(engineId: adConfig.engineId, adUnitId: adConfig.adUnitId, adPlatform: AdPlatform.adMob) | ||||
|     } | ||||
|      | ||||
|     override func load() -> Bool { | ||||
|         let isTablet = UIDevice.current.userInterfaceIdiom == .pad | ||||
|         let bannerSize = if (isTablet) { | ||||
|             GADAdSizeBanner | ||||
|         } else { | ||||
|             GADAdSizeLargeBanner | ||||
|         } | ||||
|         self.bannerAd = GADBannerView(adSize: bannerSize) | ||||
|         self.bannerAd?.adUnitID = adUnitId | ||||
|         self.bannerAd?.delegate = self | ||||
|         self.bannerAd?.paidEventHandler = { paidValue in | ||||
|             self.adRevenuePaid(ad: self.createFusionAd(self.bannerAd, adValue: paidValue)) | ||||
|         } | ||||
|         coordinator = GuruBannerCoordinator(viewController: viewController, bannerAd: GuruAdMobBannerView(bannerAd!), contentId: engineId) | ||||
|          | ||||
|         self.bannerAd?.load(GADRequest()) | ||||
|         return true | ||||
|     } | ||||
|      | ||||
|     override func show(_ request: BannerShowRequest? = nil) -> Bool { | ||||
|         return coordinator?.show(request) ?? false | ||||
|     } | ||||
|      | ||||
|     override func hide() -> Bool { | ||||
|         return coordinator?.hide() ?? false | ||||
|     } | ||||
|      | ||||
|     override func destroy() -> Bool { | ||||
|         let _ = coordinator?.destroy() | ||||
|         coordinator = nil | ||||
|         self.bannerAd?.delegate = nil | ||||
|         self.bannerAd?.paidEventHandler = nil | ||||
|         self.bannerAd = nil | ||||
|         return true | ||||
|     } | ||||
|      | ||||
|     override func updateOrientation(orientation: ScreenOrientation) -> Bool { | ||||
|         return coordinator?.updateOrientation(orientation: orientation) ?? false | ||||
|     } | ||||
|      | ||||
|     func bannerViewDidReceiveAd(_ bannerView: GADBannerView) { | ||||
|         adLoaded(ad: createFusionAd(bannerView)) | ||||
|     } | ||||
|      | ||||
|     func bannerViewDidRecordImpression(_ bannerView: GADBannerView) { | ||||
|         adDisplayed(ad: createFusionAd(bannerView)) | ||||
|     } | ||||
|      | ||||
|     func bannerViewDidRecordClick(_ bannerView: GADBannerView) { | ||||
|         adClicked(ad: createFusionAd(bannerView)) | ||||
|     } | ||||
|      | ||||
|     func bannerViewWillPresentScreen(_ bannerView: GADBannerView) { | ||||
|         adBannerExpanded(ad: createFusionAd(bannerView)) | ||||
|     } | ||||
|      | ||||
|     func bannerViewDidDismissScreen(_ bannerView: GADBannerView) { | ||||
|         adBannerCollapsed(ad: createFusionAd(bannerView)) | ||||
|     } | ||||
|      | ||||
|     func bannerView(_ bannerView: GADBannerView, didFailToReceiveAdWithError error: any Error) { | ||||
|         adLoadFailed(loadFailedInfo: LoadFailedInfo(engineId: engineId, adPlatform: adPlatform, adType: adType, adUnitId: adUnitId, error: SwiftFusionError(error))) | ||||
|     } | ||||
|      | ||||
|     private func createFusionAd(_ ad: GADBannerView?, adValue: GADAdValue? = nil) -> FusionAd { | ||||
|         return AdMobFusionAd(engineId: engineId, adType: adType, adUnitId: adUnitId, ad: ad?.responseInfo, adValue: adValue) | ||||
|     } | ||||
| } | ||||
|  | @ -0,0 +1,82 @@ | |||
| // | ||||
| //  GuruAdMobInterstitialAd.swift | ||||
| //  Pods | ||||
| // | ||||
| //  Created by 250102 on 2025/5/9. | ||||
| // | ||||
| import GoogleMobileAds | ||||
| 
 | ||||
| class GuruAdMobInterstitialAd : GuruInterstitialAd, GADFullScreenContentDelegate { | ||||
|     private let viewController: UIViewController | ||||
|     private let adConfig: AdConfig | ||||
|     private var ad: GADInterstitialAd? | ||||
|          | ||||
|     public init(viewController: UIViewController, adConfig: AdConfig) { | ||||
|         self.viewController = viewController | ||||
|         self.adConfig = adConfig | ||||
|         super.init(engineId: adConfig.engineId, adUnitId: adConfig.adUnitId, adPlatform: AdPlatform.adMob) | ||||
|     } | ||||
|      | ||||
|     override func load() -> Bool { | ||||
|         logInfo("request load") | ||||
|         Task { @MainActor in | ||||
|             do { | ||||
|                 ad?.fullScreenContentDelegate = nil | ||||
|                 ad?.paidEventHandler = nil | ||||
|                  | ||||
|                 let interstitialAd = try await GADInterstitialAd.load( | ||||
|                     withAdUnitID: adConfig.adUnitId, request: GADRequest()) | ||||
|                 interstitialAd.fullScreenContentDelegate = self | ||||
|                 interstitialAd.paidEventHandler = { paidValue in | ||||
|                     self.logInfo("revenue paid") | ||||
|                     self.adRevenuePaid(ad: self.createFusionAd(interstitialAd, adValue: paidValue)) | ||||
|                 } | ||||
|                 ad = interstitialAd | ||||
|                 logInfo("loaded") | ||||
|                 adLoaded(ad: createFusionAd(interstitialAd)) | ||||
|               } catch { | ||||
|                   logError("load failed, \(error.code) - \(error.localizedDescription)") | ||||
|                   adLoadFailed(loadFailedInfo: LoadFailedInfo(engineId: engineId, adPlatform: adPlatform, adType: adType, adUnitId: adUnitId, error: SwiftFusionError(error))) | ||||
|               } | ||||
|         } | ||||
|         return true | ||||
|     } | ||||
|      | ||||
|     override func show(_ request: InterstitialShowRequest? = nil) -> Bool { | ||||
|         logInfo("request show") | ||||
|         guard let ad = ad else { | ||||
|             logWarn("AdMob interstitialAd not ready for show") | ||||
|             return false | ||||
|         } | ||||
|         ad.present(fromRootViewController: viewController) | ||||
|         return true | ||||
|     } | ||||
|      | ||||
|     override func destroy() -> Bool { | ||||
|         logInfo("request destroy") | ||||
|         self.ad?.fullScreenContentDelegate = nil | ||||
|         self.ad?.paidEventHandler = nil | ||||
|         self.ad = nil | ||||
|         return true | ||||
|     } | ||||
|      | ||||
|     func adDidRecordClick(_ ad: any GADFullScreenPresentingAd) { | ||||
|         adClicked(ad: createFusionAd(self.ad)) | ||||
|     } | ||||
|     func adDidRecordImpression(_ ad: any GADFullScreenPresentingAd) { | ||||
|         adDisplayed(ad: createFusionAd(self.ad)) | ||||
|     } | ||||
|      | ||||
|     func adDidDismissFullScreenContent(_ ad: any GADFullScreenPresentingAd) { | ||||
|         adHidden(ad: createFusionAd(self.ad)) | ||||
|     } | ||||
|      | ||||
|     func ad(_ ad: any GADFullScreenPresentingAd, didFailToPresentFullScreenContentWithError error: any Error) { | ||||
|         logError("displayed failed, \(error.code) - \(error.localizedDescription)") | ||||
|         adDisplayFailed(ad: createFusionAd(self.ad), error: SwiftFusionError(error)) | ||||
|     } | ||||
|      | ||||
|     private func createFusionAd(_ interstitialAd: GADInterstitialAd?, adValue: GADAdValue? = nil) -> FusionAd { | ||||
|         return AdMobFusionAd(engineId: engineId, adType: adType, adUnitId: adUnitId, ad: interstitialAd?.responseInfo, adValue: adValue) | ||||
|     } | ||||
| } | ||||
|  | @ -0,0 +1,110 @@ | |||
| // | ||||
| //  GuruAdMobBannerAd.swift | ||||
| //  Pods | ||||
| // | ||||
| //  Created by 250102 on 2025/5/9. | ||||
| // | ||||
| 
 | ||||
| import GoogleMobileAds | ||||
| 
 | ||||
| class GuruAdMobMRecView : GuruMRecView { | ||||
|     func view() -> UIView { | ||||
|         return bannerAdView | ||||
|     } | ||||
|      | ||||
|     func setPlacementName(placement: String) { | ||||
|     } | ||||
|      | ||||
|     func onShow() { | ||||
|         bannerAdView.isHidden = false | ||||
|         bannerAdView.isAutoloadEnabled = true | ||||
|     } | ||||
|      | ||||
|     func onHide() { | ||||
|         bannerAdView.isHidden = true | ||||
|         bannerAdView.isAutoloadEnabled = false | ||||
|     } | ||||
|      | ||||
|     private var bannerAdView: GADBannerView | ||||
|      | ||||
|     public init(_ banner: GADBannerView) { | ||||
|         self.bannerAdView = banner | ||||
|     } | ||||
|      | ||||
| } | ||||
| 
 | ||||
| class GuruAdMobMRecAd : GuruMRecAd, GADBannerViewDelegate { | ||||
|     private let viewController: UIViewController | ||||
|     private let adConfig: AdConfig | ||||
|     private var bannerAd: GADBannerView? | ||||
|     private var coordinator: GuruMRecCoordinator? | ||||
|     private var currentUid2Token: String = "" | ||||
|      | ||||
|     public init(viewController: UIViewController, adConfig: AdConfig) { | ||||
|         self.viewController = viewController | ||||
|         self.adConfig = adConfig | ||||
|         super.init(engineId: adConfig.engineId, adUnitId: adConfig.adUnitId, adPlatform: AdPlatform.adMob) | ||||
|     } | ||||
|      | ||||
|     override func load() -> Bool { | ||||
|         self.bannerAd = GADBannerView(adSize: GADAdSizeMediumRectangle) | ||||
|         self.bannerAd?.adUnitID = adUnitId | ||||
|         self.bannerAd?.delegate = self | ||||
|         self.bannerAd?.paidEventHandler = { paidValue in | ||||
|             self.adRevenuePaid(ad: self.createFusionAd(self.bannerAd, adValue: paidValue)) | ||||
|         } | ||||
|         coordinator = GuruMRecCoordinator(viewController: viewController, mrecAd: GuruAdMobMRecView(bannerAd!), contentId: engineId) | ||||
|          | ||||
|         self.bannerAd?.load(GADRequest()) | ||||
|         return true | ||||
|     } | ||||
|      | ||||
|     override func show(request: MRecShowRequest? = nil) -> Bool { | ||||
|         return coordinator?.show(request: request) ?? false | ||||
|     } | ||||
|      | ||||
|     override func hide() -> Bool { | ||||
|         return coordinator?.hide() ?? false | ||||
|     } | ||||
|      | ||||
|     override func destroy() -> Bool { | ||||
|         coordinator?.destroy() | ||||
|         coordinator = nil | ||||
|         self.bannerAd?.delegate = nil | ||||
|         self.bannerAd?.paidEventHandler = nil | ||||
|         self.bannerAd = nil | ||||
|         return true | ||||
|     } | ||||
|      | ||||
|     override func updateOrientation(orientation: ScreenOrientation) -> Bool { | ||||
|         return coordinator?.updateOrientation(orientation: orientation) ?? false | ||||
|     } | ||||
|      | ||||
|     func bannerViewDidReceiveAd(_ bannerView: GADBannerView) { | ||||
|         adLoaded(ad: createFusionAd(bannerView)) | ||||
|     } | ||||
|      | ||||
|     func bannerViewDidRecordImpression(_ bannerView: GADBannerView) { | ||||
|         adDisplayed(ad: createFusionAd(bannerView)) | ||||
|     } | ||||
|      | ||||
|     func bannerViewDidRecordClick(_ bannerView: GADBannerView) { | ||||
|         adClicked(ad: createFusionAd(bannerView)) | ||||
|     } | ||||
|      | ||||
|     func bannerViewWillPresentScreen(_ bannerView: GADBannerView) { | ||||
|         adBannerExpanded(ad: createFusionAd(bannerView)) | ||||
|     } | ||||
|      | ||||
|     func bannerViewDidDismissScreen(_ bannerView: GADBannerView) { | ||||
|         adBannerCollapsed(ad: createFusionAd(bannerView)) | ||||
|     } | ||||
|      | ||||
|     func bannerView(_ bannerView: GADBannerView, didFailToReceiveAdWithError error: any Error) { | ||||
|         adLoadFailed(loadFailedInfo: LoadFailedInfo(engineId: engineId, adPlatform: adPlatform, adType: adType, adUnitId: adUnitId, error: SwiftFusionError(error))) | ||||
|     } | ||||
|      | ||||
|     private func createFusionAd(_ ad: GADBannerView?, adValue: GADAdValue? = nil) -> FusionAd { | ||||
|         return AdMobFusionAd(engineId: engineId, adType: adType, adUnitId: adUnitId, ad: ad?.responseInfo, adValue: adValue) | ||||
|     } | ||||
| } | ||||
|  | @ -0,0 +1,83 @@ | |||
| // | ||||
| //  GuruAdMobInterstitialAd.swift | ||||
| //  Pods | ||||
| // | ||||
| //  Created by 250102 on 2025/5/9. | ||||
| // | ||||
| import GoogleMobileAds | ||||
| 
 | ||||
| class GuruAdMobRewardedAd : GuruRewardedAd, GADFullScreenContentDelegate { | ||||
|     private let viewController: UIViewController | ||||
|     private let adConfig: AdConfig | ||||
|     private var ad: GADRewardedAd? | ||||
|          | ||||
|     public init(viewController: UIViewController, adConfig: AdConfig) { | ||||
|         self.viewController = viewController | ||||
|         self.adConfig = adConfig | ||||
|         super.init(engineId: adConfig.engineId, adUnitId: adConfig.adUnitId, adPlatform: AdPlatform.adMob) | ||||
|     } | ||||
|      | ||||
|     override func load() -> Bool { | ||||
|         logInfo("request load") | ||||
|         Task { @MainActor in | ||||
|             do { | ||||
|                 ad?.fullScreenContentDelegate = nil | ||||
|                 ad?.paidEventHandler = nil | ||||
|                  | ||||
|                 let interstitialAd = try await GADRewardedAd.load( | ||||
|                     withAdUnitID: adConfig.adUnitId, request: GADRequest()) | ||||
|                 interstitialAd.fullScreenContentDelegate = self | ||||
|                 interstitialAd.paidEventHandler = { paidValue in | ||||
|                     self.logInfo("revenue paid") | ||||
|                     self.adRevenuePaid(ad: self.createFusionAd(interstitialAd, adValue: paidValue)) | ||||
|                 } | ||||
|                 ad = interstitialAd | ||||
|                 adLoaded(ad: createFusionAd(interstitialAd)) | ||||
|               } catch { | ||||
|                   logError("load failed, \(error.code) - \(error.localizedDescription)") | ||||
|                   adLoadFailed(loadFailedInfo: LoadFailedInfo(engineId: engineId, adPlatform: adPlatform, adType: adType, adUnitId: adUnitId, error: SwiftFusionError(error))) | ||||
|               } | ||||
|         } | ||||
|         return true | ||||
|     } | ||||
|      | ||||
|     override func show(_ request: RewardedShowRequest? = nil) -> Bool { | ||||
|         logInfo("request show") | ||||
|         guard let ad = ad else { | ||||
|             logWarn("AdMob rewardAd not ready for show") | ||||
|             return false | ||||
|         } | ||||
|         ad.present(fromRootViewController: viewController) { | ||||
|             self.adUserRewarded(ad: self.createFusionAd(ad), reward: AdMobFusionReward(ad.adReward)) | ||||
|         } | ||||
|         return true | ||||
|     } | ||||
|      | ||||
|     override func destroy() -> Bool { | ||||
|         logInfo("request destroy") | ||||
|         self.ad?.fullScreenContentDelegate = nil | ||||
|         self.ad?.paidEventHandler = nil | ||||
|         self.ad = nil | ||||
|         return true | ||||
|     } | ||||
|      | ||||
|     func adDidRecordClick(_ ad: any GADFullScreenPresentingAd) { | ||||
|         adClicked(ad: createFusionAd(self.ad)) | ||||
|     } | ||||
|     func adDidRecordImpression(_ ad: any GADFullScreenPresentingAd) { | ||||
|         adDisplayed(ad: createFusionAd(self.ad)) | ||||
|     } | ||||
|      | ||||
|     func adDidDismissFullScreenContent(_ ad: any GADFullScreenPresentingAd) { | ||||
|         adHidden(ad: createFusionAd(self.ad)) | ||||
|     } | ||||
|      | ||||
|     func ad(_ ad: any GADFullScreenPresentingAd, didFailToPresentFullScreenContentWithError error: any Error) { | ||||
|         logError("displayed failed, \(error.code) - \(error.localizedDescription)") | ||||
|         adDisplayFailed(ad: createFusionAd(self.ad), error: SwiftFusionError(error)) | ||||
|     } | ||||
|      | ||||
|     private func createFusionAd(_ rewardedAd: GADRewardedAd?, adValue: GADAdValue? = nil) -> FusionAd { | ||||
|         return AdMobFusionAd(engineId: engineId, adType: adType, adUnitId: adUnitId, ad: rewardedAd?.responseInfo, adValue: adValue) | ||||
|     } | ||||
| } | ||||
|  | @ -0,0 +1,24 @@ | |||
| // AdConfig.swift | ||||
| // Configuration for ads | ||||
| // Corresponds to AdConfig.kt in Android implementation | ||||
| 
 | ||||
| import Foundation | ||||
| 
 | ||||
| public struct AdConfig { | ||||
|     public let engineId: Int | ||||
|     public let adUnitId: String | ||||
|     public let adAmazonSlotId: String? | ||||
|      | ||||
|     public let requireDisableAutoRetries: Bool | ||||
|      | ||||
|     public init(engineId: Int, adUnitId: String, adAmazonSlotId: String? = nil, requireDisableAutoRetries: Bool = false) { | ||||
|         self.engineId = engineId | ||||
|         self.adUnitId = adUnitId | ||||
|         self.adAmazonSlotId = adAmazonSlotId | ||||
|         self.requireDisableAutoRetries = requireDisableAutoRetries | ||||
|     } | ||||
|      | ||||
|     public var cacheKey: String { | ||||
|         return "\(engineId)_\(adUnitId)_\(adAmazonSlotId ?? "noamz")" | ||||
|     } | ||||
| } | ||||
|  | @ -0,0 +1,104 @@ | |||
| // AdsProfile.swift | ||||
| // Defines advertisement profile configurations | ||||
| // Corresponds to AdsProfile.kt in Android implementation | ||||
| 
 | ||||
| import Foundation | ||||
| 
 | ||||
| public struct SegmentWrapper: Codable { | ||||
|     public let key: Int | ||||
|     public let values: [Int] | ||||
|      | ||||
|     public init(key: Int, values: [Int] = []) { | ||||
|         self.key = key | ||||
|         self.values = values | ||||
|     } | ||||
|      | ||||
|     private enum CodingKeys: String, CodingKey { | ||||
|         case key = "k" | ||||
|         case values = "v" | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| public struct SegmentCollectionWrapper: Codable { | ||||
|     public let segments: [SegmentWrapper] | ||||
|      | ||||
|     public init(segments: [SegmentWrapper] = []) { | ||||
|         self.segments = segments | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| public struct AdsProfile: Codable { | ||||
|     public let adPlatforms: [AdPlatform] | ||||
|     public let maxSdkKey: String | ||||
|     public let ironSourceSdkKey: String? | ||||
|     public let amazonAppId: String? | ||||
|     public let pubmaticStoreUrl: String? | ||||
|     public let uid2Token: String? | ||||
|     public let userId: String | ||||
|     public let tpCreativeKey: String | ||||
|     public let debugMode: Bool | ||||
|     public let segments: SegmentCollectionWrapper | ||||
|      | ||||
|     public let maxB2BDisabledAdUnitIds: [String]? | ||||
|          | ||||
|     public init(adPlatforms: [AdPlatform], maxSdkKey: String, | ||||
|                 ironSourceSdkKey: String? = nil, | ||||
|                 amazonAppId: String? = nil, | ||||
|                 pubmaticStoreUrl: String? = nil, | ||||
|                 uid2Token: String? = nil, | ||||
|                 userId: String = "", | ||||
|                 tpCreativeKey: String = "", | ||||
|                 debugMode: Bool = false, | ||||
|                 segments: SegmentCollectionWrapper = SegmentCollectionWrapper(segments: []), | ||||
|                 maxB2BDisabledAdUnitIds: [String]? = nil | ||||
|     ) { | ||||
|         self.adPlatforms = adPlatforms | ||||
|         self.maxSdkKey = maxSdkKey | ||||
|         self.ironSourceSdkKey = ironSourceSdkKey | ||||
|         self.amazonAppId = amazonAppId | ||||
|         self.pubmaticStoreUrl = pubmaticStoreUrl | ||||
|         self.uid2Token = uid2Token | ||||
|         self.userId = userId | ||||
|         self.tpCreativeKey = tpCreativeKey | ||||
|         self.debugMode = debugMode | ||||
|         self.segments = segments | ||||
|         self.maxB2BDisabledAdUnitIds = maxB2BDisabledAdUnitIds | ||||
|     } | ||||
|      | ||||
|     private enum CodingKeys : String, CodingKey { | ||||
|         case adPlatforms = "ad_platforms", | ||||
|              maxSdkKey = "max_sdk_key", | ||||
|              ironSourceSdkKey = "iron_source_sdk_key", | ||||
|              amazonAppId = "amazon_app_id", | ||||
|              pubmaticStoreUrl = "pubmatic_store_url", | ||||
|              uid2Token = "uid2_token", | ||||
|              segments = "segments", | ||||
|              userId = "user_id", | ||||
|              debugMode = "debug_mode", | ||||
|              tpCreativeKey = "tp_creative_key", | ||||
|              maxB2BDisabledAdUnitIds = "max_disable_b2b_ad_unit_ids" | ||||
| 
 | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| public enum AdsCommand { | ||||
|     // Add specific commands as needed | ||||
| } | ||||
| 
 | ||||
| public class ActionResult { | ||||
|     // Base result class | ||||
| } | ||||
| 
 | ||||
| public class BoolActionResult: ActionResult { | ||||
|     public let value: Bool | ||||
|      | ||||
|     public init(value: Bool) { | ||||
|         self.value = value | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| public struct AdsCrossAction { | ||||
|     public let cmd: AdsCommand | ||||
|     public let params: Any | ||||
|     public let onResult: (ActionResult) -> Void | ||||
| } | ||||
|  | @ -0,0 +1,26 @@ | |||
| // FusionAd.swift | ||||
| // Base class for all ad types | ||||
| // Corresponds to FusionAd.kt in Android implementation | ||||
| 
 | ||||
| import Foundation | ||||
| 
 | ||||
| open class FusionAd { | ||||
|     public let engineId: Int | ||||
|     public let adType: AdType | ||||
|      | ||||
|     public init(engineId: Int, adType: AdType) { | ||||
|         self.engineId = engineId | ||||
|         self.adType = adType | ||||
|     } | ||||
|      | ||||
|     open var adPlatform: AdPlatform { fatalError("Subclass must implement") } | ||||
|     open var adUnitId: String? { fatalError("Subclass must implement") } | ||||
|     open var revenue: Double { fatalError("Subclass must implement") } | ||||
|     public var format: String? { adType.label } | ||||
|     open var waterfallName: String? { fatalError("Subclass must implement") } | ||||
|     open var placement: String? { fatalError("Subclass must implement") } | ||||
|     open var networkName: String? { fatalError("Subclass must implement") } | ||||
|     open var networkPlacement: String? { fatalError("Subclass must implement") } | ||||
|     open var creativeId: String? { fatalError("Subclass must implement") } | ||||
|     open var adReviewCreativeId: String? { fatalError("Subclass must implement") } | ||||
| } | ||||
|  | @ -0,0 +1,43 @@ | |||
| // FusionAdListener.swift | ||||
| // Listener interface for ad events | ||||
| // Corresponds to FusionAdListener.kt in Android implementation | ||||
| 
 | ||||
| import Foundation | ||||
| 
 | ||||
| public struct LoadFailedInfo { | ||||
|     public let engineId: Int | ||||
|     public let adPlatform: AdPlatform | ||||
|     public let adType: AdType | ||||
|     public let adUnitId: String | ||||
|     public let error: FusionError? | ||||
|      | ||||
|     public init(engineId: Int, adPlatform: AdPlatform, adType: AdType, adUnitId: String, error: FusionError?) { | ||||
|         self.engineId = engineId | ||||
|         self.adPlatform = adPlatform | ||||
|         self.adType = adType | ||||
|         self.adUnitId = adUnitId | ||||
|         self.error = error | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| public protocol FusionAdListener: AnyObject { | ||||
|     func onAdLoaded(ad: FusionAd) | ||||
|     func onAdDisplayed(ad: FusionAd) | ||||
|     func onAdHidden(ad: FusionAd) | ||||
|     func onAdClicked(ad: FusionAd) | ||||
|     func onAdLoadFailed(loadFailedInfo: LoadFailedInfo) | ||||
|     func onAdDisplayFailed(ad: FusionAd, error: FusionError?) | ||||
|     func onAdRevenuePaid(ad: FusionAd) | ||||
|      | ||||
|     // Optional methods with default empty implementations | ||||
|     func onUserRewarded(ad: FusionAd, reward: FusionReward?) | ||||
|     func onBannerAdExpanded(ad: FusionAd) | ||||
|     func onBannerAdCollapsed(ad: FusionAd) | ||||
| } | ||||
| 
 | ||||
| // Extension to provide default implementations for optional methods | ||||
| public extension FusionAdListener { | ||||
|     func onUserRewarded(ad: FusionAd, reward: FusionReward?) {} | ||||
|     func onBannerAdExpanded(ad: FusionAd) {} | ||||
|     func onBannerAdCollapsed(ad: FusionAd) {} | ||||
| } | ||||
|  | @ -0,0 +1,21 @@ | |||
| // FusionError.swift | ||||
| // Interface for error handling in FusionAds | ||||
| // Corresponds to FusionError.kt in Android implementation | ||||
| 
 | ||||
| import Foundation | ||||
| enum FusionErrorCodes: Int { | ||||
|     case TIMEOUT = -100001 | ||||
|     case UNKNOWN = -100000 | ||||
|      | ||||
|     var code: Int { | ||||
|         return self.rawValue | ||||
|     } | ||||
| } | ||||
| public protocol FusionError { | ||||
|      | ||||
|     var errorCode: Int { get } | ||||
|     var message: String { get } | ||||
|     var cause: Error? { get } | ||||
|     var info: String? { get } | ||||
|     var waterfallName: String? { get } | ||||
| } | ||||
|  | @ -0,0 +1,9 @@ | |||
| // FusionReward.swift | ||||
| // Interface for reward handling in FusionAds | ||||
| // Corresponds to FusionReward.kt in Android implementation | ||||
| 
 | ||||
| import Foundation | ||||
| 
 | ||||
| public protocol FusionReward { | ||||
|     var rewardAmount: Int { get } | ||||
| } | ||||
|  | @ -0,0 +1,26 @@ | |||
| // GuruAdsSdk.swift | ||||
| // Base class for SDK implementations | ||||
| // Corresponds to GuruAdsSdk.kt in Android implementation | ||||
| 
 | ||||
| import Foundation | ||||
| import UIKit | ||||
| 
 | ||||
| // Typealias for callbacks | ||||
| public typealias GuruAdsInitializeCompletedCallback = (GuruAdsSdk, Bool) -> Void | ||||
| public typealias GuruAdsInitializer = (UIViewController) -> GuruAdsSdk | ||||
| 
 | ||||
| public protocol GuruAdsSdk: AnyObject { | ||||
|     var adPlatform: AdPlatform { get } | ||||
|      | ||||
|     func initialize(adsProfile: AdsProfile) async -> Bool | ||||
|      | ||||
|     func obtainInterstitialAd(adConfig: AdConfig) -> GuruInterstitialAd | ||||
|      | ||||
|     func obtainRewardedAd(adConfig: AdConfig) -> GuruRewardedAd | ||||
|      | ||||
|     func obtainBannerAd(adConfig: AdConfig) -> GuruBannerAd | ||||
|      | ||||
|     func obtainMRecAd(adConfig: AdConfig) -> GuruMRecAd | ||||
|      | ||||
|     func processCrossAction(action: AdsCrossAction) -> Bool | ||||
| } | ||||
|  | @ -0,0 +1,74 @@ | |||
| // GuruBannerAd.swift | ||||
| // Base class for banner ad implementations | ||||
| // Corresponds to GuruBannerAd.kt in Android implementation | ||||
| 
 | ||||
| import Foundation | ||||
| import UIKit | ||||
| 
 | ||||
| public struct BannerShowRequest: Equatable { | ||||
|     public let placement: String? | ||||
|     public let position: BannerPosition? | ||||
|     public let offset: Float? | ||||
|      | ||||
|     public init(placement: String? = nil, position: BannerPosition? = nil, offset: Float? = nil) { | ||||
|         self.placement = placement | ||||
|         self.position = position | ||||
|         self.offset = offset | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| open class GuruBannerAd: RelayAd { | ||||
|     public let adUnitId: String | ||||
|     public let adPlatform: AdPlatform | ||||
|      | ||||
|     private let name: String | ||||
|      | ||||
|     public let adType = AdType.Banner | ||||
|      | ||||
|     public init(engineId: Int, adUnitId: String, adPlatform: AdPlatform) { | ||||
|         self.adUnitId = adUnitId | ||||
|         self.adPlatform = adPlatform | ||||
|         self.name = "[BANNER-\(adPlatform)]" | ||||
|         super.init(engineId: engineId) | ||||
|     } | ||||
|      | ||||
|     // MARK: - Logging Methods | ||||
|      | ||||
|     public func logWarn(_ message: String) { | ||||
|         Logger.w(tag: "BANNER-\(adPlatform.name)[\(adUnitId)]", message: message) | ||||
|     } | ||||
|      | ||||
|     public func logDebug(_ message: String) { | ||||
|         Logger.d(tag: "BANNER-\(adPlatform.name)[\(adUnitId)]", message: message) | ||||
|     } | ||||
|      | ||||
|     public func logInfo(_ message: String) { | ||||
|         Logger.i(tag: "BANNER-\(adPlatform.name)[\(adUnitId)]", message: message) | ||||
|     } | ||||
|      | ||||
|     public func logError(_ message: String) { | ||||
|         Logger.e(tag: "BANNER-\(adPlatform.name)[\(adUnitId)]", message: message) | ||||
|     } | ||||
|      | ||||
|     // MARK: - Methods to override | ||||
|      | ||||
|     open func load() -> Bool { | ||||
|         fatalError("Subclass must implement") | ||||
|     } | ||||
|      | ||||
|     open func show(_ request: BannerShowRequest? = nil) -> Bool { | ||||
|         fatalError("Subclass must implement") | ||||
|     } | ||||
|      | ||||
|     open func hide() -> Bool { | ||||
|         fatalError("Subclass must implement") | ||||
|     } | ||||
|      | ||||
|     open func destroy() -> Bool { | ||||
|         fatalError("Subclass must implement") | ||||
|     } | ||||
|      | ||||
|     open func updateOrientation(orientation: ScreenOrientation) -> Bool { | ||||
|         fatalError("Subclass must implement") | ||||
|     } | ||||
| } | ||||
|  | @ -0,0 +1,62 @@ | |||
| // GuruInterstitialAd.swift | ||||
| // Base class for interstitial ad implementations | ||||
| // Corresponds to GuruInterstitialAd.kt in Android implementation | ||||
| 
 | ||||
| import Foundation | ||||
| import UIKit | ||||
| 
 | ||||
| public struct InterstitialShowRequest { | ||||
|     public let placement: String? | ||||
|      | ||||
|     public init(placement: String?) { | ||||
|         self.placement = placement | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| open class GuruInterstitialAd: RelayAd { | ||||
|     public let adUnitId: String | ||||
|     public let adPlatform: AdPlatform | ||||
|      | ||||
|     public let adType = AdType.Interstitial | ||||
|      | ||||
|     private let name: String | ||||
|      | ||||
|     public init(engineId: Int, adUnitId: String, adPlatform: AdPlatform) { | ||||
|         self.adUnitId = adUnitId | ||||
|         self.adPlatform = adPlatform | ||||
|         self.name = "[INTER-\(adPlatform)]" | ||||
|         super.init(engineId: engineId) | ||||
|     } | ||||
|      | ||||
|     // MARK: - Logging Methods | ||||
|      | ||||
|     public func logInfo(_ message: String) { | ||||
|         Logger.i(tag: "INTER-\(adPlatform.name)[\(adUnitId)]", message: message) | ||||
|     } | ||||
|      | ||||
|     public func logWarn(_ message: String) { | ||||
|         Logger.w(tag: "INTER-\(adPlatform.name)[\(adUnitId)]", message: message) | ||||
|     } | ||||
|      | ||||
|     public func logDebug(_ message: String) { | ||||
|         Logger.d(tag: "INTER-\(adPlatform.name)[\(adUnitId)]", message: message) | ||||
|     } | ||||
|      | ||||
|     public func logError(_ message: String) { | ||||
|         Logger.e(tag: "INTER-\(adPlatform.name)[\(adUnitId)]", message: message) | ||||
|     } | ||||
|      | ||||
|     // MARK: - Methods to override | ||||
|      | ||||
|     open func load() -> Bool { | ||||
|         fatalError("Subclass must implement") | ||||
|     } | ||||
|      | ||||
|     open func show(_ request: InterstitialShowRequest? = nil) -> Bool { | ||||
|         fatalError("Subclass must implement") | ||||
|     } | ||||
|      | ||||
|     open func destroy() -> Bool { | ||||
|         fatalError("Subclass must implement") | ||||
|     } | ||||
| } | ||||
|  | @ -0,0 +1,61 @@ | |||
| // GuruRewardedAd.swift | ||||
| // Base class for rewarded ad implementations | ||||
| // Corresponds to GuruRewardedAd.kt in Android implementation | ||||
| 
 | ||||
| import Foundation | ||||
| import UIKit | ||||
| 
 | ||||
| public struct RewardedShowRequest { | ||||
|     public let placement: String? | ||||
|      | ||||
|     public init(placement: String?) { | ||||
|         self.placement = placement | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| open class GuruRewardedAd: RelayAd { | ||||
|     public let adUnitId: String | ||||
|     public let adPlatform: AdPlatform | ||||
|      | ||||
|     private let name: String | ||||
|      | ||||
|     public let adType = AdType.Rewarded | ||||
|      | ||||
|     public init(engineId: Int, adUnitId: String, adPlatform: AdPlatform) { | ||||
|         self.adUnitId = adUnitId | ||||
|         self.adPlatform = adPlatform | ||||
|         self.name = "[RWD-\(adPlatform)]" | ||||
|         super.init(engineId: engineId) | ||||
|     } | ||||
|      | ||||
|     // MARK: - Logging Methods | ||||
|     public func logInfo(_ message: String) { | ||||
|         Logger.i(tag: "RWD-\(adPlatform.name)[\(adUnitId)]", message: message) | ||||
|     } | ||||
|      | ||||
|     public func logWarn(_ message: String) { | ||||
|         Logger.w(tag: "RWD-\(adPlatform.name)[\(adUnitId)]", message: message) | ||||
|     } | ||||
|      | ||||
|     public func logDebug(_ message: String) { | ||||
|         Logger.d(tag: "RWD-\(adPlatform.name)[\(adUnitId)]", message: message) | ||||
|     } | ||||
|      | ||||
|     public func logError(_ message: String) { | ||||
|         Logger.e(tag: "RWD-\(adPlatform.name)[\(adUnitId)]", message: message) | ||||
|     } | ||||
|      | ||||
|     // MARK: - Methods to override | ||||
|      | ||||
|     open func load() -> Bool { | ||||
|         fatalError("Subclass must implement") | ||||
|     } | ||||
|      | ||||
|     open func show(_ request: RewardedShowRequest? = nil) -> Bool { | ||||
|         fatalError("Subclass must implement") | ||||
|     } | ||||
|      | ||||
|     open func destroy() -> Bool { | ||||
|         fatalError("Subclass must implement") | ||||
|     } | ||||
| } | ||||
|  | @ -0,0 +1,56 @@ | |||
| // RelayAd.swift | ||||
| // Base class for ad implementations with delegate forwarding | ||||
| // Corresponds to RelayAd.kt in Android implementation | ||||
| 
 | ||||
| import Foundation | ||||
| 
 | ||||
| open class RelayAd: NSObject { | ||||
|     public var listener: FusionAdListener? | ||||
|     public let engineId: Int | ||||
|      | ||||
|     public init(engineId: Int) { | ||||
|         self.engineId = engineId | ||||
|     } | ||||
|      | ||||
|     // public methods for notifying listeners (called from subclasses) | ||||
|      | ||||
|     public func adLoaded(ad: FusionAd) { | ||||
|         listener?.onAdLoaded(ad: ad) | ||||
|     } | ||||
|      | ||||
|     public func adDisplayed(ad: FusionAd) { | ||||
|         listener?.onAdDisplayed(ad: ad) | ||||
|     } | ||||
|      | ||||
|     public func adHidden(ad: FusionAd) { | ||||
|         listener?.onAdHidden(ad: ad) | ||||
|     } | ||||
|      | ||||
|     public func adClicked(ad: FusionAd) { | ||||
|         listener?.onAdClicked(ad: ad) | ||||
|     } | ||||
|      | ||||
|     public func adUserRewarded(ad: FusionAd, reward: FusionReward) { | ||||
|         listener?.onUserRewarded(ad: ad, reward: reward) | ||||
|     } | ||||
|      | ||||
|     public func adLoadFailed(loadFailedInfo: LoadFailedInfo) { | ||||
|         listener?.onAdLoadFailed(loadFailedInfo: loadFailedInfo) | ||||
|     } | ||||
|      | ||||
|     public func adDisplayFailed(ad: FusionAd, error: FusionError) { | ||||
|         listener?.onAdDisplayFailed(ad: ad, error: error) | ||||
|     } | ||||
|      | ||||
|     public func adRevenuePaid(ad: FusionAd) { | ||||
|         listener?.onAdRevenuePaid(ad: ad) | ||||
|     } | ||||
|      | ||||
|     public func adBannerExpanded(ad: FusionAd) { | ||||
|         listener?.onBannerAdExpanded(ad: ad) | ||||
|     } | ||||
|      | ||||
|     public func adBannerCollapsed(ad: FusionAd) { | ||||
|         listener?.onBannerAdCollapsed(ad: ad) | ||||
|     } | ||||
| } | ||||
|  | @ -0,0 +1,62 @@ | |||
| // GuruMRecAd.swift | ||||
| // Base class for MRec ad implementations | ||||
| // Corresponds to GuruMRecAd.kt in Android implementation | ||||
| 
 | ||||
| import Foundation | ||||
| import UIKit | ||||
| 
 | ||||
| open class GuruMRecAd: RelayAd { | ||||
|     public let adUnitId: String | ||||
|     public let adPlatform: AdPlatform | ||||
|      | ||||
|     public let adType = AdType.MRec | ||||
|      | ||||
|     private let name: String | ||||
|      | ||||
|     public init(engineId: Int, adUnitId: String, adPlatform: AdPlatform) { | ||||
|         self.adUnitId = adUnitId | ||||
|         self.adPlatform = adPlatform | ||||
|         self.name = "[MREC-\(adPlatform)]" | ||||
|         super.init(engineId: engineId) | ||||
|     } | ||||
|      | ||||
|     // MARK: - Logging Methods | ||||
|      | ||||
|     public func logWarn(_ message: String) { | ||||
|         Logger.w(tag: "MRecAd", message: "\(name) \(message)") | ||||
|     } | ||||
|      | ||||
|     public func logDebug(_ message: String) { | ||||
|         Logger.d(tag: "MRecAd", message: "\(name) \(message)") | ||||
|     } | ||||
|      | ||||
|     public func logInfo(_ message: String) { | ||||
|         Logger.i(tag: "MRecAd", message: "\(name) \(message)") | ||||
|     } | ||||
|      | ||||
|     public func logError(_ message: String) { | ||||
|         Logger.e(tag: "MRecAd", message: "\(name) \(message)") | ||||
|     } | ||||
|      | ||||
|     // MARK: - Methods to override | ||||
|      | ||||
|     open func load() -> Bool { | ||||
|         fatalError("Subclass must implement") | ||||
|     } | ||||
|      | ||||
|     open func show(request: MRecShowRequest? = nil) -> Bool { | ||||
|         fatalError("Subclass must implement") | ||||
|     } | ||||
|      | ||||
|     open func hide() -> Bool { | ||||
|         fatalError("Subclass must implement") | ||||
|     } | ||||
|      | ||||
|     open func destroy() -> Bool { | ||||
|         fatalError("Subclass must implement") | ||||
|     } | ||||
|      | ||||
|     open func updateOrientation(orientation: ScreenOrientation) -> Bool { | ||||
|         fatalError("Subclass must implement") | ||||
|     } | ||||
| } | ||||
|  | @ -0,0 +1,17 @@ | |||
| // MRecShowRequest.swift | ||||
| // Request parameters for showing MRec ads | ||||
| // Corresponds to MRecShowRequest.kt in Android implementation | ||||
| 
 | ||||
| import Foundation | ||||
| 
 | ||||
| public struct MRecShowRequest { | ||||
|     public let placement: String | ||||
|     public let horizontalOffset: Float | ||||
|     public let verticalOffset: Float | ||||
|      | ||||
|     public init(placement: String, horizontalOffset: Float = 0.0, verticalOffset: Float = 0.0) { | ||||
|         self.placement = placement | ||||
|         self.horizontalOffset = horizontalOffset | ||||
|         self.verticalOffset = verticalOffset | ||||
|     } | ||||
| } | ||||
|  | @ -0,0 +1,220 @@ | |||
| // AdModels.swift | ||||
| // Defines core ad models for the FusionAds SDK | ||||
| // Corresponds to AdModels.kt in Android implementation | ||||
| 
 | ||||
| import Foundation | ||||
| import UIKit | ||||
| 
 | ||||
| // MARK: - AdPlatform | ||||
| 
 | ||||
| public class AdPlatform: Hashable, CustomStringConvertible, Codable { | ||||
|     public let name: String | ||||
|      | ||||
|     private init(_ name: String) { | ||||
|         self.name = name | ||||
|     } | ||||
|     required public init(from decoder: any Decoder) throws { | ||||
|         let container = try decoder.singleValueContainer() | ||||
|         self.name = try container.decode(String.self) | ||||
|     } | ||||
|      | ||||
|     public func encode(to encoder: any Encoder) throws { | ||||
|         var container = encoder.singleValueContainer() | ||||
|         try container.encode(self.name) | ||||
|     } | ||||
|      | ||||
|     public static let adMob = AdPlatform("ADMOB") | ||||
|     public static let amazon = AdPlatform("AMZ") | ||||
|     public static let facebook = AdPlatform("FB") | ||||
|     public static let ironSource = AdPlatform("ULP") | ||||
|     public static let max = AdPlatform("MAX") | ||||
|     public static let tradPlus = AdPlatform("TP") | ||||
|     public static let pangle = AdPlatform("PG") | ||||
|     public static let invalid = AdPlatform("INVALID") | ||||
|     public static let fusion = AdPlatform("FUSION") | ||||
|     public static let test = AdPlatform("TEST") | ||||
|      | ||||
|     public static func fromName(_ name: String) -> AdPlatform { | ||||
|         let upperName = name.uppercased() | ||||
|         switch upperName { | ||||
|         case AdPlatform.adMob.name: | ||||
|             return AdPlatform.adMob | ||||
|         case AdPlatform.amazon.name: | ||||
|             return AdPlatform.amazon | ||||
|         case AdPlatform.facebook.name: | ||||
|             return AdPlatform.facebook | ||||
|         case AdPlatform.ironSource.name: | ||||
|             return AdPlatform.ironSource | ||||
|         case AdPlatform.max.name: | ||||
|             return AdPlatform.max | ||||
|         case AdPlatform.test.name: | ||||
|             return AdPlatform.test | ||||
|         default: | ||||
|             fatalError("Invalid Ad Platform name") | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     public static func == (lhs: AdPlatform, rhs: AdPlatform) -> Bool { | ||||
|         return lhs.name == rhs.name | ||||
|     } | ||||
|      | ||||
|     public func hash(into hasher: inout Hasher) { | ||||
|         hasher.combine(name) | ||||
|     } | ||||
|      | ||||
|     public var description: String { | ||||
|         return name | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| // MARK: - AdType | ||||
| 
 | ||||
| /** | ||||
|  * Ad format or type of an ad unit, | ||||
|  * [name] mainly for display scenario | ||||
|  * [label] for more formal scenario, like stats | ||||
|  */ | ||||
| public class AdType: Equatable, Hashable, CustomStringConvertible { | ||||
|     public let name: String | ||||
|     public let label: String | ||||
|      | ||||
|     private init(_ name: String, label: String) { | ||||
|         self.name = name | ||||
|         self.label = label | ||||
|     } | ||||
|      | ||||
|     public static let Interstitial = AdType("Interstitial", label: "INTER") | ||||
|     public static let Rewarded = AdType("Rewarded", label: "REWARDED") | ||||
|     public static let Banner = AdType("Banner", label: "BANNER") | ||||
|     public static let MRec = AdType("MRec", label: "MREC") | ||||
|      | ||||
|     public static func fromName(_ name: String) -> AdType { | ||||
|         switch name { | ||||
|         case AdType.Interstitial.name: | ||||
|             return AdType.Interstitial | ||||
|         case AdType.Rewarded.name: | ||||
|             return AdType.Rewarded | ||||
|         case AdType.Banner.name: | ||||
|             return AdType.Banner | ||||
|         case AdType.MRec.name: | ||||
|             return AdType.MRec | ||||
|         default: | ||||
|             fatalError("Invalid Ad Type name") | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     public static func == (lhs: AdType, rhs: AdType) -> Bool { | ||||
|         return lhs.name == rhs.name | ||||
|     } | ||||
|      | ||||
|     public func hash(into hasher: inout Hasher) { | ||||
|         hasher.combine(name) | ||||
|     } | ||||
|      | ||||
|     public var description: String { | ||||
|         return name | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| // MARK: - BannerPosition | ||||
| 
 | ||||
| public enum BannerPosition: String { | ||||
|     case top | ||||
|     case bottom | ||||
|     case start | ||||
|     case end | ||||
| } | ||||
| 
 | ||||
| // MARK: - ScreenOrientation | ||||
| 
 | ||||
| public enum ScreenOrientation: Int { | ||||
|     case portrait = 0 | ||||
|     case landscape = 1 | ||||
| } | ||||
| 
 | ||||
| public class LoadAdTimeoutError: FusionError { | ||||
|     public var errorCode: Int { return -100001 } | ||||
|     public var message: String { return "Load ad timeout" } | ||||
|     public var cause: Error? { return nil } | ||||
|     public var info: String? { return nil } | ||||
|     public var waterfallName: String? { return nil } | ||||
| } | ||||
| 
 | ||||
| public class LoadAdRequestError: FusionError { | ||||
|     public var errorCode: Int { return -100002 } | ||||
|     public let message: String | ||||
|     public var cause: Error? { return nil } | ||||
|     public var info: String? { return nil } | ||||
|     public var waterfallName: String? { return nil } | ||||
|      | ||||
|      | ||||
|     init(message: String = "Load Ad Request Error") { | ||||
|         self.message = message | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| public class ShowAdTimeoutError: FusionError { | ||||
|     public var errorCode: Int { return -100003 } | ||||
|     public var message: String { return "Show Ad Timeout Error" } | ||||
|     public var cause: Error? { return nil } | ||||
|     public var info: String? { return nil } | ||||
|     public var waterfallName: String? { return nil } | ||||
| } | ||||
| 
 | ||||
| public class ShowAdRequestError: FusionError { | ||||
|     public var errorCode: Int { return -100004 } | ||||
|     public let message: String | ||||
|     public var cause: Error? { return nil } | ||||
|     public var info: String? { return nil } | ||||
|     public var waterfallName: String? { return nil } | ||||
|      | ||||
|     init(message: String = "Show Ad Request Error") { | ||||
|         self.message = message | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| public class LoadAdNotAvailableError: FusionError { | ||||
|     public var errorCode: Int { return -100005 } | ||||
|     public let message: String | ||||
|     public var cause: Error? { return nil } | ||||
|     public var info: String? { return nil } | ||||
|     public var waterfallName: String? { return nil } | ||||
|      | ||||
|     init(message: String = "Load Ad Not Avaialable Error") { | ||||
|         self.message = message | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| // MARK: - InvalidFusionAd | ||||
| 
 | ||||
| public class InvalidFusionAd: FusionAd { | ||||
|     public override var adPlatform: AdPlatform { return AdPlatform.invalid } | ||||
|     public override var adUnitId: String? { return "" } | ||||
|     public override var revenue: Double { return 0.0 } | ||||
|     public override var waterfallName: String? { return "" } | ||||
|     public override var placement: String? { return "" } | ||||
|     public override var networkName: String? { return "" } | ||||
|     public override var networkPlacement: String? { return "" } | ||||
|     public override var creativeId: String? { return "" } | ||||
|     public override var adReviewCreativeId: String? { return "" } | ||||
|      | ||||
|     private override init(engineId: Int, adType: AdType) { | ||||
|         super.init(engineId: engineId, adType: adType) | ||||
|     } | ||||
|      | ||||
|     public static func banner(engineId: Int) -> InvalidFusionAd { | ||||
|         return InvalidFusionAd(engineId: engineId, adType: AdType.Banner) | ||||
|     } | ||||
|      | ||||
|     public static func interstitial(engineId: Int) -> InvalidFusionAd { | ||||
|         return InvalidFusionAd(engineId: engineId, adType: AdType.Interstitial) | ||||
|     } | ||||
|      | ||||
|     public static func rewarded(engineId: Int) -> InvalidFusionAd { | ||||
|         return InvalidFusionAd(engineId: engineId, adType: AdType.Rewarded) | ||||
|     } | ||||
|      | ||||
|     public static func mRec(engineId: Int) -> InvalidFusionAd { | ||||
|         return InvalidFusionAd(engineId: engineId, adType: AdType.MRec) | ||||
|     } | ||||
| } | ||||
|  | @ -0,0 +1,361 @@ | |||
| // AdsEngineManager.swift | ||||
| // Manages all ad engines | ||||
| // Corresponds to AdsEngineManager.kt in Android implementation | ||||
| 
 | ||||
| import Foundation | ||||
| import UIKit | ||||
| 
 | ||||
| class AdsEngineManager { | ||||
|      | ||||
|     private let viewController: UIViewController | ||||
|      | ||||
|     private var interstitialAdEngines: [Int: InterstitialAdEngine] = [:] | ||||
|     private var rewardedAdEngines: [Int: RewardedAdEngine] = [:] | ||||
|     private var bannerAdEngines: [Int: BannerAdEngine] = [:] | ||||
|     private var mrecAdEngines: [Int: MRecAdEngine] = [:] | ||||
|      | ||||
|     private var supportedAdsSdk: [AdPlatform: GuruAdsSdk] = [:] | ||||
|      | ||||
|     init(viewController: UIViewController) { | ||||
|         self.viewController = viewController | ||||
|     } | ||||
|      | ||||
|     // MARK: - Logging | ||||
|      | ||||
|     private func logInfo(_ message: String) { | ||||
|         Logger.i(tag: "StrategyManager", message: message) | ||||
|     } | ||||
|      | ||||
|     private func logError(_ message: String, error: Error? = nil) { | ||||
|         Logger.e(tag: "StrategyManager", message: message) | ||||
|     } | ||||
|      | ||||
|     // MARK: - SDK Management | ||||
|      | ||||
|     internal func addAdsSdk(_ adsSdk: GuruAdsSdk) { | ||||
|         supportedAdsSdk[adsSdk.adPlatform] = adsSdk | ||||
|     } | ||||
|      | ||||
|     private func getAdsSdk(adPlatform: AdPlatform) -> GuruAdsSdk? { | ||||
|         return supportedAdsSdk[adPlatform] | ||||
|     } | ||||
|      | ||||
|     // MARK: - Interstitial Engine Methods | ||||
|      | ||||
|     private func createDefaultInterstitialAdEngine( | ||||
|         id: Int, | ||||
|         config: DefaultInterstitialConfig | ||||
|     ) -> DefaultInterstitialAdEngine? { | ||||
|         if let adSdk = getAdsSdk(adPlatform: AdPlatform.fromName(config.adPlatform))?.obtainInterstitialAd( | ||||
|             adConfig: AdConfig( | ||||
|                 engineId: id, | ||||
|                 adUnitId: config.adUnitId, | ||||
|                 adAmazonSlotId: config.adAmazonSlotId | ||||
|             ) | ||||
|         ) { | ||||
|            return DefaultInterstitialAdEngine(viewController: viewController, id: id, ad: adSdk) | ||||
|         } else { | ||||
|             return nil; | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     private func createMabInterstitialEngine( | ||||
|         id: Int, | ||||
|         faUnitId: String, | ||||
|         config: MabInterstitialConfig | ||||
|     ) -> MabInterstitialEngine { | ||||
|         return MabInterstitialEngine(viewController: viewController, id: id, faUnitId: faUnitId, mabConfig: config) { | ||||
|             [weak self] adPlatform, adConfig in | ||||
|             self?.getAdsSdk(adPlatform: adPlatform)?.obtainInterstitialAd(adConfig: adConfig) | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     func getInterstitialAdEngine(id: Int) -> InterstitialAdEngine? { | ||||
|         return interstitialAdEngines[id] | ||||
|     } | ||||
|      | ||||
|     func createInterstitialAdEngine(adEngineConfig: AdEngineConfig) -> InterstitialAdEngine? { | ||||
|         // Potential hashcode collision | ||||
|         let id = adEngineConfig.hashCode() | ||||
|         logInfo("createInterstitialAdEngine: \(id)") | ||||
|      | ||||
|          | ||||
|         let engine:InterstitialAdEngine? = getInterstitialAdEngine(id: id) ?? { | ||||
|             logInfo("config parsed!, with strategy \(InterstitialStrategy.default.rawValue)") | ||||
|             switch adEngineConfig.strategy { | ||||
|             case InterstitialStrategy.default.rawValue: | ||||
|                 guard let config = try? DefaultInterstitialConfig(adEngineConfig.config) else { | ||||
|                     return nil; | ||||
|                 } | ||||
|                 return createDefaultInterstitialAdEngine( | ||||
|                     id: id, | ||||
|                     config: config | ||||
|                 ) | ||||
|             case InterstitialStrategy.mab.rawValue: | ||||
|                 guard let config = try? MabInterstitialConfig(adEngineConfig.config) else { | ||||
|                     return nil; | ||||
|                 } | ||||
|                 return createMabInterstitialEngine(id: id, faUnitId: adEngineConfig.faUnitId, config: config) | ||||
|             default: | ||||
|                 // todo forward compatible | ||||
|                 guard let config = try? DefaultInterstitialConfig(adEngineConfig.config) else { | ||||
|                     return nil; | ||||
|                 } | ||||
|                 return createDefaultInterstitialAdEngine( | ||||
|                     id: id, | ||||
|                     config:config | ||||
|                 ) | ||||
|             } | ||||
|         }() | ||||
|          | ||||
|         if let engine = engine { | ||||
|             interstitialAdEngines[id] = engine | ||||
|         } | ||||
|          | ||||
|         return engine | ||||
|     } | ||||
|      | ||||
|     // MARK: - Rewarded Engine Methods | ||||
|      | ||||
|     private func createDefaultRewardedAdEngine( | ||||
|         id: Int, | ||||
|         config: DefaultRewardedConfig | ||||
|     ) -> DefaultRewardedAdEngine? { | ||||
|         if let adSdk = getAdsSdk(adPlatform: AdPlatform.fromName(config.adPlatform))?.obtainRewardedAd( | ||||
|             adConfig: AdConfig( | ||||
|                 engineId: id, | ||||
|                 adUnitId: config.adUnitId, | ||||
|                 adAmazonSlotId: config.adAmazonSlotId | ||||
|             ) | ||||
|         ) { | ||||
|            return DefaultRewardedAdEngine(viewController: viewController, id: id, ad: adSdk) | ||||
|         } else { | ||||
|             return nil; | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     private func createMabRewardedEngine( | ||||
|         id: Int, | ||||
|         faUnitId: String, | ||||
|         config: MabRewardedConfig | ||||
|     ) -> MabRewardedEngine { | ||||
|         return MabRewardedEngine(viewController: viewController, id: id, faUnitId: faUnitId, mabConfig: config) { | ||||
|             [weak self] adPlatform, adConfig in | ||||
|             self?.getAdsSdk(adPlatform: adPlatform)?.obtainRewardedAd(adConfig: adConfig) | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     func getRewardedAdEngine(id: Int) -> RewardedAdEngine? { | ||||
|         return rewardedAdEngines[id] | ||||
|     } | ||||
|      | ||||
|     func createRewardedAdEngine(adEngineConfig: AdEngineConfig) -> RewardedAdEngine? { | ||||
|         let id = adEngineConfig.hashCode() | ||||
|         logInfo("createRewardedAdEngine: \(id)") | ||||
|          | ||||
|         let engine = getRewardedAdEngine(id: id) ?? { | ||||
|             switch adEngineConfig.strategy { | ||||
|             case RewardedStrategy.default.rawValue: | ||||
|                 guard let config = try? DefaultRewardedConfig(adEngineConfig.config) else { | ||||
|                     return nil; | ||||
|                 } | ||||
|                 return createDefaultRewardedAdEngine( | ||||
|                     id: id, | ||||
|                     config: config | ||||
|                 ) | ||||
|             case RewardedStrategy.mab.rawValue: | ||||
|                 guard let config = try? MabRewardedConfig(adEngineConfig.config) else { | ||||
|                     return nil; | ||||
|                 } | ||||
|                 return createMabRewardedEngine(id: id, faUnitId: adEngineConfig.faUnitId, config: config) | ||||
|                  | ||||
|             default: | ||||
|                 guard let config = try? DefaultRewardedConfig(adEngineConfig.config) else { | ||||
|                     return nil; | ||||
|                 } | ||||
|                 return createDefaultRewardedAdEngine( | ||||
|                     id: id, | ||||
|                     config: config | ||||
|                 ) | ||||
|             } | ||||
|         }() | ||||
|          | ||||
|         if let engine = engine { | ||||
|             rewardedAdEngines[id] = engine | ||||
|         } | ||||
|          | ||||
|         return engine | ||||
|     } | ||||
|      | ||||
|     // MARK: - Banner Engine Methods | ||||
|      | ||||
|     private func createDefaultBannerAdEngine( | ||||
|         id: Int, | ||||
|         config: DefaultBannerConfig | ||||
|     ) -> DefaultBannerAdEngine? { | ||||
|         if let adSdk = getAdsSdk(adPlatform: AdPlatform.fromName(config.adPlatform))?.obtainBannerAd( | ||||
|             adConfig: AdConfig( | ||||
|                 engineId: id, | ||||
|                 adUnitId: config.adUnitId, | ||||
|                 adAmazonSlotId: config.adAmazonSlotId | ||||
|             ) | ||||
|         ) { | ||||
|            return DefaultBannerAdEngine(viewController: viewController, id: id, ad: adSdk) | ||||
|         } else { | ||||
|             return nil; | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     func getBannerAdEngine(id: Int) -> BannerAdEngine? { | ||||
|         return bannerAdEngines[id] | ||||
|     } | ||||
|      | ||||
|     func createBannerAdEngine(adEngineConfig: AdEngineConfig) -> BannerAdEngine? { | ||||
|         let id = adEngineConfig.hashCode() | ||||
|         logInfo("createBannerAdEngine: \(id)") | ||||
|          | ||||
|         let engine = getBannerAdEngine(id: id) ?? { | ||||
|             switch adEngineConfig.strategy { | ||||
|             case BannerStrategy.default.rawValue: | ||||
|                 guard let config = try? DefaultBannerConfig(adEngineConfig.config) else { | ||||
|                     return nil; | ||||
|                 } | ||||
|                 return createDefaultBannerAdEngine( | ||||
|                     id: id, | ||||
|                     config: config | ||||
|                 ) | ||||
|                  | ||||
|             default: | ||||
|                 guard let config = try? DefaultBannerConfig(adEngineConfig.config) else { | ||||
|                     return nil; | ||||
|                 } | ||||
|                 return createDefaultBannerAdEngine( | ||||
|                     id: id, | ||||
|                     config: config | ||||
|                 ) | ||||
|             } | ||||
|         }() | ||||
|          | ||||
|         if let engine = engine { | ||||
|             bannerAdEngines[id] = engine | ||||
|         } | ||||
|          | ||||
|         return engine | ||||
|     } | ||||
|      | ||||
|     // MARK: - MRec Engine Methods | ||||
|      | ||||
|     private func createDefaultMRecAdEngine( | ||||
|         id: Int, | ||||
|         config: DefaultMRecConfig | ||||
|     ) -> DefaultMRecAdEngine? { | ||||
|         if let adSdk = getAdsSdk(adPlatform: AdPlatform.fromName(config.adPlatform))?.obtainMRecAd( | ||||
|             adConfig: AdConfig( | ||||
|                 engineId: id, | ||||
|                 adUnitId: config.adUnitId, | ||||
|                 adAmazonSlotId: config.adAmazonSlotId | ||||
|             ) | ||||
|         ) { | ||||
|            return DefaultMRecAdEngine(viewController: viewController, id: id, ad: adSdk) | ||||
|         } else { | ||||
|             return nil; | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     func getMRecAdEngine(id: Int) -> MRecAdEngine? { | ||||
|         return mrecAdEngines[id] | ||||
|     } | ||||
|      | ||||
|     func createMRecAdEngine(adEngineConfig: AdEngineConfig) -> MRecAdEngine? { | ||||
|         let id = adEngineConfig.hashCode() | ||||
|         logInfo("createMRecAdEngine: \(id)") | ||||
|          | ||||
|         let engine = getMRecAdEngine(id: id) ?? { | ||||
|             switch adEngineConfig.strategy { | ||||
|             case BannerStrategy.default.rawValue: | ||||
|                 guard let config = try? DefaultMRecConfig(adEngineConfig.config) else { | ||||
|                     return nil; | ||||
|                 } | ||||
|                 return createDefaultMRecAdEngine( | ||||
|                     id: id, | ||||
|                     config: config | ||||
|                 ) | ||||
|                  | ||||
|             default: | ||||
|                 guard let config = try? DefaultMRecConfig(adEngineConfig.config) else { | ||||
|                     return nil; | ||||
|                 } | ||||
|                 return createDefaultMRecAdEngine( | ||||
|                     id: id, | ||||
|                     config: config | ||||
|                 ) | ||||
|             } | ||||
|         }() | ||||
|          | ||||
|         if let engine = engine { | ||||
|             mrecAdEngines[id] = engine | ||||
|         } | ||||
|          | ||||
|         return engine | ||||
|     } | ||||
|      | ||||
|     func dispatchOrientationUpdate(orientation: ScreenOrientation) { | ||||
|         let body: (Any) throws -> Void = { it in | ||||
|             (it as? OrientationAware)?.onOrientationUpdate(orientation: orientation) | ||||
|         } | ||||
|         try? interstitialAdEngines.values.forEach(body) | ||||
|         try? bannerAdEngines.values.forEach(body) | ||||
|         try? rewardedAdEngines.values.forEach(body) | ||||
|         try? mrecAdEngines.values.forEach(body) | ||||
|     } | ||||
|      | ||||
|     // for testing only! | ||||
|     func clearCachedEngines() { | ||||
|         interstitialAdEngines.removeAll() | ||||
|         bannerAdEngines.removeAll() | ||||
|         rewardedAdEngines.removeAll() | ||||
|         mrecAdEngines.removeAll() | ||||
|     } | ||||
|      | ||||
| } | ||||
| 
 | ||||
| // MARK: - Hashable Extension for AdEngineConfig | ||||
| 
 | ||||
| extension AdEngineConfig: Hashable { | ||||
|     func hashCode() -> Int { | ||||
|         return hashValue | ||||
|     } | ||||
|     public static func == (lhs: AdEngineConfig, rhs: AdEngineConfig) -> Bool { | ||||
|         return lhs.strategy == rhs.strategy | ||||
|         && (lhs.config as NSDictionary).isEqual(to: rhs.config) | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
|     public func hash(into hasher: inout Hasher) { | ||||
|         hasher.combine(strategy) | ||||
|         for (key, value) in config { | ||||
|             hasher.combine(key) | ||||
|             if let hashableValue = value as? (any Hashable) { | ||||
|                 hasher.combine(hashableValue) | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| // MARK: - Hashable Extensions | ||||
| 
 | ||||
| extension Int { | ||||
|     func hashCode() -> Int { | ||||
|         return self | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| extension String { | ||||
|     func hashCode() -> Int { | ||||
|         var h: Int = 0 | ||||
|         for c in self.unicodeScalars { | ||||
|             h = 31 &* h &+ Int(c.value) | ||||
|         } | ||||
|         return h | ||||
|     } | ||||
| } | ||||
|  | @ -0,0 +1,101 @@ | |||
| // RelayAdEngine.swift | ||||
| // Base class for all ad engines with state machine | ||||
| // Corresponds to RelayAdEngine.kt in Android implementation | ||||
| 
 | ||||
| import Foundation | ||||
| import UIKit | ||||
| 
 | ||||
| public struct EventParams { | ||||
|     public let ad: FusionAd? | ||||
|     public let reward: FusionReward? | ||||
|     public let displayFailedInfo: FusionError? | ||||
|     public let loadFailedInfo: LoadFailedInfo? | ||||
|      | ||||
|     public let id: Int | ||||
|      | ||||
|     public init( | ||||
|         ad: FusionAd? = nil, | ||||
|         reward: FusionReward? = nil, | ||||
|         displayFailedInfo: FusionError? = nil, | ||||
|         loadFailedInfo: LoadFailedInfo? = nil, | ||||
|         id: Int = -1 | ||||
|     ) { | ||||
|         self.ad = ad | ||||
|         self.reward = reward | ||||
|         self.displayFailedInfo = displayFailedInfo | ||||
|         self.loadFailedInfo = loadFailedInfo | ||||
|         self.id = id | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| protocol OrientationAware { | ||||
|     func onOrientationUpdate(orientation: ScreenOrientation) | ||||
| } | ||||
| 
 | ||||
| public class RelayAdEngine: StateMachine { | ||||
|     public let id: Int | ||||
|     public let adType: AdType | ||||
|     public var listener: FusionAdListener? | ||||
|      | ||||
|     public var supportedAdPlatforms: Set<AdPlatform> { | ||||
|         fatalError("Subclass must implement") | ||||
|     } | ||||
|      | ||||
|     public init(viewController: UIViewController, id: Int, adType: AdType, strategyName: String) { | ||||
|         self.id = id | ||||
|         self.adType = adType | ||||
|         super.init(name: "\(adType.name)-\(strategyName)-\(id)") | ||||
|     } | ||||
|      | ||||
|     // MARK: - Logging Methods | ||||
|      | ||||
|     public func logDebug(_ message: String) { | ||||
|         Logger.d(tag: name, message: message) | ||||
|     } | ||||
|      | ||||
|     public func logInfo(_ message: String) { | ||||
|         Logger.i(tag: name, message: message) | ||||
|     } | ||||
|      | ||||
|     public func logWarn(_ message: String) { | ||||
|         Logger.w(tag: name, message: message) | ||||
|     } | ||||
|      | ||||
|     public func logError(_ message: String) { | ||||
|         Logger.e(tag: name, message: message) | ||||
|     } | ||||
|      | ||||
|     // MARK: - Listener Callback Methods | ||||
|      | ||||
|     public func adLoaded(ad: FusionAd) { | ||||
|         listener?.onAdLoaded(ad: ad) | ||||
|     } | ||||
|      | ||||
|     public func adDisplayed(ad: FusionAd) { | ||||
|         listener?.onAdDisplayed(ad: ad) | ||||
|     } | ||||
|      | ||||
|     public func adHidden(ad: FusionAd) { | ||||
|         listener?.onAdHidden(ad: ad) | ||||
|     } | ||||
|      | ||||
|     public func adClicked(ad: FusionAd) { | ||||
|         listener?.onAdClicked(ad: ad) | ||||
|     } | ||||
|      | ||||
|     public func adLoadFailed(loadFailedInfo: LoadFailedInfo) { | ||||
|         listener?.onAdLoadFailed(loadFailedInfo: loadFailedInfo) | ||||
|     } | ||||
|      | ||||
|     public func adDisplayFailed(ad: FusionAd, error: FusionError?) { | ||||
|         listener?.onAdDisplayFailed(ad: ad, error: error) | ||||
|     } | ||||
|      | ||||
|     public func adRevenuePaid(ad: FusionAd) { | ||||
|         listener?.onAdRevenuePaid(ad: ad) | ||||
|     } | ||||
|      | ||||
|     public func adUserRewarded(ad: FusionAd, reward: FusionReward?) { | ||||
|         listener?.onUserRewarded(ad: ad, reward: reward) | ||||
|     } | ||||
| } | ||||
|  | @ -0,0 +1,299 @@ | |||
| // StrategyConfig.swift | ||||
| // Defines configuration structures for ad strategies | ||||
| // Corresponds to StrategyConfig.kt in Android implementation | ||||
| 
 | ||||
| import Foundation | ||||
| 
 | ||||
| // MARK: - Strategy Types | ||||
| 
 | ||||
| public enum InterstitialStrategy: Int { | ||||
|     case `default` = 0 | ||||
|     case mab = 1 | ||||
| } | ||||
| 
 | ||||
| public enum RewardedStrategy: Int { | ||||
|     case `default` = 0 | ||||
|     case mab = 1 | ||||
| } | ||||
| 
 | ||||
| public enum BannerStrategy: Int { | ||||
|     case `default` = 0 | ||||
| } | ||||
| 
 | ||||
| protocol StrategyConfig: Decodable {} | ||||
| 
 | ||||
| // MARK: - Default Config Classes | ||||
| 
 | ||||
| public struct DefaultInterstitialConfig: Codable, StrategyConfig { | ||||
|     public let adPlatform: String | ||||
|     public let adUnitId: String | ||||
|     public let adAmazonSlotId: String? | ||||
|      | ||||
|     public init(adPlatform: String, adUnitId: String, adAmazonSlotId: String? = nil) { | ||||
|         self.adPlatform = adPlatform | ||||
|         self.adUnitId = adUnitId | ||||
|         self.adAmazonSlotId = adAmazonSlotId | ||||
|     } | ||||
|      | ||||
|     private enum CodingKeys : String, CodingKey { | ||||
|         case adPlatform = "ad_platform", | ||||
|              adUnitId = "ad_unit_id", | ||||
|              adAmazonSlotId = "ad_amz_slot_id" | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| public struct DefaultRewardedConfig: Codable, StrategyConfig { | ||||
|     public let adPlatform: String | ||||
|     public let adUnitId: String | ||||
|     public let adAmazonSlotId: String? | ||||
|      | ||||
|     public init(adPlatform: String, adUnitId: String, adAmazonSlotId: String? = nil) { | ||||
|         self.adPlatform = adPlatform | ||||
|         self.adUnitId = adUnitId | ||||
|         self.adAmazonSlotId = adAmazonSlotId | ||||
|     } | ||||
|      | ||||
|     private enum CodingKeys : String, CodingKey { | ||||
|         case adPlatform = "ad_platform", | ||||
|              adUnitId = "ad_unit_id", | ||||
|              adAmazonSlotId = "ad_amz_slot_id" | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| public struct DefaultBannerConfig: Codable, StrategyConfig { | ||||
|     public let adPlatform: String | ||||
|     public let adUnitId: String | ||||
|     public let adAmazonSlotId: String? | ||||
|      | ||||
|     public init(adPlatform: String, adUnitId: String, adAmazonSlotId: String? = nil) { | ||||
|         self.adPlatform = adPlatform | ||||
|         self.adUnitId = adUnitId | ||||
|         self.adAmazonSlotId = adAmazonSlotId | ||||
|     } | ||||
|      | ||||
|     private enum CodingKeys : String, CodingKey { | ||||
|         case adPlatform = "ad_platform", | ||||
|              adUnitId = "ad_unit_id", | ||||
|              adAmazonSlotId = "ad_amz_slot_id" | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| public struct DefaultMRecConfig: Codable, StrategyConfig { | ||||
|     public let adPlatform: String | ||||
|     public let adUnitId: String | ||||
|     public let adAmazonSlotId: String? | ||||
|      | ||||
|     public init(adPlatform: String, adUnitId: String, adAmazonSlotId: String? = nil) { | ||||
|         self.adPlatform = adPlatform | ||||
|         self.adUnitId = adUnitId | ||||
|         self.adAmazonSlotId = adAmazonSlotId | ||||
|     } | ||||
|      | ||||
|     private enum CodingKeys : String, CodingKey { | ||||
|         case adPlatform = "ad_platform", | ||||
|              adUnitId = "ad_unit_id", | ||||
|              adAmazonSlotId = "ad_amz_slot_id" | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| public struct MabRewardedConfig: Codable, StrategyConfig { | ||||
|     public let aggregatorConfigs: [AggregatorConfig] | ||||
|      | ||||
|     public let showTimeoutInSecond: Double? | ||||
|      | ||||
|     init(aggregatorConfigs: [AggregatorConfig], showTimeoutInSecond: Double?) { | ||||
|         self.aggregatorConfigs = aggregatorConfigs | ||||
|         self.showTimeoutInSecond = showTimeoutInSecond | ||||
|     } | ||||
|      | ||||
|     private enum CodingKeys : String, CodingKey { | ||||
|         case aggregatorConfigs = "aggregator_configs", | ||||
|              showTimeoutInSecond = "show_timeout_second" | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| public struct MabInterstitialConfig: Codable, StrategyConfig { | ||||
|     public let aggregatorConfigs: [AggregatorConfig] | ||||
|      | ||||
|     public let showTimeoutInSecond: Double? | ||||
|      | ||||
|     init(aggregatorConfigs: [AggregatorConfig], showTimeoutInSecond: Double?) { | ||||
|         self.aggregatorConfigs = aggregatorConfigs | ||||
|         self.showTimeoutInSecond = showTimeoutInSecond | ||||
|     } | ||||
|      | ||||
|     private enum CodingKeys : String, CodingKey { | ||||
|         case aggregatorConfigs = "aggregator_configs", | ||||
|              showTimeoutInSecond = "show_timeout_second" | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| public struct AggregatorConfig: Codable { | ||||
|     public let aggregatorId: String | ||||
|     public let adPlatform: String | ||||
|     public let raiseRate: Double | ||||
|     public let selfRaiseRate: Double? | ||||
|     public let cacheExpireInSecond: Double | ||||
|     public let priority: Int | ||||
|     public let adUnitIds: [AdUnitSpec] | ||||
|     public let loadTimeoutInSecond: Double? | ||||
|      | ||||
|     init(aggregatorId: String, adPlatform: String, raiseRate: Double, selfRaiseRate: Double?, cacheExpireInSecond: Double, priority: Int, adUnitIds: [AdUnitSpec], loadTimeoutInSecond: Double?) { | ||||
|         self.aggregatorId = aggregatorId | ||||
|         self.adPlatform = adPlatform | ||||
|         self.raiseRate = raiseRate | ||||
|         self.selfRaiseRate = selfRaiseRate | ||||
|         self.cacheExpireInSecond = cacheExpireInSecond | ||||
|         self.priority = priority | ||||
|         self.adUnitIds = adUnitIds | ||||
|         self.loadTimeoutInSecond = loadTimeoutInSecond | ||||
|     } | ||||
|      | ||||
|     private enum CodingKeys : String, CodingKey { | ||||
|         case aggregatorId = "aggregator_id", | ||||
|              adPlatform = "ad_platform", | ||||
|              raiseRate = "raise_rate", | ||||
|              selfRaiseRate = "self_raise_rate", | ||||
|              cacheExpireInSecond = "cache_expire_second", | ||||
|              priority = "priority", | ||||
|              adUnitIds = "ad_unit_ids", | ||||
|              loadTimeoutInSecond = "load_timeout_second" | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| public struct AdUnitSpec: Codable { | ||||
|     public let adUnitId: String | ||||
|     public let adAmazonSlotId: String? | ||||
|      | ||||
|     public let floorEcpm: Double | ||||
|      | ||||
|     public var floor: Double { | ||||
|         return floorEcpm / 1000 | ||||
|     } | ||||
|      | ||||
|     init(adUnitId: String, adAmazonSlotId: String?, floorEcpm: Double = 0.0) { | ||||
|         self.adUnitId = adUnitId | ||||
|         self.adAmazonSlotId = adAmazonSlotId | ||||
|         self.floorEcpm = floorEcpm | ||||
|     } | ||||
|      | ||||
|     private enum CodingKeys : String, CodingKey { | ||||
|         case adUnitId = "ad_unit_id", | ||||
|              adAmazonSlotId = "ad_amz_slot_id", | ||||
|              floorEcpm = "floor_ecpm" | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| // MARK: - AdEngineConfig | ||||
| 
 | ||||
| 
 | ||||
| public struct AdEngineConfig: Codable { | ||||
|     public let strategy: Int | ||||
|     public let faUnitId: String | ||||
|     public let config: [String: Any] | ||||
|      | ||||
|     public init(strategy: Int, faUnitId: String, config: [String: Any]) { | ||||
|         self.strategy = strategy | ||||
|         self.faUnitId = faUnitId | ||||
|         self.config = config | ||||
|     } | ||||
|      | ||||
|     public func encode(to encoder: Encoder) throws { | ||||
|         var container = encoder.container(keyedBy: CodingKeys.self) | ||||
|         try container.encode(strategy, forKey: .strategy) | ||||
|         try container.encode(faUnitId, forKey: .faUnitId) | ||||
|         try container.encode(JSONCodable(config), forKey: .config) | ||||
|     } | ||||
|      | ||||
|     public init(from decoder: Decoder) throws { | ||||
|         let container = try decoder.container(keyedBy: CodingKeys.self) | ||||
|         strategy = try container.decode(Int.self, forKey: .strategy) | ||||
|         faUnitId = try container.decode(String?.self, forKey: .faUnitId) ?? "default" | ||||
|         config = try container.decode(JSONCodable.self, forKey: .config).value as! Dictionary | ||||
|     } | ||||
|      | ||||
|     private enum CodingKeys: String, CodingKey { | ||||
|         case strategy = "strategy", | ||||
|              config = "config", | ||||
|             faUnitId = "fa_unit_id" | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
| private struct JSONCodable: Codable { | ||||
|     let value: Any | ||||
|      | ||||
|     init(_ value: Any) { | ||||
|         self.value = value | ||||
|     } | ||||
|      | ||||
|     init(from decoder: Decoder) throws { | ||||
|         let container = try decoder.singleValueContainer() | ||||
|          | ||||
|         if container.decodeNil() { | ||||
|             self.value = NSNull() | ||||
|         } else if let bool = try? container.decode(Bool.self) { | ||||
|             self.value = bool | ||||
|         } else if let int = try? container.decode(Int.self) { | ||||
|             self.value = int | ||||
|         } else if let uint = try? container.decode(UInt.self) { | ||||
|             self.value = uint | ||||
|         } else if let double = try? container.decode(Double.self) { | ||||
|             self.value = double | ||||
|         } else if let string = try? container.decode(String.self) { | ||||
|             self.value = string | ||||
|         } else if let array = try? container.decode([JSONCodable].self) { | ||||
|             self.value = array.map { $0.value } | ||||
|         } else if let dictionary = try? container.decode([String: JSONCodable].self) { | ||||
|             self.value = dictionary.mapValues { $0.value } | ||||
|         } else { | ||||
|             throw DecodingError.dataCorruptedError( | ||||
|                 in: container, | ||||
|                 debugDescription: "AnyCodable cannot decode value" | ||||
|             ) | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     func encode(to encoder: Encoder) throws { | ||||
|         var container = encoder.singleValueContainer() | ||||
|          | ||||
|         switch self.value { | ||||
|         case is NSNull: | ||||
|             try container.encodeNil() | ||||
|         case let bool as Bool: | ||||
|             try container.encode(bool) | ||||
|         case let int as Int: | ||||
|             try container.encode(int) | ||||
|         case let uint as UInt: | ||||
|             try container.encode(uint) | ||||
|         case let double as Double: | ||||
|             try container.encode(double) | ||||
|         case let string as String: | ||||
|             try container.encode(string) | ||||
|         case let array as [Any]: | ||||
|             try container.encode(array.map { JSONCodable($0) }) | ||||
|         case let dictionary as [String: Any]: | ||||
|             try container.encode(dictionary.mapValues { JSONCodable($0) }) | ||||
|         default: | ||||
|             let context = EncodingError.Context( | ||||
|                 codingPath: container.codingPath, | ||||
|                 debugDescription: "AnyCodable cannot encode value \(self.value)" | ||||
|             ) | ||||
|             throw EncodingError.invalidValue(self.value, context) | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| extension StrategyConfig | ||||
| { | ||||
|     init<Key: Hashable>(_ dict: [Key: Any]) throws | ||||
|     { | ||||
|         do { | ||||
|             let data = try JSONSerialization.data(withJSONObject: dict, options: []) | ||||
|             self = try JSONDecoder().decode(Self.self, from: data) | ||||
|         } catch { | ||||
|             Logger.e(tag: "StrategyConfig", message: "strategy config deserialize to \(Self.self) fail, data is \(dict)") | ||||
|             throw error; | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | @ -0,0 +1,170 @@ | |||
| // BannerAdEngine.swift | ||||
| // Base class for banner ad engines | ||||
| // Corresponds to BannerAdEngine.kt in Android implementation | ||||
| 
 | ||||
| import Foundation | ||||
| import UIKit | ||||
| 
 | ||||
| public class BannerAdEngine: RelayAdEngine { | ||||
|      | ||||
|     private(set) var idleState: Idle! | ||||
|     private(set) var loadingState: Loading! | ||||
|     private(set) var activeState: Active! | ||||
|     private(set) var showingState: Showing! | ||||
|     public private(set) var hiddenState: Hidden! | ||||
|      | ||||
|     public var currentStateIdentifier: AdState.Identifier { | ||||
|         return (currentState as? AdState)?.stateId ?? AdState.Identifier(name: currentState?.name ?? "unknown") | ||||
|     } | ||||
|      | ||||
|     public override init(viewController: UIViewController, id: Int, adType: AdType, strategyName: String) { | ||||
|         super.init(viewController: viewController, id: id, adType: adType, strategyName: strategyName) | ||||
|     } | ||||
|      | ||||
|     public func initialize() { | ||||
|         idleState = createIdleState() | ||||
|         loadingState = createLoadingState() | ||||
|         activeState = createActiveState() | ||||
|         showingState = createShowingState() | ||||
|         hiddenState = createHiddenState() | ||||
|          | ||||
|         addState(idleState) | ||||
|         addState(loadingState) | ||||
|         addState(activeState) | ||||
|         addState(showingState, parent: activeState) | ||||
|         addState(hiddenState, parent: activeState) | ||||
|         setInitialState(idleState) | ||||
|         start() | ||||
|     } | ||||
|      | ||||
|     // Methods to be overridden by subclasses | ||||
|     public class AdState: State { | ||||
|         public let stateId: Identifier | ||||
|          | ||||
|         public init(name: String, stateId: Identifier) { | ||||
|             self.stateId = stateId | ||||
|             super.init() | ||||
|         } | ||||
|          | ||||
|         public override var name: String { | ||||
|             return stateId.name | ||||
|         } | ||||
|          | ||||
|         public class Identifier: Equatable { | ||||
|             public let name: String | ||||
|              | ||||
|             public init(name: String) { | ||||
|                 self.name = name | ||||
|             } | ||||
|              | ||||
|             // Pre-defined identifiers | ||||
|             public static let IDLE = Identifier(name: "idle") | ||||
|             public static let LOADING = Identifier(name: "loading") | ||||
|             public static let ACTIVE = Identifier(name: "active") | ||||
|             public static let SHOWING = Identifier(name: "showing") | ||||
|             public static let HIDDEN = Identifier(name: "hidden") | ||||
|              | ||||
|             public static func == (lhs: Identifier, rhs: Identifier) -> Bool { | ||||
|                 return lhs.name == rhs.name | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     public class Idle: AdState { | ||||
|         public init() { | ||||
|             super.init(name: "[IDLE]", stateId: Identifier.IDLE) | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     public class Loading: AdState { | ||||
|         public init() { | ||||
|             super.init(name: "[LOADING]", stateId: Identifier.LOADING) | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     public class Active: AdState { | ||||
|         public init() { | ||||
|             super.init(name: "[ACTIVE]", stateId: Identifier.ACTIVE) | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     public class Showing: AdState { | ||||
|         public init() { | ||||
|             super.init(name: "[SHOWING]", stateId: Identifier.SHOWING) | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     public class Hidden: AdState { | ||||
|         public init() { | ||||
|             super.init(name: "[HIDDEN]", stateId: Identifier.HIDDEN) | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     // Factory methods for creating the states | ||||
|     open func createIdleState() -> Idle { | ||||
|         return Idle() | ||||
|     } | ||||
|      | ||||
|     open func createLoadingState() -> Loading { | ||||
|         return Loading() | ||||
|     } | ||||
|      | ||||
|     open func createActiveState() -> Active { | ||||
|         return Active() | ||||
|     } | ||||
|      | ||||
|     open func createShowingState() -> Showing { | ||||
|         return Showing() | ||||
|     } | ||||
|      | ||||
|     open func createHiddenState() -> Hidden { | ||||
|         return Hidden() | ||||
|     } | ||||
|      | ||||
|     // Abstract methods to be implemented by subclasses | ||||
|     internal func requestLoad() { | ||||
|         fatalError("Subclass must implement") | ||||
|     } | ||||
|      | ||||
|     internal func requestShow(_ request: BannerShowRequest? = nil) { | ||||
|         fatalError("Subclass must implement") | ||||
|     } | ||||
|      | ||||
|     internal func requestHide() { | ||||
|         fatalError("Subclass must implement") | ||||
|     } | ||||
|      | ||||
|     internal func requestDestroy() { | ||||
|         fatalError("Subclass must implement") | ||||
|     } | ||||
|      | ||||
|     // Public API methods | ||||
|     public func show(_ request: BannerShowRequest? = nil) -> Bool { | ||||
|         if isCurrent(activeState) || isCurrent(hiddenState) { | ||||
|             requestShow(request) | ||||
|             return true | ||||
|         } | ||||
|         return false | ||||
|     } | ||||
|      | ||||
|     public func hide() -> Bool { | ||||
|         if isCurrent(showingState) { | ||||
|             requestHide() | ||||
|             return true | ||||
|         } | ||||
|         return false | ||||
|     } | ||||
|      | ||||
|     public func load() -> Bool { | ||||
|         if isCurrent(idleState) { | ||||
|             requestLoad() | ||||
|             return true | ||||
|         } | ||||
|         return false | ||||
|     } | ||||
|      | ||||
|     public func destroy() -> Bool { | ||||
|         requestDestroy() | ||||
|         return true | ||||
|     } | ||||
| } | ||||
|  | @ -0,0 +1,428 @@ | |||
| // DefaultBannerAdEngine.swift | ||||
| // Default implementation of BannerAdEngine | ||||
| // Corresponds to DefaultBannerAdEngine.kt in Android implementation | ||||
| 
 | ||||
| import Foundation | ||||
| import UIKit | ||||
| 
 | ||||
| public class DefaultBannerAdEngine: BannerAdEngine, OrientationAware { | ||||
|      | ||||
|     private let ad: GuruBannerAd | ||||
|      | ||||
|     // Constants | ||||
|     private enum Action { | ||||
|         static let LOAD = 1 | ||||
|         static let SHOW = 2 | ||||
|         static let HIDE = 3 | ||||
|         static let DESTROY = 4 | ||||
|         static let RETRY = 5 | ||||
|     } | ||||
|      | ||||
|     private enum Event { | ||||
|         static let ON_LOADED = 100 | ||||
|         static let ON_LOAD_FAILED = 101 | ||||
|         static let ON_DISPLAYED = 102 | ||||
|         static let ON_DISPLAY_FAILED = 103 | ||||
|         static let ON_CLICK = 104 | ||||
|         static let ON_HIDDEN = 105 | ||||
|         static let ON_USER_REWARDED = 106 | ||||
|         static let REVENUE_PAID = 107 | ||||
|         static let LOAD_TIMEOUT = 108 | ||||
|         static let SHOW_TIMEOUT = 109 | ||||
|     } | ||||
|      | ||||
|     private static let LOAD_TIMEOUT_MILLIS: TimeInterval = 120000 / 1000.0 | ||||
|     private static let SHOW_TIMEOUT_MILLIS: TimeInterval = 120000 / 1000.0 | ||||
|      | ||||
|     public override var supportedAdPlatforms: Set<AdPlatform> { | ||||
|         return [ad.adPlatform] | ||||
|     } | ||||
|      | ||||
|     public init(viewController: UIViewController, id: Int, ad: GuruBannerAd) { | ||||
|         self.ad = ad | ||||
|         super.init(viewController: viewController, id: id, adType: .Banner, strategyName: ad.adPlatform.name) | ||||
|         setupAdListener() | ||||
|         initialize() | ||||
|     } | ||||
|      | ||||
|     private func setupAdListener() { | ||||
|         ad.listener = FusionAdListenerImpl { [weak self] event in | ||||
|             guard let self = self else { return } | ||||
|              | ||||
|             switch event { | ||||
|             case .onAdLoaded(let ad): | ||||
|                 self.sendMessage(what: Event.ON_LOADED, obj: EventParams(ad: ad)) | ||||
|                  | ||||
|             case .onAdDisplayed(let ad): | ||||
|                 // Not used for banner ads, but handled for completeness | ||||
|                 break | ||||
|                  | ||||
|             case .onAdHidden(let ad): | ||||
|                 // Not used for banner ads, but handled for completeness | ||||
|                 break | ||||
|                  | ||||
|             case .onUserRewarded(let ad, let reward): | ||||
|                 // Not used for banner ads, but handled for completeness | ||||
|                 break | ||||
|                  | ||||
|             case .onAdClicked(let ad): | ||||
|                 self.sendMessage(what: Event.ON_CLICK, obj: EventParams(ad: ad)) | ||||
|                  | ||||
|             case .onAdLoadFailed(let loadFailedInfo): | ||||
|                 self.sendMessage(what: Event.ON_LOAD_FAILED, obj: EventParams(loadFailedInfo: loadFailedInfo)) | ||||
|                  | ||||
|             case .onAdDisplayFailed(let ad, let error): | ||||
|                 self.sendMessage(what: Event.ON_DISPLAY_FAILED, obj: EventParams(ad: ad, displayFailedInfo: error)) | ||||
|                  | ||||
|             case .onAdRevenuePaid(let ad): | ||||
|                 self.sendMessage(what: Event.REVENUE_PAID, obj: EventParams(ad: ad)) | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     // MARK: - State Classes | ||||
|      | ||||
|     public override func createIdleState() -> Idle { | ||||
|         return IdleImpl(self) | ||||
|     } | ||||
|      | ||||
|     public override func createLoadingState() -> Loading { | ||||
|         return LoadingImpl(self) | ||||
|     } | ||||
|      | ||||
|     public override func createActiveState() -> Active { | ||||
|         return ActiveImpl(self) | ||||
|     } | ||||
|      | ||||
|     public override func createShowingState() -> Showing { | ||||
|         return ShowingImpl(self) | ||||
|     } | ||||
|      | ||||
|     public override func createHiddenState() -> Hidden { | ||||
|         return HiddenImpl(self) | ||||
|     } | ||||
|      | ||||
|     // MARK: - Request Methods | ||||
|      | ||||
|     override internal func requestLoad() { | ||||
|         sendMessage(what: Action.LOAD) | ||||
|     } | ||||
|      | ||||
|     override internal func requestShow(_ request: BannerShowRequest? = nil) { | ||||
|         sendMessage(what: Action.SHOW, obj: request) | ||||
|     } | ||||
|      | ||||
|     override internal func requestHide() { | ||||
|         sendMessage(what: Action.HIDE) | ||||
|     } | ||||
|      | ||||
|     override internal func requestDestroy() { | ||||
|         sendMessage(what: Action.DESTROY) | ||||
|     } | ||||
|      | ||||
|     // MARK: - Helper Methods | ||||
|      | ||||
|     private func handleUnhandledMessage(_ msg: Message) -> Bool { | ||||
|         switch msg.what { | ||||
|         case Action.DESTROY: | ||||
|             ad.destroy() | ||||
|             transitionTo(idleState) | ||||
|             return true | ||||
|         default: | ||||
|             return false | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     // MARK: - State Implementations | ||||
|      | ||||
|     class IdleImpl: Idle { | ||||
|         private let engine: DefaultBannerAdEngine | ||||
|          | ||||
|         init(_ engine: DefaultBannerAdEngine) { | ||||
|             self.engine = engine | ||||
|             super.init() | ||||
|         } | ||||
|         public override func enter(from: IState?, params: Any?) { | ||||
|             super.enter(from: from, params: params) | ||||
|         } | ||||
|          | ||||
|         public override func processMessage(_ msg: Message) -> Bool { | ||||
|              | ||||
|             switch msg.what { | ||||
|             case Action.LOAD: | ||||
|                 if engine.ad.load() { | ||||
|                     engine.transitionTo(engine.loadingState) | ||||
|                 } else { | ||||
|                     engine.adLoadFailed(loadFailedInfo: LoadFailedInfo( | ||||
|                         engineId: engine.id, | ||||
|                         adPlatform: engine.ad.adPlatform, | ||||
|                         adType: engine.adType, | ||||
|                         adUnitId: engine.ad.adUnitId, | ||||
|                         error: LoadAdRequestError() | ||||
|                     )) | ||||
|                 } | ||||
|                 return true | ||||
|             default: | ||||
|                 return engine.handleUnhandledMessage(msg) || super.processMessage(msg) | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     class LoadingImpl: Loading { | ||||
|         private var retryCount = 0 | ||||
|         private let engine: DefaultBannerAdEngine | ||||
|          | ||||
|         init(_ engine: DefaultBannerAdEngine) { | ||||
|             self.engine = engine | ||||
|             super.init() | ||||
|         } | ||||
|          | ||||
|         public override func enter(from: IState?, params: Any?) { | ||||
|             super.enter(from: from, params: params) | ||||
|             retryCount = 0 | ||||
|              | ||||
|             // Set load timeout | ||||
|             engine.sendMessageDelayed(what: Event.LOAD_TIMEOUT, delayMillis: Int(DefaultBannerAdEngine.LOAD_TIMEOUT_MILLIS * 1000)) | ||||
|         } | ||||
|          | ||||
|         public override func exit(to: IState?) { | ||||
|             engine.removeMessages(what: Event.LOAD_TIMEOUT) | ||||
|             engine.removeMessages(what: Action.RETRY) | ||||
|             retryCount = 0 | ||||
|             super.exit(to: to) | ||||
|         } | ||||
|          | ||||
|         public override func processMessage(_ msg: Message) -> Bool { | ||||
|              | ||||
|             switch msg.what { | ||||
|             case Action.RETRY: | ||||
|                 retryCount += 1 | ||||
|                 if !engine.ad.load() { | ||||
|                     engine.sendMessage(what: Event.ON_LOAD_FAILED, obj: EventParams( | ||||
|                         loadFailedInfo: LoadFailedInfo( | ||||
|                             engineId: engine.id, | ||||
|                             adPlatform: engine.ad.adPlatform, | ||||
|                             adType: engine.adType, | ||||
|                             adUnitId: engine.ad.adUnitId, | ||||
|                             error: LoadAdRequestError() | ||||
|                         ) | ||||
|                     )) | ||||
|                 } else { | ||||
|                     engine.sendMessageDelayed(what: Event.LOAD_TIMEOUT, delayMillis: Int(DefaultBannerAdEngine.LOAD_TIMEOUT_MILLIS * 1000)) | ||||
|                 } | ||||
|                 return true | ||||
|                  | ||||
|             case Action.LOAD: | ||||
|                 engine.logWarn("Already loading! Ignore load request") | ||||
|                 return true | ||||
|                  | ||||
|             case Event.LOAD_TIMEOUT: | ||||
|                 engine.removeMessages(what: Action.RETRY) | ||||
|                 engine.sendMessage(what: Action.RETRY) | ||||
|                 return true | ||||
|                  | ||||
|             case Event.ON_LOADED: | ||||
|                 if let eventParams = msg.obj as? EventParams, eventParams.ad != nil { | ||||
|                     engine.transitionTo(engine.hiddenState, params: eventParams) | ||||
|                 } | ||||
|                 engine.deferMessage(msg) | ||||
|                 return true | ||||
|                  | ||||
|             case Event.ON_LOAD_FAILED: | ||||
|                 let params = msg.obj as? EventParams | ||||
|                 engine.adLoadFailed(loadFailedInfo: params?.loadFailedInfo ?? LoadFailedInfo( | ||||
|                     engineId: engine.id, | ||||
|                     adPlatform: engine.ad.adPlatform, | ||||
|                     adType: engine.adType, | ||||
|                     adUnitId: engine.ad.adUnitId, | ||||
|                     error: LoadAdRequestError() | ||||
|                 )) | ||||
|                 engine.transitionTo(engine.idleState) | ||||
|                 return true | ||||
|                  | ||||
|             default: | ||||
|                 return engine.handleUnhandledMessage(msg) || super.processMessage(msg) | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     class ActiveImpl: Active { | ||||
|         private let engine: DefaultBannerAdEngine | ||||
|          | ||||
|         init(_ engine: DefaultBannerAdEngine) { | ||||
|             self.engine = engine | ||||
|             super.init() | ||||
|         } | ||||
|          | ||||
|          | ||||
|         public override func enter(from: IState?, params: Any?) { | ||||
|             super.enter(from: from, params: params) | ||||
|         } | ||||
|          | ||||
|         public override func processMessage(_ msg: Message) -> Bool { | ||||
|              | ||||
|             switch msg.what { | ||||
|             case Event.ON_CLICK: | ||||
|                 if let eventParams = msg.obj as? EventParams, let ad = eventParams.ad { | ||||
|                     engine.adClicked(ad: ad) | ||||
|                 } | ||||
|                 return true | ||||
|                  | ||||
|             case Event.REVENUE_PAID: | ||||
|                 if let eventParams = msg.obj as? EventParams, let ad = eventParams.ad { | ||||
|                     engine.adRevenuePaid(ad: ad) | ||||
|                 } | ||||
|                 return true | ||||
|                  | ||||
|             default: | ||||
|                 return engine.handleUnhandledMessage(msg) || super.processMessage(msg) | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     class ShowingImpl: Showing { | ||||
|         private let engine: DefaultBannerAdEngine | ||||
|          | ||||
|         init(_ engine: DefaultBannerAdEngine) { | ||||
|             self.engine = engine | ||||
|             super.init() | ||||
|         } | ||||
|          | ||||
|         public override func enter(from: IState?, params: Any?) { | ||||
|             super.enter(from: from, params: params) | ||||
|              | ||||
|             if let eventParams = params as? EventParams, let ad = eventParams.ad { | ||||
|                 engine.adDisplayed(ad: ad) | ||||
|             } | ||||
|         } | ||||
|          | ||||
|         public override func processMessage(_ msg: Message) -> Bool { | ||||
|              | ||||
|             switch msg.what { | ||||
|             case Action.HIDE: | ||||
|                 if engine.ad.hide() { | ||||
|                     engine.transitionTo(engine.hiddenState) | ||||
|                 } else { | ||||
|                     engine.logWarn("Failed to hide banner") | ||||
|                 } | ||||
|                 return true | ||||
|                  | ||||
|             case Action.SHOW: | ||||
|                 engine.logWarn("Already showing! Ignore show request") | ||||
|                 return true | ||||
|                  | ||||
|             case Event.ON_DISPLAY_FAILED: | ||||
|                 if let eventParams = msg.obj as? EventParams, let ad = eventParams.ad { | ||||
|                     engine.adDisplayFailed(ad: ad, error: eventParams.displayFailedInfo) | ||||
|                 } | ||||
|                 engine.transitionTo(engine.hiddenState) | ||||
|                 engine.requestLoad() | ||||
|                 return true | ||||
|                  | ||||
|             default: | ||||
|                 return engine.handleUnhandledMessage(msg) || super.processMessage(msg) | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     class HiddenImpl: Hidden { | ||||
|         private let engine: DefaultBannerAdEngine | ||||
|          | ||||
|         init(_ engine: DefaultBannerAdEngine) { | ||||
|             self.engine = engine | ||||
|             super.init() | ||||
|         } | ||||
|          | ||||
|         public override func enter(from: IState?, params: Any?) { | ||||
|             super.enter(from: from, params: params) | ||||
|              | ||||
|             if let eventParams = params as? EventParams, let ad = eventParams.ad { | ||||
|                 engine.adLoaded(ad: ad) | ||||
|             } | ||||
|         } | ||||
|          | ||||
|         public override func processMessage(_ msg: Message) -> Bool { | ||||
|              | ||||
|             switch msg.what { | ||||
|             case Action.SHOW: | ||||
|                 let request = msg.obj as? BannerShowRequest | ||||
|                 if engine.ad.show(request) { | ||||
|                     engine.transitionTo(engine.showingState) | ||||
|                 } else { | ||||
|                     engine.logWarn("Failed to show banner") | ||||
|                     // no FusionAd here | ||||
| //                    if let eventParams = msg.obj as? EventParams, let ad = eventParams.ad { | ||||
| //                        engine.adDisplayFailed(ad: ad, error: ShowAdRequestError()) | ||||
| //                    } | ||||
|                     if(engine.ad.load()) { | ||||
|                         engine.transitionTo(engine.loadingState) | ||||
|                     } | ||||
|                 } | ||||
|                 return true | ||||
|                  | ||||
|             case Action.HIDE: | ||||
|                 engine.logWarn("Already hidden! Ignore hide request") | ||||
|                 return true | ||||
|                  | ||||
|             default: | ||||
|                 return engine.handleUnhandledMessage(msg) || super.processMessage(msg) | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     func onOrientationUpdate(orientation: ScreenOrientation) { | ||||
|         ad.updateOrientation(orientation: orientation) | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| // Helper class to implement the FusionAdListener protocol | ||||
| private class FusionAdListenerImpl: FusionAdListener { | ||||
|     enum Event { | ||||
|         case onAdLoaded(ad: FusionAd) | ||||
|         case onAdDisplayed(ad: FusionAd) | ||||
|         case onAdHidden(ad: FusionAd) | ||||
|         case onUserRewarded(ad: FusionAd, reward: FusionReward?) | ||||
|         case onAdClicked(ad: FusionAd) | ||||
|         case onAdLoadFailed(loadFailedInfo: LoadFailedInfo) | ||||
|         case onAdDisplayFailed(ad: FusionAd, error: FusionError?) | ||||
|         case onAdRevenuePaid(ad: FusionAd) | ||||
|     } | ||||
|      | ||||
|     private let handler: (Event) -> Void | ||||
|      | ||||
|     init(handler: @escaping (Event) -> Void) { | ||||
|         self.handler = handler | ||||
|     } | ||||
|      | ||||
|     func onAdLoaded(ad: FusionAd) { | ||||
|         handler(.onAdLoaded(ad: ad)) | ||||
|     } | ||||
|      | ||||
|     func onAdDisplayed(ad: FusionAd) { | ||||
|         handler(.onAdDisplayed(ad: ad)) | ||||
|     } | ||||
|      | ||||
|     func onAdHidden(ad: FusionAd) { | ||||
|         handler(.onAdHidden(ad: ad)) | ||||
|     } | ||||
|      | ||||
|     func onUserRewarded(ad: FusionAd, reward: FusionReward?) { | ||||
|         handler(.onUserRewarded(ad: ad, reward: reward)) | ||||
|     } | ||||
|      | ||||
|     func onAdClicked(ad: FusionAd) { | ||||
|         handler(.onAdClicked(ad: ad)) | ||||
|     } | ||||
|      | ||||
|     func onAdLoadFailed(loadFailedInfo: LoadFailedInfo) { | ||||
|         handler(.onAdLoadFailed(loadFailedInfo: loadFailedInfo)) | ||||
|     } | ||||
|      | ||||
|     func onAdDisplayFailed(ad: FusionAd, error: FusionError?) { | ||||
|         handler(.onAdDisplayFailed(ad: ad, error: error)) | ||||
|     } | ||||
|      | ||||
|     func onAdRevenuePaid(ad: FusionAd) { | ||||
|         handler(.onAdRevenuePaid(ad: ad)) | ||||
|     } | ||||
| } | ||||
|  | @ -0,0 +1,383 @@ | |||
| // DefaultInterstitialAdEngine.swift | ||||
| // Default implementation of InterstitialAdEngine | ||||
| // Corresponds to DefaultInterstitialAdEngine.kt in Android implementation | ||||
| 
 | ||||
| import Foundation | ||||
| import UIKit | ||||
| 
 | ||||
| public class DefaultInterstitialAdEngine: InterstitialAdEngine { | ||||
|      | ||||
|     private let ad: GuruInterstitialAd | ||||
|      | ||||
|     // Constants | ||||
|     private enum Action { | ||||
|         static let LOAD = 1 | ||||
|         static let SHOW = 2 | ||||
|         static let DESTROY = 3 | ||||
|         static let RETRY = 4 | ||||
|     } | ||||
|      | ||||
|     private enum Event { | ||||
|         static let ON_LOADED = 100 | ||||
|         static let ON_LOAD_FAILED = 101 | ||||
|         static let ON_DISPLAYED = 102 | ||||
|         static let ON_DISPLAY_FAILED = 103 | ||||
|         static let ON_CLICK = 104 | ||||
|         static let ON_HIDDEN = 105 | ||||
|         static let ON_USER_REWARDED = 106 | ||||
|         static let REVENUE_PAID = 107 | ||||
|         static let LOAD_TIMEOUT = 108 | ||||
|         static let SHOW_TIMEOUT = 109 | ||||
|     } | ||||
|      | ||||
|     private static let LOAD_TIMEOUT_MILLIS: Int = 120_000 | ||||
|     private static let SHOW_TIMEOUT_MILLIS: Int = 120_000 | ||||
|      | ||||
|     public override var supportedAdPlatforms: Set<AdPlatform> { | ||||
|         return [ad.adPlatform] | ||||
|     } | ||||
|      | ||||
|     public init(viewController: UIViewController, id: Int, ad: GuruInterstitialAd) { | ||||
|         self.ad = ad | ||||
|         super.init(viewController: viewController, id: id, adType: .Interstitial, strategyName: ad.adPlatform.name) | ||||
|         setupAdListener() | ||||
|         initialize() | ||||
|     } | ||||
|      | ||||
|     private func setupAdListener() { | ||||
|         ad.listener = FusionAdListenerImpl { [weak self] event in | ||||
|             guard let self = self else { return } | ||||
|              | ||||
|             switch event { | ||||
|             case .onAdLoaded(let ad): | ||||
|                 self.sendMessage(what: Event.ON_LOADED, obj: EventParams(ad: ad)) | ||||
|                  | ||||
|             case .onAdDisplayed(let ad): | ||||
|                 self.sendMessage(what: Event.ON_DISPLAYED, obj: EventParams(ad: ad)) | ||||
|                  | ||||
|             case .onAdHidden(let ad): | ||||
|                 self.sendMessage(what: Event.ON_HIDDEN, obj: EventParams(ad: ad)) | ||||
|                  | ||||
|             case .onUserRewarded(let ad, let reward): | ||||
|                 // Not used for interstitial ads, but handled for completeness | ||||
|                 break | ||||
|                  | ||||
|             case .onAdClicked(let ad): | ||||
|                 self.sendMessage(what: Event.ON_CLICK, obj: EventParams(ad: ad)) | ||||
|                  | ||||
|             case .onAdLoadFailed(let loadFailedInfo): | ||||
|                 self.sendMessage(what: Event.ON_LOAD_FAILED, obj: EventParams(loadFailedInfo: loadFailedInfo)) | ||||
|                  | ||||
|             case .onAdDisplayFailed(let ad, let error): | ||||
|                 self.sendMessage(what: Event.ON_DISPLAY_FAILED, obj: EventParams(ad: ad, displayFailedInfo: error)) | ||||
|                  | ||||
|             case .onAdRevenuePaid(let ad): | ||||
|                 self.sendMessage(what: Event.REVENUE_PAID, obj: EventParams(ad: ad)) | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     // MARK: - State Classes | ||||
|      | ||||
|     public override func createIdleState() -> Idle { | ||||
|         return IdleImpl(self) | ||||
|     } | ||||
|      | ||||
|     public override func createLoadingState() -> Loading { | ||||
|         return LoadingImpl(self) | ||||
|     } | ||||
|      | ||||
|     public override func createLoadedState() -> Loaded { | ||||
|         return LoadedImpl(self) | ||||
|     } | ||||
|      | ||||
|     public override func createShowingState() -> Showing { | ||||
|         return ShowingImpl(self) | ||||
|     } | ||||
|      | ||||
|     // MARK: - Request Methods | ||||
|      | ||||
|     override internal func requestLoad() { | ||||
|         sendMessage(what: Action.LOAD) | ||||
|     } | ||||
|      | ||||
|     override internal func requestShow(request: InterstitialShowRequest?) { | ||||
|         sendMessage(what: Action.SHOW, obj: request) | ||||
|     } | ||||
|      | ||||
|     override internal func requestDestroy() { | ||||
|         sendMessage(what: Action.DESTROY) | ||||
|     } | ||||
|      | ||||
|     // MARK: - Helper Methods | ||||
|      | ||||
|     private func handleUnhandledMessage(_ msg: Message) -> Bool { | ||||
|         switch msg.what { | ||||
|         case Action.DESTROY: | ||||
|             ad.destroy() | ||||
|             transitionTo(idleState) | ||||
|             return true | ||||
|         default: | ||||
|             return false | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     // MARK: - State Implementations | ||||
|      | ||||
|     private class IdleImpl: Idle { | ||||
|         private let engine: DefaultInterstitialAdEngine | ||||
|          | ||||
|         init(_ engine: DefaultInterstitialAdEngine) { | ||||
|             self.engine = engine | ||||
|             super.init() | ||||
|         } | ||||
|          | ||||
|         public override func enter(from: IState?, params: Any?) { | ||||
|             super.enter(from: from, params: params) | ||||
|         } | ||||
|          | ||||
|         public override func processMessage(_ msg: Message) -> Bool { | ||||
|             switch msg.what { | ||||
|             case Action.LOAD: | ||||
|                 if engine.ad.load() { | ||||
|                     engine.transitionTo(engine.loadingState) | ||||
|                 } else { | ||||
|                     engine.adLoadFailed(loadFailedInfo: LoadFailedInfo( | ||||
|                         engineId: engine.id, | ||||
|                         adPlatform: engine.ad.adPlatform, | ||||
|                         adType: engine.adType, | ||||
|                         adUnitId: engine.ad.adUnitId, | ||||
|                         error: LoadAdRequestError() | ||||
|                     )) | ||||
|                 } | ||||
|                 return true | ||||
|             default: | ||||
|                 return engine.handleUnhandledMessage(msg) || super.processMessage(msg) | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     class LoadingImpl: Loading { | ||||
|         private var retryCount = 0 | ||||
|          | ||||
|         private let engine: DefaultInterstitialAdEngine | ||||
|          | ||||
|         init(_ engine: DefaultInterstitialAdEngine) { | ||||
|             self.engine = engine | ||||
|             super.init() | ||||
|         } | ||||
|          | ||||
|         public override func enter(from: IState?, params: Any?) { | ||||
|             super.enter(from: from, params: params) | ||||
|             retryCount = 0 | ||||
|              | ||||
|             // Set load timeout | ||||
|             engine.sendMessageDelayed(what: Event.LOAD_TIMEOUT, delayMillis: DefaultInterstitialAdEngine.LOAD_TIMEOUT_MILLIS) | ||||
|         } | ||||
|          | ||||
|         public override func exit(to: IState?) { | ||||
|             engine.removeMessages(what: Event.LOAD_TIMEOUT) | ||||
|             engine.removeMessages(what: Action.RETRY) | ||||
|             retryCount = 0 | ||||
|             super.exit(to: to) | ||||
|         } | ||||
|          | ||||
|         public override func processMessage(_ msg: Message) -> Bool { | ||||
|              | ||||
|             switch msg.what { | ||||
|             case Action.RETRY: | ||||
|                 retryCount += 1 | ||||
|                 if !engine.ad.load() { | ||||
|                     engine.sendMessage(what: Event.ON_LOAD_FAILED, obj: EventParams( | ||||
|                         loadFailedInfo: LoadFailedInfo( | ||||
|                             engineId: engine.id, | ||||
|                             adPlatform: engine.ad.adPlatform, | ||||
|                             adType: engine.adType, | ||||
|                             adUnitId: engine.ad.adUnitId, | ||||
|                             error: LoadAdRequestError() | ||||
|                         ) | ||||
|                     )) | ||||
|                 } else { | ||||
|                     engine.sendMessageDelayed(what: Event.LOAD_TIMEOUT, delayMillis: DefaultInterstitialAdEngine.LOAD_TIMEOUT_MILLIS) | ||||
|                 } | ||||
|                 return true | ||||
|                  | ||||
|             case Action.LOAD: | ||||
|                 engine.logWarn("Already loading! Ignore load request") | ||||
|                 return true | ||||
|                  | ||||
|             case Event.LOAD_TIMEOUT: | ||||
|                 engine.removeMessages(what: Action.RETRY) | ||||
|                 engine.sendMessage(what: Action.RETRY) | ||||
|                 return true | ||||
|                  | ||||
|             case Event.ON_LOADED: | ||||
|                 if let eventParams = msg.obj as? EventParams, eventParams.ad != nil { | ||||
|                     engine.transitionTo(engine.loadedState, params: eventParams) | ||||
|                 } | ||||
|                 engine.deferMessage(msg) | ||||
|                 return true | ||||
|                  | ||||
|             case Event.ON_LOAD_FAILED: | ||||
|                 let params = msg.obj as? EventParams | ||||
|                 engine.adLoadFailed(loadFailedInfo: params?.loadFailedInfo ?? LoadFailedInfo( | ||||
|                     engineId: engine.id, | ||||
|                     adPlatform: engine.ad.adPlatform, | ||||
|                     adType: engine.adType, | ||||
|                     adUnitId: engine.ad.adUnitId, | ||||
|                     error: LoadAdRequestError() | ||||
|                 )) | ||||
|                 engine.transitionTo(engine.idleState) | ||||
|                 return true | ||||
|                  | ||||
|             default: | ||||
|                 return engine.handleUnhandledMessage(msg) || super.processMessage(msg) | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     class LoadedImpl: Loaded { | ||||
|         private let engine: DefaultInterstitialAdEngine | ||||
|          | ||||
|         init(_ engine: DefaultInterstitialAdEngine) { | ||||
|             self.engine = engine | ||||
|             super.init() | ||||
|         } | ||||
|          | ||||
|         public override func enter(from: IState?, params: Any?) { | ||||
|             super.enter(from: from, params: params) | ||||
|             if let eventParams = params as? EventParams, let ad = eventParams.ad { | ||||
|                 engine.adLoaded(ad: ad) | ||||
|             } | ||||
|         } | ||||
|          | ||||
|         public override func processMessage(_ msg: Message) -> Bool { | ||||
|             switch msg.what { | ||||
|             case Action.SHOW: | ||||
|                 let request = msg.obj as? InterstitialShowRequest | ||||
|                  | ||||
|                 if engine.ad.show(request) { | ||||
|                     engine.transitionTo(engine.showingState) | ||||
|                 } else { | ||||
|                     engine.logWarn("Failed to show interstitial ad") | ||||
|                     // no FusionAd here | ||||
| //                    engine.adDisplayFailed(ad: engine.ad, error: ShowAdRequestError()) | ||||
|                     if(engine.ad.load()) { | ||||
|                         engine.transitionTo(engine.loadingState) | ||||
|                     } | ||||
|                 } | ||||
|                 return true | ||||
|                  | ||||
|             default: | ||||
|                 return engine.handleUnhandledMessage(msg) || super.processMessage(msg) | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     class ShowingImpl: Showing { | ||||
|         private let engine: DefaultInterstitialAdEngine | ||||
|          | ||||
|         init(_ engine: DefaultInterstitialAdEngine) { | ||||
|             self.engine = engine | ||||
|             super.init() | ||||
|         } | ||||
|          | ||||
|         public override func enter(from: IState?, params: Any?) { | ||||
|             super.enter(from: from, params: params) | ||||
|             engine.sendMessageDelayed(what: Event.SHOW_TIMEOUT, delayed: .milliseconds(SHOW_TIMEOUT_MILLIS)) | ||||
|         } | ||||
|          | ||||
|         public override func processMessage(_ msg: Message) -> Bool { | ||||
|              | ||||
|             switch msg.what { | ||||
|             case Event.ON_DISPLAYED: | ||||
|                 if let eventParams = msg.obj as? EventParams, let ad = eventParams.ad { | ||||
|                     engine.adDisplayed(ad: ad) | ||||
|                 } | ||||
|                 engine.removeMessages(what: Event.SHOW_TIMEOUT) | ||||
|                 return true | ||||
|             case Event.ON_CLICK: | ||||
|                 if let eventParams = msg.obj as? EventParams, let ad = eventParams.ad { | ||||
|                     engine.adClicked(ad: ad) | ||||
|                 } | ||||
|                 return true | ||||
|                  | ||||
|             case Event.ON_HIDDEN: | ||||
|                 if let eventParams = msg.obj as? EventParams, let ad = eventParams.ad { | ||||
|                     engine.adHidden(ad: ad) | ||||
|                     engine.transitionTo(engine.idleState) | ||||
|                 } | ||||
|                 return true | ||||
|                  | ||||
|             case Event.REVENUE_PAID: | ||||
|                 if let eventParams = msg.obj as? EventParams, let ad = eventParams.ad { | ||||
|                     engine.adRevenuePaid(ad: ad) | ||||
|                 } | ||||
|                 return true | ||||
|                  | ||||
|             case Event.ON_DISPLAY_FAILED: | ||||
|                 if let eventParams = msg.obj as? EventParams, let ad = eventParams.ad { | ||||
|                     engine.adDisplayFailed(ad: ad, error: eventParams.displayFailedInfo) | ||||
|                 } | ||||
|                 engine.transitionTo(engine.idleState) | ||||
|                 engine.requestLoad() | ||||
|                 return true | ||||
|                  | ||||
|             default: | ||||
|                 return engine.handleUnhandledMessage(msg) || super.processMessage(msg) | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| // Helper class to implement the FusionAdListener protocol | ||||
| private class FusionAdListenerImpl: FusionAdListener { | ||||
|     enum Event { | ||||
|         case onAdLoaded(ad: FusionAd) | ||||
|         case onAdDisplayed(ad: FusionAd) | ||||
|         case onAdHidden(ad: FusionAd) | ||||
|         case onUserRewarded(ad: FusionAd, reward: FusionReward?) | ||||
|         case onAdClicked(ad: FusionAd) | ||||
|         case onAdLoadFailed(loadFailedInfo: LoadFailedInfo) | ||||
|         case onAdDisplayFailed(ad: FusionAd, error: FusionError?) | ||||
|         case onAdRevenuePaid(ad: FusionAd) | ||||
|     } | ||||
|      | ||||
|     private let handler: (Event) -> Void | ||||
|      | ||||
|     init(handler: @escaping (Event) -> Void) { | ||||
|         self.handler = handler | ||||
|     } | ||||
|      | ||||
|     func onAdLoaded(ad: FusionAd) { | ||||
|         handler(.onAdLoaded(ad: ad)) | ||||
|     } | ||||
|      | ||||
|     func onAdDisplayed(ad: FusionAd) { | ||||
|         handler(.onAdDisplayed(ad: ad)) | ||||
|     } | ||||
|      | ||||
|     func onAdHidden(ad: FusionAd) { | ||||
|         handler(.onAdHidden(ad: ad)) | ||||
|     } | ||||
|      | ||||
|     func onUserRewarded(ad: FusionAd, reward: FusionReward?) { | ||||
|         handler(.onUserRewarded(ad: ad, reward: reward)) | ||||
|     } | ||||
|      | ||||
|     func onAdClicked(ad: FusionAd) { | ||||
|         handler(.onAdClicked(ad: ad)) | ||||
|     } | ||||
|      | ||||
|     func onAdLoadFailed(loadFailedInfo: LoadFailedInfo) { | ||||
|         handler(.onAdLoadFailed(loadFailedInfo: loadFailedInfo)) | ||||
|     } | ||||
|      | ||||
|     func onAdDisplayFailed(ad: FusionAd, error: FusionError?) { | ||||
|         handler(.onAdDisplayFailed(ad: ad, error: error)) | ||||
|     } | ||||
|      | ||||
|     func onAdRevenuePaid(ad: FusionAd) { | ||||
|         handler(.onAdRevenuePaid(ad: ad)) | ||||
|     } | ||||
| } | ||||
|  | @ -0,0 +1,144 @@ | |||
| // InterstitialAdEngine.swift | ||||
| // Base class for interstitial ad engines | ||||
| // Corresponds to InterstitialAdEngine.kt in Android implementation | ||||
| 
 | ||||
| import Foundation | ||||
| import UIKit | ||||
| 
 | ||||
| public class InterstitialAdEngine: RelayAdEngine { | ||||
|      | ||||
|     private(set) var idleState: Idle! | ||||
|     private(set) var loadingState: Loading! | ||||
|     private(set) var loadedState: Loaded! | ||||
|     private(set) var showingState: Showing! | ||||
|      | ||||
|     public var currentStateIdentifier: AdState.Identifier { | ||||
|         return (currentState as? AdState)?.stateId ?? AdState.Identifier(name: currentState?.name ?? "unknown") | ||||
|     } | ||||
|      | ||||
|     public override init(viewController: UIViewController, id: Int, adType: AdType, strategyName: String) { | ||||
|         super.init(viewController: viewController, id: id, adType: .Interstitial, strategyName: strategyName) | ||||
|     } | ||||
|      | ||||
|     public func initialize() { | ||||
|         idleState = createIdleState() | ||||
|         loadingState = createLoadingState() | ||||
|         loadedState = createLoadedState() | ||||
|         showingState = createShowingState() | ||||
|          | ||||
|         addState(idleState) | ||||
|         addState(loadingState) | ||||
|         addState(loadedState) | ||||
|         addState(showingState) | ||||
|         setInitialState(idleState) | ||||
|         start() | ||||
|     } | ||||
|      | ||||
|     // State classes | ||||
|     public class AdState: State { | ||||
|         public let stateId: Identifier | ||||
|          | ||||
|         public init(name: String, stateId: Identifier) { | ||||
|             self.stateId = stateId | ||||
|             super.init() | ||||
|         } | ||||
|          | ||||
|         public override var name: String { | ||||
|             return stateId.name | ||||
|         } | ||||
|          | ||||
|         public class Identifier: Equatable { | ||||
|             public let name: String | ||||
|              | ||||
|             public init(name: String) { | ||||
|                 self.name = name | ||||
|             } | ||||
|              | ||||
|             // Pre-defined identifiers | ||||
|             public static let IDLE = Identifier(name: "idle") | ||||
|             public static let LOADING = Identifier(name: "loading") | ||||
|             public static let LOADED = Identifier(name: "loaded") | ||||
|             public static let SHOWING = Identifier(name: "showing") | ||||
|              | ||||
|             public static func == (lhs: Identifier, rhs: Identifier) -> Bool { | ||||
|                 return lhs.name == rhs.name | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     public class Idle: AdState { | ||||
|         public init() { | ||||
|             super.init(name: "[IDLE]", stateId: Identifier.IDLE) | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     public class Loading: AdState { | ||||
|         public init() { | ||||
|             super.init(name: "[LOADING]", stateId: Identifier.LOADING) | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     public class Loaded: AdState { | ||||
|         public init() { | ||||
|             super.init(name: "[LOADED]", stateId: Identifier.LOADED) | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     public class Showing: AdState { | ||||
|         public init() { | ||||
|             super.init(name: "[SHOWING]", stateId: Identifier.SHOWING) | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     // Factory methods for creating the states | ||||
|     open func createIdleState() -> Idle { | ||||
|         return Idle() | ||||
|     } | ||||
|      | ||||
|     open func createLoadingState() -> Loading { | ||||
|         return Loading() | ||||
|     } | ||||
|      | ||||
|     open func createLoadedState() -> Loaded { | ||||
|         return Loaded() | ||||
|     } | ||||
|      | ||||
|     open func createShowingState() -> Showing { | ||||
|         return Showing() | ||||
|     } | ||||
|      | ||||
|     // Abstract methods to be implemented by subclasses | ||||
|     internal func requestLoad() { | ||||
|         fatalError("Subclass must implement") | ||||
|     } | ||||
|      | ||||
|     internal func requestShow(request: InterstitialShowRequest?) { | ||||
|         fatalError("Subclass must implement") | ||||
|     } | ||||
|      | ||||
|     internal func requestDestroy() { | ||||
|         fatalError("Subclass must implement") | ||||
|     } | ||||
|      | ||||
|     // Public API methods | ||||
|     public func show(_ request: InterstitialShowRequest? = nil) -> Bool { | ||||
|         if isCurrent(loadedState) { | ||||
|             requestShow(request: request) | ||||
|             return true | ||||
|         } | ||||
|         return false | ||||
|     } | ||||
|      | ||||
|     public func load() -> Bool { | ||||
|         if isCurrent(idleState) { | ||||
|             requestLoad() | ||||
|             return true | ||||
|         } | ||||
|         return false | ||||
|     } | ||||
|      | ||||
|     public func destroy() -> Bool { | ||||
|         requestDestroy() | ||||
|         return true | ||||
|     } | ||||
| } | ||||
|  | @ -0,0 +1,424 @@ | |||
| // DefaultMRecAdEngine.swift | ||||
| // Default implementation of MRecAdEngine | ||||
| // Corresponds to DefaultMRecAdEngine.kt in Android implementation | ||||
| 
 | ||||
| import Foundation | ||||
| import UIKit | ||||
| 
 | ||||
| public class DefaultMRecAdEngine: MRecAdEngine { | ||||
|      | ||||
|     private let ad: GuruMRecAd | ||||
|     private var currentShowRequest: MRecShowRequest? | ||||
|      | ||||
|     // Constants | ||||
|     private enum Action { | ||||
|         static let LOAD = 1 | ||||
|         static let SHOW = 2 | ||||
|         static let HIDE = 3 | ||||
|         static let DESTROY = 4 | ||||
|         static let RETRY = 5 | ||||
|     } | ||||
|      | ||||
|     private enum Event { | ||||
|         static let ON_LOADED = 100 | ||||
|         static let ON_LOAD_FAILED = 101 | ||||
|         static let ON_DISPLAYED = 102 | ||||
|         static let ON_DISPLAY_FAILED = 103 | ||||
|         static let ON_CLICK = 104 | ||||
|         static let ON_HIDDEN = 105 | ||||
|         static let ON_USER_REWARDED = 106 | ||||
|         static let REVENUE_PAID = 107 | ||||
|         static let LOAD_TIMEOUT = 108 | ||||
|         static let SHOW_TIMEOUT = 109 | ||||
|     } | ||||
|      | ||||
|     private static let LOAD_TIMEOUT_MILLIS: TimeInterval = 120000 / 1000.0 | ||||
|     private static let SHOW_TIMEOUT_MILLIS: TimeInterval = 120000 / 1000.0 | ||||
|      | ||||
|     public override var supportedAdPlatforms: Set<AdPlatform> { | ||||
|         return [ad.adPlatform] | ||||
|     } | ||||
|      | ||||
|     public init(viewController: UIViewController, id: Int, ad: GuruMRecAd) { | ||||
|         self.ad = ad | ||||
|         super.init(viewController: viewController, id: id, adType: .MRec, strategyName: ad.adPlatform.name) | ||||
|         setupAdListener() | ||||
|         initialize() | ||||
|     } | ||||
|      | ||||
|     private func setupAdListener() { | ||||
|         ad.listener = FusionAdListenerImpl { [weak self] event in | ||||
|             guard let self = self else { return } | ||||
|              | ||||
|             switch event { | ||||
|             case .onAdLoaded(let ad): | ||||
|                 self.sendMessage(what: Event.ON_LOADED, obj: EventParams(ad: ad)) | ||||
|                  | ||||
|             case .onAdDisplayed(let ad): | ||||
|                 // Not used for MREC ads, but handled for completeness | ||||
|                 break | ||||
|                  | ||||
|             case .onAdHidden(let ad): | ||||
|                 // Not used for MREC ads, but handled for completeness | ||||
|                 break | ||||
|                  | ||||
|             case .onUserRewarded(let ad, let reward): | ||||
|                 // Not used for MREC ads, but handled for completeness | ||||
|                 break | ||||
|                  | ||||
|             case .onAdClicked(let ad): | ||||
|                 self.sendMessage(what: Event.ON_CLICK, obj: EventParams(ad: ad)) | ||||
|                  | ||||
|             case .onAdLoadFailed(let loadFailedInfo): | ||||
|                 self.sendMessage(what: Event.ON_LOAD_FAILED, obj: EventParams(loadFailedInfo: loadFailedInfo)) | ||||
|                  | ||||
|             case .onAdDisplayFailed(let ad, let error): | ||||
|                 self.sendMessage(what: Event.ON_DISPLAY_FAILED, obj: EventParams(ad: ad, displayFailedInfo: error)) | ||||
|                  | ||||
|             case .onAdRevenuePaid(let ad): | ||||
|                 self.sendMessage(what: Event.REVENUE_PAID, obj: EventParams(ad: ad)) | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     // MARK: - State Classes | ||||
|      | ||||
|     public override func createIdleState() -> Idle { | ||||
|         return IdleImpl(self) | ||||
|     } | ||||
|      | ||||
|     public override func createLoadingState() -> Loading { | ||||
|         return LoadingImpl(self) | ||||
|     } | ||||
|      | ||||
|     public override func createActiveState() -> Active { | ||||
|         return ActiveImpl(self) | ||||
|     } | ||||
|      | ||||
|     public override func createShowingState() -> Showing { | ||||
|         return ShowingImpl(self) | ||||
|     } | ||||
|      | ||||
|     public override func createHiddenState() -> Hidden { | ||||
|         return HiddenImpl(self) | ||||
|     } | ||||
|      | ||||
|     // MARK: - Request Methods | ||||
|      | ||||
|     override internal func requestLoad() { | ||||
|         sendMessage(what: Action.LOAD) | ||||
|     } | ||||
|      | ||||
|     override internal func requestShow(_ request: MRecShowRequest? = nil) { | ||||
|         currentShowRequest = request | ||||
|         sendMessage(what: Action.SHOW, obj: request) | ||||
|     } | ||||
|      | ||||
|     override internal func requestHide() { | ||||
|         sendMessage(what: Action.HIDE) | ||||
|     } | ||||
|      | ||||
|     override internal func requestDestroy() { | ||||
|         sendMessage(what: Action.DESTROY) | ||||
|     } | ||||
|      | ||||
|     // MARK: - Helper Methods | ||||
|      | ||||
|     private func handleUnhandledMessage(_ msg: Message) -> Bool { | ||||
|         switch msg.what { | ||||
|         case Action.DESTROY: | ||||
|             ad.destroy() | ||||
|             transitionTo(idleState) | ||||
|             return true | ||||
|         default: | ||||
|             return false | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     // MARK: - State Implementations | ||||
|      | ||||
|     class IdleImpl: Idle { | ||||
|         private let engine: DefaultMRecAdEngine | ||||
|          | ||||
|         init(_ engine: DefaultMRecAdEngine) { | ||||
|             self.engine = engine | ||||
|             super.init() | ||||
|         } | ||||
|          | ||||
|         public override func enter(from: IState?, params: Any?) { | ||||
|             super.enter(from: from, params: params) | ||||
|         } | ||||
|          | ||||
|         public override func processMessage(_ msg: Message) -> Bool { | ||||
|              | ||||
|             switch msg.what { | ||||
|             case Action.LOAD: | ||||
|                 if engine.ad.load() { | ||||
|                     engine.transitionTo(engine.loadingState) | ||||
|                 } else { | ||||
|                     engine.adLoadFailed(loadFailedInfo: LoadFailedInfo( | ||||
|                         engineId: engine.id, | ||||
|                         adPlatform: engine.ad.adPlatform, | ||||
|                         adType: engine.adType, | ||||
|                         adUnitId: engine.ad.adUnitId, | ||||
|                         error: LoadAdRequestError() | ||||
|                     )) | ||||
|                 } | ||||
|                 return true | ||||
|             default: | ||||
|                 return engine.handleUnhandledMessage(msg) || super.processMessage(msg) | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     class LoadingImpl: Loading { | ||||
|         private var retryCount = 0 | ||||
|         private let engine: DefaultMRecAdEngine | ||||
|          | ||||
|         init(_ engine: DefaultMRecAdEngine) { | ||||
|             self.engine = engine | ||||
|             super.init() | ||||
|         } | ||||
|          | ||||
|         public override func enter(from: IState?, params: Any?) { | ||||
|             super.enter(from: from, params: params) | ||||
|             retryCount = 0 | ||||
|              | ||||
|             // Set load timeout | ||||
|             engine.sendMessageDelayed(what: Event.LOAD_TIMEOUT, delayMillis: Int(DefaultMRecAdEngine.LOAD_TIMEOUT_MILLIS * 1000)) | ||||
|         } | ||||
|          | ||||
|         public override func exit(to: IState?) { | ||||
|             engine.removeMessages(what: Event.LOAD_TIMEOUT) | ||||
|             engine.removeMessages(what: Action.RETRY) | ||||
|             retryCount = 0 | ||||
|             super.exit(to: to) | ||||
|         } | ||||
|          | ||||
|         public override func processMessage(_ msg: Message) -> Bool { | ||||
|              | ||||
|             switch msg.what { | ||||
|             case Action.RETRY: | ||||
|                 retryCount += 1 | ||||
|                 if !engine.ad.load() { | ||||
|                     engine.sendMessage(what: Event.ON_LOAD_FAILED, obj: EventParams( | ||||
|                         loadFailedInfo: LoadFailedInfo( | ||||
|                             engineId: engine.id, | ||||
|                             adPlatform: engine.ad.adPlatform, | ||||
|                             adType: engine.adType, | ||||
|                             adUnitId: engine.ad.adUnitId, | ||||
|                             error: LoadAdRequestError() | ||||
|                         ) | ||||
|                     )) | ||||
|                 } else { | ||||
|                     engine.sendMessageDelayed(what: Event.LOAD_TIMEOUT, delayMillis: Int(DefaultMRecAdEngine.LOAD_TIMEOUT_MILLIS * 1000)) | ||||
|                 } | ||||
|                 return true | ||||
|                  | ||||
|             case Action.LOAD: | ||||
|                 engine.logWarn("Already loading! Ignore load request") | ||||
|                 return true | ||||
|                  | ||||
|             case Event.LOAD_TIMEOUT: | ||||
|                 engine.removeMessages(what: Action.RETRY) | ||||
|                 engine.sendMessage(what: Action.RETRY) | ||||
|                 return true | ||||
|                  | ||||
|             case Event.ON_LOADED: | ||||
|                 if let eventParams = msg.obj as? EventParams, eventParams.ad != nil { | ||||
|                     engine.transitionTo(engine.hiddenState, params: eventParams) | ||||
|                 } | ||||
|                 engine.deferMessage(msg) | ||||
|                 return true | ||||
|                  | ||||
|             case Event.ON_LOAD_FAILED: | ||||
|                 let params = msg.obj as? EventParams | ||||
|                 engine.adLoadFailed(loadFailedInfo: params?.loadFailedInfo ?? LoadFailedInfo( | ||||
|                     engineId: engine.id, | ||||
|                     adPlatform: engine.ad.adPlatform, | ||||
|                     adType: engine.adType, | ||||
|                     adUnitId: engine.ad.adUnitId, | ||||
|                     error: LoadAdRequestError() | ||||
|                 )) | ||||
|                 engine.transitionTo(engine.idleState) | ||||
|                 return true | ||||
|                  | ||||
|             default: | ||||
|                 return engine.handleUnhandledMessage(msg) || super.processMessage(msg) | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     class ActiveImpl: Active { | ||||
|         private let engine: DefaultMRecAdEngine | ||||
|          | ||||
|         init(_ engine: DefaultMRecAdEngine) { | ||||
|             self.engine = engine | ||||
|             super.init() | ||||
|         } | ||||
|          | ||||
|         public override func enter(from: IState?, params: Any?) { | ||||
|             super.enter(from: from, params: params) | ||||
|         } | ||||
|          | ||||
|         public override func processMessage(_ msg: Message) -> Bool { | ||||
|              | ||||
|             switch msg.what { | ||||
|             case Event.ON_CLICK: | ||||
|                 if let eventParams = msg.obj as? EventParams, let ad = eventParams.ad { | ||||
|                     engine.adClicked(ad: ad) | ||||
|                 } | ||||
|                 return true | ||||
|                  | ||||
|             case Event.REVENUE_PAID: | ||||
|                 if let eventParams = msg.obj as? EventParams, let ad = eventParams.ad { | ||||
|                     engine.adRevenuePaid(ad: ad) | ||||
|                 } | ||||
|                 return true | ||||
|                  | ||||
|             default: | ||||
|                 return engine.handleUnhandledMessage(msg) || super.processMessage(msg) | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     class ShowingImpl: Showing { | ||||
|         private let engine: DefaultMRecAdEngine | ||||
|          | ||||
|         init(_ engine: DefaultMRecAdEngine) { | ||||
|             self.engine = engine | ||||
|             super.init() | ||||
|         } | ||||
|          | ||||
|         public override func enter(from: IState?, params: Any?) { | ||||
|             super.enter(from: from, params: params) | ||||
|              | ||||
|             if let eventParams = params as? EventParams, let ad = eventParams.ad { | ||||
|                 engine.adDisplayed(ad: ad) | ||||
|             } | ||||
|         } | ||||
|          | ||||
|         public override func processMessage(_ msg: Message) -> Bool { | ||||
|              | ||||
|             switch msg.what { | ||||
|             case Action.HIDE: | ||||
|                 if engine.ad.hide() { | ||||
|                     engine.transitionTo(engine.hiddenState) | ||||
|                 } else { | ||||
|                     engine.logWarn("Failed to hide MREC") | ||||
|                 } | ||||
|                 return true | ||||
|                  | ||||
|             case Action.SHOW: | ||||
|                 engine.logWarn("Already showing! Ignore show request") | ||||
|                 return true | ||||
|                  | ||||
|             case Event.ON_DISPLAY_FAILED: | ||||
|                 if let eventParams = msg.obj as? EventParams, let ad = eventParams.ad { | ||||
|                     engine.adDisplayFailed(ad: ad, error: eventParams.displayFailedInfo) | ||||
|                 } | ||||
|                 engine.transitionTo(engine.hiddenState) | ||||
|                 engine.requestLoad() | ||||
|                 return true | ||||
|                  | ||||
|             default: | ||||
|                 return engine.handleUnhandledMessage(msg) || super.processMessage(msg) | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     class HiddenImpl: Hidden { | ||||
|         private let engine: DefaultMRecAdEngine | ||||
|          | ||||
|         init(_ engine: DefaultMRecAdEngine) { | ||||
|             self.engine = engine | ||||
|             super.init() | ||||
|         } | ||||
|          | ||||
|         public override func enter(from: IState?, params: Any?) { | ||||
|             super.enter(from: from, params: params) | ||||
|              | ||||
|             if let eventParams = params as? EventParams, let ad = eventParams.ad { | ||||
|                 engine.adLoaded(ad: ad) | ||||
|             } | ||||
|         } | ||||
|          | ||||
|         public override func processMessage(_ msg: Message) -> Bool { | ||||
|              | ||||
|             switch msg.what { | ||||
|             case Action.SHOW: | ||||
|                 let request = msg.obj as? MRecShowRequest | ||||
|                 if engine.ad.show(request: request) { | ||||
|                     engine.transitionTo(engine.showingState) | ||||
|                 } else { | ||||
|                     engine.logWarn("Failed to show MREC") | ||||
|                     // no FusionAd here | ||||
| //                    engine.adDisplayFailed(ad: engine.ad, error: ShowAdRequestError()) | ||||
|                     if(engine.load()) { | ||||
|                         engine.transitionTo(engine.loadingState) | ||||
|                     } | ||||
|                 } | ||||
|                 return true | ||||
|                  | ||||
|             case Action.HIDE: | ||||
|                 engine.logWarn("Already hidden! Ignore hide request") | ||||
|                 return true | ||||
|                  | ||||
|             default: | ||||
|                 return engine.handleUnhandledMessage(msg) || super.processMessage(msg) | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| // Helper class to implement the FusionAdListener protocol | ||||
| private class FusionAdListenerImpl: FusionAdListener { | ||||
|     enum Event { | ||||
|         case onAdLoaded(ad: FusionAd) | ||||
|         case onAdDisplayed(ad: FusionAd) | ||||
|         case onAdHidden(ad: FusionAd) | ||||
|         case onUserRewarded(ad: FusionAd, reward: FusionReward?) | ||||
|         case onAdClicked(ad: FusionAd) | ||||
|         case onAdLoadFailed(loadFailedInfo: LoadFailedInfo) | ||||
|         case onAdDisplayFailed(ad: FusionAd, error: FusionError?) | ||||
|         case onAdRevenuePaid(ad: FusionAd) | ||||
|     } | ||||
|      | ||||
|     private let handler: (Event) -> Void | ||||
|      | ||||
|     init(handler: @escaping (Event) -> Void) { | ||||
|         self.handler = handler | ||||
|     } | ||||
|      | ||||
|     func onAdLoaded(ad: FusionAd) { | ||||
|         handler(.onAdLoaded(ad: ad)) | ||||
|     } | ||||
|      | ||||
|     func onAdDisplayed(ad: FusionAd) { | ||||
|         handler(.onAdDisplayed(ad: ad)) | ||||
|     } | ||||
|      | ||||
|     func onAdHidden(ad: FusionAd) { | ||||
|         handler(.onAdHidden(ad: ad)) | ||||
|     } | ||||
|      | ||||
|     func onUserRewarded(ad: FusionAd, reward: FusionReward?) { | ||||
|         handler(.onUserRewarded(ad: ad, reward: reward)) | ||||
|     } | ||||
|      | ||||
|     func onAdClicked(ad: FusionAd) { | ||||
|         handler(.onAdClicked(ad: ad)) | ||||
|     } | ||||
|      | ||||
|     func onAdLoadFailed(loadFailedInfo: LoadFailedInfo) { | ||||
|         handler(.onAdLoadFailed(loadFailedInfo: loadFailedInfo)) | ||||
|     } | ||||
|      | ||||
|     func onAdDisplayFailed(ad: FusionAd, error: FusionError?) { | ||||
|         handler(.onAdDisplayFailed(ad: ad, error: error)) | ||||
|     } | ||||
|      | ||||
|     func onAdRevenuePaid(ad: FusionAd) { | ||||
|         handler(.onAdRevenuePaid(ad: ad)) | ||||
|     } | ||||
| } | ||||
|  | @ -0,0 +1,173 @@ | |||
| // MRecAdEngine.swift | ||||
| // Base class for MREC ad engines | ||||
| // Corresponds to MRecAdEngine.kt in Android implementation | ||||
| 
 | ||||
| import Foundation | ||||
| import UIKit | ||||
| 
 | ||||
| public class MRecAdEngine: RelayAdEngine { | ||||
|      | ||||
|     private(set) var idleState: Idle! | ||||
|     private(set) var loadingState: Loading! | ||||
|     private(set) var activeState: Active! | ||||
|     private(set) var showingState: Showing! | ||||
|     private(set) var hiddenState: Hidden! | ||||
|      | ||||
|     public var currentStateIdentifier: AdState.Identifier { | ||||
|         return (currentState as? AdState)?.stateId ?? AdState.Identifier(name: currentState?.name ?? "unknown") | ||||
|     } | ||||
|      | ||||
|     public override init(viewController: UIViewController, id: Int, adType: AdType, strategyName: String) { | ||||
|         super.init(viewController: viewController, id: id, adType: .MRec, strategyName: strategyName) | ||||
|     } | ||||
|      | ||||
|     public func initialize() { | ||||
|         idleState = createIdleState() | ||||
|         loadingState = createLoadingState() | ||||
|         activeState = createActiveState() | ||||
|         showingState = createShowingState() | ||||
|         hiddenState = createHiddenState() | ||||
|          | ||||
|         addState(idleState) | ||||
|         addState(loadingState) | ||||
|         addState(activeState) | ||||
|         addState(showingState, parent: activeState) | ||||
|         addState(hiddenState, parent: activeState) | ||||
|         setInitialState(idleState) | ||||
|         start() | ||||
|     } | ||||
|      | ||||
|     // State classes | ||||
|     public class AdState: State { | ||||
|         public let stateId: Identifier | ||||
|          | ||||
|         public init(name: String, stateId: Identifier) { | ||||
|             self.stateId = stateId | ||||
|             self.displayName = name | ||||
|             super.init() | ||||
|         } | ||||
|          | ||||
|         private var displayName: String; | ||||
|          | ||||
|         public override var name: String { | ||||
|             return displayName | ||||
|         } | ||||
|          | ||||
|         public class Identifier: Equatable { | ||||
|             public let name: String | ||||
|              | ||||
|             public init(name: String) { | ||||
|                 self.name = name | ||||
|             } | ||||
|              | ||||
|             // Pre-defined identifiers | ||||
|             public static let IDLE = Identifier(name: "idle") | ||||
|             public static let LOADING = Identifier(name: "loading") | ||||
|             public static let ACTIVE = Identifier(name: "active") | ||||
|             public static let SHOWING = Identifier(name: "showing") | ||||
|             public static let HIDDEN = Identifier(name: "hidden") | ||||
|              | ||||
|             public static func == (lhs: Identifier, rhs: Identifier) -> Bool { | ||||
|                 return lhs.name == rhs.name | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     public class Idle: AdState { | ||||
|         public init() { | ||||
|             super.init(name: "[IDLE]", stateId: Identifier.IDLE) | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     public class Loading: AdState { | ||||
|         public init() { | ||||
|             super.init(name: "[LOADING]", stateId: Identifier.LOADING) | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     public class Active: AdState { | ||||
|         public init() { | ||||
|             super.init(name: "[ACTIVE]", stateId: Identifier.ACTIVE) | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     public class Showing: AdState { | ||||
|         public init() { | ||||
|             super.init(name: "[SHOWING]", stateId: Identifier.SHOWING) | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     public class Hidden: AdState { | ||||
|         public init() { | ||||
|             super.init(name: "[HIDDEN]", stateId: Identifier.HIDDEN) | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     // Factory methods for creating the states | ||||
|     open func createIdleState() -> Idle { | ||||
|         return Idle() | ||||
|     } | ||||
|      | ||||
|     open func createLoadingState() -> Loading { | ||||
|         return Loading() | ||||
|     } | ||||
|      | ||||
|     open func createActiveState() -> Active { | ||||
|         return Active() | ||||
|     } | ||||
|      | ||||
|     open func createShowingState() -> Showing { | ||||
|         return Showing() | ||||
|     } | ||||
|      | ||||
|     open func createHiddenState() -> Hidden { | ||||
|         return Hidden() | ||||
|     } | ||||
|      | ||||
|     // Abstract methods to be implemented by subclasses | ||||
|     internal func requestLoad() { | ||||
|         fatalError("Subclass must implement") | ||||
|     } | ||||
|      | ||||
|     internal func requestShow(_ request: MRecShowRequest? = nil) { | ||||
|         fatalError("Subclass must implement") | ||||
|     } | ||||
|      | ||||
|     internal func requestHide() { | ||||
|         fatalError("Subclass must implement") | ||||
|     } | ||||
|      | ||||
|     internal func requestDestroy() { | ||||
|         fatalError("Subclass must implement") | ||||
|     } | ||||
|      | ||||
|     // Public API methods | ||||
|     public func show(request: MRecShowRequest? = nil) -> Bool { | ||||
|         if isCurrent(activeState) || isCurrent(hiddenState) { | ||||
|             requestShow(request) | ||||
|             return true | ||||
|         } | ||||
|         return false | ||||
|     } | ||||
|      | ||||
|     public func hide() -> Bool { | ||||
|         if isCurrent(showingState) { | ||||
|             requestHide() | ||||
|             return true | ||||
|         } | ||||
|         return false | ||||
|     } | ||||
|      | ||||
|     public func load() -> Bool { | ||||
|         if isCurrent(idleState) { | ||||
|             requestLoad() | ||||
|             return true | ||||
|         } | ||||
|         return false | ||||
|     } | ||||
|      | ||||
|     public func destroy() -> Bool { | ||||
|         requestDestroy() | ||||
|         return true | ||||
|     } | ||||
| } | ||||
|  | @ -0,0 +1,390 @@ | |||
| // DefaultRewardedAdEngine.swift | ||||
| // Default implementation of RewardedAdEngine | ||||
| // Corresponds to DefaultRewardedAdEngine.kt in Android implementation | ||||
| 
 | ||||
| import Foundation | ||||
| import UIKit | ||||
| 
 | ||||
| public class DefaultRewardedAdEngine: RewardedAdEngine { | ||||
|      | ||||
|     private let ad: GuruRewardedAd | ||||
|      | ||||
|     // Constants | ||||
|     private enum Action { | ||||
|         static let LOAD = 1 | ||||
|         static let SHOW = 2 | ||||
|         static let DESTROY = 3 | ||||
|         static let RETRY = 4 | ||||
|     } | ||||
|      | ||||
|     private enum Event { | ||||
|         static let ON_LOADED = 100 | ||||
|         static let ON_LOAD_FAILED = 101 | ||||
|         static let ON_DISPLAYED = 102 | ||||
|         static let ON_DISPLAY_FAILED = 103 | ||||
|         static let ON_CLICK = 104 | ||||
|         static let ON_HIDDEN = 105 | ||||
|         static let ON_USER_REWARDED = 106 | ||||
|         static let REVENUE_PAID = 107 | ||||
|         static let LOAD_TIMEOUT = 108 | ||||
|         static let SHOW_TIMEOUT = 109 | ||||
|     } | ||||
|      | ||||
|     private static let LOAD_TIMEOUT_MILLIS: Int = 120000 | ||||
|     private static let SHOW_TIMEOUT_MILLIS: Int = 120000 | ||||
|      | ||||
|     public override var supportedAdPlatforms: Set<AdPlatform> { | ||||
|         return [ad.adPlatform] | ||||
|     } | ||||
|      | ||||
|     public init(viewController: UIViewController, id: Int, ad: GuruRewardedAd) { | ||||
|         self.ad = ad | ||||
|         super.init(viewController: viewController, id: id, adType: .Rewarded, strategyName: ad.adPlatform.name) | ||||
|         setupAdListener() | ||||
|         initialize() | ||||
|     } | ||||
|      | ||||
|     private func setupAdListener() { | ||||
|         ad.listener = FusionAdListenerImpl { [weak self] event in | ||||
|             guard let self = self else { return } | ||||
|              | ||||
|             switch event { | ||||
|             case .onAdLoaded(let ad): | ||||
|                 self.sendMessage(what: Event.ON_LOADED, obj: EventParams(ad: ad)) | ||||
|                  | ||||
|             case .onAdDisplayed(let ad): | ||||
|                 self.sendMessage(what: Event.ON_DISPLAYED, obj: EventParams(ad: ad)) | ||||
|                  | ||||
|             case .onAdHidden(let ad): | ||||
|                 self.sendMessage(what: Event.ON_HIDDEN, obj: EventParams(ad: ad)) | ||||
|                  | ||||
|             case .onUserRewarded(let ad, let reward): | ||||
|                 self.sendMessage(what: Event.ON_USER_REWARDED, obj: EventParams(ad: ad, reward: reward)) | ||||
|                  | ||||
|             case .onAdClicked(let ad): | ||||
|                 self.sendMessage(what: Event.ON_CLICK, obj: EventParams(ad: ad)) | ||||
|                  | ||||
|             case .onAdLoadFailed(let loadFailedInfo): | ||||
|                 self.sendMessage(what: Event.ON_LOAD_FAILED, obj: EventParams(loadFailedInfo: loadFailedInfo)) | ||||
|                  | ||||
|             case .onAdDisplayFailed(let ad, let error): | ||||
|                 self.sendMessage(what: Event.ON_DISPLAY_FAILED, obj: EventParams(ad: ad, displayFailedInfo: error)) | ||||
|                  | ||||
|             case .onAdRevenuePaid(let ad): | ||||
|                 self.sendMessage(what: Event.REVENUE_PAID, obj: EventParams(ad: ad)) | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     // MARK: - State Classes | ||||
|      | ||||
|     public override func createIdleState() -> Idle { | ||||
|         return IdleImpl(self) | ||||
|     } | ||||
|      | ||||
|     public override func createLoadingState() -> Loading { | ||||
|         return LoadingImpl(self) | ||||
|     } | ||||
|      | ||||
|     public override func createLoadedState() -> Loaded { | ||||
|         return LoadedImpl(self) | ||||
|     } | ||||
|      | ||||
|     public override func createShowingState() -> Showing { | ||||
|         return ShowingImpl(self) | ||||
|     } | ||||
|      | ||||
|     // MARK: - Request Methods | ||||
|      | ||||
|     override internal func requestLoad() { | ||||
|         sendMessage(what: Action.LOAD) | ||||
|     } | ||||
|      | ||||
|     override internal func requestShow(_ request: RewardedShowRequest? = nil) { | ||||
|         sendMessage(what: Action.SHOW, obj: request) | ||||
|     } | ||||
|      | ||||
|     override internal func requestDestroy() { | ||||
|         sendMessage(what: Action.DESTROY) | ||||
|     } | ||||
|      | ||||
|     // MARK: - Helper Methods | ||||
|      | ||||
|     private func handleUnhandledMessage(_ msg: Message) -> Bool { | ||||
|         switch msg.what { | ||||
|         case Action.DESTROY: | ||||
|             ad.destroy() | ||||
|             transitionTo(idleState) | ||||
|             return true | ||||
|         default: | ||||
|             return false | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     // MARK: - State Implementations | ||||
|      | ||||
|     class IdleImpl: Idle { | ||||
|         private let engine: DefaultRewardedAdEngine | ||||
|          | ||||
|         init(_ engine: DefaultRewardedAdEngine) { | ||||
|             self.engine = engine | ||||
|             super.init() | ||||
|         } | ||||
|          | ||||
|         public override func enter(from: IState?, params: Any?) { | ||||
|             super.enter(from: from, params: params) | ||||
|         } | ||||
|          | ||||
|         public override func processMessage(_ msg: Message) -> Bool { | ||||
|              | ||||
|             switch msg.what { | ||||
|             case Action.LOAD: | ||||
|                 if engine.ad.load() { | ||||
|                     engine.transitionTo(engine.loadingState) | ||||
|                 } else { | ||||
|                     engine.adLoadFailed(loadFailedInfo: LoadFailedInfo( | ||||
|                         engineId: engine.id, | ||||
|                         adPlatform: engine.ad.adPlatform, | ||||
|                         adType: engine.adType, | ||||
|                         adUnitId: engine.ad.adUnitId, | ||||
|                         error: LoadAdRequestError() | ||||
|                     )) | ||||
|                 } | ||||
|                 return true | ||||
|             default: | ||||
|                 return engine.handleUnhandledMessage(msg) || super.processMessage(msg) | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     class LoadingImpl: Loading { | ||||
|         private var retryCount = 0 | ||||
|         private let engine: DefaultRewardedAdEngine | ||||
|          | ||||
|         init(_ engine: DefaultRewardedAdEngine) { | ||||
|             self.engine = engine | ||||
|             super.init() | ||||
|         } | ||||
|          | ||||
|         public override func enter(from: IState?, params: Any?) { | ||||
|             super.enter(from: from, params: params) | ||||
|             retryCount = 0 | ||||
|              | ||||
|             // Set load timeout | ||||
|             engine.sendMessageDelayed(what: Event.LOAD_TIMEOUT, delayMillis: DefaultRewardedAdEngine.LOAD_TIMEOUT_MILLIS) | ||||
|         } | ||||
|          | ||||
|         public override func exit(to: IState?) { | ||||
|             engine.removeMessages(what: Event.LOAD_TIMEOUT) | ||||
|             engine.removeMessages(what: Action.RETRY) | ||||
|             retryCount = 0 | ||||
|             super.exit(to: to) | ||||
|         } | ||||
|          | ||||
|         public override func processMessage(_ msg: Message) -> Bool { | ||||
|              | ||||
|             switch msg.what { | ||||
|             case Action.RETRY: | ||||
|                 retryCount += 1 | ||||
|                 if !engine.ad.load() { | ||||
|                     engine.sendMessage(what: Event.ON_LOAD_FAILED, obj: EventParams( | ||||
|                         loadFailedInfo: LoadFailedInfo( | ||||
|                             engineId: engine.id, | ||||
|                             adPlatform: engine.ad.adPlatform, | ||||
|                             adType: engine.adType, | ||||
|                             adUnitId: engine.ad.adUnitId, | ||||
|                             error: LoadAdRequestError() | ||||
|                         ) | ||||
|                     )) | ||||
|                 } else { | ||||
|                     engine.sendMessageDelayed(what: Event.LOAD_TIMEOUT, delayMillis: DefaultRewardedAdEngine.LOAD_TIMEOUT_MILLIS) | ||||
|                 } | ||||
|                 return true | ||||
|                  | ||||
|             case Action.LOAD: | ||||
|                 engine.logWarn("Already loading! Ignore load request") | ||||
|                 return true | ||||
|                  | ||||
|             case Event.LOAD_TIMEOUT: | ||||
|                 engine.removeMessages(what: Action.RETRY) | ||||
|                 engine.sendMessage(what: Action.RETRY) | ||||
|                 return true | ||||
|                  | ||||
|             case Event.ON_LOADED: | ||||
|                 if let eventParams = msg.obj as? EventParams, eventParams.ad != nil { | ||||
|                     engine.transitionTo(engine.loadedState, params: eventParams) | ||||
|                 } | ||||
|                 engine.deferMessage(msg) | ||||
|                 return true | ||||
|                  | ||||
|             case Event.ON_LOAD_FAILED: | ||||
|                 let params = msg.obj as? EventParams | ||||
|                 engine.adLoadFailed(loadFailedInfo: params?.loadFailedInfo ?? LoadFailedInfo( | ||||
|                     engineId: engine.id, | ||||
|                     adPlatform: engine.ad.adPlatform, | ||||
|                     adType: engine.adType, | ||||
|                     adUnitId: engine.ad.adUnitId, | ||||
|                     error: LoadAdRequestError() | ||||
|                 )) | ||||
|                 engine.transitionTo(engine.idleState) | ||||
|                 return true | ||||
|                  | ||||
|             default: | ||||
|                 return engine.handleUnhandledMessage(msg) || super.processMessage(msg) | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     class LoadedImpl: Loaded { | ||||
|         private let engine: DefaultRewardedAdEngine | ||||
|          | ||||
|         init(_ engine: DefaultRewardedAdEngine) { | ||||
|             self.engine = engine | ||||
|             super.init() | ||||
|         } | ||||
|          | ||||
|         public override func enter(from: IState?, params: Any?) { | ||||
|             super.enter(from: from, params: params) | ||||
|              | ||||
|             if let eventParams = params as? EventParams, let ad = eventParams.ad { | ||||
|                 engine.adLoaded(ad: ad) | ||||
|             } | ||||
|         } | ||||
|          | ||||
|         public override func processMessage(_ msg: Message) -> Bool { | ||||
|             switch msg.what { | ||||
|             case Action.SHOW: | ||||
|                 let request = msg.obj as? RewardedShowRequest | ||||
|                 if engine.ad.show(request) { | ||||
|                     engine.transitionTo(engine.showingState) | ||||
|                 } else { | ||||
|                     engine.logWarn("Failed to show rewarded ad") | ||||
|                     // no FusionAd here | ||||
| //                    engine.adDisplayFailed(ad: engine.ad, error: ShowAdRequestError()) | ||||
|                     if(engine.ad.load()) { | ||||
|                         engine.transitionTo(engine.loadingState) | ||||
|                     } | ||||
|                 } | ||||
|                 return true | ||||
|                  | ||||
|             default: | ||||
|                 return engine.handleUnhandledMessage(msg) || super.processMessage(msg) | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     class ShowingImpl: Showing { | ||||
|         private let engine: DefaultRewardedAdEngine | ||||
|          | ||||
|         init(_ engine: DefaultRewardedAdEngine) { | ||||
|             self.engine = engine | ||||
|             super.init() | ||||
|         } | ||||
|          | ||||
|         public override func enter(from: IState?, params: Any?) { | ||||
|             super.enter(from: from, params: params) | ||||
|             engine.sendMessageDelayed(what: Event.SHOW_TIMEOUT, delayMillis: SHOW_TIMEOUT_MILLIS) | ||||
|         } | ||||
|          | ||||
|         public override func processMessage(_ msg: Message) -> Bool { | ||||
|              | ||||
|             switch msg.what { | ||||
|                  | ||||
|             case Event.ON_DISPLAYED: | ||||
|                 if let eventParams = msg.obj as? EventParams, let ad = eventParams.ad { | ||||
|                     engine.adDisplayed(ad: ad) | ||||
|                 } | ||||
|                 engine.removeMessages(what: Event.SHOW_TIMEOUT) | ||||
|                 return true | ||||
| 
 | ||||
|             case Event.ON_CLICK: | ||||
|                 if let eventParams = msg.obj as? EventParams, let ad = eventParams.ad { | ||||
|                     engine.adClicked(ad: ad) | ||||
|                 } | ||||
|                 return true | ||||
|                  | ||||
|             case Event.ON_HIDDEN: | ||||
|                 if let eventParams = msg.obj as? EventParams, let ad = eventParams.ad { | ||||
|                     engine.adHidden(ad: ad) | ||||
|                     engine.transitionTo(engine.idleState) | ||||
|                 } | ||||
|                 return true | ||||
|                  | ||||
|             case Event.ON_USER_REWARDED: | ||||
|                 if let eventParams = msg.obj as? EventParams, let ad = eventParams.ad { | ||||
|                     engine.adUserRewarded(ad: ad, reward: eventParams.reward) | ||||
|                 } | ||||
|                 return true | ||||
|                  | ||||
|             case Event.REVENUE_PAID: | ||||
|                 if let eventParams = msg.obj as? EventParams, let ad = eventParams.ad { | ||||
|                     engine.adRevenuePaid(ad: ad) | ||||
|                 } | ||||
|                 return true | ||||
|                  | ||||
|             case Event.ON_DISPLAY_FAILED: | ||||
|                 if let eventParams = msg.obj as? EventParams, let ad = eventParams.ad { | ||||
|                     engine.adDisplayFailed(ad: ad, error: eventParams.displayFailedInfo) | ||||
|                 } | ||||
|                 engine.transitionTo(engine.idleState) | ||||
|                 engine.requestLoad() | ||||
|                 return true | ||||
|                  | ||||
|             default: | ||||
|                 return engine.handleUnhandledMessage(msg) || super.processMessage(msg) | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| // Helper class to implement the FusionAdListener protocol | ||||
| private class FusionAdListenerImpl: FusionAdListener { | ||||
|     enum Event { | ||||
|         case onAdLoaded(ad: FusionAd) | ||||
|         case onAdDisplayed(ad: FusionAd) | ||||
|         case onAdHidden(ad: FusionAd) | ||||
|         case onUserRewarded(ad: FusionAd, reward: FusionReward?) | ||||
|         case onAdClicked(ad: FusionAd) | ||||
|         case onAdLoadFailed(loadFailedInfo: LoadFailedInfo) | ||||
|         case onAdDisplayFailed(ad: FusionAd, error: FusionError?) | ||||
|         case onAdRevenuePaid(ad: FusionAd) | ||||
|     } | ||||
|      | ||||
|     private let handler: (Event) -> Void | ||||
|      | ||||
|     init(handler: @escaping (Event) -> Void) { | ||||
|         self.handler = handler | ||||
|     } | ||||
|      | ||||
|     func onAdLoaded(ad: FusionAd) { | ||||
|         handler(.onAdLoaded(ad: ad)) | ||||
|     } | ||||
|      | ||||
|     func onAdDisplayed(ad: FusionAd) { | ||||
|         handler(.onAdDisplayed(ad: ad)) | ||||
|     } | ||||
|      | ||||
|     func onAdHidden(ad: FusionAd) { | ||||
|         handler(.onAdHidden(ad: ad)) | ||||
|     } | ||||
|      | ||||
|     func onUserRewarded(ad: FusionAd, reward: FusionReward?) { | ||||
|         handler(.onUserRewarded(ad: ad, reward: reward)) | ||||
|     } | ||||
|      | ||||
|     func onAdClicked(ad: FusionAd) { | ||||
|         handler(.onAdClicked(ad: ad)) | ||||
|     } | ||||
|      | ||||
|     func onAdLoadFailed(loadFailedInfo: LoadFailedInfo) { | ||||
|         handler(.onAdLoadFailed(loadFailedInfo: loadFailedInfo)) | ||||
|     } | ||||
|      | ||||
|     func onAdDisplayFailed(ad: FusionAd, error: FusionError?) { | ||||
|         handler(.onAdDisplayFailed(ad: ad, error: error)) | ||||
|     } | ||||
|      | ||||
|     func onAdRevenuePaid(ad: FusionAd) { | ||||
|         handler(.onAdRevenuePaid(ad: ad)) | ||||
|     } | ||||
| } | ||||
|  | @ -0,0 +1,144 @@ | |||
| // RewardedAdEngine.swift | ||||
| // Base class for rewarded ad engines | ||||
| // Corresponds to RewardedAdEngine.kt in Android implementation | ||||
| 
 | ||||
| import Foundation | ||||
| import UIKit | ||||
| 
 | ||||
| public class RewardedAdEngine: RelayAdEngine { | ||||
|      | ||||
|     private(set) var idleState: Idle! | ||||
|     private(set) var loadingState: Loading! | ||||
|     private(set) var loadedState: Loaded! | ||||
|     private(set) var showingState: Showing! | ||||
|      | ||||
|     public var currentStateIdentifier: AdState.Identifier { | ||||
|         return (currentState as? AdState)?.stateId ?? AdState.Identifier(name: currentState?.name ?? "unknown") | ||||
|     } | ||||
|      | ||||
|     public override init(viewController: UIViewController, id: Int, adType: AdType, strategyName: String) { | ||||
|         super.init(viewController: viewController, id: id, adType: .Rewarded, strategyName: strategyName) | ||||
|     } | ||||
|      | ||||
|     public func initialize() { | ||||
|         idleState = createIdleState() | ||||
|         loadingState = createLoadingState() | ||||
|         loadedState = createLoadedState() | ||||
|         showingState = createShowingState() | ||||
|          | ||||
|         addState(idleState) | ||||
|         addState(loadingState) | ||||
|         addState(loadedState) | ||||
|         addState(showingState) | ||||
|         setInitialState(idleState) | ||||
|         start() | ||||
|     } | ||||
|      | ||||
|     // State classes | ||||
|     public class AdState: State { | ||||
|         public let stateId: Identifier | ||||
|          | ||||
|         public init(name: String, stateId: Identifier) { | ||||
|             self.stateId = stateId | ||||
|             super.init() | ||||
|         } | ||||
|          | ||||
|         public override var name: String { | ||||
|             return stateId.name | ||||
|         } | ||||
|          | ||||
|         public class Identifier: Equatable { | ||||
|             public let name: String | ||||
|              | ||||
|             public init(name: String) { | ||||
|                 self.name = name | ||||
|             } | ||||
|              | ||||
|             // Pre-defined identifiers | ||||
|             public static let IDLE = Identifier(name: "idle") | ||||
|             public static let LOADING = Identifier(name: "loading") | ||||
|             public static let LOADED = Identifier(name: "loaded") | ||||
|             public static let SHOWING = Identifier(name: "showing") | ||||
|              | ||||
|             public static func == (lhs: Identifier, rhs: Identifier) -> Bool { | ||||
|                 return lhs.name == rhs.name | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     public class Idle: AdState { | ||||
|         public init() { | ||||
|             super.init(name: "[IDLE]", stateId: Identifier.IDLE) | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     public class Loading: AdState { | ||||
|         public init() { | ||||
|             super.init(name: "[LOADING]", stateId: Identifier.LOADING) | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     public class Loaded: AdState { | ||||
|         public init() { | ||||
|             super.init(name: "[LOADED]", stateId: Identifier.LOADED) | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     public class Showing: AdState { | ||||
|         public init() { | ||||
|             super.init(name: "[SHOWING]", stateId: Identifier.SHOWING) | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     // Factory methods for creating the states | ||||
|     open func createIdleState() -> Idle { | ||||
|         return Idle() | ||||
|     } | ||||
|      | ||||
|     open func createLoadingState() -> Loading { | ||||
|         return Loading() | ||||
|     } | ||||
|      | ||||
|     open func createLoadedState() -> Loaded { | ||||
|         return Loaded() | ||||
|     } | ||||
|      | ||||
|     open func createShowingState() -> Showing { | ||||
|         return Showing() | ||||
|     } | ||||
|      | ||||
|     // Abstract methods to be implemented by subclasses | ||||
|     internal func requestLoad() { | ||||
|         fatalError("Subclass must implement") | ||||
|     } | ||||
|      | ||||
|     internal func requestShow(_ request: RewardedShowRequest? = nil) { | ||||
|         fatalError("Subclass must implement") | ||||
|     } | ||||
|      | ||||
|     internal func requestDestroy() { | ||||
|         fatalError("Subclass must implement") | ||||
|     } | ||||
|      | ||||
|     // Public API methods | ||||
|     public func show(_ request: RewardedShowRequest? = nil) -> Bool { | ||||
|         if isCurrent(loadedState) { | ||||
|             requestShow(request) | ||||
|             return true | ||||
|         } | ||||
|         return false | ||||
|     } | ||||
|      | ||||
|     public func load() -> Bool { | ||||
|         if isCurrent(idleState) { | ||||
|             requestLoad() | ||||
|             return true | ||||
|         } | ||||
|         return false | ||||
|     } | ||||
|      | ||||
|     public func destroy() -> Bool { | ||||
|         requestDestroy() | ||||
|         return true | ||||
|     } | ||||
| } | ||||
|  | @ -0,0 +1,97 @@ | |||
| // | ||||
| //  MabAnalyticsEvents.swift | ||||
| //  Pods | ||||
| // | ||||
| //  Created by 250102 on 2025/5/10. | ||||
| // | ||||
| 
 | ||||
| // 单例实现 | ||||
| internal class MabAnalyticEvents { | ||||
|     // 单例访问点 | ||||
|     static let shared = MabAnalyticEvents() | ||||
|      | ||||
|     // 私有初始化方法确保单例模式 | ||||
|     private init() {} | ||||
|      | ||||
|     static func faLoad( | ||||
|         faUnitId: String, | ||||
|         adType: AdType, | ||||
|         aggregatorId: String, | ||||
|         adUnitId: String, | ||||
|         requestReason: RequestReason? | ||||
|     ) { | ||||
|         var params: [String: Any] = [:] | ||||
|         params["fa_unitid"] = faUnitId | ||||
|         params["aggregator_id"] = aggregatorId | ||||
|         if let requestReason = requestReason { | ||||
|             params["req_type"] = requestReason.statsName | ||||
|         } | ||||
|         params["ad_format"] = adType.label | ||||
|         params["ad_unit_name"] = shrink(adUnitId) | ||||
|          | ||||
|         AnalyticsEvents.shared.onEvent(name: "fa_load", param: params) | ||||
|     } | ||||
|      | ||||
|     static func faLoaded( | ||||
|         faUnitId: String, | ||||
|         adType: AdType, | ||||
|         adSource: String?, | ||||
|         aggregatorId: String, | ||||
|         adUnitId: String, | ||||
|         durationInMillis: Int64? | ||||
|     ) { | ||||
|         guard let durationInMillis = durationInMillis else { | ||||
|             return | ||||
|         } | ||||
|          | ||||
|         AnalyticsEvents.shared.onEvent( | ||||
|             name: "fa_loaded", | ||||
|             pairs: | ||||
|                 ("fa_unitid", faUnitId), | ||||
|                 ("aggregator_id", aggregatorId), | ||||
|                 ("ad_format", adType.label), | ||||
|                 ("duration", durationInMillis), | ||||
|                 ("ad_unit_name", shrink(adUnitId)), | ||||
|                 ("ad_source", (adSource ?? "unknown")) | ||||
|         ) | ||||
|     } | ||||
|      | ||||
|     static func faFailed( | ||||
|         faUnitId: String, | ||||
|         adType: AdType, | ||||
|         aggregatorId: String, | ||||
|         adUnitId: String, | ||||
|         durationInMillis: Int64?, | ||||
|         errorCode: Int | ||||
|     ) { | ||||
|         guard let durationInMillis = durationInMillis else { | ||||
|             return | ||||
|         } | ||||
|          | ||||
|         AnalyticsEvents.shared.onEvent( | ||||
|             name: "fa_failed", | ||||
|             pairs: | ||||
|                 ("fa_unitid", faUnitId), | ||||
|                 ("aggregator_id", aggregatorId), | ||||
|                 ("ad_format", adType.label), | ||||
|                 ("duration", durationInMillis), | ||||
|                 ("ad_unit_name", shrink(adUnitId)), | ||||
|                 ("error_code", errorCode) | ||||
|         ) | ||||
|     } | ||||
|      | ||||
|     /** | ||||
|      * 精简adUnitId以便于不过多占用打点数据包 | ||||
|      */ | ||||
|     private static func shrink(_ adUnitId: String) -> String { | ||||
|         // shrink a adUnitId from AdMob to a shorter form, | ||||
|         // for example, ca-app-pub-2436733915645843/9966308712 | ||||
|         // shrink("ca-app-pub-2436733915645843/9966308712") == "9966308712" | ||||
|         if adUnitId.hasPrefix("ca-app-pub-") && adUnitId.count == 38 && adUnitId[adUnitId.index(adUnitId.startIndex, offsetBy: 27)] == "/" { | ||||
|             return String(adUnitId.suffix(10)) | ||||
|         } | ||||
|          | ||||
|         // no need to shrink | ||||
|         return adUnitId | ||||
|     } | ||||
| } | ||||
|  | @ -0,0 +1,544 @@ | |||
| import Foundation | ||||
| 
 | ||||
| // AggregatorBid 私有结构体 | ||||
| private struct AggregatorBid { | ||||
|     let config: AggregatorConfig | ||||
|     /** | ||||
|      * 是否进行自抬价。 | ||||
|      * 在某个聚合show完成时,会向竞价队列头部加入一个此聚合当前 revenue * selfRaise 自抬价的bid | ||||
|      * 在设置为true时,同时设置[floor]为revenue,基于此值进行自抬价 | ||||
|      */ | ||||
|     let useSelfRaise: Bool | ||||
|     /** | ||||
|      * 当前bid的固定底价,若为nil,则使用winners中最大的revenue, see [MabWinners.maxRevenue] | ||||
|      * 单位为美元 每次展示 | ||||
|      */ | ||||
|     let floor: Double? | ||||
|      | ||||
|     /** | ||||
|      * 本次参与竞价请求的原因 | ||||
|      */ | ||||
|     let reason: RequestReason? | ||||
|      | ||||
|     init(config: AggregatorConfig, useSelfRaise: Bool = false, floor: Double? = nil, reason: RequestReason? = nil) { | ||||
|         self.config = config | ||||
|         self.useSelfRaise = useSelfRaise | ||||
|         self.floor = floor | ||||
|         self.reason = reason | ||||
|     } | ||||
|      | ||||
|     var raiseRate: Double { | ||||
|         return useSelfRaise ? config.selfRaiseRate ?? config.raiseRate : config.raiseRate | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| // 扩展属性 | ||||
| extension AggregatorConfig { | ||||
|     var deferRetryUntilShown: Bool { | ||||
|         return adPlatform == AdPlatform.adMob.name | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  * 储存已填充的广告 | ||||
|  */ | ||||
| private class MabWinners<T: MabBidder> { | ||||
|     private var winners: [String: T] = [:] | ||||
|     private let onEvict: ((T) -> Void)? | ||||
|      | ||||
|     init(onEvict: ((T) -> Void)? = nil) { | ||||
|         self.onEvict = onEvict | ||||
|     } | ||||
|      | ||||
|     /** | ||||
|      * 清除已过期的缓存 | ||||
|      */ | ||||
|     func evictCache() { | ||||
|         let expiredKeys = winners.filter { (_, bidder) in | ||||
|             if bidder.isExpired != false { | ||||
|                 onEvict?(bidder) | ||||
|                 return true | ||||
|             } | ||||
|             return false | ||||
|         }.map { $0.key } | ||||
|          | ||||
|         for key in expiredKeys { | ||||
|             winners.removeValue(forKey: key) | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     /** | ||||
|      * 添加一个已填充的广告 | ||||
|      */ | ||||
|     func add(bidder: T) { | ||||
|         evictCache() | ||||
|         winners[bidder.aggregatorId] = bidder | ||||
|     } | ||||
|      | ||||
|     /** | ||||
|      * 是否包含此聚合的已填充广告 | ||||
|      */ | ||||
|     func contains(aggregatorId: String) -> Bool { | ||||
|         evictCache() | ||||
|         return winners[aggregatorId] != nil | ||||
|     } | ||||
|      | ||||
|     /** | ||||
|      * 已填充广告内当前最高的填充价格,若无广告填充或全部过期则返回0 | ||||
|      */ | ||||
|     func maxRevenue() -> Double { | ||||
|         evictCache() | ||||
|         if winners.isEmpty { | ||||
|             return 0.0 | ||||
|         } | ||||
|         return winners.values.map { $0.revenue }.max() ?? 0.0 | ||||
|     } | ||||
|      | ||||
|     /** | ||||
|      * 查看当前最高填充价格的广告 | ||||
|      */ | ||||
|     func peekMaxRevenue() -> T? { | ||||
|         evictCache() | ||||
|         return winners.values.max(by: { | ||||
|             if($0.revenue == $1.revenue) { | ||||
|                 // 优先级数值越低(优先级越高)越排序靠后(升序排列) | ||||
|                 return $0.config.priority > $1.config.priority | ||||
|             } | ||||
|             return $0.revenue < $1.revenue | ||||
|         }) | ||||
|     } | ||||
|      | ||||
|     /** | ||||
|      * 移出当前最高填充价格的广告 | ||||
|      */ | ||||
|     func pollMaxRevenue() -> T? { | ||||
|         evictCache() | ||||
|         guard let entry = peekMaxRevenue() else { | ||||
|             return nil | ||||
|         } | ||||
|         return winners.removeValue(forKey: entry.aggregatorId) | ||||
|     } | ||||
|      | ||||
|     /** | ||||
|      * 是否存在已填充广告 | ||||
|      */ | ||||
|     func isEmpty() -> Bool { | ||||
|         evictCache() | ||||
|         return winners.isEmpty | ||||
|     } | ||||
|      | ||||
|     /** | ||||
|      * 清空所有填充广告 | ||||
|      */ | ||||
|     func clear() { | ||||
|         winners.removeAll() | ||||
|     } | ||||
|      | ||||
|     func buildStateLog() -> String { | ||||
|         var result = "winners[" | ||||
|         for winner in winners { | ||||
|             result += "\(winner.key)(\(winner.value.revenue))," | ||||
|         } | ||||
|         result += "]" | ||||
|         return result | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  * 对多次load敏感的聚合(如AdMob)进行load重试控制,确保在首次load后(无论失败或成功),后续load与重新load必须在一次广告show之后才能进行。 | ||||
|  * 换句话说-> "每次广告show完,此聚合重新获得一次load机会(不能累加)" | ||||
|  */ | ||||
| private class RetryGate { | ||||
|     // Swift没有AtomicInteger,所以使用一个类包装 | ||||
|     private class AtomicInteger { | ||||
|         private var value: Int | ||||
|          | ||||
|         init(_ initialValue: Int = 0) { | ||||
|             self.value = initialValue | ||||
|         } | ||||
|          | ||||
|         func get() -> Int { | ||||
|             return value | ||||
|         } | ||||
|          | ||||
|         func set(_ newValue: Int) { | ||||
|             value = newValue | ||||
|         } | ||||
|          | ||||
|         func incrementAndGet() -> Int { | ||||
|             value += 1 | ||||
|             return value | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     private var version = AtomicInteger()  // 每次ad show成功时更新一个版本 | ||||
|     private var lastUsed: [String: AtomicInteger] = [:] | ||||
|      | ||||
|     func onAdShown() { | ||||
|         _ = version.incrementAndGet() | ||||
|     } | ||||
|      | ||||
|     /** | ||||
|      * 对比重试版本与show版本,若不一致则代表有机会进行retry,若一致则代表需等待下次show完 | ||||
|      */ | ||||
|     func canRetry(aggregatorId: String) -> Bool { | ||||
|         return getLastUsed(aggregatorId: aggregatorId).get() < version.get() | ||||
|     } | ||||
|      | ||||
|     /** | ||||
|      * 标记本次重试版本 | ||||
|      */ | ||||
|     func markRetryUsed(aggregatorId: String) { | ||||
|         getLastUsed(aggregatorId: aggregatorId).set(version.get()) | ||||
|     } | ||||
|      | ||||
|     private func getLastUsed(aggregatorId: String) -> AtomicInteger { | ||||
|         if let lastUsed = lastUsed[aggregatorId] { | ||||
|             return lastUsed | ||||
|         } else { | ||||
|             let atomicInteger = AtomicInteger(-1) | ||||
|             lastUsed[aggregatorId] = atomicInteger | ||||
|             return atomicInteger | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| private func selectAdUnitSpec( | ||||
|     aggregatorConfig: AggregatorConfig, | ||||
|     floor: Double, | ||||
|     selfRaise: Bool = false | ||||
| ) -> AdUnitSpec? { | ||||
|     let adUnitIds = aggregatorConfig.adUnitIds | ||||
|     if adUnitIds.isEmpty { | ||||
|         return nil | ||||
|     } | ||||
|      | ||||
|     let targetFloor = floor * (selfRaise ? aggregatorConfig.selfRaiseRate ?? aggregatorConfig.raiseRate : aggregatorConfig.raiseRate) | ||||
|      | ||||
|     let firstGtIndex = adUnitIds.firstIndex { $0.floor >= targetFloor } | ||||
|      | ||||
|     if firstGtIndex == nil { | ||||
|         return adUnitIds.last | ||||
|     } | ||||
|      | ||||
|     if firstGtIndex == 0 { | ||||
|         return adUnitIds.first | ||||
|     } | ||||
|      | ||||
|     // 寻找最接近targetFloor的一个 | ||||
|     let index = firstGtIndex! | ||||
|     let nearestIndex = (abs(targetFloor - adUnitIds[index].floor) <= abs(adUnitIds[index - 1].floor - targetFloor)) ? index : index - 1 | ||||
|      | ||||
|     let selection = adUnitIds[nearestIndex] | ||||
|      | ||||
|     return selection | ||||
| } | ||||
| 
 | ||||
| enum RequestReason: String { | ||||
|     case INIT = "init" | ||||
|     case FAIL = "fail" | ||||
|     case SHOW = "show" | ||||
|     case EXPIRATION = "exp" | ||||
|      | ||||
|     var statsName: String { | ||||
|         return rawValue | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| protocol MabBidder { | ||||
|     var id: Int { get } | ||||
|     var config: AggregatorConfig { get } | ||||
|     var isExpired: Bool? { get } | ||||
|     var revenue: Double { get } | ||||
|      | ||||
|     func load(reason: RequestReason?) -> Bool | ||||
|     func destroy() | ||||
| } | ||||
| 
 | ||||
| extension MabBidder { | ||||
|     var aggregatorId: String { | ||||
|         return config.aggregatorId | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| // 静态方法转换为类函数 | ||||
| class MabBidderUtils { | ||||
|     static func adjustRevenue(ad: FusionAd, floor: Double) -> Double { | ||||
|         switch ad.adPlatform { | ||||
|         case .max: | ||||
|             return ad.revenue | ||||
|         case .ironSource: | ||||
|             return max(ad.revenue, floor) | ||||
|         case .adMob: | ||||
|             return max(ad.revenue, floor) | ||||
|         default: | ||||
|             return max(ad.revenue, floor) | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| class MabAuctioneer<T: MabBidder> { | ||||
|     enum RequestResult { | ||||
|         case success | ||||
|         case busy | ||||
|         case noAvailableSource | ||||
|     } | ||||
|      | ||||
|     private let tag: String | ||||
|     private let faUnitId: String | ||||
|     private let priorityAggregators: [AggregatorConfig] | ||||
|     private var candidatesQueue: [AggregatorBid] = [] | ||||
|     private let bidderFactory: (AggregatorConfig, AdUnitSpec) -> T? | ||||
|      | ||||
|     private var bidding: T? | ||||
|     private var bidders: [String: T] = [:] | ||||
|      | ||||
|     private let winners: MabWinners<T> | ||||
|     private let retryGate = RetryGate() | ||||
|      | ||||
|     var hasWinner: Bool { | ||||
|         return !winners.isEmpty() | ||||
|     } | ||||
|      | ||||
|     init( | ||||
|         engineId: Int, | ||||
|         faUnitId: String, | ||||
|         aggregatorConfigs: [AggregatorConfig], | ||||
|         bidderFactory: @escaping (AggregatorConfig, AdUnitSpec) -> T? | ||||
|     ) { | ||||
|         self.tag = "MabAuctioneer@\(engineId)" | ||||
|         self.faUnitId = faUnitId | ||||
|         self.priorityAggregators = aggregatorConfigs.sorted(by: { $0.priority < $1.priority }) | ||||
|         self.bidderFactory = bidderFactory | ||||
|          | ||||
|         self.winners = MabWinners<T>() | ||||
|     } | ||||
|      | ||||
|     private func logI(_ message: String) { | ||||
|         Logger.i(tag: tag, message: message) | ||||
|     } | ||||
|      | ||||
|     private func logD(_ message: String) { | ||||
|         Logger.d(tag: tag, message: message) | ||||
|     } | ||||
|      | ||||
|     private func logW(_ message: String) { | ||||
|         Logger.w(tag: tag, message: message) | ||||
|     } | ||||
|      | ||||
|     private func logE(_ message: String) { | ||||
|         Logger.e(tag: tag, message: message) | ||||
|     } | ||||
|      | ||||
|     private func logState() { | ||||
|         logI("mab state: \(buildStateLog())") | ||||
|     } | ||||
|      | ||||
|     private func obtainBidder( | ||||
|         config: AggregatorConfig, | ||||
|         adUnitSpec: AdUnitSpec | ||||
|     ) -> T? { | ||||
|         if let bidder = bidders[adUnitSpec.adUnitId] { | ||||
|             return bidder | ||||
|         } else { | ||||
|             if let newBidder = bidderFactory(config, adUnitSpec) { | ||||
|                 bidders[adUnitSpec.adUnitId] = newBidder | ||||
|                 return newBidder | ||||
|             } | ||||
|             return nil | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     func requestNext() -> RequestResult { | ||||
|         logI("requestNext") | ||||
|         let result = internalRequestNext() | ||||
|         logI("requestNext result \(result)") | ||||
|         logState() | ||||
|         return result | ||||
|     } | ||||
|      | ||||
|     private func internalRequestNext() -> RequestResult { | ||||
|         if bidding != nil { | ||||
|             return .busy | ||||
|         } | ||||
|          | ||||
|         while !candidatesQueue.isEmpty { | ||||
|             guard let bid = candidatesQueue.first else { | ||||
|                 break | ||||
|             } | ||||
|             candidatesQueue.removeFirst() | ||||
|              | ||||
|             if winners.contains(aggregatorId: bid.config.aggregatorId) { | ||||
|                 // 若当前bid已存在填充,则忽略 | ||||
|                 continue | ||||
|             } | ||||
|              | ||||
|             guard let adUnitSpec = selectAdUnitSpec( | ||||
|                 aggregatorConfig: bid.config, | ||||
|                 floor: bid.floor ?? winners.maxRevenue(), | ||||
|                 selfRaise: bid.useSelfRaise | ||||
|             ) else { | ||||
|                 continue | ||||
|             } | ||||
|              | ||||
|             logD("selected \(adUnitSpec.adUnitId) with \(adUnitSpec.floorEcpm)") | ||||
|              | ||||
|             if let bidder = obtainBidder(config: bid.config, adUnitSpec: adUnitSpec) { | ||||
|                 if bidder.load(reason: bid.reason) { | ||||
|                     bidding = bidder | ||||
|                     return .success | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|          | ||||
|         return .noAvailableSource | ||||
|     } | ||||
|      | ||||
|     func refillCandidatesQueue(reason: RequestReason? = nil) { | ||||
|         logI("refilling candidates") | ||||
|         logState() | ||||
|          | ||||
|         for aggregator in priorityAggregators { | ||||
|             fillCandidate(AggregatorBid(config: aggregator, useSelfRaise: false, reason: reason)) | ||||
|         } | ||||
|          | ||||
|         logI("candidates refilled") | ||||
|         logState() | ||||
|     } | ||||
|      | ||||
|     private func fillCandidate(_ bid: AggregatorBid) { | ||||
|         let aggregator = bid.config | ||||
|          | ||||
|         // 已填充并未过期的,忽略 | ||||
|         if winners.contains(aggregatorId: aggregator.aggregatorId) { | ||||
|             logD("refilling - \(aggregator.aggregatorId) filled, skip") | ||||
|             return | ||||
|         } | ||||
|          | ||||
|         // 正在填充中的,忽略 | ||||
|         if let currentBidding = bidding, currentBidding.aggregatorId == aggregator.aggregatorId { | ||||
|             logW("refilling - \(aggregator.aggregatorId) is loading, skip") | ||||
|             return | ||||
|         } | ||||
|          | ||||
|         // 如果未满足某个聚合show完一次才能重试一次的条件,忽略。 | ||||
|         if aggregator.deferRetryUntilShown { | ||||
|             if !retryGate.canRetry(aggregatorId: aggregator.aggregatorId) { | ||||
|                 logD("refilling - \(aggregator.aggregatorId) deferred until next shown") | ||||
|                 return | ||||
|             } | ||||
|             retryGate.markRetryUsed(aggregatorId: aggregator.aggregatorId) | ||||
|         } | ||||
|          | ||||
|         logD("refilling - \(aggregator.aggregatorId) added to queue") | ||||
|         candidatesQueue.append(bid) | ||||
|     } | ||||
|      | ||||
|     private func onCacheEvict(_ bidder: T) { | ||||
|         // candidatesQueue.append(AggregatorBid(config: bidder.config, reason: .EXPIRATION)) | ||||
|     } | ||||
|      | ||||
|     func reset() { | ||||
|         logI("reset") | ||||
|          | ||||
|         for bidder in bidders.values { | ||||
|             bidder.destroy() | ||||
|         } | ||||
|          | ||||
|         bidding?.destroy() | ||||
|         bidding = nil | ||||
|         winners.clear() | ||||
|         bidders.removeAll() | ||||
|         candidatesQueue.removeAll() | ||||
|         refillCandidatesQueue() | ||||
|     } | ||||
|      | ||||
|     func onShown(winner: T) { | ||||
|         logI("winner shown, \(winner)") | ||||
|         logState() | ||||
|          | ||||
|         retryGate.onAdShown() | ||||
|          | ||||
|         // 消耗完后,将待填充聚合队列清空,等待hidden后重新填补队列。 | ||||
|         candidatesQueue.removeAll() | ||||
|          | ||||
|         // 如果配置了自抬价字段,则进入自抬价逻辑 | ||||
|         if winner.config.selfRaiseRate != nil { | ||||
|             // 当前show成功,加入自抬价意图,自抬价如果失败则重新请求一轮 | ||||
|             candidatesQueue.append( | ||||
|                 AggregatorBid( | ||||
|                     config: winner.config, | ||||
|                     useSelfRaise: true, | ||||
|                     floor: winner.revenue, | ||||
|                     reason: .SHOW | ||||
|                 ) | ||||
|             ) | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     func onLoaded(bidderId: Int) { | ||||
|         logI("onLoaded \(bidderId)") | ||||
|          | ||||
|         if let bidding = bidding, bidding.id == bidderId { | ||||
|             winners.add(bidder: bidding) | ||||
|             self.bidding = nil | ||||
|         } else { | ||||
|             logW("onLoaded bidderId mismatch \(String(describing: bidding?.id)) != \(bidderId)") | ||||
|         } | ||||
|          | ||||
|         logState() | ||||
|     } | ||||
|      | ||||
|     func onLoadFailed(bidderId: Int) { | ||||
|         logI("onLoadFailed \(bidderId)") | ||||
|          | ||||
|         if let bidder = bidding, bidder.id == bidderId { | ||||
|             bidding = nil | ||||
|         } else { | ||||
|             logW("onLoadFailed bidderId mismatch \(String(describing: bidding?.id)) != \(bidderId)") | ||||
|         } | ||||
|          | ||||
|         logState() | ||||
|     } | ||||
|      | ||||
|     func pollMaxRevenue() -> T? { | ||||
|         return winners.pollMaxRevenue() | ||||
|     } | ||||
|      | ||||
|     func peekMaxRevenue() -> T? { | ||||
|         return winners.peekMaxRevenue() | ||||
|     } | ||||
|      | ||||
|     func evictCache() { | ||||
|         winners.evictCache() | ||||
|     } | ||||
|      | ||||
|     private func buildStateLog() -> String { | ||||
|         var result = "faUnitId(\(faUnitId)), " | ||||
|         result += winners.buildStateLog() | ||||
|         result += ", " | ||||
|         result += "bidding[" | ||||
|          | ||||
|         if let bidding = bidding { | ||||
|             result += "\(bidding)" | ||||
|         } else { | ||||
|             result += "null" | ||||
|         } | ||||
|          | ||||
|         result += "], " | ||||
|         result += "candidates[" | ||||
|          | ||||
|         for bid in candidatesQueue { | ||||
|             result += "\(bid.config.aggregatorId)(" | ||||
|             result += "\(bid.raiseRate)" | ||||
|             if bid.useSelfRaise { | ||||
|                 result += " self-raise" | ||||
|             } | ||||
|             result += "), " | ||||
|         } | ||||
|          | ||||
|         result += "]" | ||||
|          | ||||
|         return result | ||||
|     } | ||||
| } | ||||
|  | @ -0,0 +1,784 @@ | |||
| 
 | ||||
| // | ||||
| //  MabInterstitialEngine.swift | ||||
| //  Pods | ||||
| // | ||||
| //  Created by 250102 on 2025/5/10. | ||||
| // | ||||
| 
 | ||||
| import Foundation | ||||
| 
 | ||||
| // 类型别名定义 | ||||
| typealias InterstitialAdObtainer = (_ adPlatform: AdPlatform, _ adConfig: AdConfig) -> GuruInterstitialAd? | ||||
| 
 | ||||
| // InterstitialBidder 类实现 | ||||
| private class InterstitialBidder: CustomStringConvertible, MabBidder, FusionAdListener { | ||||
|      | ||||
|     // 静态属性和常量 | ||||
|     private static let idPool = AtomicInteger(0) | ||||
|     static let TAG = "InterstitialBidder" | ||||
|      | ||||
|     // 实例属性 | ||||
|     private let engine: InterstitialAdEngine | ||||
|     var config: AggregatorConfig | ||||
|     private let ad: GuruInterstitialAd | ||||
|     private let adUnitSpec: AdUnitSpec | ||||
|     private let faUnitId: String | ||||
|      | ||||
|     var id: Int | ||||
|     var revenue: Double = 0.0 | ||||
|      | ||||
|     private var fusionAd: FusionAd? | ||||
|     private var loadedTimeInMillis: Int64? | ||||
|      | ||||
|     private var loaded: Bool { | ||||
|         return loadedTimeInMillis != nil | ||||
|     } | ||||
|      | ||||
|     var isExpired: Bool? { | ||||
|         get { | ||||
|             // 若配置为0 则永不过期 | ||||
|             if config.cacheExpireInSecond == 0.0 { | ||||
|                 return false | ||||
|             } | ||||
|             guard let loadedTime = loadedTimeInMillis else { | ||||
|                 return nil | ||||
|             } | ||||
|             return (SystemClock.elapsedRealtime() - loadedTime) > Int64(config.cacheExpireInSecond * 1000) | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     var isFilled: Bool { | ||||
|         return loaded && !(isExpired ?? true) | ||||
|     } | ||||
|      | ||||
|     func getFusionAd() -> FusionAd? { | ||||
|         return fusionAd | ||||
|     } | ||||
|      | ||||
|     // 私有属性 | ||||
|     private let loadTimeOutToken = UUID() | ||||
|     private let cacheExpireToken = UUID() | ||||
|     private var loadStartTime: Int64? | ||||
|      | ||||
|     // 初始化方法 | ||||
|     init( | ||||
|         engine: InterstitialAdEngine, | ||||
|         config: AggregatorConfig, | ||||
|         ad: GuruInterstitialAd, | ||||
|         adUnitSpec: AdUnitSpec, | ||||
|         faUnitId: String | ||||
|     ) { | ||||
|         self.engine = engine | ||||
|         self.config = config | ||||
|         self.ad = ad | ||||
|         self.adUnitSpec = adUnitSpec | ||||
|         self.faUnitId = faUnitId | ||||
|         self.id = InterstitialBidder.idPool.incrementAndGet() | ||||
|         ad.listener = self | ||||
|     } | ||||
|      | ||||
|     private func consumeLoadDuration() -> Int64? { | ||||
|         guard let start = loadStartTime else { | ||||
|             return nil | ||||
|         } | ||||
|         loadStartTime = nil | ||||
|         return SystemClock.elapsedRealtime() - start | ||||
|     } | ||||
|      | ||||
|     private var handler: StateMachine { | ||||
|         return engine | ||||
|     } | ||||
|      | ||||
|     func load(reason: RequestReason?) -> Bool { | ||||
|         MabAnalyticEvents.faLoad(faUnitId: faUnitId, adType: engine.adType, aggregatorId: config.aggregatorId, adUnitId: adUnitSpec.adUnitId, requestReason: reason) | ||||
|         let result = ad.load() | ||||
|         if result { | ||||
|             handler.removeCallbacksAndMessages(cacheExpireToken) | ||||
|             loadedTimeInMillis = nil | ||||
|             loadStartTime = SystemClock.elapsedRealtime() | ||||
|              | ||||
|             let timeout: Int64 | ||||
|             if let configTimeout = config.loadTimeoutInSecond { | ||||
|                 timeout = Int64(configTimeout * 1000) | ||||
|             } else { | ||||
|                 timeout = MabInterstitialEngine.BIDDING_LOAD_TIMEOUT_MILLIS | ||||
|             } | ||||
|              | ||||
|             handler.postDelayed(runnable: { [weak self] in self?.performLoadTimeout() }, token: loadTimeOutToken, delayed: .milliseconds(Int(timeout))) | ||||
|         } | ||||
|         return result | ||||
|     } | ||||
|      | ||||
|     func show(request: InterstitialShowRequest?) -> Bool { | ||||
|         let result = ad.show(request) | ||||
|         if result { | ||||
|             handler.removeCallbacksAndMessages(cacheExpireToken) | ||||
|             loadedTimeInMillis = nil | ||||
|         } | ||||
|         return result | ||||
|     } | ||||
|      | ||||
|     func destroy() { | ||||
|         handler.removeCallbacksAndMessages(loadTimeOutToken) | ||||
|         handler.removeCallbacksAndMessages(cacheExpireToken) | ||||
|         revenue = 0.0 | ||||
|         ad.destroy() | ||||
|         fusionAd = nil | ||||
|         loadedTimeInMillis = nil | ||||
|         loadStartTime = nil | ||||
|     } | ||||
|      | ||||
|     // MARK: - FusionAdListener 方法 | ||||
|      | ||||
|     func onAdLoaded(ad: FusionAd) { | ||||
|         logLoaded(ad) | ||||
|         handler.removeCallbacksAndMessages(loadTimeOutToken) | ||||
|         fusionAd = ad | ||||
|         loadedTimeInMillis = SystemClock.elapsedRealtime() | ||||
|         revenue = MabBidderUtils.adjustRevenue(ad: ad, floor: adUnitSpec.floor) | ||||
|         engine.sendMessage(what: MabInterstitialEngine.EVENT_ON_LOADED, obj: EventParams(ad: ad, id: id)) | ||||
|          | ||||
|         handler.removeCallbacksAndMessages(cacheExpireToken) | ||||
|         let cacheTimeInMillis = Int64(config.cacheExpireInSecond * 1000) | ||||
|         if cacheTimeInMillis > 0 { | ||||
|             handler.postDelayed(runnable: { [weak self] in self?.performCacheExpired() }, token: cacheExpireToken, delayed: .milliseconds(Int(cacheTimeInMillis + 50))) | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     func onAdDisplayed(ad: FusionAd) { | ||||
|         engine.sendMessage(what: MabInterstitialEngine.EVENT_ON_DISPLAYED, obj: EventParams(ad: ad, id: id)) | ||||
|     } | ||||
|      | ||||
|     func onAdHidden(ad: FusionAd) { | ||||
|         engine.sendMessage(what: MabInterstitialEngine.EVENT_ON_HIDDEN, obj: EventParams(ad: ad, id: id)) | ||||
|     } | ||||
|      | ||||
|     func onAdClicked(ad: FusionAd) { | ||||
|         engine.sendMessage(what: MabInterstitialEngine.EVENT_ON_CLICK, obj: EventParams(ad: ad, id: id)) | ||||
|     } | ||||
|      | ||||
|     func onAdLoadFailed(loadFailedInfo: LoadFailedInfo) { | ||||
|         let errorCode = loadFailedInfo.error?.errorCode ?? FusionErrorCodes.UNKNOWN.code | ||||
|         logLoadFailed(errorCode) | ||||
|         loadedTimeInMillis = nil | ||||
|         handler.removeCallbacksAndMessages(loadTimeOutToken) | ||||
|         engine.sendMessage(what: | ||||
|             MabInterstitialEngine.EVENT_ON_LOAD_FAILED, | ||||
|                            obj: EventParams(loadFailedInfo: loadFailedInfo, id: id) | ||||
|         ) | ||||
|     } | ||||
|      | ||||
|     func onAdDisplayFailed(ad: FusionAd, error: FusionError?) { | ||||
|         engine.sendMessage( | ||||
|             what: MabInterstitialEngine.EVENT_ON_DISPLAY_FAILED, | ||||
|             obj: EventParams(ad: ad, displayFailedInfo: error, id: id) | ||||
|         ) | ||||
|     } | ||||
|      | ||||
|     func onAdRevenuePaid(ad: FusionAd) { | ||||
|         engine.sendMessage(what: MabInterstitialEngine.EVENT_REVENUE_PAID, obj: EventParams(ad: ad, id: id)) | ||||
|     } | ||||
|      | ||||
|     // MARK: - 私有方法 | ||||
|      | ||||
|     private func performLoadTimeout() { | ||||
|         ad.destroy() | ||||
|         onAdLoadFailed( | ||||
|             loadFailedInfo: LoadFailedInfo( | ||||
|                 engineId: ad.engineId, | ||||
|                 adPlatform: ad.adPlatform, | ||||
|                 adType: engine.adType, | ||||
|                 adUnitId: ad.adUnitId, | ||||
|                 error: LoadAdTimeoutError() | ||||
|             ) | ||||
|         ) | ||||
|     } | ||||
|      | ||||
|     private func performCacheExpired() { | ||||
|         engine.sendMessage(what: MabInterstitialEngine.EVENT_CACHE_EXPIRED, obj: EventParams(id: id)) | ||||
|     } | ||||
|      | ||||
|     private func logLoaded(_ fusionAd: FusionAd) { | ||||
|         guard let duration = consumeLoadDuration() else { | ||||
|             Logger.w(tag: InterstitialBidder.TAG, message: "logLoaded but duration is nil, ignored") | ||||
|             return | ||||
|         } | ||||
|          | ||||
|         MabAnalyticEvents.faLoaded( | ||||
|             faUnitId: faUnitId, | ||||
|             adType: engine.adType, | ||||
|             adSource: fusionAd.networkName, | ||||
|             aggregatorId: config.aggregatorId, | ||||
|             adUnitId: ad.adUnitId, | ||||
|             durationInMillis: duration | ||||
|         ) | ||||
|     } | ||||
|      | ||||
|     private func logLoadFailed(_ errorCode: Int) { | ||||
|         guard let duration = consumeLoadDuration() else { | ||||
|             Logger.w(tag: InterstitialBidder.TAG, message: "logLoadFailed but duration is nil, ignored") | ||||
|             return | ||||
|         } | ||||
|          | ||||
|         MabAnalyticEvents.faFailed( | ||||
|             faUnitId: faUnitId, | ||||
|             adType: engine.adType, | ||||
|             aggregatorId: config.aggregatorId, | ||||
|             adUnitId: ad.adUnitId, | ||||
|             durationInMillis: duration, | ||||
|             errorCode: errorCode | ||||
|         ) | ||||
|     } | ||||
|      | ||||
|     var description: String { | ||||
|         return "InterstitialBidder[\(aggregatorId)(\(adUnitSpec.adUnitId))]" | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| // MabInterstitialEngine 类实现 | ||||
| internal class MabInterstitialEngine: InterstitialAdEngine { | ||||
|      | ||||
|     // MARK: - 常量定义 | ||||
|      | ||||
|     static let ACTION_LOAD = 1 | ||||
|     static let ACTION_SHOW = 2 | ||||
|     static let ACTION_DESTROY = 4 | ||||
|     static let ACTION_RETRY = 5 | ||||
|      | ||||
|     static let EVENT_ON_LOADED = 100 | ||||
|     static let EVENT_ON_LOAD_FAILED = 101 | ||||
|     static let EVENT_ON_DISPLAYED = 102 | ||||
|     static let EVENT_ON_DISPLAY_FAILED = 103 | ||||
|     static let EVENT_ON_CLICK = 104 | ||||
|     static let EVENT_ON_HIDDEN = 105 | ||||
|     static let EVENT_REVENUE_PAID = 107 | ||||
|     static let EVENT_SHOW_TIMEOUT = 109 | ||||
|     static let EVENT_CACHE_EXPIRED = 110 | ||||
|      | ||||
|     static let BIDDING_LOAD_TIMEOUT_MILLIS: Int64 = 60_000 | ||||
|     static let SHOW_TIMEOUT_MILLIS: Int64 = 120_000 | ||||
|      | ||||
|     // MARK: - 属性 | ||||
|      | ||||
|     private let faUnitId: String | ||||
|     private let mabConfig: MabInterstitialConfig | ||||
|     private let adObtainer: InterstitialAdObtainer | ||||
|      | ||||
|     private let auctioneer: MabAuctioneer<InterstitialBidder> | ||||
|      | ||||
|     // MARK: - 初始化方法 | ||||
|      | ||||
|     init( | ||||
|         viewController: UIViewController, | ||||
|         id: Int, | ||||
|         faUnitId: String, | ||||
|         mabConfig: MabInterstitialConfig, | ||||
|         adObtainer: @escaping InterstitialAdObtainer | ||||
|     ) { | ||||
|         self.mabConfig = mabConfig | ||||
|         self.adObtainer = adObtainer | ||||
|         self.faUnitId = faUnitId | ||||
|          | ||||
|         // 创建拍卖器 | ||||
|         weak var futureSelf: MabInterstitialEngine? = nil | ||||
|         auctioneer = MabAuctioneer<InterstitialBidder>( | ||||
|             engineId: id, | ||||
|             faUnitId: faUnitId, | ||||
|             aggregatorConfigs: mabConfig.aggregatorConfigs, | ||||
|             bidderFactory: { config, adUnitSpec in | ||||
|                 guard let self = futureSelf else { return nil } | ||||
|                 return self.createBidder(config: config, adUnitSpec: adUnitSpec) | ||||
|             } | ||||
|         ) | ||||
|         super.init(viewController: viewController, id: id, adType: AdType.Interstitial, strategyName: "Mab") | ||||
|         futureSelf = self | ||||
|     } | ||||
|      | ||||
|     public override func createIdleState() -> Idle { | ||||
|         return IdleImpl(self) | ||||
|     } | ||||
|      | ||||
|     public override func createLoadingState() -> Loading { | ||||
|         return LoadingImpl(self) | ||||
|     } | ||||
|      | ||||
|     public override func createLoadedState() -> Loaded { | ||||
|         return LoadedImpl(self) | ||||
|     } | ||||
|      | ||||
|     public override func createShowingState() -> Showing { | ||||
|         return ShowingImpl(self) | ||||
|     } | ||||
|      | ||||
|     // MARK: - 私有方法 | ||||
|      | ||||
|     private func createBidder( | ||||
|         config: AggregatorConfig, | ||||
|         adUnitSpec: AdUnitSpec | ||||
|     ) -> InterstitialBidder? { | ||||
|         let adAmzId = adUnitSpec.adAmazonSlotId | ||||
|         let adPlatform = AdPlatform.fromName(config.adPlatform) | ||||
|          | ||||
|         guard let ad = adObtainer( | ||||
|             adPlatform, | ||||
|             AdConfig(engineId: id, adUnitId: adUnitSpec.adUnitId, adAmazonSlotId: adAmzId, requireDisableAutoRetries: true) | ||||
|         ) else { | ||||
|             return nil | ||||
|         } | ||||
|          | ||||
|         return InterstitialBidder( | ||||
|             engine: self, | ||||
|             config: config, | ||||
|             ad: ad, | ||||
|             adUnitSpec: adUnitSpec, | ||||
|             faUnitId: faUnitId | ||||
|         ) | ||||
|     } | ||||
|      | ||||
|     private func handleUnhandledMessage(_ msg: Message?) -> Bool { | ||||
|         guard let what = msg?.what else { return false } | ||||
|          | ||||
|         switch what { | ||||
|         case MabInterstitialEngine.ACTION_LOAD: | ||||
|             logWarn("ACTION_LOAD on \(String(describing: currentState?.name))") | ||||
|             adLoadFailed( | ||||
|                 loadFailedInfo: LoadFailedInfo( | ||||
|                     engineId: id, | ||||
|                     adPlatform: AdPlatform.fusion, | ||||
|                     adType: adType, | ||||
|                     adUnitId: "mab_interstitial", | ||||
|                     error: LoadAdRequestError(message: "unhandled ACTION_LOAD, current state is \(String(describing: currentState?.name))") | ||||
|                 ) | ||||
|             ) | ||||
|             return true | ||||
|              | ||||
|         case MabInterstitialEngine.ACTION_SHOW: | ||||
|             adDisplayFailed(ad: InvalidFusionAd.interstitial(engineId: id), error: ShowAdRequestError()) | ||||
|             return true | ||||
|              | ||||
|         case MabInterstitialEngine.ACTION_DESTROY: | ||||
|             logWarn("Destroy may not supported in a Mab Engine, may lead memory leakages if you are trying to destroy one.") | ||||
|             transitionTo(idleState) | ||||
|             // destroy all ads | ||||
|             return true | ||||
|              | ||||
|         case MabInterstitialEngine.EVENT_REVENUE_PAID: | ||||
|             logWarn("revenue paid event unhandled!") | ||||
|              | ||||
|         case MabInterstitialEngine.EVENT_ON_LOADED: | ||||
|             logWarn("event EVENT_ON_LOADED unhandled!") | ||||
|              | ||||
|         case MabInterstitialEngine.EVENT_ON_LOAD_FAILED: | ||||
|             logWarn("event EVENT_ON_LOAD_FAILED unhandled!") | ||||
|          | ||||
|         default: | ||||
|             break | ||||
|         } | ||||
|          | ||||
|         return false | ||||
|     } | ||||
|      | ||||
|     private func reset() { | ||||
|         auctioneer.reset() | ||||
|         removeMessages(what: MabInterstitialEngine.EVENT_SHOW_TIMEOUT) | ||||
|     } | ||||
|      | ||||
|     // MARK: - InterstitialAdEngine 方法实现 | ||||
|      | ||||
|     override func requestLoad() { | ||||
|         sendMessage(what: MabInterstitialEngine.ACTION_LOAD) | ||||
|     } | ||||
|      | ||||
|     override func requestShow(request: InterstitialShowRequest?) { | ||||
|         sendMessage(what: MabInterstitialEngine.ACTION_SHOW, obj: request) | ||||
|     } | ||||
|      | ||||
|     override func requestDestroy() { | ||||
|         sendMessage(what: MabInterstitialEngine.ACTION_DESTROY) | ||||
|     } | ||||
|      | ||||
|     override var supportedAdPlatforms: Set<AdPlatform> { | ||||
|         return [AdPlatform.fusion] | ||||
|     } | ||||
|      | ||||
|     // MARK: - 内部状态类 | ||||
|      | ||||
|     class IdleImpl: Idle { | ||||
|          | ||||
|         public let engine: MabInterstitialEngine | ||||
|          | ||||
|         init(_ engine: MabInterstitialEngine) { | ||||
|             self.engine = engine | ||||
|         } | ||||
|          | ||||
|         override func enter(from: IState?, params: Any?) { | ||||
|             super.enter(from: from, params: params) | ||||
|             self.engine.reset() | ||||
|         } | ||||
|          | ||||
|         override func exit(to: IState?) { | ||||
|             super.exit(to: to) | ||||
|         } | ||||
|          | ||||
|         override func processMessage(_ msg: Message) -> Bool { | ||||
|             let what = msg.what | ||||
|             if what == MabInterstitialEngine.ACTION_LOAD { | ||||
|                 let result = engine.auctioneer.requestNext() | ||||
|                 if result != MabAuctioneer.RequestResult.noAvailableSource { | ||||
|                     engine.transitionTo(engine.loadingState) | ||||
|                     return true | ||||
|                 } else { | ||||
|                     engine.logError("No available source") | ||||
|                     engine.adLoadFailed( | ||||
|                         loadFailedInfo: LoadFailedInfo( | ||||
|                             engineId: engine.id, | ||||
|                             adPlatform: AdPlatform.fusion, | ||||
|                             adType: engine.adType, | ||||
|                             adUnitId: "mab_interstitial", | ||||
|                             error: LoadAdNotAvailableError() | ||||
|                         ) | ||||
|                     ) | ||||
|                     return true | ||||
|                 } | ||||
|             } | ||||
|              | ||||
|             return engine.handleUnhandledMessage(msg) || super.processMessage(msg) | ||||
|         } | ||||
|          | ||||
|         override var name: String { | ||||
|             return super.name | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     class LoadingImpl: Loading { | ||||
|         public let engine: MabInterstitialEngine | ||||
|          | ||||
|         init(_ engine: MabInterstitialEngine) { | ||||
|             self.engine = engine | ||||
|         } | ||||
|          | ||||
|         private var retryCount = 0 | ||||
|          | ||||
|         override func enter(from: IState?, params: Any?) { | ||||
|             super.enter(from: from, params: params) | ||||
|         } | ||||
|          | ||||
|         override func exit(to: IState?) { | ||||
|             super.exit(to: to) | ||||
|         } | ||||
|          | ||||
|         override func processMessage(_ msg: Message) -> Bool { | ||||
|             let what = msg.what | ||||
|              | ||||
|             switch what { | ||||
|             case MabInterstitialEngine.ACTION_RETRY: | ||||
|                 retryCount += 1 | ||||
|                 engine.transitionTo(engine.idleState) | ||||
|                 engine.requestLoad() | ||||
|                 return true | ||||
|                  | ||||
|             case MabInterstitialEngine.ACTION_LOAD: | ||||
|                 engine.logWarn("Already loading! Ignore load request") | ||||
|                 return true | ||||
|                  | ||||
|             case MabInterstitialEngine.EVENT_ON_LOADED: | ||||
|                 if let eventParams = msg.obj as? EventParams { | ||||
|                     if eventParams.ad != nil { | ||||
|                         engine.auctioneer.onLoaded(bidderId: eventParams.id) | ||||
|                         engine.auctioneer.requestNext() | ||||
|                         if engine.auctioneer.hasWinner { | ||||
|                             engine.transitionTo(engine.loadedState, params: eventParams) | ||||
|                         } | ||||
|                     } | ||||
|                 } | ||||
|                 return true | ||||
|                  | ||||
|             case MabInterstitialEngine.EVENT_ON_LOAD_FAILED: | ||||
|                 guard let eventParams = msg.obj as? EventParams else { | ||||
|                     return true | ||||
|                 } | ||||
|                  | ||||
|                 engine.auctioneer.onLoadFailed(bidderId: eventParams.id) | ||||
|                 let result = engine.auctioneer.requestNext() | ||||
|                  | ||||
|                 if result == MabAuctioneer.RequestResult.noAvailableSource { | ||||
|                     let failedInfo = eventParams.loadFailedInfo ?? LoadFailedInfo( | ||||
|                         engineId: engine.id, | ||||
|                         adPlatform: AdPlatform.fusion, | ||||
|                         adType: engine.adType, | ||||
|                         adUnitId: "mab_interstitial", | ||||
|                         error: LoadAdNotAvailableError() | ||||
|                     ) | ||||
|                      | ||||
|                     engine.adLoadFailed(loadFailedInfo: failedInfo) | ||||
|                      | ||||
|                     let delay = min(max(pow(2.0, Double(retryCount)) as Double, 10), 300) | ||||
|                     engine.sendMessageDelayed(what: MabInterstitialEngine.ACTION_RETRY, delayMillis: Int(delay * 1000)) | ||||
|                 } | ||||
|                 return true | ||||
|                  | ||||
|             default: | ||||
|                 break | ||||
|             } | ||||
|              | ||||
|             return engine.handleUnhandledMessage(msg) || super.processMessage(msg) | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     class LoadedImpl: Loaded { | ||||
|         public let engine: MabInterstitialEngine | ||||
|          | ||||
|         init(_ engine: MabInterstitialEngine) { | ||||
|             self.engine = engine | ||||
|         } | ||||
|         override func enter(from: IState?, params: Any?) { | ||||
|             if let params = params as? EventParams, let ad = params.ad { | ||||
|                 self.engine.adLoaded(ad: ad) | ||||
|             } else { | ||||
|                 self.engine.logWarn("Loaded state enter with invalid params") | ||||
|                 self.engine.transitionTo(self.engine.idleState) | ||||
|             } | ||||
|             super.enter(from: from, params: params) | ||||
|         } | ||||
|          | ||||
|         override func exit(to: IState?) { | ||||
|             super.exit(to: to) | ||||
|         } | ||||
|          | ||||
|         override func processMessage(_ msg: Message) -> Bool { | ||||
|              | ||||
|             let what = msg.what | ||||
|              | ||||
|             switch what { | ||||
|             case MabInterstitialEngine.ACTION_SHOW: | ||||
|                 let showRequest = msg.obj as? InterstitialShowRequest | ||||
|                 var showResult: Bool? = nil | ||||
|                 var finalWinner: InterstitialBidder? = nil | ||||
|                  | ||||
|                 while true { | ||||
|                     guard let winner = engine.auctioneer.pollMaxRevenue() else { | ||||
|                         break | ||||
|                     } | ||||
|                      | ||||
|                     if !winner.isFilled { | ||||
|                         continue | ||||
|                     } | ||||
|                      | ||||
|                     showResult = winner.show(request: showRequest) | ||||
|                      | ||||
|                     if showResult != true { | ||||
|                         continue | ||||
|                     } | ||||
|                      | ||||
|                     finalWinner = winner | ||||
|                     break | ||||
|                 } | ||||
|                  | ||||
|                 if let winner = finalWinner { | ||||
|                     engine.auctioneer.onShown(winner: winner) | ||||
|                     engine.transitionTo(engine.showingState) | ||||
|                 } else { | ||||
|                     let errorMessage = showResult == nil | ||||
|                         ? "requestShow while NO winner in candidates" | ||||
|                         : "Show Ad returns false" | ||||
|                      | ||||
|                     engine.adDisplayFailed( | ||||
|                         ad: InvalidFusionAd.interstitial(engineId: engine.id), | ||||
|                         error: ShowAdRequestError(message: errorMessage) | ||||
|                     ) | ||||
|                      | ||||
|                     let result = engine.auctioneer.requestNext() | ||||
|                     if result != MabAuctioneer.RequestResult.noAvailableSource { | ||||
|                         engine.transitionTo(engine.loadingState) | ||||
|                     } else { | ||||
|                         engine.transitionTo(engine.idleState) | ||||
|                         engine.requestLoad() | ||||
|                     } | ||||
|                 } | ||||
|                 return true | ||||
|                  | ||||
|             case MabInterstitialEngine.EVENT_ON_LOADED: | ||||
|                 if let eventParams = msg.obj as? EventParams, eventParams.ad != nil { | ||||
|                     engine.auctioneer.onLoaded(bidderId: eventParams.id) | ||||
|                     engine.auctioneer.requestNext() | ||||
|                 } | ||||
|                 return true | ||||
|                  | ||||
|             case MabInterstitialEngine.EVENT_ON_LOAD_FAILED: | ||||
|                 processLoadFailed(msg) | ||||
|                 return true | ||||
|                  | ||||
|             case MabInterstitialEngine.EVENT_CACHE_EXPIRED: | ||||
|                 engine.auctioneer.evictCache() | ||||
|                 if !engine.auctioneer.hasWinner { | ||||
|                     engine.transitionTo(engine.idleState) | ||||
|                     engine.requestLoad() | ||||
|                 } | ||||
|                 return true | ||||
|                  | ||||
|             default: | ||||
|                 break | ||||
|             } | ||||
|              | ||||
|             return engine.handleUnhandledMessage(msg) || super.processMessage(msg) | ||||
|         } | ||||
|          | ||||
|         private func processLoadFailed(_ msg: Message) { | ||||
|             guard let eventParams = msg.obj as? EventParams else { | ||||
|                 return | ||||
|             } | ||||
|              | ||||
|             engine.auctioneer.onLoadFailed(bidderId: eventParams.id) | ||||
|             engine.auctioneer.requestNext() | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     class ShowingImpl: Showing { | ||||
|         public let engine: MabInterstitialEngine | ||||
|          | ||||
|         init(_ engine: MabInterstitialEngine) { | ||||
|             self.engine = engine | ||||
|         } | ||||
|          | ||||
|         override func enter(from: IState?, params: Any?) { | ||||
|              | ||||
|             let timeout: Int64 | ||||
|             if let configTimeout = engine.mabConfig.showTimeoutInSecond { | ||||
|                 timeout = Int64(configTimeout * 1000) | ||||
|             } else { | ||||
|                 timeout = MabInterstitialEngine.SHOW_TIMEOUT_MILLIS | ||||
|             } | ||||
|              | ||||
|             engine.sendMessageDelayed(what: MabInterstitialEngine.EVENT_SHOW_TIMEOUT, delayed: .milliseconds(Int(timeout))) | ||||
|             super.enter(from: from, params: params) | ||||
|         } | ||||
|          | ||||
|         override func exit(to: IState?) { | ||||
|             self.engine.removeMessages(what: MabInterstitialEngine.EVENT_SHOW_TIMEOUT) | ||||
|             super.exit(to: to) | ||||
|         } | ||||
|          | ||||
|         override func processMessage(_ msg: Message) -> Bool { | ||||
|             let what = msg.what | ||||
|              | ||||
|             switch what { | ||||
|             case MabInterstitialEngine.EVENT_ON_DISPLAYED: | ||||
|                 if let eventParams = msg.obj as? EventParams, let ad = eventParams.ad { | ||||
|                     engine.adDisplayed(ad: ad) | ||||
|                 } else { | ||||
|                     engine.logWarn("Showing state enter with invalid params") | ||||
|                 } | ||||
|                 engine.removeMessages(what: MabInterstitialEngine.EVENT_SHOW_TIMEOUT) | ||||
|                 return true | ||||
|                  | ||||
|             case MabInterstitialEngine.EVENT_ON_CLICK: | ||||
|                 if let eventParams = msg.obj as? EventParams, let ad = eventParams.ad { | ||||
|                     engine.adClicked(ad: ad) | ||||
|                 } else { | ||||
|                     engine.logWarn("Showing state enter with invalid params") | ||||
|                 } | ||||
|                 engine.removeMessages(what: MabInterstitialEngine.EVENT_SHOW_TIMEOUT) | ||||
|                 return true | ||||
|                  | ||||
|             case MabInterstitialEngine.EVENT_ON_HIDDEN: | ||||
|                 if let eventParams = msg.obj as? EventParams, let ad = eventParams.ad { | ||||
|                     engine.adHidden(ad: ad) | ||||
|                 } else { | ||||
|                     engine.logWarn("Showing state enter with invalid params") | ||||
|                 } | ||||
|                 processRequestAfterConsumed() | ||||
|                 return true | ||||
|                  | ||||
|             case MabInterstitialEngine.EVENT_ON_DISPLAY_FAILED: | ||||
|                 if let eventParams = msg.obj as? EventParams, let ad = eventParams.ad { | ||||
|                     engine.adDisplayFailed(ad: ad, error: eventParams.displayFailedInfo) | ||||
|                 } else { | ||||
|                     engine.logWarn("Showing state enter with invalid params") | ||||
|                 } | ||||
|                 processRequestAfterConsumed() | ||||
|                 return true | ||||
|                  | ||||
|             case MabInterstitialEngine.EVENT_SHOW_TIMEOUT: | ||||
|                 engine.adDisplayFailed(ad: InvalidFusionAd.interstitial(engineId: engine.id), error: ShowAdTimeoutError()) | ||||
|                 processRequestAfterConsumed() | ||||
|                 return true | ||||
|                  | ||||
|             case MabInterstitialEngine.EVENT_REVENUE_PAID: | ||||
|                 if let eventParams = msg.obj as? EventParams, let ad = eventParams.ad { | ||||
|                     engine.adRevenuePaid(ad: ad) | ||||
|                 } else { | ||||
|                     engine.logWarn("revenue paid event with invalid params") | ||||
|                 } | ||||
|                 return true | ||||
|                  | ||||
|             case MabInterstitialEngine.EVENT_ON_LOADED: | ||||
|                 if let eventParams = msg.obj as? EventParams { | ||||
|                     engine.auctioneer.onLoaded(bidderId: eventParams.id) | ||||
|                 } | ||||
|                 return true | ||||
|                  | ||||
|             case MabInterstitialEngine.EVENT_ON_LOAD_FAILED: | ||||
|                 if let eventParams = msg.obj as? EventParams { | ||||
|                     engine.auctioneer.onLoadFailed(bidderId: eventParams.id) | ||||
|                 } | ||||
|                 return true | ||||
|                  | ||||
|             default: | ||||
|                 break | ||||
|             } | ||||
|              | ||||
|             return engine.handleUnhandledMessage(msg) || super.processMessage(msg) | ||||
|         } | ||||
|          | ||||
|         /** | ||||
|          * 消耗后重新填充请求队列 | ||||
|          * 正常情况下执行完毕后,队列头部为自抬价聚合请求,后续为按priority降序排列的聚合 | ||||
|          * 并开始一轮requestNext | ||||
|          */ | ||||
|         private func processRequestAfterConsumed() { | ||||
|              | ||||
|             engine.auctioneer.refillCandidatesQueue() | ||||
|              | ||||
|             let result = engine.auctioneer.requestNext() | ||||
|             if let winner = engine.auctioneer.peekMaxRevenue() { | ||||
|                 engine.transitionTo( | ||||
|                     engine.loadedState, | ||||
|                     params: EventParams(ad: winner.getFusionAd()) | ||||
|                 ) | ||||
|             } else if result != MabAuctioneer.RequestResult.noAvailableSource { | ||||
|                 engine.transitionTo(engine.loadingState) | ||||
|             } else { | ||||
|                 engine.transitionTo(engine.idleState) | ||||
|                 engine.requestLoad() | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| private class AtomicInteger { | ||||
|     private var value: Int | ||||
|      | ||||
|     init(_ initialValue: Int) { | ||||
|         self.value = initialValue | ||||
|     } | ||||
|      | ||||
|     func get() -> Int { | ||||
|         return value | ||||
|     } | ||||
|      | ||||
|     func incrementAndGet() -> Int { | ||||
|         value += 1 | ||||
|         return value | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| private class SystemClock { | ||||
|     static func elapsedRealtime() -> Int64 { | ||||
|         return Int64(ProcessInfo.processInfo.systemUptime * 1000) | ||||
|     } | ||||
|      | ||||
|     static func elapsedRealtimeNanos() -> Int64 { | ||||
|         return Int64(ProcessInfo.processInfo.systemUptime * 1_000_000_000) | ||||
|     } | ||||
| } | ||||
|  | @ -0,0 +1,796 @@ | |||
| 
 | ||||
| // | ||||
| //  MabRewardedEngine.swift | ||||
| //  Pods | ||||
| // | ||||
| //  Created by 250102 on 2025/5/10. | ||||
| // | ||||
| 
 | ||||
| import Foundation | ||||
| 
 | ||||
| // 类型别名定义 | ||||
| typealias RewardedAdObtainer = (_ adPlatform: AdPlatform, _ adConfig: AdConfig) -> GuruRewardedAd? | ||||
| 
 | ||||
| // RewardedBidder 类实现 | ||||
| private class RewardedBidder: CustomStringConvertible, MabBidder, FusionAdListener { | ||||
|      | ||||
|     // 静态属性和常量 | ||||
|     private static let idPool = AtomicInteger(0) | ||||
|     static let TAG = "RewardedBidder" | ||||
|      | ||||
|     // 实例属性 | ||||
|     private let engine: RewardedAdEngine | ||||
|     var config: AggregatorConfig | ||||
|     private let ad: GuruRewardedAd | ||||
|     private let adUnitSpec: AdUnitSpec | ||||
|     private let faUnitId: String | ||||
|      | ||||
|     var id: Int | ||||
|     var revenue: Double = 0.0 | ||||
|      | ||||
|     private var fusionAd: FusionAd? | ||||
|     private var loadedTimeInMillis: Int64? | ||||
|      | ||||
|     private var loaded: Bool { | ||||
|         return loadedTimeInMillis != nil | ||||
|     } | ||||
|      | ||||
|     var isExpired: Bool? { | ||||
|         get { | ||||
|             // 若配置为0 则永不过期 | ||||
|             if config.cacheExpireInSecond == 0.0 { | ||||
|                 return false | ||||
|             } | ||||
|             guard let loadedTime = loadedTimeInMillis else { | ||||
|                 return nil | ||||
|             } | ||||
|             return (SystemClock.elapsedRealtime() - loadedTime) > Int64(config.cacheExpireInSecond * 1000) | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     var isFilled: Bool { | ||||
|         return loaded && !(isExpired ?? true) | ||||
|     } | ||||
|      | ||||
|     func getFusionAd() -> FusionAd? { | ||||
|         return fusionAd | ||||
|     } | ||||
|      | ||||
|     // 私有属性 | ||||
|     private let loadTimeOutToken = UUID() | ||||
|     private let cacheExpireToken = UUID() | ||||
|     private var loadStartTime: Int64? | ||||
|      | ||||
|     // 初始化方法 | ||||
|     init( | ||||
|         engine: RewardedAdEngine, | ||||
|         config: AggregatorConfig, | ||||
|         ad: GuruRewardedAd, | ||||
|         adUnitSpec: AdUnitSpec, | ||||
|         faUnitId: String | ||||
|     ) { | ||||
|         self.engine = engine | ||||
|         self.config = config | ||||
|         self.ad = ad | ||||
|         self.adUnitSpec = adUnitSpec | ||||
|         self.faUnitId = faUnitId | ||||
|         self.id = RewardedBidder.idPool.incrementAndGet() | ||||
|         ad.listener = self | ||||
|     } | ||||
|      | ||||
|     private func consumeLoadDuration() -> Int64? { | ||||
|         guard let start = loadStartTime else { | ||||
|             return nil | ||||
|         } | ||||
|         loadStartTime = nil | ||||
|         return SystemClock.elapsedRealtime() - start | ||||
|     } | ||||
|      | ||||
|     private var handler: StateMachine { | ||||
|         return engine | ||||
|     } | ||||
|      | ||||
|     func load(reason: RequestReason?) -> Bool { | ||||
|         MabAnalyticEvents.faLoad(faUnitId: faUnitId, adType: engine.adType, aggregatorId: config.aggregatorId, adUnitId: adUnitSpec.adUnitId, requestReason: reason) | ||||
|         let result = ad.load() | ||||
|         if result { | ||||
|             handler.removeCallbacksAndMessages(cacheExpireToken) | ||||
|             loadedTimeInMillis = nil | ||||
|             loadStartTime = SystemClock.elapsedRealtime() | ||||
|              | ||||
|             let timeout: Int64 | ||||
|             if let configTimeout = config.loadTimeoutInSecond { | ||||
|                 timeout = Int64(configTimeout * 1000) | ||||
|             } else { | ||||
|                 timeout = MabRewardedEngine.BIDDING_LOAD_TIMEOUT_MILLIS | ||||
|             } | ||||
|              | ||||
|             handler.postDelayed(runnable: { [weak self] in self?.performLoadTimeout() }, token: loadTimeOutToken, delayed: .milliseconds(Int(timeout))) | ||||
|         } | ||||
|         return result | ||||
|     } | ||||
|      | ||||
|     func show(request: RewardedShowRequest?) -> Bool { | ||||
|         let result = ad.show(request) | ||||
|         if result { | ||||
|             handler.removeCallbacksAndMessages(cacheExpireToken) | ||||
|             loadedTimeInMillis = nil | ||||
|         } | ||||
|         return result | ||||
|     } | ||||
|      | ||||
|     func destroy() { | ||||
|         handler.removeCallbacksAndMessages(loadTimeOutToken) | ||||
|         handler.removeCallbacksAndMessages(cacheExpireToken) | ||||
|         revenue = 0.0 | ||||
|         ad.destroy() | ||||
|         fusionAd = nil | ||||
|         loadedTimeInMillis = nil | ||||
|         loadStartTime = nil | ||||
|     } | ||||
|      | ||||
|     // MARK: - FusionAdListener 方法 | ||||
|      | ||||
|     func onAdLoaded(ad: FusionAd) { | ||||
|         logLoaded(ad) | ||||
|         handler.removeCallbacksAndMessages(loadTimeOutToken) | ||||
|         fusionAd = ad | ||||
|         loadedTimeInMillis = SystemClock.elapsedRealtime() | ||||
|         revenue = MabBidderUtils.adjustRevenue(ad: ad, floor: adUnitSpec.floor) | ||||
|         engine.sendMessage(what: MabRewardedEngine.EVENT_ON_LOADED, obj: EventParams(ad: ad, id: id)) | ||||
|          | ||||
|         handler.removeCallbacksAndMessages(cacheExpireToken) | ||||
|         let cacheTimeInMillis = Int64(config.cacheExpireInSecond * 1000) | ||||
|         if cacheTimeInMillis > 0 { | ||||
|             handler.postDelayed(runnable: { [weak self] in self?.performCacheExpired() }, token: cacheExpireToken, delayed: .milliseconds(Int(cacheTimeInMillis + 50))) | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     func onAdDisplayed(ad: FusionAd) { | ||||
|         engine.sendMessage(what: MabRewardedEngine.EVENT_ON_DISPLAYED, obj: EventParams(ad: ad, id: id)) | ||||
|     } | ||||
|      | ||||
|     func onAdHidden(ad: FusionAd) { | ||||
|         engine.sendMessage(what: MabRewardedEngine.EVENT_ON_HIDDEN, obj: EventParams(ad: ad, id: id)) | ||||
|     } | ||||
|      | ||||
|     func onAdClicked(ad: FusionAd) { | ||||
|         engine.sendMessage(what: MabRewardedEngine.EVENT_ON_CLICK, obj: EventParams(ad: ad, id: id)) | ||||
|     } | ||||
|      | ||||
|     func onAdLoadFailed(loadFailedInfo: LoadFailedInfo) { | ||||
|         let errorCode = loadFailedInfo.error?.errorCode ?? FusionErrorCodes.UNKNOWN.code | ||||
|         logLoadFailed(errorCode) | ||||
|         loadedTimeInMillis = nil | ||||
|         handler.removeCallbacksAndMessages(loadTimeOutToken) | ||||
|         engine.sendMessage(what: | ||||
|             MabRewardedEngine.EVENT_ON_LOAD_FAILED, | ||||
|                            obj: EventParams(loadFailedInfo: loadFailedInfo, id: id) | ||||
|         ) | ||||
|     } | ||||
|      | ||||
|     func onAdDisplayFailed(ad: FusionAd, error: FusionError?) { | ||||
|         engine.sendMessage( | ||||
|             what: MabRewardedEngine.EVENT_ON_DISPLAY_FAILED, | ||||
|             obj: EventParams(ad: ad, displayFailedInfo: error, id: id) | ||||
|         ) | ||||
|     } | ||||
|      | ||||
|     func onAdRevenuePaid(ad: FusionAd) { | ||||
|         engine.sendMessage(what: MabRewardedEngine.EVENT_REVENUE_PAID, obj: EventParams(ad: ad, id: id)) | ||||
|     } | ||||
|      | ||||
|     func onUserRewarded(ad: FusionAd, reward: (any FusionReward)?) { | ||||
|         engine.sendMessage(what: MabRewardedEngine.EVENT_USER_REWARDED, obj: EventParams(ad: ad, reward: reward)) | ||||
|     } | ||||
|      | ||||
|     // MARK: - 私有方法 | ||||
|      | ||||
|     private func performLoadTimeout() { | ||||
|         ad.destroy() | ||||
|         onAdLoadFailed( | ||||
|             loadFailedInfo: LoadFailedInfo( | ||||
|                 engineId: ad.engineId, | ||||
|                 adPlatform: ad.adPlatform, | ||||
|                 adType: engine.adType, | ||||
|                 adUnitId: ad.adUnitId, | ||||
|                 error: LoadAdTimeoutError() | ||||
|             ) | ||||
|         ) | ||||
|     } | ||||
|      | ||||
|     private func performCacheExpired() { | ||||
|         engine.sendMessage(what: MabRewardedEngine.EVENT_CACHE_EXPIRED, obj: EventParams(id: id)) | ||||
|     } | ||||
|      | ||||
|     private func logLoaded(_ fusionAd: FusionAd) { | ||||
|         guard let duration = consumeLoadDuration() else { | ||||
|             Logger.w(tag: RewardedBidder.TAG, message: "logLoaded but duration is nil, ignored") | ||||
|             return | ||||
|         } | ||||
|          | ||||
|         MabAnalyticEvents.faLoaded( | ||||
|             faUnitId: faUnitId, | ||||
|             adType: engine.adType, | ||||
|             adSource: fusionAd.networkName, | ||||
|             aggregatorId: config.aggregatorId, | ||||
|             adUnitId: ad.adUnitId, | ||||
|             durationInMillis: duration | ||||
|         ) | ||||
|     } | ||||
|      | ||||
|     private func logLoadFailed(_ errorCode: Int) { | ||||
|         guard let duration = consumeLoadDuration() else { | ||||
|             Logger.w(tag: RewardedBidder.TAG, message: "logLoadFailed but duration is nil, ignored") | ||||
|             return | ||||
|         } | ||||
|          | ||||
|         MabAnalyticEvents.faFailed( | ||||
|             faUnitId: faUnitId, | ||||
|             adType: engine.adType, | ||||
|             aggregatorId: config.aggregatorId, | ||||
|             adUnitId: ad.adUnitId, | ||||
|             durationInMillis: duration, | ||||
|             errorCode: errorCode | ||||
|         ) | ||||
|     } | ||||
|      | ||||
|     var description: String { | ||||
|         return "RewardedBidder[\(aggregatorId)(\(adUnitSpec.adUnitId))]" | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| // MabRewardedEngine 类实现 | ||||
| internal class MabRewardedEngine: RewardedAdEngine { | ||||
|      | ||||
|     // MARK: - 常量定义 | ||||
|      | ||||
|     static let ACTION_LOAD = 1 | ||||
|     static let ACTION_SHOW = 2 | ||||
|     static let ACTION_DESTROY = 4 | ||||
|     static let ACTION_RETRY = 5 | ||||
|      | ||||
|     static let EVENT_ON_LOADED = 100 | ||||
|     static let EVENT_ON_LOAD_FAILED = 101 | ||||
|     static let EVENT_ON_DISPLAYED = 102 | ||||
|     static let EVENT_ON_DISPLAY_FAILED = 103 | ||||
|     static let EVENT_ON_CLICK = 104 | ||||
|     static let EVENT_ON_HIDDEN = 105 | ||||
|     static let EVENT_USER_REWARDED = 106 | ||||
|     static let EVENT_REVENUE_PAID = 107 | ||||
|     static let EVENT_SHOW_TIMEOUT = 109 | ||||
|     static let EVENT_CACHE_EXPIRED = 110 | ||||
|      | ||||
|     static let BIDDING_LOAD_TIMEOUT_MILLIS: Int64 = 60_000 | ||||
|     static let SHOW_TIMEOUT_MILLIS: Int64 = 120_000 | ||||
|      | ||||
|     // MARK: - 属性 | ||||
|      | ||||
|     private let faUnitId: String | ||||
|     private let mabConfig: MabRewardedConfig | ||||
|     private let adObtainer: RewardedAdObtainer | ||||
|      | ||||
|     private let auctioneer: MabAuctioneer<RewardedBidder> | ||||
|      | ||||
|     // MARK: - 初始化方法 | ||||
|      | ||||
|     init( | ||||
|         viewController: UIViewController, | ||||
|         id: Int, | ||||
|         faUnitId: String, | ||||
|         mabConfig: MabRewardedConfig, | ||||
|         adObtainer: @escaping RewardedAdObtainer | ||||
|     ) { | ||||
|         self.mabConfig = mabConfig | ||||
|         self.adObtainer = adObtainer | ||||
|         self.faUnitId = faUnitId | ||||
|          | ||||
|         // 创建拍卖器 | ||||
|         weak var futureSelf: MabRewardedEngine? = nil | ||||
|         auctioneer = MabAuctioneer<RewardedBidder>( | ||||
|             engineId: id, | ||||
|             faUnitId: faUnitId, | ||||
|             aggregatorConfigs: mabConfig.aggregatorConfigs, | ||||
|             bidderFactory: { config, adUnitSpec in | ||||
|                 guard let self = futureSelf else { return nil } | ||||
|                 return self.createBidder(config: config, adUnitSpec: adUnitSpec) | ||||
|             } | ||||
|         ) | ||||
|         super.init(viewController: viewController, id: id, adType: AdType.Rewarded, strategyName: "Mab") | ||||
|         futureSelf = self | ||||
|     } | ||||
|      | ||||
|     public override func createIdleState() -> Idle { | ||||
|         return IdleImpl(self) | ||||
|     } | ||||
|      | ||||
|     public override func createLoadingState() -> Loading { | ||||
|         return LoadingImpl(self) | ||||
|     } | ||||
|      | ||||
|     public override func createLoadedState() -> Loaded { | ||||
|         return LoadedImpl(self) | ||||
|     } | ||||
|      | ||||
|     public override func createShowingState() -> Showing { | ||||
|         return ShowingImpl(self) | ||||
|     } | ||||
|      | ||||
|     // MARK: - 私有方法 | ||||
|      | ||||
|     private func createBidder( | ||||
|         config: AggregatorConfig, | ||||
|         adUnitSpec: AdUnitSpec | ||||
|     ) -> RewardedBidder? { | ||||
|         let adAmzId = adUnitSpec.adAmazonSlotId | ||||
|         let adPlatform = AdPlatform.fromName(config.adPlatform) | ||||
|          | ||||
|         guard let ad = adObtainer( | ||||
|             adPlatform, | ||||
|             AdConfig(engineId: id, adUnitId: adUnitSpec.adUnitId, adAmazonSlotId: adAmzId, requireDisableAutoRetries: true) | ||||
|         ) else { | ||||
|             return nil | ||||
|         } | ||||
|          | ||||
|         return RewardedBidder( | ||||
|             engine: self, | ||||
|             config: config, | ||||
|             ad: ad, | ||||
|             adUnitSpec: adUnitSpec, | ||||
|             faUnitId: faUnitId | ||||
|         ) | ||||
|     } | ||||
|      | ||||
|     private func handleUnhandledMessage(_ msg: Message?) -> Bool { | ||||
|         guard let what = msg?.what else { return false } | ||||
|          | ||||
|         switch what { | ||||
|         case MabRewardedEngine.ACTION_LOAD: | ||||
|             logWarn("ACTION_LOAD on \(String(describing: currentState?.name))") | ||||
|             adLoadFailed( | ||||
|                 loadFailedInfo: LoadFailedInfo( | ||||
|                     engineId: id, | ||||
|                     adPlatform: AdPlatform.fusion, | ||||
|                     adType: adType, | ||||
|                     adUnitId: "mab_rewarded", | ||||
|                     error: LoadAdRequestError(message: "unhandled ACTION_LOAD, current state is \(String(describing: currentState?.name))") | ||||
|                 ) | ||||
|             ) | ||||
|             return true | ||||
|              | ||||
|         case MabRewardedEngine.ACTION_SHOW: | ||||
|             adDisplayFailed(ad: InvalidFusionAd.rewarded(engineId: id), error: ShowAdRequestError()) | ||||
|             return true | ||||
|              | ||||
|         case MabRewardedEngine.ACTION_DESTROY: | ||||
|             logWarn("Destroy may not supported in a Mab Engine, may lead memory leakages if you are trying to destroy one.") | ||||
|             transitionTo(idleState) | ||||
|             // destroy all ads | ||||
|             return true | ||||
|              | ||||
|         case MabRewardedEngine.EVENT_REVENUE_PAID: | ||||
|             logWarn("revenue paid event unhandled!") | ||||
|              | ||||
|         case MabRewardedEngine.EVENT_ON_LOADED: | ||||
|             logWarn("event EVENT_ON_LOADED unhandled!") | ||||
|              | ||||
|         case MabRewardedEngine.EVENT_ON_LOAD_FAILED: | ||||
|             logWarn("event EVENT_ON_LOAD_FAILED unhandled!") | ||||
|          | ||||
|         default: | ||||
|             break | ||||
|         } | ||||
|          | ||||
|         return false | ||||
|     } | ||||
|      | ||||
|     private func reset() { | ||||
|         auctioneer.reset() | ||||
|         removeMessages(what: MabRewardedEngine.EVENT_SHOW_TIMEOUT) | ||||
|     } | ||||
|      | ||||
|     // MARK: - RewardedAdEngine 方法实现 | ||||
|      | ||||
|     override func requestLoad() { | ||||
|         sendMessage(what: MabRewardedEngine.ACTION_LOAD) | ||||
|     } | ||||
|      | ||||
|     override func requestShow(_ request: RewardedShowRequest?) { | ||||
|         sendMessage(what: MabRewardedEngine.ACTION_SHOW, obj: request) | ||||
|     } | ||||
|      | ||||
|     override func requestDestroy() { | ||||
|         sendMessage(what: MabRewardedEngine.ACTION_DESTROY) | ||||
|     } | ||||
|      | ||||
|     override var supportedAdPlatforms: Set<AdPlatform> { | ||||
|         return [AdPlatform.fusion] | ||||
|     } | ||||
|      | ||||
|     // MARK: - 内部状态类 | ||||
|      | ||||
|     class IdleImpl: Idle { | ||||
|          | ||||
|         public let engine: MabRewardedEngine | ||||
|          | ||||
|         init(_ engine: MabRewardedEngine) { | ||||
|             self.engine = engine | ||||
|         } | ||||
|          | ||||
|         override func enter(from: IState?, params: Any?) { | ||||
|             super.enter(from: from, params: params) | ||||
|             self.engine.reset() | ||||
|         } | ||||
|          | ||||
|         override func exit(to: IState?) { | ||||
|             super.exit(to: to) | ||||
|         } | ||||
|          | ||||
|         override func processMessage(_ msg: Message) -> Bool { | ||||
|             let what = msg.what | ||||
|             if what == MabRewardedEngine.ACTION_LOAD { | ||||
|                 let result = engine.auctioneer.requestNext() | ||||
|                 if result != MabAuctioneer.RequestResult.noAvailableSource { | ||||
|                     engine.transitionTo(engine.loadingState) | ||||
|                     return true | ||||
|                 } else { | ||||
|                     engine.logError("No available source") | ||||
|                     engine.adLoadFailed( | ||||
|                         loadFailedInfo: LoadFailedInfo( | ||||
|                             engineId: engine.id, | ||||
|                             adPlatform: AdPlatform.fusion, | ||||
|                             adType: engine.adType, | ||||
|                             adUnitId: "mab_rewarded", | ||||
|                             error: LoadAdNotAvailableError() | ||||
|                         ) | ||||
|                     ) | ||||
|                     return true | ||||
|                 } | ||||
|             } | ||||
|              | ||||
|             return engine.handleUnhandledMessage(msg) || super.processMessage(msg) | ||||
|         } | ||||
|          | ||||
|         override var name: String { | ||||
|             return super.name | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     class LoadingImpl: Loading { | ||||
|         public let engine: MabRewardedEngine | ||||
|          | ||||
|         init(_ engine: MabRewardedEngine) { | ||||
|             self.engine = engine | ||||
|         } | ||||
|          | ||||
|         private var retryCount = 0 | ||||
|          | ||||
|         override func enter(from: IState?, params: Any?) { | ||||
|             super.enter(from: from, params: params) | ||||
|         } | ||||
|          | ||||
|         override func exit(to: IState?) { | ||||
|             super.exit(to: to) | ||||
|         } | ||||
|          | ||||
|         override func processMessage(_ msg: Message) -> Bool { | ||||
|             let what = msg.what | ||||
|              | ||||
|             switch what { | ||||
|             case MabRewardedEngine.ACTION_RETRY: | ||||
|                 retryCount += 1 | ||||
|                 engine.transitionTo(engine.idleState) | ||||
|                 engine.requestLoad() | ||||
|                 return true | ||||
|                  | ||||
|             case MabRewardedEngine.ACTION_LOAD: | ||||
|                 engine.logWarn("Already loading! Ignore load request") | ||||
|                 return true | ||||
|                  | ||||
|             case MabRewardedEngine.EVENT_ON_LOADED: | ||||
|                 if let eventParams = msg.obj as? EventParams { | ||||
|                     if eventParams.ad != nil { | ||||
|                         engine.auctioneer.onLoaded(bidderId: eventParams.id) | ||||
|                         engine.auctioneer.requestNext() | ||||
|                         if engine.auctioneer.hasWinner { | ||||
|                             engine.transitionTo(engine.loadedState, params: eventParams) | ||||
|                         } | ||||
|                     } | ||||
|                 } | ||||
|                 return true | ||||
|                  | ||||
|             case MabRewardedEngine.EVENT_ON_LOAD_FAILED: | ||||
|                 guard let eventParams = msg.obj as? EventParams else { | ||||
|                     return true | ||||
|                 } | ||||
|                  | ||||
|                 engine.auctioneer.onLoadFailed(bidderId: eventParams.id) | ||||
|                 let result = engine.auctioneer.requestNext() | ||||
|                  | ||||
|                 if result == MabAuctioneer.RequestResult.noAvailableSource { | ||||
|                     let failedInfo = eventParams.loadFailedInfo ?? LoadFailedInfo( | ||||
|                         engineId: engine.id, | ||||
|                         adPlatform: AdPlatform.fusion, | ||||
|                         adType: engine.adType, | ||||
|                         adUnitId: "mab_rewarded", | ||||
|                         error: LoadAdNotAvailableError() | ||||
|                     ) | ||||
|                      | ||||
|                     engine.adLoadFailed(loadFailedInfo: failedInfo) | ||||
|                      | ||||
|                     let delay = min(max(pow(2.0, Double(retryCount)) as Double, 10), 300) | ||||
|                     engine.sendMessageDelayed(what: MabRewardedEngine.ACTION_RETRY, delayMillis: Int(delay * 1000)) | ||||
|                 } | ||||
|                 return true | ||||
|                  | ||||
|             default: | ||||
|                 break | ||||
|             } | ||||
|              | ||||
|             return engine.handleUnhandledMessage(msg) || super.processMessage(msg) | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     class LoadedImpl: Loaded { | ||||
|         public let engine: MabRewardedEngine | ||||
|          | ||||
|         init(_ engine: MabRewardedEngine) { | ||||
|             self.engine = engine | ||||
|         } | ||||
|         override func enter(from: IState?, params: Any?) { | ||||
|             if let params = params as? EventParams, let ad = params.ad { | ||||
|                 self.engine.adLoaded(ad: ad) | ||||
|             } else { | ||||
|                 self.engine.logWarn("Loaded state enter with invalid params") | ||||
|                 self.engine.transitionTo(self.engine.idleState) | ||||
|             } | ||||
|             super.enter(from: from, params: params) | ||||
|         } | ||||
|          | ||||
|         override func exit(to: IState?) { | ||||
|             super.exit(to: to) | ||||
|         } | ||||
|          | ||||
|         override func processMessage(_ msg: Message) -> Bool { | ||||
|              | ||||
|             let what = msg.what | ||||
|              | ||||
|             switch what { | ||||
|             case MabRewardedEngine.ACTION_SHOW: | ||||
|                 let showRequest = msg.obj as? RewardedShowRequest | ||||
|                 var showResult: Bool? = nil | ||||
|                 var finalWinner: RewardedBidder? = nil | ||||
|                  | ||||
|                 while true { | ||||
|                     guard let winner = engine.auctioneer.pollMaxRevenue() else { | ||||
|                         break | ||||
|                     } | ||||
|                      | ||||
|                     if !winner.isFilled { | ||||
|                         continue | ||||
|                     } | ||||
|                      | ||||
|                     showResult = winner.show(request: showRequest) | ||||
|                      | ||||
|                     if showResult != true { | ||||
|                         continue | ||||
|                     } | ||||
|                      | ||||
|                     finalWinner = winner | ||||
|                     break | ||||
|                 } | ||||
|                  | ||||
|                 if let winner = finalWinner { | ||||
|                     engine.auctioneer.onShown(winner: winner) | ||||
|                     engine.transitionTo(engine.showingState) | ||||
|                 } else { | ||||
|                     let errorMessage = showResult == nil | ||||
|                         ? "requestShow while NO winner in candidates" | ||||
|                         : "Show Ad returns false" | ||||
|                      | ||||
|                     engine.adDisplayFailed( | ||||
|                         ad: InvalidFusionAd.rewarded(engineId: engine.id), | ||||
|                         error: ShowAdRequestError(message: errorMessage) | ||||
|                     ) | ||||
|                      | ||||
|                     let result = engine.auctioneer.requestNext() | ||||
|                     if result != MabAuctioneer.RequestResult.noAvailableSource { | ||||
|                         engine.transitionTo(engine.loadingState) | ||||
|                     } else { | ||||
|                         engine.transitionTo(engine.idleState) | ||||
|                         engine.requestLoad() | ||||
|                     } | ||||
|                 } | ||||
|                 return true | ||||
|                  | ||||
|             case MabRewardedEngine.EVENT_ON_LOADED: | ||||
|                 if let eventParams = msg.obj as? EventParams, eventParams.ad != nil { | ||||
|                     engine.auctioneer.onLoaded(bidderId: eventParams.id) | ||||
|                     engine.auctioneer.requestNext() | ||||
|                 } | ||||
|                 return true | ||||
|                  | ||||
|             case MabRewardedEngine.EVENT_ON_LOAD_FAILED: | ||||
|                 processLoadFailed(msg) | ||||
|                 return true | ||||
|                  | ||||
|             case MabRewardedEngine.EVENT_CACHE_EXPIRED: | ||||
|                 engine.auctioneer.evictCache() | ||||
|                 if !engine.auctioneer.hasWinner { | ||||
|                     engine.transitionTo(engine.idleState) | ||||
|                     engine.requestLoad() | ||||
|                 } | ||||
|                 return true | ||||
|                  | ||||
|             default: | ||||
|                 break | ||||
|             } | ||||
|              | ||||
|             return engine.handleUnhandledMessage(msg) || super.processMessage(msg) | ||||
|         } | ||||
|          | ||||
|         private func processLoadFailed(_ msg: Message) { | ||||
|             guard let eventParams = msg.obj as? EventParams else { | ||||
|                 return | ||||
|             } | ||||
|              | ||||
|             engine.auctioneer.onLoadFailed(bidderId: eventParams.id) | ||||
|             engine.auctioneer.requestNext() | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     class ShowingImpl: Showing { | ||||
|         public let engine: MabRewardedEngine | ||||
|          | ||||
|         init(_ engine: MabRewardedEngine) { | ||||
|             self.engine = engine | ||||
|         } | ||||
|          | ||||
|         override func enter(from: IState?, params: Any?) { | ||||
|              | ||||
|             let timeout: Int64 | ||||
|             if let configTimeout = engine.mabConfig.showTimeoutInSecond { | ||||
|                 timeout = Int64(configTimeout * 1000) | ||||
|             } else { | ||||
|                 timeout = MabRewardedEngine.SHOW_TIMEOUT_MILLIS | ||||
|             } | ||||
|              | ||||
|             engine.sendMessageDelayed(what: MabRewardedEngine.EVENT_SHOW_TIMEOUT, delayed: .milliseconds(Int(timeout))) | ||||
|             super.enter(from: from, params: params) | ||||
|         } | ||||
|          | ||||
|         override func exit(to: IState?) { | ||||
|             self.engine.removeMessages(what: MabRewardedEngine.EVENT_SHOW_TIMEOUT) | ||||
|             super.exit(to: to) | ||||
|         } | ||||
|          | ||||
|         override func processMessage(_ msg: Message) -> Bool { | ||||
|             let what = msg.what | ||||
|              | ||||
|             switch what { | ||||
|             case MabRewardedEngine.EVENT_ON_DISPLAYED: | ||||
|                 if let eventParams = msg.obj as? EventParams, let ad = eventParams.ad { | ||||
|                     engine.adDisplayed(ad: ad) | ||||
|                 } else { | ||||
|                     engine.logWarn("Showing state enter with invalid params") | ||||
|                 } | ||||
|                 engine.removeMessages(what: MabRewardedEngine.EVENT_SHOW_TIMEOUT) | ||||
|                 return true | ||||
|                  | ||||
|             case MabRewardedEngine.EVENT_ON_CLICK: | ||||
|                 if let eventParams = msg.obj as? EventParams, let ad = eventParams.ad { | ||||
|                     engine.adClicked(ad: ad) | ||||
|                 } else { | ||||
|                     engine.logWarn("Showing state enter with invalid params") | ||||
|                 } | ||||
|                 engine.removeMessages(what: MabRewardedEngine.EVENT_SHOW_TIMEOUT) | ||||
|                 return true | ||||
|                  | ||||
|             case MabRewardedEngine.EVENT_ON_HIDDEN: | ||||
|                 if let eventParams = msg.obj as? EventParams, let ad = eventParams.ad { | ||||
|                     engine.adHidden(ad: ad) | ||||
|                 } else { | ||||
|                     engine.logWarn("Showing state enter with invalid params") | ||||
|                 } | ||||
|                 processRequestAfterConsumed() | ||||
|                 return true | ||||
|                  | ||||
|             case MabRewardedEngine.EVENT_ON_DISPLAY_FAILED: | ||||
|                 if let eventParams = msg.obj as? EventParams, let ad = eventParams.ad { | ||||
|                     engine.adDisplayFailed(ad: ad, error: eventParams.displayFailedInfo) | ||||
|                 } else { | ||||
|                     engine.logWarn("Showing state enter with invalid params") | ||||
|                 } | ||||
|                 processRequestAfterConsumed() | ||||
|                 return true | ||||
|                  | ||||
|             case MabRewardedEngine.EVENT_SHOW_TIMEOUT: | ||||
|                 engine.adDisplayFailed(ad: InvalidFusionAd.rewarded(engineId: engine.id), error: ShowAdTimeoutError()) | ||||
|                 processRequestAfterConsumed() | ||||
|                 return true | ||||
|                  | ||||
|             case MabRewardedEngine.EVENT_USER_REWARDED: | ||||
|                 guard let rewarded = (msg.obj as? EventParams)?.reward, let ad = (msg.obj as? EventParams)?.ad else { | ||||
|                     return true | ||||
|                 } | ||||
|                 engine.adUserRewarded(ad: ad, reward: rewarded) | ||||
|                 return true | ||||
|                  | ||||
|             case MabRewardedEngine.EVENT_REVENUE_PAID: | ||||
|                 if let eventParams = msg.obj as? EventParams, let ad = eventParams.ad { | ||||
|                     engine.adRevenuePaid(ad: ad) | ||||
|                 } else { | ||||
|                     engine.logWarn("revenue paid event with invalid params") | ||||
|                 } | ||||
|                 return true | ||||
|                  | ||||
|             case MabRewardedEngine.EVENT_ON_LOADED: | ||||
|                 if let eventParams = msg.obj as? EventParams { | ||||
|                     engine.auctioneer.onLoaded(bidderId: eventParams.id) | ||||
|                 } | ||||
|                 return true | ||||
|                  | ||||
|             case MabRewardedEngine.EVENT_ON_LOAD_FAILED: | ||||
|                 if let eventParams = msg.obj as? EventParams { | ||||
|                     engine.auctioneer.onLoadFailed(bidderId: eventParams.id) | ||||
|                 } | ||||
|                 return true | ||||
|                  | ||||
|             default: | ||||
|                 break | ||||
|             } | ||||
|              | ||||
|             return engine.handleUnhandledMessage(msg) || super.processMessage(msg) | ||||
|         } | ||||
|          | ||||
|         /** | ||||
|          * 消耗后重新填充请求队列 | ||||
|          * 正常情况下执行完毕后,队列头部为自抬价聚合请求,后续为按priority降序排列的聚合 | ||||
|          * 并开始一轮requestNext | ||||
|          */ | ||||
|         private func processRequestAfterConsumed() { | ||||
|              | ||||
|             engine.auctioneer.refillCandidatesQueue() | ||||
|              | ||||
|             let result = engine.auctioneer.requestNext() | ||||
|             if let winner = engine.auctioneer.peekMaxRevenue() { | ||||
|                 engine.transitionTo( | ||||
|                     engine.loadedState, | ||||
|                     params: EventParams(ad: winner.getFusionAd()) | ||||
|                 ) | ||||
|             } else if result != MabAuctioneer.RequestResult.noAvailableSource { | ||||
|                 engine.transitionTo(engine.loadingState) | ||||
|             } else { | ||||
|                 engine.transitionTo(engine.idleState) | ||||
|                 engine.requestLoad() | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| private class AtomicInteger { | ||||
|     private var value: Int | ||||
|      | ||||
|     init(_ initialValue: Int) { | ||||
|         self.value = initialValue | ||||
|     } | ||||
|      | ||||
|     func get() -> Int { | ||||
|         return value | ||||
|     } | ||||
|      | ||||
|     func incrementAndGet() -> Int { | ||||
|         value += 1 | ||||
|         return value | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| private class SystemClock { | ||||
|     static func elapsedRealtime() -> Int64 { | ||||
|         return Int64(ProcessInfo.processInfo.systemUptime * 1000) | ||||
|     } | ||||
|      | ||||
|     static func elapsedRealtimeNanos() -> Int64 { | ||||
|         return Int64(ProcessInfo.processInfo.systemUptime * 1_000_000_000) | ||||
|     } | ||||
| } | ||||
|  | @ -0,0 +1,140 @@ | |||
| // | ||||
| //  GuruIronSourceAdsSdk.swift | ||||
| //  Pods | ||||
| // | ||||
| //  Created by 250102 on 2025/5/6. | ||||
| // | ||||
| import IronSource | ||||
| 
 | ||||
| public class GuruIronSourceAdsSdk: GuruAdsSdk { | ||||
|      | ||||
|     private static var instance: GuruIronSourceAdsSdk? | ||||
| 
 | ||||
|      | ||||
|     public var adPlatform: AdPlatform { | ||||
|         return AdPlatform.ironSource | ||||
|     } | ||||
|      | ||||
|     private var initialized = false | ||||
|      | ||||
|     private static var interstitialAds: [String: GuruInterstitialAd] = [:] | ||||
|     private static var rewardedAds: [String: GuruRewardedAd] = [:] | ||||
|     private static var bannerAds: [String: GuruBannerAd] = [:] | ||||
|     private static var mrecAds: [String: GuruMRecAd] = [:] | ||||
|      | ||||
|     private func logDebug(_ message: String) { | ||||
|         Logger.d(tag: "GuruIronSourceAdsSdk", message: message) | ||||
|     } | ||||
|      | ||||
|     private func logInfo(_ message: String) { | ||||
|         Logger.i(tag: "GuruIronSourceAdsSdk", message: message) | ||||
|     } | ||||
|      | ||||
|     private func logWarn(_ message: String) { | ||||
|         Logger.w(tag: "GuruIronSourceAdsSdk", message: message) | ||||
|     } | ||||
|      | ||||
|     private func logError(_ message: String) { | ||||
|         Logger.e(tag: "GuruIronSourceAdsSdk", message: message) | ||||
|     } | ||||
|      | ||||
|     private let viewController: UIViewController | ||||
|      | ||||
|     private let impressionDataDispatcher = GuruIronSourceImpressionDataDispatcher() | ||||
|      | ||||
|     private init(viewController: UIViewController) { | ||||
|         self.viewController = viewController | ||||
|     } | ||||
|      | ||||
|     public static func obtain(viewController: UIViewController) -> GuruAdsSdk { | ||||
|         if let existingInstance = instance { | ||||
|             return existingInstance | ||||
|         } | ||||
|          | ||||
|         let newInstance = GuruIronSourceAdsSdk(viewController: viewController) | ||||
|         instance = newInstance | ||||
|         return newInstance | ||||
|     } | ||||
|      | ||||
|     public func initialize(adsProfile: AdsProfile) async -> Bool { | ||||
|         guard !initialized else { | ||||
|             logWarn("SDK already initialized! Ignoring.") | ||||
|             return false | ||||
|         } | ||||
|                  | ||||
|         if(adsProfile.ironSourceSdkKey?.isEmpty != false) { | ||||
|             logError("ironSourceSdkKey is empty, unable to initialize") | ||||
|             return false; | ||||
|         } | ||||
|          | ||||
|         initialized = true | ||||
|          | ||||
|         if(adsProfile.debugMode) { | ||||
|             IronSource.setMetaDataWithKey("is_test_suite", value: "enable") | ||||
|             ISIntegrationHelper.validateIntegration() | ||||
|         } | ||||
|         IronSource.add(impressionDataDispatcher) | ||||
|         return await withCheckedContinuation(isolation: MainActor.shared) { continuation in | ||||
|              | ||||
|             let requestBuilder = LPMInitRequestBuilder(appKey: adsProfile.ironSourceSdkKey!) | ||||
|                 .withUserId(adsProfile.userId) | ||||
|              | ||||
|             let initRequest = requestBuilder.build() | ||||
|              | ||||
|             LevelPlay.initWith(initRequest) | ||||
|             { config, error in | ||||
|                 if let error = error { | ||||
|                     self.logError("initialize failed, \(error)") | ||||
|                     continuation.resume(returning: false) | ||||
|                 } else { | ||||
|                     continuation.resume(returning: true) | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     public func obtainInterstitialAd(adConfig: AdConfig) -> GuruInterstitialAd { | ||||
|         if let existingAd = Self.interstitialAds[adConfig.cacheKey] { | ||||
|             return existingAd | ||||
|         } | ||||
|          | ||||
|         let newAd = GuruIronSourceInterstitialAd(viewController: viewController, adConfig: adConfig) | ||||
|         Self.interstitialAds[adConfig.cacheKey] = newAd | ||||
|         return newAd | ||||
|     } | ||||
|      | ||||
|     public func obtainRewardedAd(adConfig: AdConfig) -> GuruRewardedAd { | ||||
|         if let existingAd = Self.rewardedAds[adConfig.cacheKey] { | ||||
|             return existingAd | ||||
|         } | ||||
|          | ||||
|         let newAd = GuruIronSourceRewardedAd(viewController: viewController, adConfig: adConfig) | ||||
|         Self.rewardedAds[adConfig.cacheKey] = newAd | ||||
|         return newAd | ||||
|     } | ||||
|      | ||||
|     public func obtainBannerAd(adConfig: AdConfig) -> GuruBannerAd { | ||||
|         if let existingAd = Self.bannerAds[adConfig.cacheKey] { | ||||
|             return existingAd | ||||
|         } | ||||
|          | ||||
|         let newAd = GuruIronSourceBannerAd(viewController: viewController, adConfig: adConfig) | ||||
|         Self.bannerAds[adConfig.cacheKey] = newAd | ||||
|         return newAd | ||||
|     } | ||||
|      | ||||
|     public func obtainMRecAd(adConfig: AdConfig) -> GuruMRecAd { | ||||
|         if let existingAd = Self.mrecAds[adConfig.cacheKey] { | ||||
|             return existingAd | ||||
|         } | ||||
|          | ||||
|         let newAd = GuruIronSourceMRecAd(viewController: viewController, adConfig: adConfig) | ||||
|         Self.mrecAds[adConfig.cacheKey] = newAd | ||||
|         return newAd | ||||
|     } | ||||
|      | ||||
|     public func processCrossAction(action: AdsCrossAction) -> Bool { | ||||
|         return false | ||||
|     } | ||||
|      | ||||
| } | ||||
|  | @ -0,0 +1,124 @@ | |||
| // | ||||
| //  GuruIronSourceBannerAd.swift | ||||
| //  Pods | ||||
| // | ||||
| //  Created by 250102 on 2025/5/7. | ||||
| // | ||||
| import IronSource | ||||
| 
 | ||||
| class GuruIronSourceBannerView : GuruBannerView { | ||||
|     func view() -> UIView { | ||||
|         return bannerAdView | ||||
|     } | ||||
|      | ||||
|     func setPlacementName(placement: String) { | ||||
|         bannerAdView.setPlacementName(placement) | ||||
|     } | ||||
|      | ||||
|     func onShow() { | ||||
|         bannerAdView.isHidden = false | ||||
|         bannerAdView.resumeAutoRefresh() | ||||
|     } | ||||
|      | ||||
|     func onHide() { | ||||
|         bannerAdView.isHidden = true | ||||
|         bannerAdView.pauseAutoRefresh() | ||||
|     } | ||||
|      | ||||
|     public var bannerAdView: LPMBannerAdView | ||||
|      | ||||
|     public init(_ banner: LPMBannerAdView) { | ||||
|         self.bannerAdView = banner | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| class GuruIronSourceBannerAd : GuruBannerAd, LPMBannerAdViewDelegate, ISImpressionDataDelegate { | ||||
|      | ||||
|     private let viewController: UIViewController | ||||
|     private let adConfig: AdConfig | ||||
|     private var bannerAdCoordinator: GuruBannerCoordinator<GuruIronSourceBannerView>? | ||||
|     private var bannerAd: LPMBannerAdView? { return bannerAdCoordinator?.bannerAd.bannerAdView } | ||||
|     private var contentView: UIView? | ||||
|     private var currentUid2Token: String = "" | ||||
|      | ||||
|     private var contentId: Int { | ||||
|         return self.adConfig.engineId | ||||
|     } | ||||
|      | ||||
|     private let impressionListenerRegistrar: GuruIronSourceImpressionDataListenerRegistrar? | ||||
|          | ||||
|     public init(viewController: UIViewController, adConfig: AdConfig, impressionListenerRegistrar: GuruIronSourceImpressionDataListenerRegistrar? = nil) { | ||||
|         self.viewController = viewController | ||||
|         self.adConfig = adConfig | ||||
|         self.impressionListenerRegistrar = impressionListenerRegistrar | ||||
|         super.init(engineId: adConfig.engineId, adUnitId: adConfig.adUnitId, adPlatform: AdPlatform.ironSource) | ||||
|     } | ||||
|      | ||||
|     override func load() -> Bool { | ||||
|         let bannerAd = LPMBannerAdView(adUnitId: adConfig.adUnitId) | ||||
|         bannerAd.loadAd(with: self.viewController) | ||||
|         bannerAd.setDelegate(self) | ||||
|         impressionListenerRegistrar?.unregister(listener: self) | ||||
|         impressionListenerRegistrar?.register(adUnitId: adUnitId, listener: self) | ||||
|         bannerAd.setAdSize(LPMAdSize.banner()) | ||||
|         bannerAd.pauseAutoRefresh() | ||||
|         bannerAdCoordinator = GuruBannerCoordinator(viewController: viewController, bannerAd: GuruIronSourceBannerView(bannerAd), contentId: contentId) | ||||
|         return true | ||||
|     } | ||||
|      | ||||
|     public override func show(_ request: BannerShowRequest? = nil) -> Bool { | ||||
|         return bannerAdCoordinator?.show() ?? false | ||||
|     } | ||||
|      | ||||
|     public override func hide() -> Bool { | ||||
|         return bannerAdCoordinator?.hide() ?? false | ||||
|     } | ||||
|      | ||||
|     override public func destroy() -> Bool { | ||||
|         bannerAd?.destroy() | ||||
|         let _ = bannerAdCoordinator?.destroy() | ||||
|         bannerAdCoordinator = nil | ||||
|         impressionListenerRegistrar?.unregister(listener: self) | ||||
|         return true | ||||
|     } | ||||
|      | ||||
|     override public func updateOrientation(orientation: ScreenOrientation) -> Bool { | ||||
|         return bannerAdCoordinator?.updateOrientation(orientation: orientation) ?? false | ||||
|     } | ||||
|      | ||||
|     func didLoadAd(with adInfo: LPMAdInfo) { | ||||
|         adLoaded(ad: IronSourceFusionAd(engineId: engineId, adType: adType, ad: adInfo)) | ||||
|     } | ||||
|      | ||||
|     func didFailToLoadAd(withAdUnitId adUnitId: String, error: any Error) { | ||||
|         adLoadFailed(loadFailedInfo: LoadFailedInfo(engineId: engineId, adPlatform: adPlatform, adType: adType, adUnitId: adUnitId, error: SwiftFusionError(error))) | ||||
|     } | ||||
|      | ||||
|     func didClickAd(with adInfo: LPMAdInfo) { | ||||
|         adClicked(ad: IronSourceFusionAd(engineId: engineId, adType: adType, ad: adInfo)) | ||||
|     } | ||||
|      | ||||
|     func didExpandAd(with adInfo: LPMAdInfo) { | ||||
|         adBannerExpanded(ad: IronSourceFusionAd(engineId: engineId, adType: adType, ad: adInfo)) | ||||
|     } | ||||
|      | ||||
|     func didDisplayAd(with adInfo: LPMAdInfo) { | ||||
|         adDisplayed(ad: IronSourceFusionAd(engineId: engineId, adType: adType, ad: adInfo)) | ||||
|     } | ||||
|      | ||||
|     func didCollapseAd(with adInfo: LPMAdInfo) { | ||||
|         adBannerCollapsed(ad: IronSourceFusionAd(engineId: engineId, adType: adType, ad: adInfo)) | ||||
|     } | ||||
|      | ||||
|     func didFailToDisplayAd(with adInfo: LPMAdInfo, error: any Error) { | ||||
|         adDisplayFailed(ad: IronSourceFusionAd(engineId: engineId, adType: adType, ad: adInfo), error: SwiftFusionError(error)) | ||||
|     } | ||||
|      | ||||
|     func didLeaveApp(with adInfo: LPMAdInfo) { | ||||
|          | ||||
|     } | ||||
|      | ||||
|     func impressionDataDidSucceed(_ impressionData: ISImpressionData!) { | ||||
|         adRevenuePaid(ad: IronSourceImpressedFusionAd(engineId: engineId, adType: adType, impressionData: impressionData)) | ||||
|     } | ||||
| } | ||||
|  | @ -0,0 +1,88 @@ | |||
| // | ||||
| //  GuruIronSourceImpressionDataDispatcher.swift | ||||
| //  Pods | ||||
| // | ||||
| //  Created by 250102 on 2025/5/12. | ||||
| // | ||||
| 
 | ||||
| import IronSource | ||||
| internal protocol GuruIronSourceImpressionDataListenerRegistrar { | ||||
|     func register(adUnitId: String, listener: ISImpressionDataDelegate) | ||||
|     func unregister(listener: ISImpressionDataDelegate) | ||||
| } | ||||
| 
 | ||||
| internal class GuruIronSourceImpressionDataDispatcher : NSObject, ISImpressionDataDelegate, | ||||
|                                                         GuruIronSourceImpressionDataListenerRegistrar { | ||||
|     func impressionDataDidSucceed(_ impressionData: ISImpressionData?) { | ||||
|         guard let data = impressionData else { return } | ||||
|         dispatchImpressionData(data: data) | ||||
|     } | ||||
|      | ||||
| 
 | ||||
|     private var listeners: [(String, any ISImpressionDataDelegate)] = [] | ||||
| 
 | ||||
|     private func dispatchImpressionData(data: ISImpressionData) { | ||||
|         listeners.forEach { | ||||
|             let adUnitId = $0.0 | ||||
|             let listener = $0.1 | ||||
|             if (adUnitId == data.mediation_ad_unit_id) { | ||||
|                 listener.impressionDataDidSucceed(data) | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     func register( | ||||
|         adUnitId: String, | ||||
|         listener: ISImpressionDataDelegate | ||||
|     ) { | ||||
|         listeners.append((adUnitId, listener)) | ||||
|     } | ||||
| 
 | ||||
|     func unregister(listener: ISImpressionDataDelegate) { | ||||
|         listeners.removeAll { $0.1 === listener } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| internal class IronSourceImpressedFusionAd: FusionAd { | ||||
|     private let impressionData: ISImpressionData | ||||
|     init(engineId: Int, adType: AdType, impressionData: ISImpressionData) { | ||||
|         self.impressionData = impressionData | ||||
|         super.init(engineId: engineId, adType: adType) | ||||
|     } | ||||
|      | ||||
|     override public var adPlatform: AdPlatform { | ||||
|         return AdPlatform.ironSource | ||||
|     } | ||||
|      | ||||
|     override public var adUnitId: String? { | ||||
|         return impressionData.mediation_ad_unit_id | ||||
|     } | ||||
|      | ||||
|     override public var revenue: Double { | ||||
|         return impressionData.revenue?.doubleValue ?? 0.0 | ||||
|     } | ||||
|      | ||||
|     override public var waterfallName: String? { | ||||
|         return impressionData.auction_id | ||||
|     } | ||||
|      | ||||
|     override public var placement: String? { | ||||
|         return impressionData.placement | ||||
|     } | ||||
|      | ||||
|     override public var networkName: String? { | ||||
|         return impressionData.ad_network | ||||
|     } | ||||
|      | ||||
|     override public var networkPlacement: String? { | ||||
|         return nil | ||||
|     } | ||||
|      | ||||
|     override public var creativeId: String? { | ||||
|         return impressionData.creative_id | ||||
|     } | ||||
|      | ||||
|     override public var adReviewCreativeId: String? { | ||||
|         return nil | ||||
|     } | ||||
| } | ||||
|  | @ -0,0 +1,80 @@ | |||
| // | ||||
| //  GuruIronSourceInterstitialAd.swift | ||||
| //  Pods | ||||
| // | ||||
| //  Created by 250102 on 2025/5/6. | ||||
| // | ||||
| import IronSource | ||||
| 
 | ||||
| class GuruIronSourceInterstitialAd: GuruInterstitialAd, LPMInterstitialAdDelegate, ISImpressionDataDelegate { | ||||
|      | ||||
|     private let viewController: UIViewController | ||||
|     private let adConfig: AdConfig | ||||
|     private var ad: LPMInterstitialAd? | ||||
|     private let impressionListenerRegistrar: GuruIronSourceImpressionDataListenerRegistrar? | ||||
|          | ||||
|     public init(viewController: UIViewController, adConfig: AdConfig, impressionListenerRegistrar: GuruIronSourceImpressionDataListenerRegistrar? = nil) { | ||||
|         self.viewController = viewController | ||||
|         self.adConfig = adConfig | ||||
|         self.impressionListenerRegistrar = impressionListenerRegistrar | ||||
|         super.init(engineId: adConfig.engineId, adUnitId: adConfig.adUnitId, adPlatform: AdPlatform.ironSource) | ||||
|     } | ||||
|      | ||||
|     override func load() -> Bool { | ||||
|         logInfo("request load") | ||||
|         self.ad = LPMInterstitialAd(adUnitId: adConfig.adUnitId) | ||||
|         self.ad?.setDelegate(self) | ||||
|         impressionListenerRegistrar?.unregister(listener: self) | ||||
|         impressionListenerRegistrar?.register(adUnitId: adUnitId, listener: self) | ||||
|         self.ad?.loadAd() | ||||
|         return true | ||||
|     } | ||||
|      | ||||
|     override func show(_ request: InterstitialShowRequest? = nil) -> Bool { | ||||
|         logInfo("request show") | ||||
|         guard let ad = ad, ad.isAdReady() else { | ||||
|             return false | ||||
|         } | ||||
|         ad.showAd(viewController: viewController, placementName: request?.placement) | ||||
|         return true | ||||
|     } | ||||
|      | ||||
|     override func destroy() -> Bool { | ||||
|         self.ad = nil | ||||
|         impressionListenerRegistrar?.unregister(listener: self) | ||||
|         return true | ||||
|     } | ||||
|      | ||||
|     func didLoadAd(with adInfo: LPMAdInfo) { | ||||
|         logInfo("loaded") | ||||
|         adLoaded(ad: IronSourceFusionAd(engineId: engineId, adType: adType, ad: adInfo)) | ||||
|     } | ||||
|      | ||||
|     func didFailToLoadAd(withAdUnitId adUnitId: String, error: any Error) { | ||||
|         logError("load failed, \(error.code) - \(error.localizedDescription)") | ||||
|         adLoadFailed(loadFailedInfo: LoadFailedInfo(engineId: engineId, adPlatform: adPlatform, adType: adType, adUnitId: adUnitId, error: SwiftFusionError(error))) | ||||
|     } | ||||
|      | ||||
|     func didDisplayAd(with adInfo: LPMAdInfo) { | ||||
|         logInfo("displayed") | ||||
|         adDisplayed(ad: IronSourceFusionAd(engineId: engineId, adType: adType, ad: adInfo)) | ||||
|     } | ||||
|      | ||||
|     func didFailToDisplayAd(with adInfo: LPMAdInfo, error: any Error) { | ||||
|         logError("displayed failed, \(error.code) - \(error.localizedDescription)") | ||||
|         adDisplayFailed(ad: IronSourceFusionAd(engineId: engineId, adType: adType, ad: adInfo), error: SwiftFusionError(error)) | ||||
|     } | ||||
|      | ||||
|     func didClickAd(with adInfo: LPMAdInfo) { | ||||
|         adClicked(ad: IronSourceFusionAd(engineId: engineId, adType: adType, ad: adInfo)) | ||||
|     } | ||||
|      | ||||
|     func didCloseAd(with adInfo: LPMAdInfo) { | ||||
|         adHidden(ad: IronSourceFusionAd(engineId: engineId, adType: adType, ad: adInfo)) | ||||
|     } | ||||
|      | ||||
|     func impressionDataDidSucceed(_ impressionData: ISImpressionData!) { | ||||
|         logInfo("revenue paid") | ||||
|         adRevenuePaid(ad: IronSourceImpressedFusionAd(engineId: engineId, adType: adType, impressionData: impressionData)) | ||||
|     } | ||||
| } | ||||
|  | @ -0,0 +1,167 @@ | |||
| 
 | ||||
| // | ||||
| //  GuruIronSourceBannerAd.swift | ||||
| //  Pods | ||||
| // | ||||
| //  Created by 250102 on 2025/5/7. | ||||
| // | ||||
| import IronSource | ||||
| 
 | ||||
| class GuruIronSourceMRecAd : GuruMRecAd, LPMBannerAdViewDelegate, ISImpressionDataDelegate { | ||||
|      | ||||
|     private let viewController: UIViewController | ||||
|     private let adConfig: AdConfig | ||||
|     private var mrecAd: LPMBannerAdView? | ||||
|     private var contentView: UIView? | ||||
|     private var currentUid2Token: String = "" | ||||
|     private var horizontalOffset: CGFloat = 0.0 | ||||
|     private var verticalOffset: CGFloat = 0.0 | ||||
|     private var orientation: ScreenOrientation = .portrait | ||||
|      | ||||
|     private var contentId: Int { | ||||
|         return self.adConfig.engineId | ||||
|     } | ||||
|      | ||||
|     private let impressionListenerRegistrar: GuruIronSourceImpressionDataListenerRegistrar? | ||||
|          | ||||
|     public init(viewController: UIViewController, adConfig: AdConfig, impressionListenerRegistrar: GuruIronSourceImpressionDataListenerRegistrar? = nil) { | ||||
|         self.viewController = viewController | ||||
|         self.adConfig = adConfig | ||||
|         self.impressionListenerRegistrar = impressionListenerRegistrar | ||||
|         super.init(engineId: adConfig.engineId, adUnitId: adConfig.adUnitId, adPlatform: AdPlatform.ironSource) | ||||
|     } | ||||
|      | ||||
|     override func load() -> Bool { | ||||
|         self.mrecAd = LPMBannerAdView(adUnitId: adConfig.adUnitId) | ||||
|         self.mrecAd?.loadAd(with: self.viewController) | ||||
|         self.mrecAd?.setDelegate(self) | ||||
|         impressionListenerRegistrar?.unregister(listener: self) | ||||
|         impressionListenerRegistrar?.register(adUnitId: adUnitId, listener: self) | ||||
|         self.mrecAd?.setAdSize(LPMAdSize.mediumRectangle()) | ||||
|         self.mrecAd?.pauseAutoRefresh() | ||||
|         return true | ||||
|     } | ||||
|      | ||||
|     public override func show(request: MRecShowRequest? = nil) -> Bool { | ||||
|         let placement = request?.placement | ||||
|         let newHorizontalOffset = request?.horizontalOffset | ||||
|         let newVerticalOffset = request?.verticalOffset | ||||
|          | ||||
|         var shouldReattach = false | ||||
|          | ||||
|         // Check if position or offset has changed | ||||
|         if let offset = newVerticalOffset, offset != Float(verticalOffset) { | ||||
|             verticalOffset = CGFloat(offset) | ||||
|             shouldReattach = true | ||||
|         } | ||||
|          | ||||
|         if let offset = newHorizontalOffset, offset != Float(horizontalOffset) { | ||||
|             horizontalOffset = CGFloat(offset) | ||||
|             shouldReattach = true | ||||
|         } | ||||
|          | ||||
|         // If we already have a view and parameters changed, remove it | ||||
|         if let contentView = contentView, shouldReattach { | ||||
|             logInfo("Parameters changed! Re-attaching view!") | ||||
|             contentView.removeFromSuperview() | ||||
|             self.contentView = nil | ||||
|         } | ||||
|          | ||||
|         // Create new content view if needed | ||||
|         if contentView == nil { | ||||
|             let container = UIView() | ||||
|             container.tag = contentId | ||||
|              | ||||
|             viewController.view.addSubview(container) | ||||
|             container.translatesAutoresizingMaskIntoConstraints = false | ||||
|             NSLayoutConstraint.activate([ | ||||
|                 container.leadingAnchor.constraint(equalTo: viewController.view.leadingAnchor), | ||||
|                 container.trailingAnchor.constraint(equalTo: viewController.view.trailingAnchor), | ||||
|                 container.topAnchor.constraint(equalTo: viewController.view.topAnchor), | ||||
|                 container.bottomAnchor.constraint(equalTo: viewController.view.bottomAnchor) | ||||
|             ]) | ||||
|              | ||||
|             contentView = container | ||||
|              | ||||
|             guard let mrecAd = mrecAd else { return false } | ||||
|              | ||||
|             // Standard MREC size is 300x250 | ||||
|             let mrecWidth: CGFloat = 300 | ||||
|             let mrecHeight: CGFloat = 250 | ||||
|              | ||||
|             mrecAd.translatesAutoresizingMaskIntoConstraints = false | ||||
|             container.addSubview(mrecAd) | ||||
|              | ||||
|             NSLayoutConstraint.activate([ | ||||
|                 mrecAd.leadingAnchor.constraint(equalTo: container.leadingAnchor, constant: horizontalOffset), | ||||
|                 mrecAd.topAnchor.constraint(equalTo: container.topAnchor, constant: verticalOffset), | ||||
|                 mrecAd.widthAnchor.constraint(equalToConstant: mrecWidth), | ||||
|                 mrecAd.heightAnchor.constraint(equalToConstant: mrecHeight) | ||||
|             ]) | ||||
|              | ||||
|             mrecAd.isHidden = false | ||||
|             mrecAd.resumeAutoRefresh() | ||||
|         } else { | ||||
|             mrecAd?.resumeAutoRefresh() | ||||
|             mrecAd?.isHidden = false | ||||
|         } | ||||
|          | ||||
|         return true | ||||
|     } | ||||
|      | ||||
|     public override func hide() -> Bool { | ||||
|         guard let _ = viewController.view.viewWithTag(contentId) else { | ||||
|             return true | ||||
|         } | ||||
|          | ||||
|         if let mrecAd = mrecAd, !mrecAd.isHidden { | ||||
|             mrecAd.pauseAutoRefresh() | ||||
|             mrecAd.isHidden = true | ||||
|         } | ||||
|          | ||||
|         return true | ||||
|     } | ||||
|      | ||||
|     override public func destroy() -> Bool{ | ||||
|         mrecAd?.removeFromSuperview() | ||||
|         mrecAd = nil | ||||
|          | ||||
|         contentView?.removeFromSuperview() | ||||
|         contentView = nil | ||||
|          | ||||
|         impressionListenerRegistrar?.unregister(listener: self) | ||||
|         return true | ||||
|     } | ||||
|      | ||||
|     func didLoadAd(with adInfo: LPMAdInfo) { | ||||
|         adLoaded(ad: IronSourceFusionAd(engineId: engineId, adType: adType, ad: adInfo)) | ||||
|     } | ||||
|      | ||||
|     func didFailToLoadAd(withAdUnitId adUnitId: String, error: any Error) { | ||||
|         adLoadFailed(loadFailedInfo: LoadFailedInfo(engineId: engineId, adPlatform: adPlatform, adType: adType, adUnitId: adUnitId, error: SwiftFusionError(error))) | ||||
|     } | ||||
|      | ||||
|     func didClickAd(with adInfo: LPMAdInfo) { | ||||
|         adClicked(ad: IronSourceFusionAd(engineId: engineId, adType: adType, ad: adInfo)) | ||||
|     } | ||||
|      | ||||
|     func didExpandAd(with adInfo: LPMAdInfo) { | ||||
|         adBannerExpanded(ad: IronSourceFusionAd(engineId: engineId, adType: adType, ad: adInfo)) | ||||
|     } | ||||
|      | ||||
|     func didDisplayAd(with adInfo: LPMAdInfo) { | ||||
|         adDisplayed(ad: IronSourceFusionAd(engineId: engineId, adType: adType, ad: adInfo)) | ||||
|     } | ||||
|      | ||||
|     func didCollapseAd(with adInfo: LPMAdInfo) { | ||||
|         adBannerCollapsed(ad: IronSourceFusionAd(engineId: engineId, adType: adType, ad: adInfo)) | ||||
|     } | ||||
|      | ||||
|     func didFailToDisplayAd(with adInfo: LPMAdInfo, error: any Error) { | ||||
|         adDisplayFailed(ad: IronSourceFusionAd(engineId: engineId, adType: adType, ad: adInfo), error: SwiftFusionError(error)) | ||||
|     } | ||||
|      | ||||
|     func impressionDataDidSucceed(_ impressionData: ISImpressionData!) { | ||||
|         adRevenuePaid(ad: IronSourceImpressedFusionAd(engineId: engineId, adType: adType, impressionData: impressionData)) | ||||
|     } | ||||
| } | ||||
|  | @ -0,0 +1,86 @@ | |||
| // | ||||
| //  GuruIronSourceRewardedAd.swift | ||||
| //  Pods | ||||
| // | ||||
| //  Created by 250102 on 2025/5/7. | ||||
| // | ||||
| import IronSource | ||||
| 
 | ||||
| class GuruIronSourceRewardedAd: GuruRewardedAd, LPMRewardedAdDelegate, ISImpressionDataDelegate { | ||||
|      | ||||
|     private let viewController: UIViewController | ||||
|     private let adConfig: AdConfig | ||||
|     private var ad: LPMRewardedAd? | ||||
|          | ||||
|     private let impressionListenerRegistrar: GuruIronSourceImpressionDataListenerRegistrar? | ||||
|          | ||||
|     public init(viewController: UIViewController, adConfig: AdConfig, impressionListenerRegistrar: GuruIronSourceImpressionDataListenerRegistrar? = nil) { | ||||
|         self.viewController = viewController | ||||
|         self.adConfig = adConfig | ||||
|         self.impressionListenerRegistrar = impressionListenerRegistrar | ||||
|         super.init(engineId: adConfig.engineId, adUnitId: adConfig.adUnitId, adPlatform: AdPlatform.ironSource) | ||||
|     } | ||||
|      | ||||
|     override func load() -> Bool { | ||||
|         logInfo("request load") | ||||
|         self.ad = LPMRewardedAd(adUnitId: adConfig.adUnitId) | ||||
|         self.ad?.setDelegate(self) | ||||
|         impressionListenerRegistrar?.unregister(listener: self) | ||||
|         impressionListenerRegistrar?.register(adUnitId: adUnitId, listener: self) | ||||
|         self.ad?.loadAd() | ||||
|         return true | ||||
|     } | ||||
|      | ||||
|     override func show(_ request: RewardedShowRequest? = nil) -> Bool { | ||||
|         logInfo("request show") | ||||
|         guard let ad = ad, ad.isAdReady() else { | ||||
|             return false | ||||
|         } | ||||
|         ad.showAd(viewController: viewController, placementName: request?.placement) | ||||
|         return true | ||||
|     } | ||||
|      | ||||
|     override func destroy() -> Bool { | ||||
|         self.ad = nil | ||||
|         impressionListenerRegistrar?.unregister(listener: self) | ||||
|         return true | ||||
|     } | ||||
|      | ||||
|     func didLoadAd(with adInfo: LPMAdInfo) { | ||||
|         logInfo("loaded") | ||||
|         adLoaded(ad: IronSourceFusionAd(engineId: engineId, adType: adType, ad: adInfo)) | ||||
|     } | ||||
|      | ||||
|     func didFailToLoadAd(withAdUnitId adUnitId: String, error: any Error) { | ||||
|         logError("load failed, \(error.code) - \(error.localizedDescription)") | ||||
|         adLoadFailed(loadFailedInfo: LoadFailedInfo(engineId: engineId, adPlatform: adPlatform, adType: adType, adUnitId: adUnitId, error: SwiftFusionError(error))) | ||||
|     } | ||||
|      | ||||
|     func didDisplayAd(with adInfo: LPMAdInfo) { | ||||
|         logInfo("displayed") | ||||
|         adDisplayed(ad: IronSourceFusionAd(engineId: engineId, adType: adType, ad: adInfo)) | ||||
|     } | ||||
|      | ||||
|     func didFailToDisplayAd(with adInfo: LPMAdInfo, error: any Error) { | ||||
|         logError("displayed failed, \(error.code) - \(error.localizedDescription)") | ||||
|         adDisplayFailed(ad: IronSourceFusionAd(engineId: engineId, adType: adType, ad: adInfo), error: SwiftFusionError(error)) | ||||
|     } | ||||
|      | ||||
|     func didClickAd(with adInfo: LPMAdInfo) { | ||||
|         adClicked(ad: IronSourceFusionAd(engineId: engineId, adType: adType, ad: adInfo)) | ||||
|     } | ||||
|      | ||||
|     func didCloseAd(with adInfo: LPMAdInfo) { | ||||
|         adHidden(ad: IronSourceFusionAd(engineId: engineId, adType: adType, ad: adInfo)) | ||||
|     } | ||||
|      | ||||
|     func didRewardAd(with adInfo: LPMAdInfo, reward: LPMReward) { | ||||
|         logInfo("reward user") | ||||
|         adUserRewarded(ad: IronSourceFusionAd(engineId: engineId, adType: adType, ad: adInfo), reward: IronSourceFusionReward(reward)) | ||||
|     } | ||||
|      | ||||
|     func impressionDataDidSucceed(_ impressionData: ISImpressionData!) { | ||||
|         logInfo("revenue paid") | ||||
|         adRevenuePaid(ad: IronSourceImpressedFusionAd(engineId: engineId, adType: adType, impressionData: impressionData)) | ||||
|     } | ||||
| } | ||||
|  | @ -0,0 +1,52 @@ | |||
| // | ||||
| //  GuruIronSourceAd.swift | ||||
| //  Pods | ||||
| // | ||||
| //  Created by 250102 on 2025/5/6. | ||||
| // | ||||
| import IronSource | ||||
| 
 | ||||
| class IronSourceFusionAd: FusionAd { | ||||
|     private let originAd: LPMAdInfo | ||||
|      | ||||
|     public init(engineId: Int, adType: AdType, ad: LPMAdInfo) { | ||||
|         originAd = ad | ||||
|         super.init(engineId: engineId, adType: adType) | ||||
|     } | ||||
|      | ||||
|     override public var adPlatform: AdPlatform { | ||||
|         return AdPlatform.ironSource | ||||
|     } | ||||
|      | ||||
|     override public var adUnitId: String? { | ||||
|         return originAd.adUnitId | ||||
|     } | ||||
|      | ||||
|     override public var revenue: Double { | ||||
|         return originAd.revenue.doubleValue | ||||
|     } | ||||
|      | ||||
|     override public var waterfallName: String? { | ||||
|         return originAd.auctionId | ||||
|     } | ||||
|      | ||||
|     override public var placement: String? { | ||||
|         return originAd.placementName | ||||
|     } | ||||
|      | ||||
|     override public var networkName: String? { | ||||
|         return originAd.adNetwork | ||||
|     } | ||||
|      | ||||
|     override public var networkPlacement: String? { | ||||
|         return nil | ||||
|     } | ||||
|      | ||||
|     override public var creativeId: String? { | ||||
|         return originAd.creativeId | ||||
|     } | ||||
|      | ||||
|     override public var adReviewCreativeId: String? { | ||||
|         return nil | ||||
|     } | ||||
| } | ||||
|  | @ -0,0 +1,18 @@ | |||
| // | ||||
| //  IronSourceFusionReward.swift | ||||
| //  Pods | ||||
| // | ||||
| //  Created by 250102 on 2025/5/7. | ||||
| // | ||||
| import IronSource | ||||
| 
 | ||||
| class IronSourceFusionReward : FusionReward { | ||||
|     var rewardAmount: Int { | ||||
|         return reward.amount | ||||
|     } | ||||
|      | ||||
|     private let reward: LPMReward | ||||
|     public init(_ reward: LPMReward) { | ||||
|         self.reward = reward | ||||
|     } | ||||
| } | ||||
|  | @ -0,0 +1,240 @@ | |||
| // GuruMaxAdsSdk.swift | ||||
| // Main SDK class for MAX integration | ||||
| // Corresponds to GuruMaxAdsSdk.kt in Android implementation | ||||
| 
 | ||||
| import UIKit | ||||
| import AppLovinSDK | ||||
| import DTBiOSSDK | ||||
| import GuruConsent | ||||
| import FBAudienceNetwork | ||||
| import OpenWrapSDK | ||||
| import InMobiSDK | ||||
| import TradPlusAds | ||||
| import MobileFuseSDK | ||||
| 
 | ||||
| public class GuruMaxAdsSdk: GuruAdsSdk { | ||||
|      | ||||
|     private static var instance: GuruMaxAdsSdk? | ||||
|     private var initialized = false | ||||
|      | ||||
|     private static var interstitialAds: [String: GuruMaxInterstitialAd] = [:] | ||||
|     private static var rewardedAds: [String: GuruMaxRewardedAd] = [:] | ||||
|     private static var bannerAds: [String: GuruMaxBannerAd] = [:] | ||||
|     private static var mrecAds: [String: GuruMaxMRecAd] = [:] | ||||
|      | ||||
|     private let viewController: UIViewController | ||||
|      | ||||
|     private init(viewController: UIViewController) { | ||||
|         self.viewController = viewController | ||||
|     } | ||||
|      | ||||
|     public static func obtain(viewController: UIViewController) -> GuruAdsSdk { | ||||
|         if let existingInstance = instance { | ||||
|             return existingInstance | ||||
|         } | ||||
|          | ||||
|         let newInstance = GuruMaxAdsSdk(viewController: viewController) | ||||
|         instance = newInstance | ||||
|         return newInstance | ||||
|     } | ||||
|      | ||||
|     private func logDebug(_ message: String) { | ||||
|         Logger.d(tag: "GuruMaxAdsSdk", message: message) | ||||
|     } | ||||
|      | ||||
|     private func logInfo(_ message: String) { | ||||
|         Logger.i(tag: "GuruMaxAdsSdk", message: message) | ||||
|     } | ||||
|      | ||||
|     private func logWarn(_ message: String) { | ||||
|         Logger.w(tag: "GuruMaxAdsSdk", message: message) | ||||
|     } | ||||
|      | ||||
|     public var adPlatform: AdPlatform { | ||||
|         return AdPlatform.max | ||||
|     } | ||||
|      | ||||
|     public func initialize(adsProfile: AdsProfile) async -> Bool { | ||||
|         return await internalInitialize(adsProfile: adsProfile) | ||||
| //        return true | ||||
|     } | ||||
|      | ||||
|     private func internalInitialize(adsProfile: AdsProfile) async -> Bool { | ||||
|         guard !initialized else { | ||||
|             logWarn("SDK already initialized! Ignoring.") | ||||
|             return false | ||||
|         } | ||||
|          | ||||
|         guard !adsProfile.maxSdkKey.isEmpty else { | ||||
|             logWarn("maxSdkKey is empty") | ||||
|             return false | ||||
|         } | ||||
|          | ||||
|         initialized = true | ||||
|          | ||||
|         // Initialize Amazon SDK if needed | ||||
|         if let amazonAppId = adsProfile.amazonAppId, !amazonAppId.isEmpty { | ||||
|             // In a real implementation, you would initialize Amazon SDK here | ||||
|             logInfo("Would initialize Amazon SDK with ID: \(amazonAppId)") | ||||
|         } | ||||
|          | ||||
|         // Configure PubMatic if needed | ||||
|         if let pubmaticStoreUrl = adsProfile.pubmaticStoreUrl, !pubmaticStoreUrl.isEmpty { | ||||
|             // In a real implementation, you would configure PubMatic here | ||||
|             logInfo("Would configure PubMatic with store URL: \(pubmaticStoreUrl)") | ||||
|         } | ||||
|         let amazonAppId = adsProfile.amazonAppId ?? "" | ||||
|         if (!amazonAppId.isEmpty) { | ||||
|             DTBAds.sharedInstance().setAppKey(amazonAppId) | ||||
|             DTBAds.sharedInstance().setAdNetworkInfo(DTBAdNetworkInfo.init(networkName: DTBADNETWORK_MAX)) | ||||
|             DTBAds.sharedInstance().mraidCustomVersions = ["1.0", "2.0", "3.0"] | ||||
|             DTBAds.sharedInstance().mraidPolicy = CUSTOM_MRAID | ||||
|             DTBAds.sharedInstance().useGeoLocation = true | ||||
|              | ||||
|             if (adsProfile.debugMode) { | ||||
|                 DTBAds.sharedInstance().setLogLevel(DTBLogLevelAll) | ||||
|             } | ||||
|             //            DTBAds.sharedInstance().testMode = true | ||||
|         } | ||||
|          | ||||
|         // Update UID2 token if available | ||||
|         if let uid2Token = adsProfile.uid2Token { | ||||
|             updateUid2Token(uid2Token) | ||||
|         } | ||||
|          | ||||
|         if(!(adsProfile.pubmaticStoreUrl?.isEmpty ?? true)){ | ||||
|             let appInfo = POBApplicationInfo() | ||||
|             appInfo.storeURL = URL(string: adsProfile.pubmaticStoreUrl!)! | ||||
|             OpenWrapSDK.setApplicationInfo(appInfo) | ||||
|         } | ||||
|          | ||||
|         let maxSdkKey = adsProfile.maxSdkKey | ||||
|         NSLog("===> [\(ALMediationProviderMAX)]maxSdkKey: \(maxSdkKey)") | ||||
|         let initConfig = ALSdkInitializationConfiguration(sdkKey: maxSdkKey) { builder in | ||||
|             builder.mediationProvider = ALMediationProviderMAX | ||||
|             let segments = adsProfile.segments.segments | ||||
|             if(!segments.isEmpty) { | ||||
|                 builder.segmentCollection = MASegmentCollection { segmentsBuilder in | ||||
|                     for seg in segments { | ||||
|                         if (seg.key <= 0 || seg.values.isEmpty) { | ||||
|                             continue | ||||
|                         } | ||||
|                         segmentsBuilder.add(MASegment(key: NSNumber(value: seg.key), values: seg.values.map { | ||||
|                             NSNumber(value: $0) | ||||
|                         })) | ||||
|                         NSLog("segment.key:\(seg.key) \(seg.values)") | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|          | ||||
|         // Initialize the AppLovin SDK | ||||
|         let sdk = ALSdk.shared() | ||||
|          | ||||
|         // Set user ID if available | ||||
|         if !adsProfile.userId.isEmpty { | ||||
|             sdk.settings.userIdentifier = adsProfile.userId | ||||
|             logDebug("Set userId \(adsProfile.userId) success") | ||||
|         } else { | ||||
|             logDebug("userId is null") | ||||
|         } | ||||
|          | ||||
|         // Set custom parameters | ||||
|         sdk.settings.isVerboseLoggingEnabled = adsProfile.debugMode | ||||
|         sdk.settings.isMuted = true | ||||
|         sdk.settings.setExtraParameterForKey("enable_black_screen_fixes", value: "true") | ||||
|          | ||||
|         return await withCheckedContinuation(isolation: MainActor.shared) { continuation in | ||||
|             // Initialize AppLovin SDK | ||||
|             do { | ||||
|                 sdk.initialize(with: initConfig){ [weak self] configuration in | ||||
|                     guard let self = self else { | ||||
|                         continuation.resume(returning: false) | ||||
|                         return | ||||
|                     } | ||||
|                      | ||||
|                     self.logDebug("AppLovinSdk initialized successfully") | ||||
|                      | ||||
|                     // Initialize TP Creative Key if needed | ||||
|                     if !adsProfile.tpCreativeKey.isEmpty { | ||||
|                         DispatchQueue.main.async { | ||||
|                             self.logWarn("tpCreativeKey \(adsProfile.tpCreativeKey)") | ||||
|                             // In a real implementation, you would initialize GuardManager here | ||||
|                         } | ||||
|                     } | ||||
|                     continuation.resume(returning: true) | ||||
|                 } | ||||
|             } catch { | ||||
|                 continuation.resume(returning: false) | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     private func updateUid2Token(_ uid2Token: String?) { | ||||
|         guard let uid2Token = uid2Token, !uid2Token.isEmpty else { return } | ||||
|          | ||||
|         // In a real implementation, you would set the UID2 token to various ad networks | ||||
|         logDebug("Set uid2Token success \(uid2Token)") | ||||
|          | ||||
|         // Define UID2 API constant | ||||
|         let UID2_API = "uidapi.com" | ||||
|          | ||||
|         // Update for InMobi | ||||
|         let jsonObject = ["uidapi.com": uid2Token] | ||||
|         // In a real implementation, you would use this with InMobiSdk | ||||
|          | ||||
|         // Update for PubMatic | ||||
|         // In a real implementation, you would use this with PubMatic SDK | ||||
|          | ||||
|         // Update for TradPlus | ||||
|         // In a real implementation, you would use this with TradPlus SDK | ||||
|          | ||||
|         // Update for MobileFuse | ||||
|         // In a real implementation, you would use this with MobileFuse SDK | ||||
|     } | ||||
|      | ||||
|     public func obtainInterstitialAd(adConfig: AdConfig) -> GuruInterstitialAd { | ||||
|         if let existingAd = Self.interstitialAds[adConfig.cacheKey] { | ||||
|             return existingAd | ||||
|         } | ||||
|          | ||||
|         let newAd = GuruMaxInterstitialAd(viewController: viewController, adConfig: adConfig) | ||||
|         Self.interstitialAds[adConfig.cacheKey] = newAd | ||||
|         return newAd | ||||
|     } | ||||
|      | ||||
|     public func obtainRewardedAd(adConfig: AdConfig) -> GuruRewardedAd { | ||||
|         if let existingAd = Self.rewardedAds[adConfig.cacheKey] { | ||||
|             return existingAd | ||||
|         } | ||||
|          | ||||
|         let newAd = GuruMaxRewardedAd(viewController: viewController, adConfig: adConfig) | ||||
|         Self.rewardedAds[adConfig.cacheKey] = newAd | ||||
|         return newAd | ||||
|     } | ||||
|      | ||||
|     public func obtainBannerAd(adConfig: AdConfig) -> GuruBannerAd { | ||||
|         if let existingAd = Self.bannerAds[adConfig.cacheKey] { | ||||
|             return existingAd | ||||
|         } | ||||
|          | ||||
|         let newAd = GuruMaxBannerAd(viewController: viewController, adConfig: adConfig) | ||||
|         Self.bannerAds[adConfig.cacheKey] = newAd | ||||
|         return newAd | ||||
|     } | ||||
|      | ||||
|     public func obtainMRecAd(adConfig: AdConfig) -> GuruMRecAd { | ||||
|         if let existingAd = Self.mrecAds[adConfig.cacheKey] { | ||||
|             return existingAd | ||||
|         } | ||||
|          | ||||
|         let newAd = GuruMaxMRecAd(viewController: viewController, adConfig: adConfig) | ||||
|         Self.mrecAds[adConfig.cacheKey] = newAd | ||||
|         return newAd | ||||
|     } | ||||
|      | ||||
|     public func processCrossAction(action: AdsCrossAction) -> Bool { | ||||
|         // Implement cross-platform actions as needed | ||||
|         return true | ||||
|     } | ||||
| } | ||||
|  | @ -0,0 +1,173 @@ | |||
| // GuruMaxBannerAd.swift | ||||
| // Implementation for MAX banner ads | ||||
| // Corresponds to GuruMaxBannerAd.kt in Android implementation | ||||
| 
 | ||||
| import Foundation | ||||
| import UIKit | ||||
| import AppLovinSDK | ||||
| 
 | ||||
| class GuruMaxBannerView : GuruBannerView { | ||||
|     func view() -> UIView { | ||||
|         return bannerAdView | ||||
|     } | ||||
|      | ||||
|     func setPlacementName(placement: String) { | ||||
|         bannerAdView.placement = placement | ||||
|     } | ||||
|      | ||||
|     func onShow() { | ||||
|         bannerAdView.isHidden = false | ||||
|         bannerAdView.startAutoRefresh() | ||||
|     } | ||||
|      | ||||
|     func onHide() { | ||||
|         bannerAdView.setExtraParameterForKey("allow_pause_auto_refresh_immediately", value: "true") | ||||
|         bannerAdView.isHidden = true | ||||
|         bannerAdView.stopAutoRefresh() | ||||
|     } | ||||
|      | ||||
|     public var bannerAdView: MAAdView | ||||
|      | ||||
|     public init(_ banner: MAAdView) { | ||||
|         self.bannerAdView = banner | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| public class GuruMaxBannerAd: GuruBannerAd, MAAdViewAdDelegate, MAAdRevenueDelegate { | ||||
|      | ||||
|     private let viewController: UIViewController | ||||
|     private let adConfig: AdConfig | ||||
|      | ||||
|     private var bannerAd: MAAdView? { return bannerAdCoordinator?.bannerAd.bannerAdView} | ||||
|     private var bannerAdCoordinator: GuruBannerCoordinator<GuruMaxBannerView>? | ||||
|     private var contentView: UIView? | ||||
|     private var currentUid2Token: String = "" | ||||
|      | ||||
|     private var contentId: Int { | ||||
|         return self.adConfig.engineId | ||||
|     } | ||||
|      | ||||
|     public init(viewController: UIViewController, adConfig: AdConfig) { | ||||
|         self.viewController = viewController | ||||
|         self.adConfig = adConfig | ||||
|         super.init(engineId: adConfig.engineId, adUnitId: adConfig.adUnitId, adPlatform: AdPlatform.max) | ||||
|     } | ||||
|      | ||||
|     override public func load() -> Bool { | ||||
|         logInfo("request load") | ||||
|         let bannerAd = MAAdView(adUnitIdentifier: adConfig.adUnitId, adFormat: MAAdFormat.banner) | ||||
|                  | ||||
|         bannerAd.delegate = self | ||||
|         bannerAd.revenueDelegate = self | ||||
|         bannerAd.setExtraParameterForKey("allow_pause_auto_refresh_immediately", value: "true") | ||||
|          | ||||
|         if let uid2Token = AdsHelper.getUid2Token() { | ||||
|             bannerAd.setExtraParameterForKey("uid2_token", value: uid2Token) | ||||
|             currentUid2Token = uid2Token | ||||
|         } | ||||
|          | ||||
|         bannerAd.stopAutoRefresh() | ||||
|          | ||||
|         if let amazonSlotId = adConfig.adAmazonSlotId, !amazonSlotId.isEmpty { | ||||
|             // In a real implementation, you would load Amazon ads here | ||||
|             logDebug("Would load Amazon banner ad with slot ID: \(amazonSlotId)") | ||||
|              | ||||
|             // Simulate direct load for now | ||||
|             bannerAd.loadAd() | ||||
|         } else { | ||||
|             bannerAd.loadAd() | ||||
|         } | ||||
|          | ||||
|         self.bannerAdCoordinator = GuruBannerCoordinator(viewController: viewController, bannerAd: GuruMaxBannerView(bannerAd), contentId: contentId) | ||||
|          | ||||
|         return true | ||||
|     } | ||||
|      | ||||
|     public override func show(_ request: BannerShowRequest? = nil) -> Bool { | ||||
|         logInfo("request show") | ||||
|         let result = self.bannerAdCoordinator?.show(request) | ||||
|                  | ||||
|         // Update UID2 token if needed | ||||
|         if let uid2Token = AdsHelper.getUid2Token(), !uid2Token.isEmpty, uid2Token != currentUid2Token { | ||||
|             bannerAd?.setExtraParameterForKey("uid2_token", value: uid2Token) | ||||
|             currentUid2Token = uid2Token | ||||
|             logInfo("BannerAd refresh uid2_token \(currentUid2Token)") | ||||
|         } | ||||
|          | ||||
|         return result ?? false | ||||
|     } | ||||
|      | ||||
|     public override func hide() -> Bool { | ||||
|         logInfo("request hide") | ||||
|         return bannerAdCoordinator?.hide() ?? false | ||||
|     } | ||||
|      | ||||
|     override public func destroy() -> Bool { | ||||
|         logInfo("request destroy") | ||||
|         bannerAd?.delegate = nil | ||||
|         bannerAd?.revenueDelegate = nil | ||||
|         let _ = bannerAdCoordinator?.destroy() | ||||
|         bannerAdCoordinator = nil | ||||
|         return true | ||||
|     } | ||||
|      | ||||
|     override public func updateOrientation(orientation: ScreenOrientation) -> Bool { | ||||
|         logInfo("update orientation to \(orientation)") | ||||
| 
 | ||||
|         return bannerAdCoordinator?.updateOrientation(orientation: orientation) ?? false | ||||
|     } | ||||
|      | ||||
|     // MARK: - MAXAdViewAdDelegate methods | ||||
|      | ||||
|     public func didLoad(_ ad: MAAd) { | ||||
|         logInfo("loaded from \(ad.networkName)") | ||||
|         listener?.onAdLoaded(ad: MaxFusionAd(engineId: engineId, adType: AdType.Banner, ad: ad)) | ||||
|     } | ||||
|      | ||||
|     public func didFailToLoadAd(forAdUnitIdentifier adUnitIdentifier: String, withError error: MAError) { | ||||
|         logError("load failed, \(error.code) - \(error.message)") | ||||
|         let loadFailedInfo = LoadFailedInfo( | ||||
|             engineId: engineId, | ||||
|             adPlatform: AdPlatform.max, | ||||
|             adType: AdType.Banner, | ||||
|             adUnitId: adUnitIdentifier, | ||||
|             error: MaxFusionError(error) | ||||
|         ) | ||||
|         listener?.onAdLoadFailed(loadFailedInfo: loadFailedInfo) | ||||
|     } | ||||
|      | ||||
|     public func didDisplay(_ ad: MAAd) { | ||||
|         listener?.onAdDisplayed(ad: MaxFusionAd(engineId: engineId, adType: AdType.Banner, ad: ad)) | ||||
|     } | ||||
|      | ||||
|     public func didHide(_ ad: MAAd) { | ||||
|         listener?.onAdHidden(ad: MaxFusionAd(engineId: engineId, adType: AdType.Banner, ad: ad)) | ||||
|     } | ||||
|      | ||||
|     public func didClick(_ ad: MAAd) { | ||||
|         listener?.onAdClicked(ad: MaxFusionAd(engineId: engineId, adType: AdType.Banner, ad: ad)) | ||||
|     } | ||||
|      | ||||
|     public func didFail(toDisplay ad: MAAd, withError error: MAError) { | ||||
|         logError("display failed, \(error.code) - \(error.message)") | ||||
|         listener?.onAdDisplayFailed( | ||||
|             ad: MaxFusionAd(engineId: engineId, adType: AdType.Banner, ad: ad), | ||||
|             error: MaxFusionError(error) | ||||
|         ) | ||||
|     } | ||||
|      | ||||
|     public func didExpand(_ ad: MAAd) { | ||||
|         // In a real implementation, you would call adBannerExpanded | ||||
|     } | ||||
|      | ||||
|     public func didCollapse(_ ad: MAAd) { | ||||
|         // In a real implementation, you would call adBannerCollapsed | ||||
|     } | ||||
|      | ||||
|     // MARK: - MAXAdRevenueDelegate methods | ||||
|      | ||||
|     public func didPayRevenue(for ad: MAAd) { | ||||
|         logInfo("revenue paid") | ||||
|         listener?.onAdRevenuePaid(ad: MaxFusionAd(engineId: engineId, adType: AdType.Banner, ad: ad)) | ||||
|     } | ||||
| } | ||||
|  | @ -0,0 +1,123 @@ | |||
| // GuruMaxInterstitialAd.swift | ||||
| // Implementation for MAX interstitial ads | ||||
| // Corresponds to GuruMaxInterstitialAd.kt in Android implementation | ||||
| 
 | ||||
| import Foundation | ||||
| import UIKit | ||||
| import AppLovinSDK | ||||
| 
 | ||||
| public class GuruMaxInterstitialAd: GuruInterstitialAd, MAAdDelegate, MAAdRevenueDelegate { | ||||
|      | ||||
|     private let viewController: UIViewController | ||||
|     private let adConfig: AdConfig | ||||
|     private var interstitialAd: MAInterstitialAd? | ||||
|     private var isFirstLoad = true | ||||
|      | ||||
|     public init(viewController: UIViewController, adConfig: AdConfig) { | ||||
|         self.viewController = viewController | ||||
|         self.adConfig = adConfig | ||||
|         super.init(engineId: adConfig.engineId, adUnitId: adConfig.adUnitId, adPlatform: AdPlatform.max) | ||||
|     } | ||||
|      | ||||
|     public override func logDebug(_ message: String) { | ||||
|         Logger.d(tag: "MaxInterstitialAd[\(adConfig.adUnitId)]", message: message) | ||||
|     } | ||||
|      | ||||
|     public override func logWarn(_ message: String) { | ||||
|         Logger.w(tag: "MaxInterstitialAd[\(adConfig.adUnitId)]", message: message) | ||||
|     } | ||||
|      | ||||
|     public override func load() -> Bool { | ||||
|         Logger.i(tag: "MaxInterstitialAd[\(adConfig.adUnitId)]", message: "starting load") | ||||
|         destroy() | ||||
|          | ||||
|         interstitialAd = MAInterstitialAd(adUnitIdentifier: adConfig.adUnitId) | ||||
|         interstitialAd?.delegate = self | ||||
|         interstitialAd?.revenueDelegate = self | ||||
|          | ||||
|         // Set UID2 token if available | ||||
|         if let uid2Token = AdsHelper.getUid2Token() { | ||||
|             interstitialAd?.setExtraParameterForKey("uid2_token", value: uid2Token) | ||||
|         } | ||||
|         interstitialAd?.setExtraParameterForKey("disable_auto_retries", value: self.adConfig.requireDisableAutoRetries ? "true":"false") | ||||
|          | ||||
|         if let amazonSlotId = adConfig.adAmazonSlotId, !amazonSlotId.isEmpty, isFirstLoad { | ||||
|             isFirstLoad = false | ||||
|              | ||||
|             // In a real implementation, you would load Amazon ads here | ||||
|             logDebug("Would load Amazon interstitial ad with slot ID: \(amazonSlotId)") | ||||
|              | ||||
|             // Simulate direct load for now | ||||
|             interstitialAd?.load() | ||||
|         } else { | ||||
|             logDebug("Loading interstitial ad \(adConfig.adUnitId)") | ||||
|             interstitialAd?.load() | ||||
|         } | ||||
|          | ||||
|         return true | ||||
|     } | ||||
|      | ||||
|     public override func show(_ request: InterstitialShowRequest? = nil) -> Bool { | ||||
|         guard let ad = interstitialAd, ad.isReady else { | ||||
|             logDebug("Show result: false - ad not ready") | ||||
|             return false | ||||
|         } | ||||
|         ad.show(forPlacement: request?.placement) | ||||
|         logDebug("Show result: true") | ||||
|         return true | ||||
|     } | ||||
|      | ||||
|     public override func destroy() -> Bool { | ||||
|         interstitialAd?.delegate = nil | ||||
|         interstitialAd?.revenueDelegate = nil | ||||
|         interstitialAd = nil | ||||
|         return true | ||||
|     } | ||||
|      | ||||
|     // MARK: - MAAdDelegate methods | ||||
|      | ||||
|     public func didLoad(_ ad: MAAd) { | ||||
|         Logger.i(tag: "MaxInterstitialAd[\(adConfig.adUnitId)]", message: "loaded") | ||||
|         listener?.onAdLoaded(ad: MaxFusionAd(engineId: engineId, adType: AdType.Interstitial, ad: ad)) | ||||
|     } | ||||
|      | ||||
|     public func didFailToLoadAd(forAdUnitIdentifier adUnitIdentifier: String, withError error: MAError) { | ||||
|         logError("load failed, \(error.code) - \(error.message)") | ||||
|         let loadFailedInfo = LoadFailedInfo( | ||||
|             engineId: engineId, | ||||
|             adPlatform: AdPlatform.max, | ||||
|             adType: AdType.Interstitial, | ||||
|             adUnitId: adUnitIdentifier, | ||||
|             error: MaxFusionError(error) | ||||
|         ) | ||||
|         listener?.onAdLoadFailed(loadFailedInfo: loadFailedInfo) | ||||
|     } | ||||
|      | ||||
|     public func didDisplay(_ ad: MAAd) { | ||||
|         logInfo("displayed") | ||||
|         listener?.onAdDisplayed(ad: MaxFusionAd(engineId: engineId, adType: AdType.Interstitial, ad: ad)) | ||||
|     } | ||||
|      | ||||
|     public func didHide(_ ad: MAAd) { | ||||
|         listener?.onAdHidden(ad: MaxFusionAd(engineId: engineId, adType: AdType.Interstitial, ad: ad)) | ||||
|     } | ||||
|      | ||||
|     public func didClick(_ ad: MAAd) { | ||||
|         listener?.onAdClicked(ad: MaxFusionAd(engineId: engineId, adType: AdType.Interstitial, ad: ad)) | ||||
|     } | ||||
|      | ||||
|     public func didFail(toDisplay ad: MAAd, withError error: MAError) { | ||||
|         logError("displayed failed, \(error.code) - \(error.message)") | ||||
|         listener?.onAdDisplayFailed( | ||||
|             ad: MaxFusionAd(engineId: engineId, adType: AdType.Interstitial, ad: ad), | ||||
|             error: MaxFusionError(error) | ||||
|         ) | ||||
|     } | ||||
|      | ||||
|     // MARK: - MAAdRevenueDelegate methods | ||||
|      | ||||
|     public func didPayRevenue(for ad: MAAd) { | ||||
|         logInfo("revenue paid") | ||||
|         listener?.onAdRevenuePaid(ad: MaxFusionAd(engineId: engineId, adType: AdType.Interstitial, ad: ad)) | ||||
|     } | ||||
| } | ||||
|  | @ -0,0 +1,239 @@ | |||
| // GuruMaxMRecAd.swift | ||||
| // Implementation for MAX MREC ads | ||||
| // Corresponds to GuruMaxMRecAd.kt in Android implementation | ||||
| 
 | ||||
| import Foundation | ||||
| import UIKit | ||||
| import AppLovinSDK | ||||
| 
 | ||||
| public class GuruMaxMRecAd: GuruMRecAd, MAAdViewAdDelegate, MAAdRevenueDelegate { | ||||
|      | ||||
|     private let viewController: UIViewController | ||||
|     private let adConfig: AdConfig | ||||
|     private var mrecAd: MAAdView? | ||||
|     private var contentView: UIView? | ||||
|     private var currentUid2Token: String = "" | ||||
|     private var horizontalOffset: CGFloat = 0.0 | ||||
|     private var verticalOffset: CGFloat = 0.0 | ||||
|     private var orientation: ScreenOrientation = .portrait | ||||
|      | ||||
|     private var contentId: Int { | ||||
|         return self.adConfig.engineId | ||||
|     } | ||||
|      | ||||
|     public init(viewController: UIViewController, adConfig: AdConfig) { | ||||
|         self.viewController = viewController | ||||
|         self.adConfig = adConfig | ||||
|         super.init(engineId: adConfig.engineId, adUnitId: adConfig.adUnitId, adPlatform: AdPlatform.max) | ||||
|     } | ||||
|      | ||||
|     public override func logDebug(_ message: String) { | ||||
|         Logger.d(tag: "MaxMRecAd[\(adConfig.adUnitId)]", message: message) | ||||
|     } | ||||
|      | ||||
|     public override func logInfo(_ message: String) { | ||||
|         Logger.i(tag: "MaxMRecAd[\(adConfig.adUnitId)]", message: message) | ||||
|     } | ||||
|      | ||||
|     public override func logWarn(_ message: String) { | ||||
|         Logger.w(tag: "MaxMRecAd[\(adConfig.adUnitId)]", message: message) | ||||
|     } | ||||
|      | ||||
|     public override func load() -> Bool { | ||||
|         mrecAd = MAAdView(adUnitIdentifier: adConfig.adUnitId, adFormat: MAAdFormat.mrec) | ||||
|          | ||||
|         guard let mrecAd = mrecAd else { return false } | ||||
|          | ||||
|         mrecAd.delegate = self | ||||
|         mrecAd.revenueDelegate = self | ||||
|         mrecAd.setExtraParameterForKey("allow_pause_auto_refresh_immediately", value: "true") | ||||
|          | ||||
|         if let uid2Token = AdsHelper.getUid2Token() { | ||||
|             mrecAd.setExtraParameterForKey("uid2_token", value: uid2Token) | ||||
|             currentUid2Token = uid2Token | ||||
|         } | ||||
|          | ||||
|         mrecAd.stopAutoRefresh() | ||||
|          | ||||
|         if let amazonSlotId = adConfig.adAmazonSlotId, !amazonSlotId.isEmpty { | ||||
|             // In a real implementation, you would load Amazon ads here | ||||
|             logDebug("Would load Amazon MREC ad with slot ID: \(amazonSlotId)") | ||||
|              | ||||
|             // Simulate direct load for now | ||||
|             mrecAd.loadAd() | ||||
|         } else { | ||||
|             mrecAd.loadAd() | ||||
|         } | ||||
|          | ||||
|         return true | ||||
|     } | ||||
|      | ||||
|     public override func show(request: MRecShowRequest? = nil) -> Bool { | ||||
|         let placement = request?.placement | ||||
|         let newHorizontalOffset = request?.horizontalOffset | ||||
|         let newVerticalOffset = request?.verticalOffset | ||||
|          | ||||
|         var shouldReattach = false | ||||
|          | ||||
|         // Check if position or offset has changed | ||||
|         if let offset = newVerticalOffset, offset != Float(verticalOffset) { | ||||
|             verticalOffset = CGFloat(offset) | ||||
|             shouldReattach = true | ||||
|         } | ||||
|          | ||||
|         if let offset = newHorizontalOffset, offset != Float(horizontalOffset) { | ||||
|             horizontalOffset = CGFloat(offset) | ||||
|             shouldReattach = true | ||||
|         } | ||||
|          | ||||
|         // If we already have a view and parameters changed, remove it | ||||
|         if let contentView = contentView, shouldReattach { | ||||
|             logInfo("Parameters changed! Re-attaching view!") | ||||
|             contentView.removeFromSuperview() | ||||
|             self.contentView = nil | ||||
|         } | ||||
|          | ||||
|         // Create new content view if needed | ||||
|         if contentView == nil { | ||||
|             let container = UIView() | ||||
|             container.tag = contentId | ||||
|              | ||||
|             viewController.view.addSubview(container) | ||||
|             container.translatesAutoresizingMaskIntoConstraints = false | ||||
|             NSLayoutConstraint.activate([ | ||||
|                 container.leadingAnchor.constraint(equalTo: viewController.view.leadingAnchor), | ||||
|                 container.trailingAnchor.constraint(equalTo: viewController.view.trailingAnchor), | ||||
|                 container.topAnchor.constraint(equalTo: viewController.view.topAnchor), | ||||
|                 container.bottomAnchor.constraint(equalTo: viewController.view.bottomAnchor) | ||||
|             ]) | ||||
|              | ||||
|             contentView = container | ||||
|              | ||||
|             guard let mrecAd = mrecAd else { return false } | ||||
|              | ||||
|             // Standard MREC size is 300x250 | ||||
|             let mrecWidth: CGFloat = 300 | ||||
|             let mrecHeight: CGFloat = 250 | ||||
|              | ||||
|             mrecAd.translatesAutoresizingMaskIntoConstraints = false | ||||
|             container.addSubview(mrecAd) | ||||
|              | ||||
|             NSLayoutConstraint.activate([ | ||||
|                 mrecAd.leadingAnchor.constraint(equalTo: container.leadingAnchor, constant: horizontalOffset), | ||||
|                 mrecAd.topAnchor.constraint(equalTo: container.topAnchor, constant: verticalOffset), | ||||
|                 mrecAd.widthAnchor.constraint(equalToConstant: mrecWidth), | ||||
|                 mrecAd.heightAnchor.constraint(equalToConstant: mrecHeight) | ||||
|             ]) | ||||
|              | ||||
|             mrecAd.isHidden = false | ||||
|             mrecAd.startAutoRefresh() | ||||
|         } else { | ||||
|             mrecAd?.startAutoRefresh() | ||||
|             mrecAd?.isHidden = false | ||||
|         } | ||||
|          | ||||
|         // Update UID2 token if needed | ||||
|         if let uid2Token = AdsHelper.getUid2Token(), !uid2Token.isEmpty, uid2Token != currentUid2Token { | ||||
|             mrecAd?.setExtraParameterForKey("uid2_token", value: uid2Token) | ||||
|             currentUid2Token = uid2Token | ||||
|             logInfo("MRec refresh uid2_token \(currentUid2Token)") | ||||
|         } | ||||
|          | ||||
|         return true | ||||
|     } | ||||
|      | ||||
|     public override func hide() -> Bool { | ||||
|         guard let _ = viewController.view.viewWithTag(contentId) else { | ||||
|             return true | ||||
|         } | ||||
|          | ||||
|         if let mrecAd = mrecAd, !mrecAd.isHidden { | ||||
|             mrecAd.setExtraParameterForKey("allow_pause_auto_refresh_immediately", value: "true") | ||||
|             mrecAd.stopAutoRefresh() | ||||
|             mrecAd.isHidden = true | ||||
|         } | ||||
|          | ||||
|         return true | ||||
|     } | ||||
|      | ||||
|     override public func destroy() -> Bool{ | ||||
|         mrecAd?.delegate = nil | ||||
|         mrecAd?.revenueDelegate = nil | ||||
|         mrecAd?.removeFromSuperview() | ||||
|         mrecAd = nil | ||||
|          | ||||
|         contentView?.removeFromSuperview() | ||||
|         contentView = nil | ||||
|         return true | ||||
|     } | ||||
|      | ||||
|     override public func updateOrientation(orientation: ScreenOrientation) -> Bool { | ||||
|         if let _ = viewController.view.viewWithTag(contentId), self.orientation != orientation { | ||||
|             let isVisible = !(mrecAd?.isHidden ?? true) | ||||
|              | ||||
|             mrecAd?.setExtraParameterForKey("allow_pause_auto_refresh_immediately", value: "true") | ||||
|             mrecAd?.stopAutoRefresh() | ||||
|             mrecAd?.isHidden = true | ||||
|              | ||||
|             contentView?.removeFromSuperview() | ||||
|             contentView = nil | ||||
|              | ||||
|             if isVisible { | ||||
|                 return show() | ||||
|             } | ||||
|         } | ||||
|          | ||||
|         self.orientation = orientation | ||||
|         return true | ||||
|     } | ||||
|      | ||||
|     // MARK: - MAAdViewAdDelegate methods | ||||
|      | ||||
|     public func didLoad(_ ad: MAAd) { | ||||
|         listener?.onAdLoaded(ad: MaxFusionAd(engineId: engineId, adType: AdType.MRec, ad: ad)) | ||||
|     } | ||||
|      | ||||
|     public func didFailToLoadAd(forAdUnitIdentifier adUnitIdentifier: String, withError error: MAError) { | ||||
|         let loadFailedInfo = LoadFailedInfo( | ||||
|             engineId: engineId, | ||||
|             adPlatform: AdPlatform.max, | ||||
|             adType: AdType.MRec, | ||||
|             adUnitId: adUnitIdentifier, | ||||
|             error: MaxFusionError(error) | ||||
|         ) | ||||
|         listener?.onAdLoadFailed(loadFailedInfo: loadFailedInfo) | ||||
|     } | ||||
|      | ||||
|     public func didDisplay(_ ad: MAAd) { | ||||
|         listener?.onAdDisplayed(ad: MaxFusionAd(engineId: engineId, adType: AdType.MRec, ad: ad)) | ||||
|     } | ||||
|      | ||||
|     public func didHide(_ ad: MAAd) { | ||||
|         listener?.onAdHidden(ad: MaxFusionAd(engineId: engineId, adType: AdType.MRec, ad: ad)) | ||||
|     } | ||||
|      | ||||
|     public func didClick(_ ad: MAAd) { | ||||
|         listener?.onAdClicked(ad: MaxFusionAd(engineId: engineId, adType: AdType.MRec, ad: ad)) | ||||
|     } | ||||
|      | ||||
|     public func didFail(toDisplay ad: MAAd, withError error: MAError) { | ||||
|         listener?.onAdDisplayFailed( | ||||
|             ad: MaxFusionAd(engineId: engineId, adType: AdType.MRec, ad: ad), | ||||
|             error: MaxFusionError(error) | ||||
|         ) | ||||
|     } | ||||
|      | ||||
|     public func didExpand(_ ad: MAAd) { | ||||
|         // In a real implementation, you would call adBannerExpanded | ||||
|     } | ||||
|      | ||||
|     public func didCollapse(_ ad: MAAd) { | ||||
|         // In a real implementation, you would call adBannerCollapsed | ||||
|     } | ||||
|      | ||||
|     // MARK: - MAAdRevenueDelegate methods | ||||
|      | ||||
|     public func didPayRevenue(for ad: MAAd) { | ||||
|         listener?.onAdRevenuePaid(ad: MaxFusionAd(engineId: engineId, adType: AdType.MRec, ad: ad)) | ||||
|     } | ||||
| } | ||||
|  | @ -0,0 +1,130 @@ | |||
| // GuruMaxRewardedAd.swift | ||||
| // Implementation for MAX rewarded ads | ||||
| // Corresponds to GuruMaxRewardedAd.kt in Android implementation | ||||
| 
 | ||||
| import Foundation | ||||
| import UIKit | ||||
| import AppLovinSDK | ||||
| 
 | ||||
| public class GuruMaxRewardedAd: GuruRewardedAd, MARewardedAdDelegate, MAAdRevenueDelegate { | ||||
|      | ||||
|     private let viewController: UIViewController | ||||
|     private let adConfig: AdConfig | ||||
|     private var rewardedAd: MARewardedAd? | ||||
|     private var isFirstLoad = true | ||||
|      | ||||
|     public init(viewController: UIViewController, adConfig: AdConfig) { | ||||
|         self.viewController = viewController | ||||
|         self.adConfig = adConfig | ||||
|         super.init(engineId: adConfig.engineId, adUnitId: adConfig.adUnitId, adPlatform: AdPlatform.max) | ||||
|     } | ||||
|      | ||||
|     public override func load() -> Bool { | ||||
|         logInfo("request load") | ||||
|         destroy() | ||||
|          | ||||
|         rewardedAd = MARewardedAd.shared(withAdUnitIdentifier: adConfig.adUnitId) | ||||
|         rewardedAd?.delegate = self | ||||
|         rewardedAd?.revenueDelegate = self | ||||
|          | ||||
|         // Set UID2 token if available | ||||
|         if let uid2Token = AdsHelper.getUid2Token() { | ||||
|             rewardedAd?.setExtraParameterForKey("uid2_token", value: uid2Token) | ||||
|         } | ||||
|         rewardedAd?.setExtraParameterForKey("disable_auto_retries", value: self.adConfig.requireDisableAutoRetries ? "true":"false") | ||||
|          | ||||
|         if let amazonSlotId = adConfig.adAmazonSlotId, !amazonSlotId.isEmpty, isFirstLoad { | ||||
|             isFirstLoad = false | ||||
|              | ||||
|             // In a real implementation, you would load Amazon ads here | ||||
|             logWarn("Would load Amazon rewarded ad with slot ID: \(amazonSlotId)") | ||||
|              | ||||
|             // Simulate direct load for now | ||||
|             rewardedAd?.load() | ||||
|         } else { | ||||
|             rewardedAd?.load() | ||||
|         } | ||||
|          | ||||
|         return true | ||||
|     } | ||||
|      | ||||
|     public override func show(_ request: RewardedShowRequest? = nil) -> Bool { | ||||
|         logInfo("request show") | ||||
|         guard let ad = rewardedAd, ad.isReady else { | ||||
|             logDebug("Show result: false - ad not ready") | ||||
|             return false | ||||
|         } | ||||
|          | ||||
|         ad.show(forPlacement: request?.placement) | ||||
|         logDebug("Show result: true") | ||||
|         return true | ||||
|     } | ||||
|      | ||||
|     public override func destroy()  -> Bool { | ||||
|         rewardedAd?.delegate = nil | ||||
|         rewardedAd?.revenueDelegate = nil | ||||
|         rewardedAd = nil | ||||
|         return true | ||||
|     } | ||||
|      | ||||
|     // MARK: - MARewardedAdDelegate methods | ||||
|      | ||||
|     public func didLoad(_ ad: MAAd) { | ||||
|         logInfo("loaded") | ||||
|         listener?.onAdLoaded(ad: MaxFusionAd(engineId: engineId, adType: AdType.Rewarded, ad: ad)) | ||||
|     } | ||||
|      | ||||
|      | ||||
|     public func didFail(toDisplay ad: MAAd, withError error: MAError) { | ||||
|         logError("displayed failed, \(error.code) - \(error.message)") | ||||
|         listener?.onAdDisplayFailed(ad: MaxFusionAd(engineId: engineId, adType: AdType.Rewarded, ad: ad), error: MaxFusionError(error)) | ||||
|     } | ||||
|      | ||||
|     public func didFailToLoadAd(forAdUnitIdentifier adUnitIdentifier: String, withError error: MAError) { | ||||
|         logError("load failed, \(error.code) - \(error.message)") | ||||
|         let loadFailedInfo = LoadFailedInfo( | ||||
|             engineId: engineId, | ||||
|             adPlatform: AdPlatform.max, | ||||
|             adType: AdType.Rewarded, | ||||
|             adUnitId: adUnitIdentifier, | ||||
|             error: MaxFusionError(error) | ||||
|         ) | ||||
|         listener?.onAdLoadFailed(loadFailedInfo: loadFailedInfo) | ||||
|     } | ||||
|      | ||||
|     public func didDisplay(_ ad: MAAd) { | ||||
|         logInfo("displayed") | ||||
|         listener?.onAdDisplayed(ad: MaxFusionAd(engineId: engineId, adType: AdType.Rewarded, ad: ad)) | ||||
|     } | ||||
|      | ||||
|     public func didHide(_ ad: MAAd) { | ||||
|         listener?.onAdHidden(ad: MaxFusionAd(engineId: engineId, adType: AdType.Rewarded, ad: ad)) | ||||
|     } | ||||
|      | ||||
|     public func didClick(_ ad: MAAd) { | ||||
|         listener?.onAdClicked(ad: MaxFusionAd(engineId: engineId, adType: AdType.Rewarded, ad: ad)) | ||||
|     } | ||||
|      | ||||
|     public func didStartRewardedVideo(for ad: MAAd) { | ||||
|         // Additional callback if needed | ||||
|     } | ||||
|      | ||||
|     public func didCompleteRewardedVideo(for ad: MAAd) { | ||||
|         // Additional callback if needed | ||||
|     } | ||||
|      | ||||
|     public func didRewardUser(for ad: MAAd, with reward: MAReward) { | ||||
|         logInfo("reward user") | ||||
|         listener?.onUserRewarded( | ||||
|             ad: MaxFusionAd(engineId: engineId, adType: AdType.Rewarded, ad: ad), | ||||
|             reward: MaxFusionReward(reward) | ||||
|         ) | ||||
|     } | ||||
|      | ||||
|     // MARK: - MAAdRevenueDelegate methods | ||||
|      | ||||
|     public func didPayRevenue(for ad: MAAd) { | ||||
|         logInfo("revenue paid") | ||||
|         listener?.onAdRevenuePaid(ad: MaxFusionAd(engineId: engineId, adType: AdType.Rewarded, ad: ad)) | ||||
|     } | ||||
| } | ||||
|  | @ -0,0 +1,52 @@ | |||
| // MaxFusionAd.swift | ||||
| // Adapts MaxAd to FusionAd interface | ||||
| // Corresponds to MaxFusionAd.kt in Android implementation | ||||
| 
 | ||||
| import Foundation | ||||
| import AppLovinSDK | ||||
| 
 | ||||
| public class MaxFusionAd: FusionAd { | ||||
|      | ||||
|     private let originAd: MAAd | ||||
|      | ||||
|     public init(engineId: Int, adType: AdType, ad: MAAd) { | ||||
|         originAd = ad | ||||
|         super.init(engineId: engineId, adType: adType) | ||||
|     } | ||||
|      | ||||
|     override public var adPlatform: AdPlatform { | ||||
|         return AdPlatform.max | ||||
|     } | ||||
|      | ||||
|     override public var adUnitId: String? { | ||||
|         return originAd.adUnitIdentifier | ||||
|     } | ||||
|      | ||||
|     override public var revenue: Double { | ||||
|         return originAd.revenue | ||||
|     } | ||||
|      | ||||
|     override public var waterfallName: String? { | ||||
|     return originAd.waterfall.name | ||||
|     } | ||||
|      | ||||
|     override public var placement: String? { | ||||
|         return originAd.placement | ||||
|     } | ||||
|      | ||||
|     override public var networkName: String? { | ||||
|         return originAd.networkName | ||||
|     } | ||||
|      | ||||
|     override public var networkPlacement: String? { | ||||
|         return originAd.networkPlacement | ||||
|     } | ||||
|      | ||||
|     override public var creativeId: String? { | ||||
|         return originAd.creativeIdentifier | ||||
|     } | ||||
|      | ||||
|     override public var adReviewCreativeId: String? { | ||||
|         return originAd.adReviewCreativeIdentifier | ||||
|     } | ||||
| } | ||||
|  | @ -0,0 +1,34 @@ | |||
| // MaxFusionError.swift | ||||
| // Adapts MAX errors to FusionError interface | ||||
| // Corresponds to MaxFusionError.kt in Android implementation | ||||
| 
 | ||||
| import Foundation | ||||
| import AppLovinSDK | ||||
| 
 | ||||
| public class MaxFusionError: FusionError { | ||||
|     private let maxError: MAError | ||||
|      | ||||
|     public init(_ maxError: MAError) { | ||||
|         self.maxError = maxError | ||||
|     } | ||||
|      | ||||
|     public var cause: Error? { | ||||
|         return nil | ||||
|     } | ||||
|      | ||||
|     public var info: String? { | ||||
|         return nil | ||||
|     } | ||||
|      | ||||
|     public var errorCode: Int { | ||||
|         return maxError.code.rawValue | ||||
|     } | ||||
|      | ||||
|     public var message: String { | ||||
|         return maxError.message | ||||
|     } | ||||
|      | ||||
|     public var waterfallName: String? { | ||||
|         return maxError.waterfall?.name ?? "unknown" | ||||
|     } | ||||
| } | ||||
|  | @ -0,0 +1,26 @@ | |||
| // MaxFusionReward.swift | ||||
| // Adapts MAX reward to FusionReward interface | ||||
| // Corresponds to MaxFusionReward.kt in Android implementation | ||||
| 
 | ||||
| import Foundation | ||||
| import AppLovinSDK | ||||
| 
 | ||||
| public class MaxFusionReward: FusionReward { | ||||
|     public var rewardAmount: Int { | ||||
|         return maxReward.amount | ||||
|     } | ||||
|      | ||||
|     private let maxReward: MAReward | ||||
|      | ||||
|     public init(_ reward: MAReward) { | ||||
|         self.maxReward = reward | ||||
|     } | ||||
|      | ||||
|     public var amount: Int { | ||||
|         return maxReward.amount | ||||
|     } | ||||
|      | ||||
|     public var type: String { | ||||
|         return maxReward.label | ||||
|     } | ||||
| } | ||||
|  | @ -0,0 +1,45 @@ | |||
| // AdsHelper.swift | ||||
| // Helper utilities for ad management | ||||
| // Corresponds to AdsHelper.kt in Android implementation | ||||
| 
 | ||||
| import Foundation | ||||
| import AppLovinSDK | ||||
| 
 | ||||
| public class AdsHelper { | ||||
|     private static var uid2Token: String? | ||||
|      | ||||
|     public static func setUid2Token(_ token: String?) -> Bool { | ||||
|         guard let token = token, !token.isEmpty else { | ||||
|             return false | ||||
|         } | ||||
|         uid2Token = token | ||||
|         return true | ||||
|     } | ||||
|      | ||||
|     public static func getUid2Token() -> String? { | ||||
|         return uid2Token | ||||
|     } | ||||
|      | ||||
|     public static func toAdParams(ad: FusionAd, pairs: [String: Any] = [:]) -> [String: Any] { | ||||
|         var params: [String: Any] = [ | ||||
|             "engine_id": ad.engineId, | ||||
|             "ad_platform": ad.adPlatform.name, | ||||
|             "ad_revenue": ad.revenue, | ||||
|             "ad_format": ad.format ?? "unknown", | ||||
|             "ad_source": ad.networkName ?? "unknown", | ||||
|             "ad_unit_name": ad.adUnitId ?? "unknown", | ||||
|             "ad_creative_id": ad.creativeId ?? "unknown", | ||||
|             "ad_placement": ad.placement ?? "unknown", | ||||
|             "ad_network_name": ad.networkName ?? "unknown", | ||||
|             "ad_network_placement": ad.networkPlacement ?? "unknown", | ||||
|             "review_creative_id": ad.adReviewCreativeId ?? "unknown" | ||||
|         ] | ||||
|          | ||||
|         // Add additional pairs | ||||
|         for (key, value) in pairs { | ||||
|             params[key] = value | ||||
|         } | ||||
|          | ||||
|         return params | ||||
|     } | ||||
| } | ||||
|  | @ -0,0 +1,49 @@ | |||
| // | ||||
| //  MabAnalyticsEvents.swift | ||||
| //  Pods | ||||
| // | ||||
| //  Created by 250102 on 2025/5/10. | ||||
| // | ||||
| 
 | ||||
| // AnalyticsEvents 单例 | ||||
| internal class AnalyticsEvents { | ||||
|      | ||||
|     // 静态单例实例 | ||||
|     static let shared = AnalyticsEvents() | ||||
|      | ||||
|     // 私有初始化方法确保单例模式 | ||||
|     private init() {} | ||||
|      | ||||
|     // 使用NSHashTable来存储弱引用的处理器 | ||||
|     private var handlers = NSHashTable<AnyObject>.weakObjects() | ||||
|      | ||||
|     func registerHandler(handler: AnalyticsEventHandler) { | ||||
|         handlers.add(handler as AnyObject) | ||||
|     } | ||||
|      | ||||
|     func unregisterHandler(handler: AnalyticsEventHandler) { | ||||
|         for existingHandler in handlers.allObjects { | ||||
|             if let existing = existingHandler as? AnalyticsEventHandler, | ||||
|                existing === handler as AnyObject { | ||||
|                 handlers.remove(existingHandler) | ||||
|                 break | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     func onEvent(name: String, param: [String: Any]? = nil) { | ||||
|         for case let handler as AnalyticsEventHandler in handlers.allObjects { | ||||
|             handler.onEvent(name: name, param: param) | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     func onEvent(name: String, pairs: (String, any Any)...) { | ||||
|         let paramMap = Dictionary(uniqueKeysWithValues: pairs) | ||||
|         onEvent(name: name, param: paramMap) | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| // AnalyticsEventHandler 协议 - 不需要遵循Hashable | ||||
| public protocol AnalyticsEventHandler: AnyObject { | ||||
|     func onEvent(name: String, param: [String: Any]?) | ||||
| } | ||||
|  | @ -0,0 +1,169 @@ | |||
| // | ||||
| //  GuruBannerCoordinator.swift | ||||
| //  Pods | ||||
| // | ||||
| //  Created by 250102 on 2025/5/9. | ||||
| // | ||||
| 
 | ||||
| protocol GuruBannerView { | ||||
|      | ||||
|     func view() -> UIView | ||||
|     func setPlacementName(placement: String) | ||||
|     func onShow(); | ||||
|     func onHide(); | ||||
| } | ||||
| 
 | ||||
| class GuruBannerCoordinator<T: GuruBannerView> { | ||||
|      | ||||
|     private let viewController: UIViewController | ||||
|     public private(set) var bannerAd: T | ||||
|     private var bannerPosition: BannerPosition { return lastShowRequest?.position ?? BannerPosition.bottom } | ||||
|     private var bannerOffset: CGFloat { return CGFloat(lastShowRequest?.offset ?? 0.0) } | ||||
|     private var lastShowRequest: BannerShowRequest? = nil | ||||
| 
 | ||||
|     public private(set) var orientation: ScreenOrientation = .portrait | ||||
|          | ||||
|     public init(viewController: UIViewController, bannerAd: T, contentId: Int) { | ||||
|         self.viewController = viewController | ||||
|         self.bannerAd = bannerAd | ||||
|     } | ||||
|      | ||||
|     public func logInfo(_ message: String) { | ||||
|         Logger.i(tag: "GuruBannerCoordinator", message: "\(message)") | ||||
|     } | ||||
|     public func logWarn(_ message: String) { | ||||
|         Logger.w(tag: "GuruBannerCoordinator", message: "\(message)") | ||||
|     } | ||||
|      | ||||
|     public func show(_ request: BannerShowRequest? = nil) -> Bool { | ||||
|         var shouldReattach = false | ||||
|          | ||||
|         // Check if position or offset has changed | ||||
|         if let newOffset = request?.offset, newOffset != Float(bannerOffset) { | ||||
|             shouldReattach = true | ||||
|         } | ||||
|          | ||||
|         if let newPosition = request?.position, newPosition != bannerPosition { | ||||
|             shouldReattach = true | ||||
|         } | ||||
|          | ||||
|         lastShowRequest = request | ||||
|          | ||||
|         guard let container = viewController.viewIfLoaded else { | ||||
|             logWarn("viewController.view is nil, returns false") | ||||
|             return false | ||||
|         } | ||||
|         let bannerAdView = bannerAd.view() | ||||
|          | ||||
|         var attached = container.subviews.contains(bannerAdView) | ||||
|          | ||||
|         // If we already have a view and parameters changed, remove it | ||||
|         if shouldReattach || !attached { | ||||
|             logInfo("Parameters changed! Re-attaching view!") | ||||
|             if(attached) { | ||||
|                 bannerAdView.removeFromSuperview() | ||||
|             } | ||||
|             let isTablet = UIDevice.current.userInterfaceIdiom == .pad | ||||
|             let bannerHeight: CGFloat = isTablet ? 90 : 50 | ||||
|             let screenWidth = min(viewController.view.bounds.width, viewController.view.bounds.height) | ||||
|              | ||||
|             container.addSubview(bannerAdView) | ||||
|             bannerAdView.translatesAutoresizingMaskIntoConstraints = false | ||||
|              | ||||
|              | ||||
|             logInfo("BannerAd show!!! bannerSize: \(screenWidth)x\(bannerHeight)") | ||||
|                          | ||||
|             if let placement = lastShowRequest?.placement { | ||||
|                 bannerAd.setPlacementName(placement: placement) | ||||
|             } | ||||
|             // Configure banner position and rotation | ||||
|             var transform = CGAffineTransform.identity | ||||
|             switch bannerPosition { | ||||
|             case .start: | ||||
|                 transform = CGAffineTransform(rotationAngle: .pi / 2) | ||||
|                 NSLayoutConstraint.activate([ | ||||
|                     bannerAdView.leadingAnchor.constraint(equalTo: container.safeAreaLayoutGuide.leadingAnchor, constant: bannerOffset), | ||||
|                     bannerAdView.centerYAnchor.constraint(equalTo: container.centerYAnchor), | ||||
|                     bannerAdView.widthAnchor.constraint(equalToConstant: screenWidth), | ||||
|                     bannerAdView.heightAnchor.constraint(equalToConstant: bannerHeight) | ||||
|                 ]) | ||||
|                  | ||||
|             case .end: | ||||
|                 transform = CGAffineTransform(rotationAngle: -(.pi / 2)) | ||||
|                 NSLayoutConstraint.activate([ | ||||
|                     bannerAdView.trailingAnchor.constraint(equalTo: container.safeAreaLayoutGuide.trailingAnchor, constant: -bannerOffset), | ||||
|                     bannerAdView.centerYAnchor.constraint(equalTo: container.centerYAnchor), | ||||
|                      | ||||
|                     bannerAdView.widthAnchor.constraint(equalToConstant: screenWidth), | ||||
|                     bannerAdView.heightAnchor.constraint(equalToConstant: bannerHeight) | ||||
|                 ]) | ||||
|                  | ||||
|             case .top: | ||||
|                 transform = CGAffineTransform(rotationAngle: 0) | ||||
|                  | ||||
|                 NSLayoutConstraint.activate([ | ||||
|                     bannerAdView.centerXAnchor.constraint(equalTo: container.centerXAnchor), | ||||
|                     bannerAdView.topAnchor.constraint(equalTo: container.safeAreaLayoutGuide.topAnchor, constant: bannerOffset), | ||||
|                      | ||||
|                     bannerAdView.heightAnchor.constraint(equalToConstant: bannerHeight), | ||||
|                     bannerAdView.widthAnchor.constraint(equalToConstant: screenWidth), | ||||
|                 ]) | ||||
|                  | ||||
|             default: | ||||
|                 transform = CGAffineTransform(rotationAngle: 0) | ||||
|                 NSLayoutConstraint.activate([ | ||||
|                     bannerAdView.centerXAnchor.constraint(equalTo: container.centerXAnchor), | ||||
|                     bannerAdView.bottomAnchor.constraint(equalTo: container.safeAreaLayoutGuide.bottomAnchor, constant: -bannerOffset), | ||||
|                      | ||||
|                     bannerAdView.heightAnchor.constraint(equalToConstant: bannerHeight), | ||||
|                     bannerAdView.widthAnchor.constraint(equalToConstant: screenWidth) | ||||
|                 ]) | ||||
|             } | ||||
|              | ||||
|             bannerAdView.transform = transform | ||||
|             bannerAdView.isHidden = false | ||||
|             bannerAd.onShow() | ||||
|         } else { | ||||
|             bannerAd.view().isHidden = false | ||||
|             bannerAd.onShow() | ||||
|         } | ||||
|          | ||||
|         if let placement = request?.placement { | ||||
|             bannerAd.setPlacementName(placement: placement) | ||||
|         } | ||||
|          | ||||
|         return true | ||||
|     } | ||||
|      | ||||
|     public func hide() -> Bool { | ||||
|         guard let _ = viewController.view.superview?.contains(bannerAd.view()) else { | ||||
|             return true | ||||
|         } | ||||
| 
 | ||||
|         bannerAd.view().isHidden = true | ||||
|         bannerAd.onHide() | ||||
|          | ||||
|         return true | ||||
|     } | ||||
|      | ||||
|     public func destroy() -> Bool { | ||||
|         bannerAd.view().removeFromSuperview() | ||||
| 
 | ||||
|         return true | ||||
|     } | ||||
|      | ||||
|     public func updateOrientation(orientation: ScreenOrientation) -> Bool { | ||||
|         if let _ = viewController.view.superview?.contains(bannerAd.view()), self.orientation != orientation { | ||||
|             let isVisible = !bannerAd.view().isHidden | ||||
|              | ||||
|             bannerAd.onHide() | ||||
|             bannerAd.view().isHidden = true | ||||
|             if isVisible { | ||||
|                 return show() | ||||
|             } | ||||
|         } | ||||
|          | ||||
|         self.orientation = orientation | ||||
|         return true | ||||
|     } | ||||
| } | ||||
|  | @ -0,0 +1,143 @@ | |||
| // | ||||
| //  GuruBannerCoordinator.swift | ||||
| //  Pods | ||||
| // | ||||
| //  Created by 250102 on 2025/5/9. | ||||
| // | ||||
| 
 | ||||
| protocol GuruMRecView { | ||||
|      | ||||
|     func view() -> UIView | ||||
|     func onShow(); | ||||
|     func onHide(); | ||||
| } | ||||
| 
 | ||||
| class GuruMRecCoordinator { | ||||
|      | ||||
|     private let viewController: UIViewController | ||||
|     private var mrecAd: GuruMRecView | ||||
|     private var contentView: UIView? | ||||
|     private var currentUid2Token: String = "" | ||||
|     private var horizontalOffset: CGFloat = 0.0 | ||||
|     private var verticalOffset: CGFloat = 0.0 | ||||
|     private var orientation: ScreenOrientation = .portrait | ||||
|      | ||||
|     private let contentId: Int | ||||
|      | ||||
|     public init(viewController: UIViewController, mrecAd: GuruMRecView, contentId: Int) { | ||||
|         self.viewController = viewController | ||||
|         self.mrecAd = mrecAd | ||||
|         self.contentId = contentId | ||||
|     } | ||||
|      | ||||
|     public func logInfo(_ message: String) { | ||||
|         Logger.i(tag: "GuruBannerCoordinator", message: "\(message)") | ||||
|     } | ||||
|      | ||||
|     public func show(request: MRecShowRequest? = nil) -> Bool { | ||||
|         let placement = request?.placement | ||||
|         let newHorizontalOffset = request?.horizontalOffset | ||||
|         let newVerticalOffset = request?.verticalOffset | ||||
|          | ||||
|         var shouldReattach = false | ||||
|          | ||||
|         // Check if position or offset has changed | ||||
|         if let offset = newVerticalOffset, offset != Float(verticalOffset) { | ||||
|             verticalOffset = CGFloat(offset) | ||||
|             shouldReattach = true | ||||
|         } | ||||
|          | ||||
|         if let offset = newHorizontalOffset, offset != Float(horizontalOffset) { | ||||
|             horizontalOffset = CGFloat(offset) | ||||
|             shouldReattach = true | ||||
|         } | ||||
|          | ||||
|         // If we already have a view and parameters changed, remove it | ||||
|         if let contentView = contentView, shouldReattach { | ||||
|             logInfo("Parameters changed! Re-attaching view!") | ||||
|             contentView.removeFromSuperview() | ||||
|             self.contentView = nil | ||||
|         } | ||||
|          | ||||
|         let mrecAdView = mrecAd.view() | ||||
|          | ||||
|         // Create new content view if needed | ||||
|         if contentView == nil { | ||||
|             let container = UIView() | ||||
|             container.tag = contentId | ||||
|              | ||||
|             viewController.view.addSubview(container) | ||||
|             container.translatesAutoresizingMaskIntoConstraints = false | ||||
|             NSLayoutConstraint.activate([ | ||||
|                 container.leadingAnchor.constraint(equalTo: viewController.view.leadingAnchor), | ||||
|                 container.trailingAnchor.constraint(equalTo: viewController.view.trailingAnchor), | ||||
|                 container.topAnchor.constraint(equalTo: viewController.view.topAnchor), | ||||
|                 container.bottomAnchor.constraint(equalTo: viewController.view.bottomAnchor) | ||||
|             ]) | ||||
|              | ||||
|             contentView = container | ||||
|                          | ||||
|             // Standard MREC size is 300x250 | ||||
|             let mrecWidth: CGFloat = 300 | ||||
|             let mrecHeight: CGFloat = 250 | ||||
|              | ||||
|             mrecAdView.translatesAutoresizingMaskIntoConstraints = false | ||||
|             container.addSubview(mrecAdView) | ||||
|              | ||||
|             NSLayoutConstraint.activate([ | ||||
|                 mrecAdView.leadingAnchor.constraint(equalTo: container.leadingAnchor, constant: horizontalOffset), | ||||
|                 mrecAdView.topAnchor.constraint(equalTo: container.topAnchor, constant: verticalOffset), | ||||
|                 mrecAdView.widthAnchor.constraint(equalToConstant: mrecWidth), | ||||
|                 mrecAdView.heightAnchor.constraint(equalToConstant: mrecHeight) | ||||
|             ]) | ||||
|              | ||||
|             mrecAdView.isHidden = false | ||||
|             mrecAd.onShow() | ||||
|         } else { | ||||
|             mrecAd.onShow() | ||||
|             mrecAd.view().isHidden = false | ||||
|         } | ||||
|          | ||||
|         return true | ||||
|     } | ||||
|      | ||||
|     public func hide() -> Bool { | ||||
|         guard let _ = viewController.view.viewWithTag(contentId) else { | ||||
|             return true | ||||
|         } | ||||
|          | ||||
|         if !mrecAd.view().isHidden { | ||||
|             mrecAd.onHide() | ||||
|             mrecAd.view().isHidden = true | ||||
|         } | ||||
|          | ||||
|         return true | ||||
|     } | ||||
|      | ||||
|     public func destroy() -> Bool{ | ||||
|         mrecAd.view().removeFromSuperview() | ||||
|          | ||||
|         contentView?.removeFromSuperview() | ||||
|         contentView = nil | ||||
|         return true | ||||
|     } | ||||
|      | ||||
|     public func updateOrientation(orientation: ScreenOrientation) -> Bool { | ||||
|         if let _ = viewController.view.viewWithTag(contentId), self.orientation != orientation { | ||||
|             let isVisible = !mrecAd.view().isHidden | ||||
|              | ||||
|             mrecAd.onHide() | ||||
|             mrecAd.view().isHidden = true | ||||
|              | ||||
|             contentView?.removeFromSuperview() | ||||
|             contentView = nil | ||||
|              | ||||
|             if isVisible { | ||||
|                 return show() | ||||
|             } | ||||
|         } | ||||
|          | ||||
|         self.orientation = orientation | ||||
|         return true | ||||
|     } | ||||
| } | ||||
|  | @ -0,0 +1,35 @@ | |||
| // MathUtils.swift | ||||
| // Mathematical utility functions | ||||
| // Corresponds to MathUtils.kt in Android implementation | ||||
| 
 | ||||
| import Foundation | ||||
| 
 | ||||
| public class MathUtils { | ||||
|     private static let _fibonacciArray: [Int] = [ | ||||
|         1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610, 987, 1597, 2584, 4181, 6765, 10946, | ||||
|         17711, 28657, 46368, 75025, 121393, 196418, 317811, 514229, 832040, 1346269, 2178309, 3524578, | ||||
|         5702887, 9227465, 14930352, 24157817, 39088169, 63245986, 102334155, 165580141, 267914296, | ||||
|         433494437, 701408733, 1134903170, 1836311903, 2971215073 | ||||
|     ] | ||||
|      | ||||
|     public static func fibonacci(_ n: Int, offset: Int = 0) -> Int64 { | ||||
|         let len = _fibonacciArray.count | ||||
|         let idx = n + offset | ||||
|          | ||||
|         if idx < len { | ||||
|             return Int64(_fibonacciArray[idx]) | ||||
|         } | ||||
|          | ||||
|         var n1 = Int64(_fibonacciArray[len - 2]) | ||||
|         var n2 = Int64(_fibonacciArray[len - 1]) | ||||
|         var sum: Int64 = 0 | ||||
|          | ||||
|         for _ in len..<idx { | ||||
|             sum = n1 + n2 | ||||
|             n1 = n2 | ||||
|             n2 = sum | ||||
|         } | ||||
|          | ||||
|         return sum | ||||
|     } | ||||
| } | ||||
|  | @ -0,0 +1,34 @@ | |||
| // | ||||
| //  SwiftFusionError.swift | ||||
| //  Pods | ||||
| // | ||||
| //  Created by 250102 on 2025/5/9. | ||||
| // | ||||
| 
 | ||||
| public class SwiftFusionError: FusionError { | ||||
|     private let causeValue: Error | ||||
|      | ||||
|     public init(_ cause: Error) { | ||||
|         self.causeValue = cause | ||||
|     } | ||||
|      | ||||
|     public var cause: Error? { | ||||
|         return causeValue | ||||
|     } | ||||
|      | ||||
|     public var info: String? { | ||||
|         return nil | ||||
|     } | ||||
|      | ||||
|     public var errorCode: Int { | ||||
|         return causeValue.code | ||||
|     } | ||||
|      | ||||
|     public var message: String { | ||||
|         return causeValue.localizedDescription | ||||
|     } | ||||
|      | ||||
|     public var waterfallName: String? { | ||||
|         return nil | ||||
|     } | ||||
| } | ||||
|  | @ -0,0 +1,304 @@ | |||
| // | ||||
| //  FileLogHandler.swift | ||||
| //  Pods | ||||
| // | ||||
| //  Created by 250102 on 2025/5/21. | ||||
| // | ||||
| 
 | ||||
| 
 | ||||
| import Foundation | ||||
| import os.log | ||||
| 
 | ||||
| /// 一个rotatable、robust且efficient的文件日志处理器 | ||||
| public class FileLogHandler { | ||||
|     // MARK: - 日志级别枚举 | ||||
|      | ||||
|     public enum LogLevel: Int, Comparable { | ||||
|         case verbose = 0 | ||||
|         case debug = 1 | ||||
|         case info = 2 | ||||
|         case warning = 3 | ||||
|         case error = 4 | ||||
|         case fatal = 5 | ||||
|          | ||||
|         public static func < (lhs: LogLevel, rhs: LogLevel) -> Bool { | ||||
|             return lhs.rawValue < rhs.rawValue | ||||
|         } | ||||
|          | ||||
|         public var prefix: String { | ||||
|             switch self { | ||||
|             case .verbose: return "V" | ||||
|             case .debug: return "D" | ||||
|             case .info: return "I" | ||||
|             case .warning: return "W" | ||||
|             case .error: return "E" | ||||
|             case .fatal: return "WTF" | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     // MARK: - 旋转配置结构 | ||||
|      | ||||
|     public struct RotationConfig { | ||||
|         /// 日志文件旋转前的最大大小(字节)(默认:10MB) | ||||
|         public var maxFileSize: UInt64 | ||||
|          | ||||
|         /// 要保留的归档日志文件的最大数量(默认:5) | ||||
|         public var maxBackupCount: Int | ||||
|          | ||||
|         public init(maxFileSize: UInt64 = 10_485_760, // 10MB | ||||
|                     maxBackupCount: Int = 5) { | ||||
|             self.maxFileSize = maxFileSize | ||||
|             self.maxBackupCount = maxBackupCount | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     // MARK: - 属性 | ||||
|      | ||||
|     private let fileURL: URL | ||||
|     private let rotationConfig: RotationConfig | ||||
|     private let dateFormatter: DateFormatter | ||||
|     private let minimumLogLevel: LogLevel | ||||
|     private let queue: DispatchQueue | ||||
|     private let fileManager: FileManager | ||||
|     private var fileHandle: FileHandle? | ||||
|      | ||||
|     // MARK: - 初始化 | ||||
|      | ||||
|     /// 初始化一个新的FileLogHandler | ||||
|     /// - 参数: | ||||
|     ///   - directory: 日志文件的目录路径 | ||||
|     ///   - filename: 日志文件的基本名称(不带扩展名) | ||||
|     ///   - rotationConfig: 日志rotation配置 | ||||
|     ///   - minimumLogLevel: 要记录的最小日志级别 | ||||
|     public init(directory: String? = nil,  | ||||
|                 filename: String = "app.log", | ||||
|                 rotationConfig: RotationConfig = RotationConfig(), | ||||
|                 minimumLogLevel: LogLevel = .debug) { | ||||
|         self.rotationConfig = rotationConfig | ||||
|         self.minimumLogLevel = minimumLogLevel | ||||
|          | ||||
|         // 创建时间戳的日期格式化器 | ||||
|         self.dateFormatter = DateFormatter() | ||||
|         self.dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss.SSS" | ||||
|          | ||||
|         // 创建调度队列以确保线程安全 | ||||
|         self.queue = DispatchQueue(label: "com.fileloghandler.queue", qos: .utility) | ||||
|          | ||||
|         // 使用文件管理器 | ||||
|         self.fileManager = FileManager.default | ||||
|          | ||||
|         // 确定日志目录 | ||||
|         let baseDirectory: URL | ||||
|         if let directory = directory { | ||||
|             baseDirectory = fileManager.urls(for: .cachesDirectory, in: .userDomainMask).first!.appendingPathComponent("Logs", isDirectory: true).appendingPathComponent(directory, isDirectory: true) | ||||
|         } else { | ||||
|             // 如果未指定目录,默认为缓存目录 | ||||
|             #if os(macOS) | ||||
|             let cacheDir = fileManager.urls(for: .cachesDirectory, in: .userDomainMask).first! | ||||
|             #else | ||||
|             let cacheDir = fileManager.urls(for: .cachesDirectory, in: .userDomainMask).first! | ||||
|             #endif | ||||
|             baseDirectory = cacheDir.appendingPathComponent("Logs", isDirectory: true) | ||||
|         } | ||||
|          | ||||
|         // 如果日志目录不存在,则创建它 | ||||
|         let logsDirectory = baseDirectory | ||||
|         try? fileManager.createDirectory(at: logsDirectory, withIntermediateDirectories: true, attributes: nil) | ||||
|          | ||||
|         // 设置文件URL | ||||
|         self.fileURL = logsDirectory.appendingPathComponent(filename) | ||||
|          | ||||
|         // 打开或创建日志文件 | ||||
|         self.openLogFile() | ||||
|     } | ||||
|      | ||||
|     deinit { | ||||
|         try? fileHandle?.close() | ||||
|     } | ||||
|      | ||||
|     // MARK: - 公共方法 | ||||
|      | ||||
|     /// 使用指定级别记录消息 | ||||
|     /// - 参数: | ||||
|     ///   - message: 要记录的消息 | ||||
|     ///   - level: 日志的严重级别 | ||||
|     ///   - file: 调用日志的文件 | ||||
|     ///   - function: 调用日志的函数 | ||||
|     ///   - line: 调用日志的行 | ||||
|     public func log(_ tag: String, _ message: String, | ||||
|                     level: LogLevel = .info) { | ||||
|         guard level >= minimumLogLevel else { return } | ||||
|          | ||||
|         queue.async { [weak self] in | ||||
|             guard let self = self else { return } | ||||
|              | ||||
|             // 格式化日志条目 | ||||
|             let timestamp = self.dateFormatter.string(from: Date()) | ||||
|             let pid = ProcessInfo.processInfo.processIdentifier | ||||
|             let logMessage = "\(timestamp) \(pid) \(level.prefix)/\(tag): \(message)\n" | ||||
|              | ||||
|             // 写入日志文件 | ||||
|             self.writeToFile(logMessage) | ||||
|              | ||||
|             // 检查是否需要基于文件大小进行rotate | ||||
|             self.checkSizeRotation() | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     /// 手动触发日志rotate | ||||
|     public func rotateLogFile() { | ||||
|         queue.async { [weak self] in | ||||
|             self?.performRotation() | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     // MARK: - 私有方法 | ||||
|      | ||||
|     private func openLogFile() { | ||||
|         do { | ||||
|             // 如果文件不存在,则创建它 | ||||
|             if !fileManager.fileExists(atPath: fileURL.path) { | ||||
|                 try "".write(to: fileURL, atomically: true, encoding: .utf8) | ||||
|             } | ||||
|              | ||||
|             // 打开文件以进行写入 | ||||
|             fileHandle = try FileHandle(forWritingTo: fileURL) | ||||
|              | ||||
|             // 移动到文件末尾 | ||||
|             fileHandle?.seekToEndOfFile() | ||||
|         } catch { | ||||
|             print("打开日志文件时出错: \(error)") | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     private func writeToFile(_ message: String) { | ||||
|         guard let data = message.data(using: .utf8) else { return } | ||||
|          | ||||
|         do { | ||||
|             if fileHandle == nil { | ||||
|                 // 如果fileHandle为nil,尝试重新打开文件 | ||||
|                 openLogFile() | ||||
|             } | ||||
|              | ||||
|             if #available(iOS 13.4, macOS 10.15.4, watchOS 6.4, tvOS 13.4, *) { | ||||
|                 try fileHandle?.write(contentsOf: data) | ||||
|             } else { | ||||
|                 fileHandle?.write(data) | ||||
|             } | ||||
|         } catch { | ||||
|             print("写入日志文件时出错: \(error)") | ||||
|              | ||||
|             // 如果无法写入,关闭并尝试重新打开 | ||||
|             try? fileHandle?.close() | ||||
|             fileHandle = nil | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     private func checkSizeRotation() { | ||||
|         do { | ||||
|             let attributes = try fileManager.attributesOfItem(atPath: fileURL.path) | ||||
|             let fileSize = attributes[.size] as? UInt64 ?? 0 | ||||
|              | ||||
|             if fileSize >= rotationConfig.maxFileSize { | ||||
|                 performRotation() | ||||
|             } | ||||
|         } catch { | ||||
|             print("检查文件大小时出错: \(error)") | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     private func performRotation() { | ||||
|         do { | ||||
|             // 关闭当前文件句柄 | ||||
|             try fileHandle?.close() | ||||
|             fileHandle = nil | ||||
|              | ||||
|             // 获取日志目录 | ||||
|             let directory = fileURL.deletingLastPathComponent() | ||||
|             let filename = fileURL.lastPathComponent | ||||
|              | ||||
|             // 获取当前备份日志列表 | ||||
|             let baseFilename = filename | ||||
|             let pattern = "^\(baseFilename)\\.(\\d+)$" | ||||
|             let regex = try NSRegularExpression(pattern: pattern, options: []) | ||||
|              | ||||
|             let logFiles = try fileManager.contentsOfDirectory(atPath: directory.path) | ||||
|                 .filter { filename in | ||||
|                     let range = NSRange(location: 0, length: filename.utf16.count) | ||||
|                     return regex.firstMatch(in: filename, options: [], range: range) != nil | ||||
|                 } | ||||
|                 .sorted { file1, file2 in | ||||
|                     // 提取备份号并比较 | ||||
|                     let range1 = NSRange(location: 0, length: file1.utf16.count) | ||||
|                     let range2 = NSRange(location: 0, length: file2.utf16.count) | ||||
|                      | ||||
|                     guard let match1 = regex.firstMatch(in: file1, options: [], range: range1), | ||||
|                           let match2 = regex.firstMatch(in: file2, options: [], range: range2) else { | ||||
|                         return false | ||||
|                     } | ||||
|                      | ||||
|                     let numberRange1 = match1.range(at: 1) | ||||
|                     let numberRange2 = match2.range(at: 1) | ||||
|                      | ||||
|                     let number1 = strToInt(file1.utf16.dropFirst(baseFilename.utf16.count + 1).prefix(numberRange1.length)) ?? 0 | ||||
|                     let number2 = strToInt(file2.utf16.dropFirst(baseFilename.utf16.count + 1).prefix(numberRange2.length)) ?? 0 | ||||
|                      | ||||
|                     return number1 < number2 | ||||
|                 } | ||||
|              | ||||
|             // 如果我们有太多备份日志,则删除旧的备份日志 | ||||
|             if logFiles.count >= rotationConfig.maxBackupCount { | ||||
|                 for i in (rotationConfig.maxBackupCount - 1)..<logFiles.count { | ||||
|                     try fileManager.removeItem(at: directory.appendingPathComponent(logFiles[i])) | ||||
|                 } | ||||
|             } | ||||
|              | ||||
|             // 移动现有的备份日志 | ||||
|             for i in (0..<logFiles.count).reversed() { | ||||
|                 let file = logFiles[i] | ||||
|                 let range = NSRange(location: 0, length: file.utf16.count) | ||||
|                 guard let match = regex.firstMatch(in: file, options: [], range: range) else { | ||||
|                     continue | ||||
|                 } | ||||
|                  | ||||
|                 let numberRange = match.range(at: 1) | ||||
|                 guard let number = strToInt(file.utf16.dropFirst(baseFilename.utf16.count + 1).prefix(numberRange.length)) else { | ||||
|                     continue | ||||
|                 } | ||||
|                  | ||||
|                 if number < rotationConfig.maxBackupCount - 1 { | ||||
|                     let oldPath = directory.appendingPathComponent(file) | ||||
|                     let newPath = directory.appendingPathComponent("\(baseFilename).\(number + 1)") | ||||
|                     try fileManager.moveItem(at: oldPath, to: newPath) | ||||
|                 } else { | ||||
|                     try fileManager.removeItem(at: directory.appendingPathComponent(file)) | ||||
|                 } | ||||
|             } | ||||
|              | ||||
|             // 重命名当前日志文件 | ||||
|             let newBackupPath = directory.appendingPathComponent("\(baseFilename).1") | ||||
|             try fileManager.moveItem(at: fileURL, to: newBackupPath) | ||||
|              | ||||
|             // 创建新的日志文件 | ||||
|             openLogFile() | ||||
|              | ||||
|             // 写入rotation消息 | ||||
|             let timestamp = dateFormatter.string(from: Date()) | ||||
|             let rotationMessage = "[\(timestamp)] [I] [FileLogHandler] Rotated\n" | ||||
|             writeToFile(rotationMessage) | ||||
|              | ||||
|         } catch { | ||||
|             print("rotate日志文件时出错: \(error)") | ||||
|              | ||||
|             // 即使rotate失败,也尝试重新打开文件 | ||||
|             openLogFile() | ||||
|         } | ||||
|     } | ||||
|     private func strToInt(_ number: Substring.UTF16View) -> Int? { | ||||
|         guard let numStr = String(number) else { | ||||
|             return nil | ||||
|         } | ||||
|         return Int(numStr) | ||||
|     } | ||||
| } | ||||
|  | @ -0,0 +1,37 @@ | |||
| // Formatter.swift | ||||
| // Log formatting utilities | ||||
| // Corresponds to Formatter.kt in Android implementation | ||||
| 
 | ||||
| import Foundation | ||||
| 
 | ||||
| public class MainFormatter { | ||||
|     private let dateFormatter: DateFormatter | ||||
|      | ||||
|     public init() { | ||||
|         dateFormatter = DateFormatter() | ||||
|         dateFormatter.dateFormat = "yyyyMMdd'T'HHmmss" | ||||
|         dateFormatter.locale = Locale.current | ||||
|     } | ||||
|      | ||||
|     public func format(level: Logger.LogLevel, tag: String, message: String) -> String { | ||||
|         let formatTime = dateFormatter.string(from: Date()) | ||||
|         let levelString: String | ||||
|          | ||||
|         switch level { | ||||
|         case .verbose: | ||||
|             levelString = "V" | ||||
|         case .debug: | ||||
|             levelString = "D" | ||||
|         case .info: | ||||
|             levelString = "I" | ||||
|         case .warn: | ||||
|             levelString = "W" | ||||
|         case .error: | ||||
|             levelString = "E" | ||||
|         case .none: | ||||
|             levelString = "N" | ||||
|         } | ||||
|          | ||||
|         return "\(formatTime) \(levelString)/\(tag): \(message)" | ||||
|     } | ||||
| } | ||||
|  | @ -0,0 +1,69 @@ | |||
| // Logger.swift | ||||
| // Utility class for logging | ||||
| // Provides similar functionality to Android's Logger | ||||
| 
 | ||||
| import Foundation | ||||
| 
 | ||||
| public class Logger { | ||||
|     public enum LogLevel: Int { | ||||
|         case verbose = 0 | ||||
|         case debug = 1 | ||||
|         case info = 2 | ||||
|         case warn = 3 | ||||
|         case error = 4 | ||||
|         case none = 5 | ||||
|     } | ||||
|      | ||||
|     private static let fileLogger = FileLogHandler( | ||||
|         directory: "guru_app", | ||||
|         filename: "fusion-ads.log", | ||||
|         rotationConfig: FileLogHandler.RotationConfig(maxFileSize: 5 * 1024 * 1024, maxBackupCount: 2), | ||||
|         minimumLogLevel: .verbose | ||||
|     ) | ||||
|      | ||||
|     private static var logLevel: LogLevel = .info | ||||
|      | ||||
|     public static func setLogLevel(_ level: LogLevel) { | ||||
|         logLevel = level | ||||
|     } | ||||
|      | ||||
|     // Verbose log | ||||
|     public static func v(tag: String, message: String) { | ||||
|         if logLevel.rawValue <= LogLevel.verbose.rawValue { | ||||
|             print("V/\(tag): \(message)") | ||||
|             fileLogger.log(tag, message, level: FileLogHandler.LogLevel.verbose) | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     // Debug log | ||||
|     public static func d(tag: String, message: String) { | ||||
|         if logLevel.rawValue <= LogLevel.debug.rawValue { | ||||
|             print("D/\(tag): \(message)") | ||||
|             fileLogger.log(tag, message, level: FileLogHandler.LogLevel.debug) | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     // Info log | ||||
|     public static func i(tag: String, message: String) { | ||||
|         if logLevel.rawValue <= LogLevel.info.rawValue { | ||||
|             print("I/\(tag): \(message)") | ||||
|             fileLogger.log(tag, message, level: FileLogHandler.LogLevel.info) | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     // Warning log | ||||
|     public static func w(tag: String, message: String) { | ||||
|         if logLevel.rawValue <= LogLevel.warn.rawValue { | ||||
|             print("W/\(tag): \(message)") | ||||
|             fileLogger.log(tag, message, level: FileLogHandler.LogLevel.warning) | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     // Error log | ||||
|     public static func e(tag: String, message: String) { | ||||
|         if logLevel.rawValue <= LogLevel.error.rawValue { | ||||
|             print("E/\(tag): \(message)") | ||||
|             fileLogger.log(tag, message, level: FileLogHandler.LogLevel.error) | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | @ -0,0 +1,57 @@ | |||
| // SystemProperties.swift | ||||
| // System property utilities | ||||
| // Corresponds to SystemProperties.kt in Android implementation | ||||
| 
 | ||||
| import Foundation | ||||
| import UIKit | ||||
| 
 | ||||
| public class SystemProperties { | ||||
|     private static let TAG = "SystemProperties" | ||||
|      | ||||
|     // Read system property from UserDefaults | ||||
|     public static func read(propName: String) -> String { | ||||
|         let value = UserDefaults.standard.string(forKey: propName) ?? "" | ||||
|         Logger.i(tag: TAG, message: "read System Property: \(propName)=\(value)") | ||||
|         return value | ||||
|     } | ||||
|      | ||||
|     // Write system property to UserDefaults | ||||
|     public static func write(propName: String, value: String) { | ||||
|         UserDefaults.standard.set(value, forKey: propName) | ||||
|         UserDefaults.standard.synchronize() | ||||
|         Logger.i(tag: TAG, message: "write System Property: \(propName)=\(value)") | ||||
|     } | ||||
|      | ||||
|     // Get device information | ||||
|     public static func getDeviceInfo() -> [String: String] { | ||||
|         var systemInfo = utsname() | ||||
|         uname(&systemInfo) | ||||
|         let machineMirror = Mirror(reflecting: systemInfo.machine) | ||||
|         let identifier = machineMirror.children.reduce("") { identifier, element in | ||||
|             guard let value = element.value as? Int8, value != 0 else { return identifier } | ||||
|             return identifier + String(UnicodeScalar(UInt8(value))) | ||||
|         } | ||||
|          | ||||
|         return [ | ||||
|             "model": identifier, | ||||
|             "os_version": UIDevice.current.systemVersion, | ||||
|             "device_name": UIDevice.current.name, | ||||
|             "device_model": UIDevice.current.model, | ||||
|             "system_name": UIDevice.current.systemName | ||||
|         ] | ||||
|     } | ||||
|      | ||||
|     // Get app information | ||||
|     public static func getAppInfo() -> [String: String] { | ||||
|         let bundle = Bundle.main | ||||
|         let appVersion = bundle.infoDictionary?["CFBundleShortVersionString"] as? String ?? "Unknown" | ||||
|         let buildNumber = bundle.infoDictionary?["CFBundleVersion"] as? String ?? "Unknown" | ||||
|         let bundleId = bundle.bundleIdentifier ?? "Unknown" | ||||
|          | ||||
|         return [ | ||||
|             "app_version": appVersion, | ||||
|             "build_number": buildNumber, | ||||
|             "bundle_id": bundleId | ||||
|         ] | ||||
|     } | ||||
| } | ||||
|  | @ -0,0 +1,28 @@ | |||
| // IState.swift | ||||
| // The interface for implementing states in a StateMachine | ||||
| // Ported from Android implementation | ||||
| 
 | ||||
| import Foundation | ||||
| 
 | ||||
| /// Protocol defining the interface for implementing states in a state machine | ||||
| public protocol IState { | ||||
|      | ||||
|     /// Called when a state is entered | ||||
|     /// - Parameters: | ||||
|     ///   - from: State we are coming from | ||||
|     ///   - params: Optional parameters for state transition | ||||
|     func enter(from: IState?, params: Any?) | ||||
|      | ||||
|     /// Called when a state is exited | ||||
|     /// - Parameter to: State we are transitioning to | ||||
|     func exit(to: IState?) | ||||
|      | ||||
|     /// Process a message sent to the state machine | ||||
|     /// - Parameter msg: The message to process | ||||
|     /// - Returns: HANDLED if processing has completed or NOT_HANDLED if the message wasn't processed | ||||
|     func processMessage(_ msg: Message) -> Bool | ||||
|      | ||||
|     /// Name of State for debugging purposes | ||||
|     /// - Returns: name of state | ||||
|     var name: String { get } | ||||
| } | ||||
|  | @ -0,0 +1,17 @@ | |||
| // IStateMonitor.swift | ||||
| // Protocol for monitoring state changes in a StateMachine | ||||
| // Ported from Android implementation | ||||
| 
 | ||||
| import Foundation | ||||
| 
 | ||||
| /// Protocol for monitoring state changes in a StateMachine | ||||
| public protocol IStateMonitor { | ||||
|      | ||||
|     /// Called when a state transition occurs | ||||
|     /// - Parameters: | ||||
|     ///   - stateMachine: The state machine that changed state | ||||
|     ///   - state: The new state | ||||
|     ///   - lastState: The previous state | ||||
|     ///   - params: Optional parameters associated with the transition | ||||
|     func onStateChanged(stateMachine: StateMachine, state: IState, lastState: IState?, params: Any?) | ||||
| } | ||||
|  | @ -0,0 +1,30 @@ | |||
| // Message.swift | ||||
| // Message class for State Machine communication | ||||
| // Equivalent to Android's Message class | ||||
| 
 | ||||
| import Foundation | ||||
| 
 | ||||
| /// Message class for state machine communication | ||||
| public class Message { | ||||
|     /// The message identifier | ||||
|     public let what: Int | ||||
|      | ||||
|     /// First argument value | ||||
|     public let arg1: Int | ||||
|      | ||||
|     /// Second argument value | ||||
|     public let arg2: Int | ||||
|      | ||||
|     /// Object associated with this message | ||||
|     public let obj: Any? | ||||
|      | ||||
|     public let callback: (() -> Void)? | ||||
|      | ||||
|     init(what: Int, arg1: Int = 0, arg2: Int = 0, obj: Any? = nil, callback: (() -> Void)? = nil) { | ||||
|         self.what = what | ||||
|         self.arg1 = arg1 | ||||
|         self.arg2 = arg2 | ||||
|         self.obj = obj | ||||
|         self.callback = callback | ||||
|     } | ||||
| } | ||||
|  | @ -0,0 +1,43 @@ | |||
| // State.swift | ||||
| // The base class for implementing states in a StateMachine | ||||
| // Ported from Android implementation | ||||
| 
 | ||||
| import Foundation | ||||
| 
 | ||||
| /// Base class for implementing states in a StateMachine | ||||
| open class State: IState { | ||||
|      | ||||
|     /// Initialize a new state | ||||
|     public init() {} | ||||
|      | ||||
|     /// Called when a state is entered | ||||
|     /// - Parameters: | ||||
|     ///   - from: State we are coming from | ||||
|     ///   - params: Optional parameters for state transition | ||||
|     open func enter(from: IState?, params: Any?) { | ||||
|         // Default implementation does nothing | ||||
|     } | ||||
|      | ||||
|     /// Called when a state is exited | ||||
|     /// - Parameter to: State we are transitioning to | ||||
|     open func exit(to: IState?) { | ||||
|         // Default implementation does nothing | ||||
|     } | ||||
|      | ||||
|     /// Process a message sent to the state machine | ||||
|     /// - Parameter msg: The message to process | ||||
|     /// - Returns: HANDLED if processing has completed or NOT_HANDLED if the message wasn't processed | ||||
|     open func processMessage(_ msg: Message) -> Bool { | ||||
|         return false | ||||
|     } | ||||
|      | ||||
|     /// Name of State for debugging purposes | ||||
|     /// This default implementation returns the class name | ||||
|     open var name: String { | ||||
|         let className = String(describing: type(of: self)) | ||||
|         if let lastDotIndex = className.lastIndex(of: ".") { | ||||
|             return String(className[className.index(after: lastDotIndex)...]) | ||||
|         } | ||||
|         return className | ||||
|     } | ||||
| } | ||||
|  | @ -0,0 +1,566 @@ | |||
| // StateMachine.swift | ||||
| // Core state machine implementation | ||||
| // Ported from Android implementation | ||||
| 
 | ||||
| import Foundation | ||||
| 
 | ||||
| private struct PendingWorkItem { | ||||
|     let id: UUID | ||||
|     weak var work: DispatchWorkItem? | ||||
|     let what: Int | ||||
|     let obj: Any? | ||||
| } | ||||
| 
 | ||||
| /// A hierarchical state machine for managing states and transitions | ||||
| public class StateMachine { | ||||
|     // MARK: - Constants | ||||
|      | ||||
|     /// Message.what value when quitting | ||||
|     private static let SM_QUIT_CMD = -1 | ||||
|      | ||||
|     /// Message.what value when initializing | ||||
|     private static let SM_INIT_CMD = -2 | ||||
|      | ||||
|     // MARK: - Properties | ||||
|      | ||||
|     /// Name of the state machine, used for logging | ||||
|     let name: String | ||||
|      | ||||
|     /// Current state of the state machine | ||||
|     private(set) var currentState: IState? | ||||
|      | ||||
|     /// Initial state for the state machine | ||||
|     private var initialState: IState? | ||||
|      | ||||
|     /// Destination state for transitions | ||||
|     private var destState: IState? | ||||
|      | ||||
|     private var params: Any? | ||||
|      | ||||
|     /// Parent-child state relationships | ||||
|     private var stateInfos = [String: StateInfo]() | ||||
|      | ||||
|     /// Monitors state changes | ||||
|     private var monitors = [IStateMonitor]() | ||||
|      | ||||
|     /// Processing queue | ||||
|     private let queue = DispatchQueue.main | ||||
|      | ||||
|     /// Queue for deferred messages | ||||
|     private var deferredMessages = [Message]() | ||||
|      | ||||
|     /// Flag to indicate if the state machine has quit | ||||
|     private var hasQuit = false | ||||
|      | ||||
|     /// The current message being processed | ||||
|     private var currentMessage: Message? | ||||
|      | ||||
|     /// Debug flag | ||||
|     private let isDebugEnabled = false | ||||
|      | ||||
|     private var pendingWorks: [PendingWorkItem] = [] | ||||
|      | ||||
|     // MARK: - Nested Types | ||||
|      | ||||
|     /// Halting state entered when transitionToHaltingState is called | ||||
|     private class HaltingState: State { | ||||
|         private weak var stateMachine: StateMachine? | ||||
|          | ||||
|         init(stateMachine: StateMachine) { | ||||
|             self.stateMachine = stateMachine | ||||
|             super.init() | ||||
|         } | ||||
|          | ||||
|         override func processMessage(_ msg: Message) -> Bool { | ||||
|             stateMachine?.haltedProcessMessage(msg) | ||||
|             return true | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     /// Quitting state entered when a valid quit message is handled | ||||
|     private class QuittingState: State { | ||||
|         override func processMessage(_ msg: Message) -> Bool { | ||||
|             return false | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     public func isCurrent(_ state: IState) -> Bool { | ||||
|         return (currentState as? State) === (state as? State) | ||||
|     } | ||||
|      | ||||
|     /// Information about a state and its relationships | ||||
|     private class StateInfo { | ||||
|         let state: IState | ||||
|         var parentStateInfo: StateInfo? | ||||
|         var active = false | ||||
|          | ||||
|         init(state: IState) { | ||||
|             self.state = state | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     // MARK: - Lifecycle | ||||
|      | ||||
|     /// Initialize a state machine with a name | ||||
|     /// - Parameter name: Name of the state machine | ||||
|     public init(name: String) { | ||||
|         self.name = name | ||||
|     } | ||||
|      | ||||
|     // MARK: - Public Methods | ||||
|      | ||||
|     /// Add a new state to the state machine | ||||
|     /// - Parameters: | ||||
|     ///   - state: The state to add | ||||
|     ///   - parent: Optional parent state | ||||
|     public func addState(_ state: IState, parent: IState? = nil) { | ||||
|         let stateInfo = StateInfo(state: state) | ||||
|         stateInfo.active = false | ||||
|          | ||||
|         if let parent = parent { | ||||
|             // Look up the parent state info | ||||
|             if let parentStateInfo = getStateInfo(parent) { | ||||
|                 stateInfo.parentStateInfo = parentStateInfo | ||||
|             } else { | ||||
|                 log("StateMachine: parent state not found: \(parent.name)") | ||||
|             } | ||||
|         } | ||||
|          | ||||
|         stateInfo.active = false | ||||
|         stateInfo.parentStateInfo = parent != nil ? getStateInfo(parent!) : nil | ||||
|          | ||||
|         stateInfos[state.name] = stateInfo | ||||
|     } | ||||
|      | ||||
|     /// Set the initial state of the state machine | ||||
|     /// - Parameter state: The initial state | ||||
|     public func setInitialState(_ state: IState) { | ||||
|         initialState = state | ||||
|     } | ||||
|      | ||||
|     /// Add a monitor for state changes | ||||
|     /// - Parameter monitor: The monitor to add | ||||
|     public func addStateMonitor(_ monitor: IStateMonitor) { | ||||
|         monitors.append(monitor) | ||||
|     } | ||||
|      | ||||
|     /// Remove a monitor for state changes | ||||
|     /// - Parameter monitor: The monitor to remove | ||||
|     public func removeStateMonitor(_ monitor: IStateMonitor) { | ||||
|         if let index = monitors.firstIndex(where: { $0 as AnyObject === monitor as AnyObject }) { | ||||
|             monitors.remove(at: index) | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     /// Start the state machine | ||||
|     public func start() { | ||||
|         guard !hasQuit, let initialState = initialState else { | ||||
|             log("StateMachine: start called without an initial state or after quitting") | ||||
|             return | ||||
|         } | ||||
|          | ||||
|         queue.async { [weak self] in | ||||
|             guard let self = self else { return } | ||||
|             self.setupInitialState() | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     /// Send a message to the state machine | ||||
|     /// - Parameter message: The message to send | ||||
|     public func sendMessage(_ message: Message) { | ||||
|         sendMessageDelayed(message, delayed: .milliseconds(0)) | ||||
|     } | ||||
|      | ||||
|     public func sendMessageDelayed(_ message: Message, delayed: DispatchTimeInterval) { | ||||
|         if hasQuit { | ||||
|             log("StateMachine: sendMessage called after quitting") | ||||
|             return | ||||
|         } | ||||
|         let pendingItemId = UUID() | ||||
|         var work: DispatchWorkItem? | ||||
|         work = DispatchWorkItem() { | ||||
|             [weak self] in | ||||
|             guard let self = self, !(work?.isCancelled ?? true) else { | ||||
|                 self?.pendingWorks.removeAll { | ||||
|                     item in | ||||
|                     return pendingItemId == item.id | ||||
|                 } | ||||
|                 return | ||||
|             } | ||||
|             if(message.callback != nil) { | ||||
|                 // 如果message携带callback,则视作由callback承接执行过程,模拟Android的`Handler.post(runnable)`或`Handler.postDelayed(runnable, delay)` | ||||
|                 message.callback?() | ||||
|             } else { | ||||
|                 self.processMessage(message) | ||||
|             } | ||||
|             self.pendingWorks.removeAll { | ||||
|                 item in | ||||
|                 return pendingItemId == item.id | ||||
|             } | ||||
|         } | ||||
|         if delayed == DispatchTimeInterval.milliseconds(0) { | ||||
|             queue.async(execute: work!) | ||||
|         } else { | ||||
|             pendingWorks.append(PendingWorkItem(id: pendingItemId, work: work, what: message.what, obj: message.obj)) | ||||
|             queue.asyncAfter(deadline: .now() + delayed, execute: work!) | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     public func sendMessage(what: Int, obj: Any? = nil) { | ||||
|         sendMessage(Message(what: what, obj: obj)) | ||||
|     } | ||||
|      | ||||
|     public func sendMessageDelayed(what: Int, delayMillis: Int, obj: Any? = nil) { | ||||
|          | ||||
|         sendMessageDelayed(Message(what: what, obj: obj), delayed: .milliseconds(delayMillis)) | ||||
|     } | ||||
|      | ||||
|     public func sendMessageDelayed(what: Int, delayed: DispatchTimeInterval, obj: Any? = nil) { | ||||
|          | ||||
|         sendMessageDelayed(Message(what: what, obj: obj), delayed: delayed) | ||||
|     } | ||||
|      | ||||
|     public func removeMessages(what: Int) { | ||||
|         if hasQuit { | ||||
|             log("StateMachine: removeMessages called after quitting") | ||||
|             return | ||||
|         } | ||||
|         pendingWorks.filter { | ||||
|             pendingWorkItem in | ||||
|             return pendingWorkItem.work == nil || pendingWorkItem.what == what | ||||
|         }.forEach { | ||||
|             pendingWorkItem in | ||||
|             if pendingWorkItem.work != nil && !(pendingWorkItem.work?.isCancelled ?? true) { | ||||
|                 pendingWorkItem.work?.cancel() | ||||
|             } | ||||
|         } | ||||
|          | ||||
|     } | ||||
|      | ||||
|     private func clearPendingWorks() { | ||||
|         pendingWorks.forEach { | ||||
|             pendingWorkItem in | ||||
|             if pendingWorkItem.work != nil && !(pendingWorkItem.work?.isCancelled ?? true) { | ||||
|                 pendingWorkItem.work?.cancel() | ||||
|             } | ||||
|         } | ||||
|         pendingWorks.removeAll() | ||||
|     } | ||||
|      | ||||
|     public func post(runnable: @escaping () -> Void, token: Any? = nil) { | ||||
|         postDelayed(runnable: runnable, token: token, delayed: .microseconds(0)) | ||||
|     } | ||||
|      | ||||
|     public func postDelayed(runnable: @escaping () -> Void, token: Any? = nil, delayed: DispatchTimeInterval) { | ||||
|         sendMessageDelayed(Message(what: 0, obj: token, callback: runnable), delayed: delayed) | ||||
|     } | ||||
|      | ||||
|     private func anyEquals(_ x : Any?, _ y : Any?) -> Bool { | ||||
|         guard let x = x as? AnyHashable, | ||||
|               let y = y as? AnyHashable else { return false } | ||||
|         return x == y | ||||
|     } | ||||
|      | ||||
|     public func removeCallbacksAndMessages(_ token: Any? = nil) { | ||||
|         if(token == nil) { | ||||
|             clearPendingWorks() | ||||
|         } | ||||
|         pendingWorks.removeAll { | ||||
|             pendingWorkItem in | ||||
|             let remove = anyEquals(pendingWorkItem.obj, token) | ||||
|             if(remove && !(pendingWorkItem.work?.isCancelled ?? true)) { | ||||
|                 pendingWorkItem.work?.cancel() | ||||
|             } | ||||
|             return remove | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     /// Send a message to the front of the queue | ||||
|     /// - Parameter message: The message to send | ||||
|     public func sendMessageAtFrontOfQueue(_ message: Message) { | ||||
|         if hasQuit { | ||||
|             log("StateMachine: sendMessageAtFrontOfQueue called after quitting") | ||||
|             return | ||||
|         } | ||||
|          | ||||
|         queue.async(flags: .barrier) { [weak self] in | ||||
|             guard let self = self else { return } | ||||
|             self.processMessage(message) | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     /// Transition to a new state | ||||
|     /// - Parameters: | ||||
|     ///   - state: The state to transition to | ||||
|     ///   - params: Optional parameters for the transition | ||||
|     public func transitionTo(_ state: IState, params: Any? = nil) { | ||||
|         if hasQuit { | ||||
|             log("StateMachine: transitionTo called after quitting") | ||||
|             return | ||||
|         } | ||||
|          | ||||
|         guard let stateInfo = getStateInfo(state) else { | ||||
|             log("StateMachine: transitionTo called with unrecognized state: \(state.name)") | ||||
|             return | ||||
|         } | ||||
|          | ||||
|         destState = state | ||||
|         self.params = params | ||||
|     } | ||||
|      | ||||
|     /// Transition to the halting state | ||||
|     public func transitionToHaltingState() { | ||||
|         let haltingState = HaltingState(stateMachine: self) | ||||
|         addState(haltingState) | ||||
|         transitionTo(haltingState) | ||||
|     } | ||||
|      | ||||
|     /// Quit the state machine | ||||
|     public func quit() { | ||||
|         if hasQuit { | ||||
|             log("StateMachine: quit called after already quitting") | ||||
|             return | ||||
|         } | ||||
|          | ||||
|         let quittingState = QuittingState() | ||||
|         addState(quittingState) | ||||
|         transitionTo(quittingState) | ||||
|          | ||||
|         queue.async(flags: .barrier) { [weak self] in | ||||
|             guard let self = self else { return } | ||||
|             self.cleanupAfterQuitting() | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     /// Defer a message to be processed after the next state transition | ||||
|     /// - Parameter message: The message to defer | ||||
|     public func deferMessage(_ message: Message) { | ||||
|         if hasQuit { | ||||
|             log("StateMachine: deferMessage called after quitting") | ||||
|             return | ||||
|         } | ||||
|          | ||||
|         deferredMessages.append(message) | ||||
|     } | ||||
|      | ||||
|     // MARK: - public Methods | ||||
|      | ||||
|     /// Called for any message received after transitioning to halting state | ||||
|     /// - Parameter message: The message received | ||||
|     open func haltedProcessMessage(_ message: Message) { | ||||
|         // Default implementation does nothing | ||||
|     } | ||||
|      | ||||
|     /// Called once after transitioning to halting state | ||||
|     open func onHalting() { | ||||
|         // Default implementation does nothing | ||||
|     } | ||||
|      | ||||
|     /// Called once after quitting | ||||
|     open func onQuitting() { | ||||
|         // Default implementation does nothing | ||||
|     } | ||||
|      | ||||
|     /// Called when a message is not handled by any state | ||||
|     /// - Parameter message: The unhandled message | ||||
|     open func unhandledMessage(_ message: Message) { | ||||
|         // Default implementation does nothing | ||||
|     } | ||||
|      | ||||
|     // MARK: - Private Methods | ||||
|      | ||||
|     /// Get the StateInfo for a state | ||||
|     /// - Parameter state: The state to get info for | ||||
|     /// - Returns: StateInfo object or nil if not found | ||||
|     private func getStateInfo(_ state: IState) -> StateInfo? { | ||||
|         return stateInfos[state.name] | ||||
|     } | ||||
|      | ||||
|     /// Setup the initial state | ||||
|     private func setupInitialState() { | ||||
|         guard let initialState = initialState else { return } | ||||
|          | ||||
|         currentState = initialState | ||||
|          | ||||
|         // Find all parent states of the initial state | ||||
|         var parentStates = [IState]() | ||||
|         var stateInfo = getStateInfo(initialState) | ||||
|          | ||||
|         while let info = stateInfo?.parentStateInfo { | ||||
|             parentStates.insert(info.state, at: 0) | ||||
|             stateInfo = info | ||||
|         } | ||||
|          | ||||
|         // Enter parent states from top to bottom | ||||
|         for state in parentStates { | ||||
|             state.enter(from: nil, params: nil) | ||||
|         } | ||||
|          | ||||
|         // Enter initial state | ||||
|         initialState.enter(from: nil, params: nil) | ||||
|          | ||||
|         // Process any deferred messages | ||||
|         processDeferredMessages() | ||||
|     } | ||||
|      | ||||
|     /// Process a message through the state hierarchy | ||||
|     /// - Parameter message: The message to process | ||||
|     private func processMessage(_ message: Message) { | ||||
|         if hasQuit { | ||||
|             return | ||||
|         } | ||||
|          | ||||
|         currentMessage = message | ||||
|          | ||||
|         if message.what == StateMachine.SM_QUIT_CMD { | ||||
|             transitionTo(QuittingState()) | ||||
|             return | ||||
|         } | ||||
|          | ||||
|         var state: IState? = currentState | ||||
|         var handled = false | ||||
|          | ||||
|         // Try to handle the message starting with the current state and going up the hierarchy | ||||
|         while let currentState = state, !handled { | ||||
|             handled = currentState.processMessage(message) | ||||
|             if !handled { | ||||
|                 state = getStateInfo(currentState)?.parentStateInfo?.state | ||||
|             } | ||||
|         } | ||||
|          | ||||
|         if !handled { | ||||
|             unhandledMessage(message) | ||||
|         } | ||||
|          | ||||
|         performStateTransitions(message) | ||||
|          | ||||
|         currentMessage = nil | ||||
|     } | ||||
|      | ||||
|     /// Perform any pending state transitions | ||||
|     /// - Parameter message: The message that triggered potential transitions | ||||
|     private func performStateTransitions(_ message: Message) { | ||||
|         if let destState = destState { | ||||
|             let orgState = currentState | ||||
|              | ||||
|             // Find common ancestor state | ||||
|             let (commonAncestor, statesToExit, statesToEnter) = findCommonAncestor(currentState!, destState) | ||||
|              | ||||
|             // Exit current states up to common ancestor | ||||
|             for state in statesToExit { | ||||
|                 state.exit(to: destState) | ||||
|             } | ||||
|              | ||||
|             // Enter new states from common ancestor down to destination | ||||
|             for state in statesToEnter { | ||||
|                 state.enter(from: orgState, params: params) | ||||
|             } | ||||
|              | ||||
|             // Update current state | ||||
|             currentState = destState | ||||
|              | ||||
|             // Process deferred messages | ||||
|             processDeferredMessages() | ||||
|              | ||||
|             // Notify monitors | ||||
|             notifyMonitors(destState, lastState: orgState, params: message.obj) | ||||
|              | ||||
|             // Reset destination state | ||||
|             self.destState = nil | ||||
|             self.params = nil | ||||
|              | ||||
|             // Check if we're entering a special state | ||||
|             if destState is QuittingState { | ||||
|                 onQuitting() | ||||
|                 cleanupAfterQuitting() | ||||
|             } else if destState is HaltingState { | ||||
|                 onHalting() | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     /// Find the common ancestor of two states and determine which states to exit and enter | ||||
|     /// - Parameters: | ||||
|     ///   - currentState: The current state | ||||
|     ///   - destState: The destination state | ||||
|     /// - Returns: Tuple containing common ancestor, states to exit, and states to enter | ||||
|     private func findCommonAncestor(_ currentState: IState, _ destState: IState) -> (IState?, [IState], [IState]) { | ||||
|         var commonAncestor: IState? = nil | ||||
|         var statesToExit = [IState]() | ||||
|         var statesToEnter = [IState]() | ||||
|          | ||||
|         // Build path from current state to root | ||||
|         var currentPath = [IState]() | ||||
|         var currentStateInfo = getStateInfo(currentState) | ||||
|         while let info = currentStateInfo { | ||||
|             currentPath.insert(info.state, at: 0) | ||||
|             currentStateInfo = info.parentStateInfo | ||||
|         } | ||||
|          | ||||
|         // Build path from destination state to root | ||||
|         var destPath = [IState]() | ||||
|         var destStateInfo = getStateInfo(destState) | ||||
|         while let info = destStateInfo { | ||||
|             destPath.insert(info.state, at: 0) | ||||
|             destStateInfo = info.parentStateInfo | ||||
|         } | ||||
|          | ||||
|         // Find common ancestor | ||||
|         var commonIndex = 0 | ||||
|         while commonIndex < min(currentPath.count, destPath.count) && currentPath[commonIndex].name == destPath[commonIndex].name { | ||||
|             commonAncestor = currentPath[commonIndex] | ||||
|             commonIndex += 1 | ||||
|         } | ||||
|          | ||||
|         // States to exit - from current state up to (but not including) common ancestor | ||||
|         statesToExit = Array(currentPath.reversed().prefix(currentPath.count - commonIndex)) | ||||
|          | ||||
|         // States to enter - from one below common ancestor down to destination state | ||||
|         statesToEnter = Array(destPath.suffix(destPath.count - commonIndex)) | ||||
|          | ||||
|         return (commonAncestor, statesToExit, statesToEnter) | ||||
|     } | ||||
|      | ||||
|     /// Process all deferred messages | ||||
|     private func processDeferredMessages() { | ||||
|         guard !deferredMessages.isEmpty else { return } | ||||
|          | ||||
|         let messages = deferredMessages | ||||
|         deferredMessages.removeAll() | ||||
|          | ||||
|         for message in messages { | ||||
|             sendMessageAtFrontOfQueue(message) | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     /// Notify all monitors of a state change | ||||
|     /// - Parameters: | ||||
|     ///   - newState: The new state | ||||
|     ///   - lastState: The previous state | ||||
|     ///   - params: Optional parameters | ||||
|     private func notifyMonitors(_ newState: IState, lastState: IState?, params: Any?) { | ||||
|         for monitor in monitors { | ||||
|             monitor.onStateChanged(stateMachine: self, state: newState, lastState: lastState, params: params) | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     /// Clean up resources after quitting | ||||
|     private func cleanupAfterQuitting() { | ||||
|         hasQuit = true | ||||
|         currentState = nil | ||||
|         initialState = nil | ||||
|         destState = nil | ||||
|         deferredMessages.removeAll() | ||||
|         stateInfos.removeAll() | ||||
|         monitors.removeAll() | ||||
|         clearPendingWorks() | ||||
|     } | ||||
|      | ||||
|     /// Log a message if debugging is enabled | ||||
|     /// - Parameter message: The message to log | ||||
|     private func log(_ message: String) { | ||||
|         if isDebugEnabled { | ||||
|             print("\(name): \(message)") | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | @ -0,0 +1,19 @@ | |||
| Copyright (c) 2025 Haoyi <haoyi.zhang@castbox.fm> | ||||
| 
 | ||||
| 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,29 @@ | |||
| # FusionAds | ||||
| 
 | ||||
| [](https://travis-ci.org/Haoyi/FusionAds) | ||||
| [](https://cocoapods.org/pods/FusionAds) | ||||
| [](https://cocoapods.org/pods/FusionAds) | ||||
| [](https://cocoapods.org/pods/FusionAds) | ||||
| 
 | ||||
| ## Example | ||||
| 
 | ||||
| To run the example project, clone the repo, and run `pod install` from the Example directory first. | ||||
| 
 | ||||
| ## Requirements | ||||
| 
 | ||||
| ## Installation | ||||
| 
 | ||||
| FusionAds is available through [CocoaPods](https://cocoapods.org). To install | ||||
| it, simply add the following line to your Podfile: | ||||
| 
 | ||||
| ```ruby | ||||
| pod 'FusionAds' | ||||
| ``` | ||||
| 
 | ||||
| ## Author | ||||
| 
 | ||||
| Haoyi, haoyi.zhang@castbox.fm | ||||
| 
 | ||||
| ## License | ||||
| 
 | ||||
| FusionAds is available under the MIT license. See the LICENSE file for more info. | ||||
		Loading…
	
		Reference in New Issue