FusionAds-iOS/FusionAds/Classes/fusion/engine/mab/interstitial/MabInterstitialEngine.swift

785 lines
27 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

//
// 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)
}
}