// Copyright 2022, the Flutter project authors. Please see the AUTHORS file // for details. All rights reserved. Use of this source code is governed by a // BSD-style license that can be found in the LICENSE file. import 'dart:async'; import 'dart:io'; import 'dart:math'; import 'package:applovin_max/applovin_max.dart'; import 'package:device_info_plus/device_info_plus.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:logging/logging.dart'; import 'package:puzzleweave/ads/ad_helper.dart'; import 'package:puzzleweave/firebase/adjust_helper.dart'; import 'package:puzzleweave/firebase/firebase_helper.dart'; import '../persistence/persistence.dart'; import '../remote_config/remote_config.dart'; import '../statistics/statistics.dart'; final Logger _log = Logger('ApplovinAdsController'); const int bannerReportDuration = 3 * 60 * 1000; // banner paid 3分钟报一次 /// Allows showing ads. A facade for `package:google_mobile_ads`. class ApplovinAdsController { final BuildContext context; // 新增:设备保护标记 bool _isLowRamDevice = false; bool _isProblematicDevice = false; bool get isLowRamDevice => _isLowRamDevice; // 从 Remote Config 获取设备黑名单 Set _getCrashProneDevices() { final blacklistStr = RemoteConfig().adCrashProneDevices; if (blacklistStr.isEmpty) return {}; return blacklistStr.split(',').map((e) => e.trim().toLowerCase()).toSet(); } bool _shouldSkipAds() { return _isLowRamDevice || _isProblematicDevice; } late double bannerPaidValueMicros; // banner广告累计收益 late int lastBannerPaidReportTimestamp; // 上次banner广告收益上报时间戳,用于控制banner广告收益上报频率 late double allPaidValueMicros; // 所有广告累计收益 late double revenueThreshold; // 广告累计上报阈值 bool _hasInit = false; bool get hasInit => _hasInit; final Completer completer = Completer(); ApplovinAdsController(this.context); void dispose() { // 🔥 关键修复:防止内存泄漏 if (!completer.isCompleted) { completer.complete(false); } // 清理状态通知器 interstitialAdState.dispose(); rewardedAdState.dispose(); // 清理回调 rewardCallback = null; } void showAppLovinDebugger() async { // 检查 SDK 是否已初始化(可选,但推荐) bool? isInitialized = await AppLovinMAX.isInitialized(); if (isInitialized == true) { // 弹出 AppLovin 中介调试界面 AppLovinMAX.showMediationDebugger(); } else { _log.warning("AppLovin SDK 尚未完成初始化,无法打开调试器。"); } } /// Initializes the injected [MobileAds.instance]. Future initialize() async { if (_hasInit) return; _log.info('AppLovinMAX.initialize...'); // 1. 设备兼容性检测(关键:防止外部纹理崩溃) if (Platform.isAndroid) { final deviceInfo = DeviceInfoPlugin(); final androidInfo = await deviceInfo.androidInfo; // 低端机检测 _isLowRamDevice = androidInfo.isLowRamDevice || (androidInfo.systemFeatures.contains('android.hardware.ram.low')); // 问题设备检测(从 Remote Config 读取黑名单) String manufacturer = androidInfo.manufacturer.toLowerCase(); String model = androidInfo.model.toLowerCase(); final crashProneDevices = _getCrashProneDevices(); _isProblematicDevice = crashProneDevices.any((device) => manufacturer.contains(device) || model.contains(device)); if (_isProblematicDevice) { _log.warning('🔥 Problematic device detected: $manufacturer $model'); } } // 用于模拟测试欧洲UMP是否正常,release版本注意注释掉 // AppLovinMAX.setVerboseLogging(true); // AppLovinMAX.setConsentFlowDebugUserGeography(ConsentFlowUserGeography.gdpr); // 2. 开启合规流开关 AppLovinMAX.setTermsAndPrivacyPolicyFlowEnabled(true); // 3. 设置你的隐私政策和用户协议链接(必须是有效的 URL) AppLovinMAX.setPrivacyPolicyUrl("https://longreachai.net/game/privacy_policy.html"); AppLovinMAX.setTermsOfServiceUrl("https://longreachai.net/game/terms_of_service.html"); // 用不着reward广告, 只初始化banner和interstitial AppLovinMAX.setInitializationAdUnitIds([AdHelper.getApplovinBannerUnitId(_shouldSkipAds()), AdHelper.getApplovinInterstitialUnitId(_shouldSkipAds())]); // 4. 然后再初始化 SDK MaxConfiguration? sdkConfiguration = await AppLovinMAX.initialize(AdHelper.applovinSdkKey); _log.info('AppLovinMAX.initialize success!'); lastBannerPaidReportTimestamp = Persistence().lastBannerPaidReportTimestamp; bannerPaidValueMicros = Persistence().bannerPaidValueMicros; allPaidValueMicros = Persistence().allPaidValueMicros; revenueThreshold = RemoteConfig().adRevenueThreshold; if (revenueThreshold <= 0.0) revenueThreshold = 0.01; // 5. 分步异步加载 (防止 FD 句柄数瞬间爆表) await Future.delayed(const Duration(milliseconds: 500)); if (!_shouldSkipAds()) initializeBannerAds(); await Future.delayed(const Duration(milliseconds: 500)); if (!_shouldSkipAds()) initializeInterstitialAds(); await Future.delayed(const Duration(milliseconds: 500)); if (!_shouldSkipAds()) initializeRewardedAd(); completer.complete(true); _hasInit = true; } void initializeBannerAds() { // 强制关闭低端机的自适应 Banner,因为其渲染开销极大 AppLovinMAX.setBannerExtraParameter(AdHelper.getApplovinBannerUnitId(_shouldSkipAds()), "adaptive_banner", _isLowRamDevice ? "false" : "true"); } /// 强制销毁 Banner(解决 JNI CheckException 的大杀器) /// 在 AdsState dispose 或页面跳转前调用 void destroyBanner() { _log.info("🔥 Explicitly destroying banner to release GPU buffers"); try { // 强制通知原生层销毁 Banner 视图 AppLovinMAX.destroyBanner(AdHelper.getApplovinBannerUnitId(_shouldSkipAds())); } catch (e) { _log.warning("Destroy banner error: $e"); } } Widget getBannerWidget(String positionKey) { // 增加:问题设备或未初始化,直接不渲染 Widget if (!_hasInit || _shouldSkipAds()) { return const SizedBox.shrink(); } return MaxAdView( key: ValueKey(positionKey), // 只要 positionKey 不变,页面内刷新就不会重建 adUnitId: AdHelper.getApplovinBannerUnitId(_shouldSkipAds()), adFormat: AdFormat.banner, placement: 'banner', extraParameters: {'adaptive_banner': _isLowRamDevice ? 'false' : 'true'}, listener: AdViewAdListener( onAdLoadedCallback: (ad) { // // 广告加载成功后, ad.size 包含实际的宽度和高度 (dp) // double? widthDp = ad.size?.width; // double? heightDp = ad.size?.height; // if (heightDp != null) { // context.read().bannerHeight = heightDp; // } // _log.info('banner广告 width = $widthDp, height = $heightDp'); _log.info(() => 'applovin banner Ad loaded: ${ad.hashCode}'); }, onAdLoadFailedCallback: (adUnitId, error) { _log.warning('applovin banner Ad failedToLoad: $error'); }, onAdClickedCallback: (ad) { _log.info('applovin banner Ad click registered'); }, onAdExpandedCallback: (ad) { _log.info('applovin banner Ad expanded'); }, onAdCollapsedCallback: (ad) { _log.info('applovin banner Ad collaspsed'); }, onAdRevenuePaidCallback: (ad) { _log.info('woooooooooo, applovin banner paid event: revenue: ${ad.revenue} precision: ${ad.revenuePrecision}'); if (ad.revenue > 0) { onAdRevenuePaid(ad); // revenue 单位转化为 valueMicro,以便和admod一致 } }, ), ); } // revenue 回调处理,所有广告类型统一在此处理 onAdRevenuePaid(MaxAd ad) async { _log.info('report ad revenue paid: ${ad.revenue}'); Map params = { "ad_platform": 'appLovin', "ad_source": ad.networkName, "ad_format": ad.placement, "ad_unit_name": ad.adUnitId, "value": ad.revenue, "currency": "USD", }; // for ARO : ad_impression 上报给firebase FirebaseHelper.logEvent("ad_impression", params); // for Taichi FirebaseHelper.logEvent("Ad_Impression_Revenue", params); // 累计超过0.01 USD 上报 Total_Ads_Revenue_001 double previousTroasCache = Persistence().tRoasCache; double currentTroasCache = previousTroasCache + ad.revenue; if (currentTroasCache >= revenueThreshold) { FirebaseHelper.logEvent("Total_Ads_Revenue_001", {"value": currentTroasCache, "currency": "USD"}); Persistence().tRoasCache = 0; // 缓存清零 } else { Persistence().tRoasCache = currentTroasCache; } // adRevenue 事件上报给adjust AdjustHelper.trackAdRevenue(ad.placement, 1, ad.revenue, 'USD'); // revenue 埋点时间上报给自主BI平台 Statistics.postEvent({ "project_id": Persistence().projectId, "user_id": Persistence().uuid, "library_name": Persistence().libraryName, "library_version": Persistence().packageVersion, "name": 'revenue', "tab_source": adSrc, "sku_id": skuId, "ad_src": adSrc, "ad_type": ad.placement, "ad_rev": ad.revenue, }); } //////////////////////////// interstitialAd ///////////////////////////// final int _maxExponentialRetryCount = 6; var _interstitialRetryAttempt = 0; ValueNotifier interstitialAdState = ValueNotifier(AdState.initial); void initializeInterstitialAds() { AppLovinMAX.setInterstitialListener( InterstitialListener( onAdLoadedCallback: (ad) { // Interstitial ad is ready to show. AppLovinMAX.isInterstitialReady(_interstitial_ad_unit_ID) now returns 'true'. _log.info('Interstitial ad loaded from ${ad.networkName}'); // Reset retry attempt _interstitialRetryAttempt = 0; interstitialAdState.value = AdState.ready; }, onAdLoadFailedCallback: (adUnitId, error) { // Interstitial ad failed to load. // AppLovin recommends that you retry with exponentially higher delays up to a maximum delay (in this case 64 seconds). _interstitialRetryAttempt = _interstitialRetryAttempt + 1; if (_interstitialRetryAttempt > _maxExponentialRetryCount) return; int retryDelay = pow(2, min(_maxExponentialRetryCount, _interstitialRetryAttempt)).toInt(); if (kDebugMode) { _log.info('Interstitial ad failed to load with code ${error.code} - retrying in ${retryDelay}s'); } Future.delayed(Duration(milliseconds: retryDelay * 1000), () { AppLovinMAX.loadInterstitial(AdHelper.getApplovinInterstitialUnitId(_shouldSkipAds())); }); }, onAdDisplayedCallback: (ad) { _log.info('interstitialAd displayed'); interstitialAdState.value = AdState.showing; // 广告成功展示 }, onAdDisplayFailedCallback: (ad, error) { _log.info('interstitialAd display failed'); interstitialAdState.value = AdState.error; // 广告显示异常 }, onAdClickedCallback: (ad) {}, onAdHiddenCallback: (ad) { _log.info('interstitialAd hidden'); interstitialAdState.value = AdState.dismissed; // 广告关闭 AppLovinMAX.loadInterstitial(AdHelper.getApplovinInterstitialUnitId(_shouldSkipAds())); }, onAdRevenuePaidCallback: (ad) { _log.info('woooooooooo, applovin interstitial paid event: revenue: ${ad.revenue}, precision: ${ad.revenuePrecision}'); if (ad.revenue > 0) { onAdRevenuePaid(ad); // revenue 单位转化为 valueMicro,以便和admod一致 } }, ), ); // Load the first interstitial. Future.delayed(const Duration(seconds: 3), () { loadInterstitialAd(); }); } void loadInterstitialAd() async { if (!_hasInit) return; bool? isInit = await AppLovinMAX.isInitialized(); if (isInit == null || !isInit) { return; } // 🔥 修复:安全解包 bool? isReady = await AppLovinMAX.isInterstitialReady(AdHelper.getApplovinInterstitialUnitId(_shouldSkipAds())); if (isReady == true) { _log.info("applovin interstitial ad already ready, no need to load!"); return; } AppLovinMAX.loadInterstitial(AdHelper.getApplovinInterstitialUnitId(_shouldSkipAds())); } Future isInterstitialAdReady() async { if (!_hasInit) return false; try { bool? isInit = await AppLovinMAX.isInitialized(); if (isInit == null || !isInit) { return false; } // 🔥 修复:安全解包,防止崩溃 bool? isReady = await AppLovinMAX.isInterstitialReady(AdHelper.getApplovinInterstitialUnitId(_shouldSkipAds())); return isReady ?? false; } catch (e) { _log.warning('isInterstitialReady error: $e'); } return false; } String adSrc = ""; String skuId = ""; showInterstitialAd({String adSrc = "", String skuId = ""}) async { this.adSrc = adSrc; this.skuId = skuId; // 🔥 新增:设备保护 if (_shouldSkipAds()) { _log.info('🔥 Device protection: skipping interstitial ad'); return; } try { bool? isInit = await AppLovinMAX.isInitialized(); if (isInit == null || !isInit) { return; } // 🔥 修复:安全解包 bool? isReady = await AppLovinMAX.isInterstitialReady(AdHelper.getApplovinInterstitialUnitId(_shouldSkipAds())); if (isReady == true) { AppLovinMAX.showInterstitial(AdHelper.getApplovinInterstitialUnitId(_shouldSkipAds()), placement: 'inters'); } else { AppLovinMAX.loadInterstitial(AdHelper.getApplovinInterstitialUnitId(_shouldSkipAds())); } } on PlatformException catch (e) { _log.warning('PlatformException: $e'); } catch (e) { _log.warning('showInterstitialAd: $e'); } } //////////////////////////// rewardedAd ///////////////////////////// ValueNotifier rewardedAdState = ValueNotifier(AdState.initial); var _rewardedAdRetryAttempt = 0; void initializeRewardedAd() { return; AppLovinMAX.setRewardedAdListener( RewardedAdListener( onAdLoadedCallback: (ad) { // Rewarded ad is ready to show. AppLovinMAX.isRewardedAdReady(_rewarded_ad_unit_ID) now returns 'true'. _log.info('Rewarded ad loaded from ${ad.networkName}'); // Reset retry attempt _rewardedAdRetryAttempt = 0; rewardedAdState.value = AdState.ready; }, onAdLoadFailedCallback: (adUnitId, error) { // Rewarded ad failed to load. // AppLovin recommends that you retry with exponentially higher delays up to a maximum delay (in this case 64 seconds). _rewardedAdRetryAttempt = _rewardedAdRetryAttempt + 1; if (_rewardedAdRetryAttempt > _maxExponentialRetryCount) return; int retryDelay = pow(2, min(_maxExponentialRetryCount, _rewardedAdRetryAttempt)).toInt(); _log.info('Rewarded ad failed to load with code ${error.code} - retrying in ${retryDelay}s'); Future.delayed(Duration(milliseconds: retryDelay * 1000), () { AppLovinMAX.loadRewardedAd(AdHelper.applovinRewardedAdUnitId); }); }, onAdDisplayedCallback: (ad) { _log.info('rewardedAd displayed'); rewardedAdState.value = AdState.showing; // 广告成功展示 }, onAdDisplayFailedCallback: (ad, error) { _log.info('rewardedAd display failed'); rewardedAdState.value = AdState.error; // 广告显示异常 rewardCallback = null; }, onAdClickedCallback: (ad) {}, onAdHiddenCallback: (ad) { _log.info('rewardedAd hide'); rewardedAdState.value = AdState.dismissed; // 广告关闭 rewardCallback = null; AppLovinMAX.loadRewardedAd(AdHelper.applovinRewardedAdUnitId); }, onAdReceivedRewardCallback: (ad, reward) { _log.info('rewardedAd receive reward: $reward'); rewardCallback?.call(ad, reward); }, onAdRevenuePaidCallback: (ad) { _log.info('woooooooooo, applovin rewarded paid event: revenue: ${ad.revenue} precision: ${ad.revenuePrecision}'); if (ad.revenue > 0) { onAdRevenuePaid(ad); // revenue 单位转化为 valueMicro,以便和admod一致 } }, ), ); // Load the first reward ad. // loadRewardedAd(); Future.delayed(const Duration(seconds: 3), () { loadRewardedAd(); }); } void loadRewardedAd() async { return; if (!_hasInit) return; bool? isInit = await AppLovinMAX.isInitialized(); if (isInit == null || !isInit) { _log.warning("applovin not initialized yet!"); return; } // 🔥 修复:安全解包 bool? isReady = await AppLovinMAX.isRewardedAdReady(AdHelper.applovinRewardedAdUnitId); if (isReady == true) { _log.info("applovin reward ad already ready, no need to load!"); return; } AppLovinMAX.loadRewardedAd(AdHelper.applovinRewardedAdUnitId); } Future isRewardedAdReady() async { if (!_hasInit) return false; try { bool? isInit = await AppLovinMAX.isInitialized(); if (isInit == null || !isInit) { return false; } // 🔥 修复:安全解包 bool? isReady = await AppLovinMAX.isRewardedAdReady(AdHelper.applovinRewardedAdUnitId); return isReady ?? false; } catch (e) { _log.warning('isRewardedAdReady error: $e'); } return false; } Function(MaxAd, MaxReward)? rewardCallback; Future showRewardedlAd({required onUserEarnedReward, String adSrc = "", String skuId = ""}) async { this.adSrc = adSrc; this.skuId = skuId; rewardCallback = null; try { bool? isInit = await AppLovinMAX.isInitialized(); if (isInit == null || !isInit) { return false; } // 🔥 修复:安全解包 bool? isReady = await AppLovinMAX.isRewardedAdReady(AdHelper.applovinRewardedAdUnitId); if (isReady == true) { AppLovinMAX.showRewardedAd(AdHelper.applovinRewardedAdUnitId, placement: 'reward'); rewardCallback = onUserEarnedReward; return true; } else { AppLovinMAX.loadRewardedAd(AdHelper.applovinRewardedAdUnitId); return false; } } on PlatformException catch (e) { _log.warning('PlatformException: $e'); return false; } catch (e) { _log.warning('showRewardedlAd error: $e'); return false; } } }