applovin_ads_controller.dart 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487
  1. // Copyright 2022, the Flutter project authors. Please see the AUTHORS file
  2. // for details. All rights reserved. Use of this source code is governed by a
  3. // BSD-style license that can be found in the LICENSE file.
  4. import 'dart:async';
  5. import 'dart:io';
  6. import 'dart:math';
  7. import 'package:applovin_max/applovin_max.dart';
  8. import 'package:device_info_plus/device_info_plus.dart';
  9. import 'package:flutter/foundation.dart';
  10. import 'package:flutter/material.dart';
  11. import 'package:flutter/services.dart';
  12. import 'package:logging/logging.dart';
  13. import 'package:puzzleweave/ads/ad_helper.dart';
  14. import 'package:puzzleweave/firebase/adjust_helper.dart';
  15. import 'package:puzzleweave/firebase/firebase_helper.dart';
  16. import '../persistence/persistence.dart';
  17. import '../remote_config/remote_config.dart';
  18. import '../statistics/statistics.dart';
  19. final Logger _log = Logger('ApplovinAdsController');
  20. const int bannerReportDuration = 3 * 60 * 1000; // banner paid 3分钟报一次
  21. /// Allows showing ads. A facade for `package:google_mobile_ads`.
  22. class ApplovinAdsController {
  23. final BuildContext context;
  24. // 新增:设备保护标记
  25. bool _isLowRamDevice = false;
  26. bool _isProblematicDevice = false;
  27. bool get isLowRamDevice => _isLowRamDevice;
  28. // 🔥 关键:设备黑名单检测
  29. static final Set<String> _crashProneDevices = {'hisense', 'itel', 'huawei y9' /*'samsung galaxy a50s'*/};
  30. bool _shouldSkipAds() {
  31. return _isLowRamDevice || _isProblematicDevice;
  32. }
  33. late double bannerPaidValueMicros; // banner广告累计收益
  34. late int lastBannerPaidReportTimestamp; // 上次banner广告收益上报时间戳,用于控制banner广告收益上报频率
  35. late double allPaidValueMicros; // 所有广告累计收益
  36. late double revenueThreshold; // 广告累计上报阈值
  37. bool _hasInit = false;
  38. bool get hasInit => _hasInit;
  39. final Completer<bool> completer = Completer();
  40. ApplovinAdsController(this.context);
  41. void dispose() {
  42. // 🔥 关键修复:防止内存泄漏
  43. if (!completer.isCompleted) {
  44. completer.complete(false);
  45. }
  46. // 清理状态通知器
  47. interstitialAdState.dispose();
  48. rewardedAdState.dispose();
  49. // 清理回调
  50. rewardCallback = null;
  51. }
  52. /// Initializes the injected [MobileAds.instance].
  53. Future<void> initialize() async {
  54. if (_hasInit) return;
  55. _log.info('AppLovinMAX.initialize...');
  56. // 1. 设备兼容性检测(关键:防止外部纹理崩溃)
  57. if (Platform.isAndroid) {
  58. final deviceInfo = DeviceInfoPlugin();
  59. final androidInfo = await deviceInfo.androidInfo;
  60. // 低端机检测
  61. _isLowRamDevice = androidInfo.isLowRamDevice || (androidInfo.systemFeatures.contains('android.hardware.ram.low'));
  62. // 问题设备检测
  63. String manufacturer = androidInfo.manufacturer.toLowerCase();
  64. String model = androidInfo.model.toLowerCase();
  65. _isProblematicDevice = _crashProneDevices.any((device) => manufacturer.contains(device) || model.contains(device));
  66. if (_isProblematicDevice) {
  67. _log.warning('🔥 Problematic device detected: $manufacturer $model');
  68. }
  69. }
  70. // 用于模拟测试欧洲UMP是否正常,release版本注意注释掉
  71. // AppLovinMAX.setVerboseLogging(true);
  72. // AppLovinMAX.setConsentFlowDebugUserGeography(ConsentFlowUserGeography.gdpr);
  73. // 2. 开启合规流开关
  74. AppLovinMAX.setTermsAndPrivacyPolicyFlowEnabled(true);
  75. // 3. 设置你的隐私政策和用户协议链接(必须是有效的 URL)
  76. AppLovinMAX.setPrivacyPolicyUrl("https://longreachai.net/game/privacy_policy.html");
  77. AppLovinMAX.setTermsOfServiceUrl("https://longreachai.net/game/terms_of_service.html");
  78. // 4. 然后再初始化 SDK
  79. MaxConfiguration? sdkConfiguration = await AppLovinMAX.initialize(AdHelper.applovinSdkKey);
  80. _log.info('AppLovinMAX.initialize success!');
  81. lastBannerPaidReportTimestamp = Persistence().lastBannerPaidReportTimestamp;
  82. bannerPaidValueMicros = Persistence().bannerPaidValueMicros;
  83. allPaidValueMicros = Persistence().allPaidValueMicros;
  84. revenueThreshold = RemoteConfig().adRevenueThreshold;
  85. if (revenueThreshold <= 0.0) revenueThreshold = 0.01;
  86. // 5. 分步异步加载 (防止 FD 句柄数瞬间爆表)
  87. await Future.delayed(const Duration(milliseconds: 500));
  88. if (!_shouldSkipAds()) initializeBannerAds();
  89. await Future.delayed(const Duration(milliseconds: 500));
  90. if (!_shouldSkipAds()) initializeInterstitialAds();
  91. await Future.delayed(const Duration(milliseconds: 500));
  92. if (!_shouldSkipAds()) initializeRewardedAd();
  93. completer.complete(true);
  94. _hasInit = true;
  95. }
  96. void initializeBannerAds() {
  97. // 强制关闭低端机的自适应 Banner,因为其渲染开销极大
  98. AppLovinMAX.setBannerExtraParameter(AdHelper.applovinBannerAdUnitId, "adaptive_banner", _isLowRamDevice ? "false" : "true");
  99. }
  100. /// 强制销毁 Banner(解决 JNI CheckException 的大杀器)
  101. /// 在 AdsState dispose 或页面跳转前调用
  102. void destroyBanner() {
  103. _log.info("🔥 Explicitly destroying banner to release GPU buffers");
  104. try {
  105. // 强制通知原生层销毁 Banner 视图
  106. AppLovinMAX.destroyBanner(AdHelper.applovinBannerAdUnitId);
  107. } catch (e) {
  108. _log.warning("Destroy banner error: $e");
  109. }
  110. }
  111. Widget getBannerWidget(String positionKey) {
  112. // 增加:问题设备或未初始化,直接不渲染 Widget
  113. if (!_hasInit || _shouldSkipAds()) {
  114. return const SizedBox.shrink();
  115. }
  116. return MaxAdView(
  117. key: ValueKey(positionKey), // 只要 positionKey 不变,页面内刷新就不会重建
  118. adUnitId: AdHelper.applovinBannerAdUnitId,
  119. adFormat: AdFormat.banner,
  120. placement: 'banner',
  121. extraParameters: {'adaptive_banner': _isLowRamDevice ? 'false' : 'true'},
  122. listener: AdViewAdListener(
  123. onAdLoadedCallback: (ad) {
  124. // // 广告加载成功后, ad.size 包含实际的宽度和高度 (dp)
  125. // double? widthDp = ad.size?.width;
  126. // double? heightDp = ad.size?.height;
  127. // if (heightDp != null) {
  128. // context.read<Device>().bannerHeight = heightDp;
  129. // }
  130. // _log.info('banner广告 width = $widthDp, height = $heightDp');
  131. _log.info(() => 'applovin banner Ad loaded: ${ad.hashCode}');
  132. },
  133. onAdLoadFailedCallback: (adUnitId, error) {
  134. _log.warning('applovin banner Ad failedToLoad: $error');
  135. },
  136. onAdClickedCallback: (ad) {
  137. _log.info('applovin banner Ad click registered');
  138. },
  139. onAdExpandedCallback: (ad) {
  140. _log.info('applovin banner Ad expanded');
  141. },
  142. onAdCollapsedCallback: (ad) {
  143. _log.info('applovin banner Ad collaspsed');
  144. },
  145. onAdRevenuePaidCallback: (ad) {
  146. _log.info('woooooooooo, applovin banner paid event: revenue: ${ad.revenue} precision: ${ad.revenuePrecision}');
  147. if (ad.revenue > 0) {
  148. onAdRevenuePaid(ad); // revenue 单位转化为 valueMicro,以便和admod一致
  149. }
  150. },
  151. ),
  152. );
  153. }
  154. // revenue 回调处理,所有广告类型统一在此处理
  155. onAdRevenuePaid(MaxAd ad) async {
  156. _log.info('report ad revenue paid: ${ad.revenue}');
  157. Map<String, Object> params = {
  158. "ad_platform": 'appLovin',
  159. "ad_source": ad.networkName,
  160. "ad_format": ad.placement,
  161. "ad_unit_name": ad.adUnitId,
  162. "value": ad.revenue,
  163. "currency": "USD",
  164. };
  165. // for ARO : ad_impression 上报给firebase
  166. FirebaseHelper.logEvent("ad_impression", params);
  167. // for Taichi
  168. FirebaseHelper.logEvent("Ad_Impression_Revenue", params);
  169. // 累计超过0.01 USD 上报 Total_Ads_Revenue_001
  170. double previousTroasCache = Persistence().tRoasCache;
  171. double currentTroasCache = previousTroasCache + ad.revenue;
  172. if (currentTroasCache >= revenueThreshold) {
  173. FirebaseHelper.logEvent("Total_Ads_Revenue_001", {"value": currentTroasCache, "currency": "USD"});
  174. Persistence().tRoasCache = 0; // 缓存清零
  175. } else {
  176. Persistence().tRoasCache = currentTroasCache;
  177. }
  178. // adRevenue 事件上报给adjust
  179. AdjustHelper.trackAdRevenue(ad.placement, 1, ad.revenue, 'USD');
  180. // revenue 埋点时间上报给自主BI平台
  181. Statistics.postEvent({
  182. "project_id": Persistence().projectId,
  183. "user_id": Persistence().uuid,
  184. "library_name": Persistence().libraryName,
  185. "library_version": Persistence().packageVersion,
  186. "name": 'revenue',
  187. "tab_source": adSrc,
  188. "sku_id": skuId,
  189. "ad_src": adSrc,
  190. "ad_type": ad.placement,
  191. "ad_rev": ad.revenue,
  192. });
  193. }
  194. //////////////////////////// interstitialAd /////////////////////////////
  195. final int _maxExponentialRetryCount = 6;
  196. var _interstitialRetryAttempt = 0;
  197. ValueNotifier<AdState> interstitialAdState = ValueNotifier(AdState.initial);
  198. void initializeInterstitialAds() {
  199. AppLovinMAX.setInterstitialListener(
  200. InterstitialListener(
  201. onAdLoadedCallback: (ad) {
  202. // Interstitial ad is ready to show. AppLovinMAX.isInterstitialReady(_interstitial_ad_unit_ID) now returns 'true'.
  203. _log.info('Interstitial ad loaded from ${ad.networkName}');
  204. // Reset retry attempt
  205. _interstitialRetryAttempt = 0;
  206. interstitialAdState.value = AdState.ready;
  207. },
  208. onAdLoadFailedCallback: (adUnitId, error) {
  209. // Interstitial ad failed to load.
  210. // AppLovin recommends that you retry with exponentially higher delays up to a maximum delay (in this case 64 seconds).
  211. _interstitialRetryAttempt = _interstitialRetryAttempt + 1;
  212. if (_interstitialRetryAttempt > _maxExponentialRetryCount) return;
  213. int retryDelay = pow(2, min(_maxExponentialRetryCount, _interstitialRetryAttempt)).toInt();
  214. if (kDebugMode) {
  215. _log.info('Interstitial ad failed to load with code ${error.code} - retrying in ${retryDelay}s');
  216. }
  217. Future.delayed(Duration(milliseconds: retryDelay * 1000), () {
  218. AppLovinMAX.loadInterstitial(AdHelper.applovinInterstitialAdUnitId);
  219. });
  220. },
  221. onAdDisplayedCallback: (ad) {
  222. _log.info('interstitialAd displayed');
  223. interstitialAdState.value = AdState.showing; // 广告成功展示
  224. },
  225. onAdDisplayFailedCallback: (ad, error) {
  226. _log.info('interstitialAd display failed');
  227. interstitialAdState.value = AdState.error; // 广告显示异常
  228. },
  229. onAdClickedCallback: (ad) {},
  230. onAdHiddenCallback: (ad) {
  231. _log.info('interstitialAd hidden');
  232. interstitialAdState.value = AdState.dismissed; // 广告关闭
  233. AppLovinMAX.loadInterstitial(AdHelper.applovinInterstitialAdUnitId);
  234. },
  235. onAdRevenuePaidCallback: (ad) {
  236. _log.info('woooooooooo, applovin interstitial paid event: revenue: ${ad.revenue}, precision: ${ad.revenuePrecision}');
  237. if (ad.revenue > 0) {
  238. onAdRevenuePaid(ad); // revenue 单位转化为 valueMicro,以便和admod一致
  239. }
  240. },
  241. ),
  242. );
  243. // Load the first interstitial.
  244. Future.delayed(const Duration(seconds: 3), () {
  245. loadInterstitialAd();
  246. });
  247. }
  248. void loadInterstitialAd() async {
  249. if (!_hasInit) return;
  250. bool? isInit = await AppLovinMAX.isInitialized();
  251. if (isInit == null || !isInit) {
  252. return;
  253. }
  254. // 🔥 修复:安全解包
  255. bool? isReady = await AppLovinMAX.isInterstitialReady(AdHelper.applovinInterstitialAdUnitId);
  256. if (isReady == true) {
  257. _log.info("applovin interstitial ad already ready, no need to load!");
  258. return;
  259. }
  260. AppLovinMAX.loadInterstitial(AdHelper.applovinInterstitialAdUnitId);
  261. }
  262. Future<bool> isInterstitialAdReady() async {
  263. if (!_hasInit) return false;
  264. try {
  265. bool? isInit = await AppLovinMAX.isInitialized();
  266. if (isInit == null || !isInit) {
  267. return false;
  268. }
  269. // 🔥 修复:安全解包,防止崩溃
  270. bool? isReady = await AppLovinMAX.isInterstitialReady(AdHelper.applovinInterstitialAdUnitId);
  271. return isReady ?? false;
  272. } catch (e) {
  273. _log.warning('isInterstitialReady error: $e');
  274. }
  275. return false;
  276. }
  277. String adSrc = "";
  278. String skuId = "";
  279. showInterstitialAd({String adSrc = "", String skuId = ""}) async {
  280. this.adSrc = adSrc;
  281. this.skuId = skuId;
  282. // 🔥 新增:设备保护
  283. if (_shouldSkipAds()) {
  284. _log.info('🔥 Device protection: skipping interstitial ad');
  285. return;
  286. }
  287. try {
  288. bool? isInit = await AppLovinMAX.isInitialized();
  289. if (isInit == null || !isInit) {
  290. return;
  291. }
  292. // 🔥 修复:安全解包
  293. bool? isReady = await AppLovinMAX.isInterstitialReady(AdHelper.applovinInterstitialAdUnitId);
  294. if (isReady == true) {
  295. AppLovinMAX.showInterstitial(AdHelper.applovinInterstitialAdUnitId, placement: 'inters');
  296. } else {
  297. AppLovinMAX.loadInterstitial(AdHelper.applovinInterstitialAdUnitId);
  298. }
  299. } on PlatformException catch (e) {
  300. _log.warning('PlatformException: $e');
  301. } catch (e) {
  302. _log.warning('showInterstitialAd: $e');
  303. }
  304. }
  305. //////////////////////////// rewardedAd /////////////////////////////
  306. ValueNotifier<AdState> rewardedAdState = ValueNotifier(AdState.initial);
  307. var _rewardedAdRetryAttempt = 0;
  308. void initializeRewardedAd() {
  309. AppLovinMAX.setRewardedAdListener(
  310. RewardedAdListener(
  311. onAdLoadedCallback: (ad) {
  312. // Rewarded ad is ready to show. AppLovinMAX.isRewardedAdReady(_rewarded_ad_unit_ID) now returns 'true'.
  313. _log.info('Rewarded ad loaded from ${ad.networkName}');
  314. // Reset retry attempt
  315. _rewardedAdRetryAttempt = 0;
  316. rewardedAdState.value = AdState.ready;
  317. },
  318. onAdLoadFailedCallback: (adUnitId, error) {
  319. // Rewarded ad failed to load.
  320. // AppLovin recommends that you retry with exponentially higher delays up to a maximum delay (in this case 64 seconds).
  321. _rewardedAdRetryAttempt = _rewardedAdRetryAttempt + 1;
  322. if (_rewardedAdRetryAttempt > _maxExponentialRetryCount) return;
  323. int retryDelay = pow(2, min(_maxExponentialRetryCount, _rewardedAdRetryAttempt)).toInt();
  324. _log.info('Rewarded ad failed to load with code ${error.code} - retrying in ${retryDelay}s');
  325. Future.delayed(Duration(milliseconds: retryDelay * 1000), () {
  326. AppLovinMAX.loadRewardedAd(AdHelper.applovinRewardedAdUnitId);
  327. });
  328. },
  329. onAdDisplayedCallback: (ad) {
  330. _log.info('rewardedAd displayed');
  331. rewardedAdState.value = AdState.showing; // 广告成功展示
  332. },
  333. onAdDisplayFailedCallback: (ad, error) {
  334. _log.info('rewardedAd display failed');
  335. rewardedAdState.value = AdState.error; // 广告显示异常
  336. rewardCallback = null;
  337. },
  338. onAdClickedCallback: (ad) {},
  339. onAdHiddenCallback: (ad) {
  340. _log.info('rewardedAd hide');
  341. rewardedAdState.value = AdState.dismissed; // 广告关闭
  342. rewardCallback = null;
  343. AppLovinMAX.loadRewardedAd(AdHelper.applovinRewardedAdUnitId);
  344. },
  345. onAdReceivedRewardCallback: (ad, reward) {
  346. _log.info('rewardedAd receive reward: $reward');
  347. rewardCallback?.call(ad, reward);
  348. },
  349. onAdRevenuePaidCallback: (ad) {
  350. _log.info('woooooooooo, applovin rewarded paid event: revenue: ${ad.revenue} precision: ${ad.revenuePrecision}');
  351. if (ad.revenue > 0) {
  352. onAdRevenuePaid(ad); // revenue 单位转化为 valueMicro,以便和admod一致
  353. }
  354. },
  355. ),
  356. );
  357. // Load the first reward ad.
  358. // loadRewardedAd();
  359. Future.delayed(const Duration(seconds: 3), () {
  360. loadRewardedAd();
  361. });
  362. }
  363. void loadRewardedAd() async {
  364. if (!_hasInit) return;
  365. bool? isInit = await AppLovinMAX.isInitialized();
  366. if (isInit == null || !isInit) {
  367. _log.warning("applovin not initialized yet!");
  368. return;
  369. }
  370. // 🔥 修复:安全解包
  371. bool? isReady = await AppLovinMAX.isRewardedAdReady(AdHelper.applovinRewardedAdUnitId);
  372. if (isReady == true) {
  373. _log.info("applovin reward ad already ready, no need to load!");
  374. return;
  375. }
  376. AppLovinMAX.loadRewardedAd(AdHelper.applovinRewardedAdUnitId);
  377. }
  378. Future<bool> isRewardedAdReady() async {
  379. if (!_hasInit) return false;
  380. try {
  381. bool? isInit = await AppLovinMAX.isInitialized();
  382. if (isInit == null || !isInit) {
  383. return false;
  384. }
  385. // 🔥 修复:安全解包
  386. bool? isReady = await AppLovinMAX.isRewardedAdReady(AdHelper.applovinRewardedAdUnitId);
  387. return isReady ?? false;
  388. } catch (e) {
  389. _log.warning('isRewardedAdReady error: $e');
  390. }
  391. return false;
  392. }
  393. Function(MaxAd, MaxReward)? rewardCallback;
  394. Future<bool> showRewardedlAd({required onUserEarnedReward, String adSrc = "", String skuId = ""}) async {
  395. this.adSrc = adSrc;
  396. this.skuId = skuId;
  397. rewardCallback = null;
  398. try {
  399. bool? isInit = await AppLovinMAX.isInitialized();
  400. if (isInit == null || !isInit) {
  401. return false;
  402. }
  403. // 🔥 修复:安全解包
  404. bool? isReady = await AppLovinMAX.isRewardedAdReady(AdHelper.applovinRewardedAdUnitId);
  405. if (isReady == true) {
  406. AppLovinMAX.showRewardedAd(AdHelper.applovinRewardedAdUnitId, placement: 'reward');
  407. rewardCallback = onUserEarnedReward;
  408. return true;
  409. } else {
  410. AppLovinMAX.loadRewardedAd(AdHelper.applovinRewardedAdUnitId);
  411. return false;
  412. }
  413. } on PlatformException catch (e) {
  414. _log.warning('PlatformException: $e');
  415. return false;
  416. } catch (e) {
  417. _log.warning('showRewardedlAd error: $e');
  418. return false;
  419. }
  420. }
  421. }