FusionAds-iOS/FusionAds/Classes/fusion/engine/mab/rewarded/MabRewardedEngine.swift

797 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.

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