package com.tinivo.rewardedads import android.app.Activity import android.os.Looper import android.util.Log import com.cocos.lib.CocosHelper import com.cocos.lib.JsbBridgeWrapper import com.google.android.gms.ads.AdRequest import com.google.android.gms.ads.FullScreenContentCallback import com.google.android.gms.ads.MobileAds import com.google.android.gms.ads.OnUserEarnedRewardListener import com.google.android.gms.ads.LoadAdError import com.google.android.gms.ads.rewarded.RewardItem import com.google.android.gms.ads.rewarded.RewardedAd import com.google.android.gms.ads.rewarded.RewardedAdLoadCallback import org.json.JSONObject class RewardedAdsAdMobBridge( private val activity: Activity, ) : JsbBridgeWrapper.OnScriptEventListener { companion object { private const val LOG_TAG = "RewardedAds" private const val REQUEST_EVENT = "tinivo.rewarded.request" private const val RESPONSE_EVENT = "tinivo.rewarded.response" private const val CONFIG_ASSET_PATH = "rewarded-provider-config.json" } private val config by lazy { RewardedAdsAdMobConfig.load(activity, CONFIG_ASSET_PATH) } private val rewardedAds = mutableMapOf() private val preloadRequestByPlacement = mutableMapOf() private val presentRequestByPlacement = mutableMapOf() private val rewardedPlacements = mutableSetOf() private var installed = false fun install() { if (installed) { return } installed = true MobileAds.initialize(activity) JsbBridgeWrapper.getInstance().addScriptEventListener(REQUEST_EVENT, this) } override fun onScriptEvent(arg: String?) { val payload = safePayload(arg) val command = payload?.optString("command").orEmpty() val requestId = payload?.optString("requestId").orEmpty() val placement = payload?.optString("placement").orEmpty() runOnMainThread { when (command) { "preload" -> preload(requestId, placement) "show" -> show(requestId, placement) "reset" -> reset(placement) else -> dispatchError(requestId, placement, "unknown", "Unknown AdMob rewarded command") } } } private inline fun runOnMainThread(crossinline action: () -> Unit) { if (Looper.getMainLooper() == Looper.myLooper()) { action() return } activity.runOnUiThread { action() } } private fun preload(requestId: String, placement: String) { val adUnitId = config.adUnitIdForPlacement(placement) if (adUnitId.isNullOrEmpty()) { dispatchError(requestId, placement, "configInvalid", "Missing AdMob ad unit for $placement") return } preloadRequestByPlacement[placement] = requestId RewardedAd.load( activity, adUnitId, AdRequest.Builder().build(), object : RewardedAdLoadCallback() { override fun onAdLoaded(rewardedAd: RewardedAd) { rewardedAds[placement] = rewardedAd dispatchResponse( JSONObject().apply { put("requestId", preloadRequestByPlacement.remove(placement).orEmpty()) put("command", "preload") put("placement", placement) }, ) } override fun onAdFailedToLoad(error: LoadAdError) { rewardedAds.remove(placement) Log.w( LOG_TAG, "[RewardedAds][Rewarded] load failed placement=$placement adUnitId=$adUnitId code=${error.code} domain=${error.domain} message=${error.message.orEmpty()} responseInfo=${error.responseInfo}", ) dispatchError( preloadRequestByPlacement.remove(placement).orEmpty(), placement, "loadFailed", error.message ?: "AdMob rewarded ad failed to load", ) } }, ) } private fun show(requestId: String, placement: String) { val rewardedAd = rewardedAds[placement] if (rewardedAd == null) { dispatchError(requestId, placement, "notReady", "AdMob rewarded ad is not ready for $placement") return } rewardedPlacements.remove(placement) presentRequestByPlacement[placement] = requestId rewardedAd.fullScreenContentCallback = object : FullScreenContentCallback() { override fun onAdDismissedFullScreenContent() { val earned = rewardedPlacements.remove(placement) dispatchResponse( JSONObject().apply { put("requestId", presentRequestByPlacement.remove(placement).orEmpty()) put("command", "show") put("placement", placement) put("result", JSONObject().apply { put("completed", true) put("cancelled", !earned) put("earned", earned) }) }, ) rewardedAds.remove(placement) preload("", placement) } override fun onAdFailedToShowFullScreenContent(error: com.google.android.gms.ads.AdError) { Log.w( LOG_TAG, "[RewardedAds][Rewarded] show failed placement=$placement adUnitId=${rewardedAd.adUnitId} code=${error.code} domain=${error.domain} message=${error.message.orEmpty()}", ) dispatchError( presentRequestByPlacement.remove(placement).orEmpty(), placement, "showFailed", error.message ?: "AdMob rewarded ad failed to show", ) rewardedAds.remove(placement) } } rewardedAd.show(activity, OnUserEarnedRewardListener { _: RewardItem -> rewardedPlacements.add(placement) }) } private fun reset(placement: String) { if (placement.isBlank()) { rewardedAds.clear() preloadRequestByPlacement.clear() presentRequestByPlacement.clear() rewardedPlacements.clear() return } rewardedAds.remove(placement) preloadRequestByPlacement.remove(placement) presentRequestByPlacement.remove(placement) rewardedPlacements.remove(placement) } private fun safePayload(arg: String?): JSONObject? { return try { if (arg.isNullOrBlank()) null else JSONObject(arg) } catch (_: Throwable) { null } } private fun dispatchError(requestId: String, placement: String, code: String, message: String) { dispatchResponse( JSONObject().apply { put("requestId", requestId) put("command", "error") put("placement", placement) put("error", JSONObject().apply { put("code", code) put("message", message) }) }, ) } private fun dispatchResponse(payload: JSONObject) { val content = payload.toString() CocosHelper.runOnGameThread { JsbBridgeWrapper.getInstance().dispatchEventToScript(RESPONSE_EVENT, content) } } } private data class RewardedAdsAdMobConfig( val appId: String, val placements: Map, ) { fun adUnitIdForPlacement(placement: String): String? = placements[placement] companion object { fun load(activity: Activity, assetPath: String): RewardedAdsAdMobConfig { val json = activity.assets.open(assetPath).bufferedReader().use { it.readText() } val root = JSONObject(json) val shared = root.getJSONObject("shared") val placementsJson = root.getJSONObject("placements") val placements = mutableMapOf() for (key in placementsJson.keys()) { placements[key] = placementsJson.getJSONObject(key).getString("adUnitId") } return RewardedAdsAdMobConfig( appId = shared.getString("appId"), placements = placements, ) } } }