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

797 lines
27 KiB
Swift
Raw Normal View History

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