import 'dart:async'; import 'dart:convert'; import 'dart:ui'; import 'package:connectivity_plus/connectivity_plus.dart'; import 'package:guru_app/account/model/user.dart'; import 'package:guru_app/ads/applovin/banner/applovin_banner_ads.dart'; import 'package:guru_app/ads/core/ads.dart'; import 'package:guru_app/ads/core/ads_config.dart'; import 'package:guru_app/ads/core/ads_impression.dart'; import 'package:guru_app/ads/core/strategy/interstitial/max_strategy_interstitial_ads.dart'; import 'package:guru_app/financial/asset/assets_model.dart'; import 'package:guru_app/financial/asset/assets_store.dart'; import 'package:guru_app/financial/iap/iap_manager.dart'; import 'package:guru_app/financial/iap/iap_model.dart'; import 'package:guru_app/firebase/remoteconfig/remote_config_manager.dart'; import 'package:guru_app/guru_app.dart'; import 'package:guru_utils/lifecycle/lifecycle_manager.dart'; import 'package:guru_app/property/app_property.dart'; import 'package:guru_app/property/property_keys.dart'; import 'package:guru_app/property/settings/guru_settings.dart'; import 'package:guru_applovin_flutter/guru_applovin_flutter.dart'; import 'package:guru_utils/datetime/datetime_utils.dart'; import 'package:guru_utils/extensions/extensions.dart'; import 'package:guru_utils/network/network_utils.dart'; import 'package:guru_utils/tuple/tuple.dart'; import 'package:guru_utils/ads/ads.dart'; import 'applovin/interstitial/applovin_interstitial_ads.dart'; import 'applovin/rewarded/applovin_rewarded_ads.dart'; import 'utils/ads_exception.dart'; import 'package:device_info_plus/device_info_plus.dart'; part 'ads_global_property.dart'; /// Created by Haoyi on 2022/3/2 class AdsManager extends AdsManagerDelegate { static final AdsManager _instance = AdsManager._(); static AdsManager get instance => _instance; AdsManager._(); final Map interstitialAds = {}; final Map rewardsAds = {}; final AdImpressionController adImpressionController = AdImpressionController(); final BehaviorSubject _adsConfigSubject = BehaviorSubject.seeded(AdsConfig.defaultAdsConfig); final BehaviorSubject _adsProfileSubject = BehaviorSubject.seeded(GuruApp.instance.adsProfile); final BehaviorSubject _initializedSubject = BehaviorSubject.seeded(false); final BehaviorSubject noBannerAndInterstitialAdsSubject = BehaviorSubject.seeded(false); final Map adsGlobalProperties = {}; static const Set _reservedKeywords = { "app_version", "lt", "paid", "blv", "os_version", "connection" }; static const List ltSamples = [ 0, 1, 2, 3, 4, 5, 6, 14, 30, 60, 90, 120, 180 ]; @override Stream get observableInitialized => _initializedSubject.stream; ConnectivityResult get connectivityStatus => NetworkUtils.currentConnectivityStatus; Stream get observableConnectivityStatus => NetworkUtils.observableConnectivityStatus; @override Stream get observableNoAds => noBannerAndInterstitialAdsSubject.stream; final CompositeSubscription subscriptions = CompositeSubscription(); AdsProfile get adsProfile => _adsProfileSubject.value; AdsConfig get adsConfig => _adsConfigSubject.value; bool get hasAmazonBannerAds => adsConfig.bannerConfig.amazonEnable; bool get hasAmazonInterstitialAds => adsConfig.interstitialConfig.amazonEnable; bool get hasAmazonAds => hasAmazonBannerAds || hasAmazonInterstitialAds; final BehaviorSubject> keywordsSubject = BehaviorSubject.seeded({}); Stream> get observableKeywords => keywordsSubject.stream; Map get adsKeywords => keywordsSubject.value; String? consentTestDeviceId; int? consentDebugGeography; static final RegExp _nonAlphaNumeric = RegExp('[^a-zA-Z0-9_]'); static final RegExp _alpha = RegExp('[a-zA-Z]'); @override bool get isPurchasedNoAd => noBannerAndInterstitialAdsSubject.value; void setProperty(String key, String value) { adsGlobalProperties[key] = value; } void setNoAds(bool noAds) { noBannerAndInterstitialAdsSubject.addIfChanged(noAds); GuruSettings.instance.isNoAds.set(noAds); GuruAnalytics.instance .setUserProperty("user_type", noAds ? "noads" : "default"); setProperty("user_type", noAds ? "noads" : "default"); } void ensureInitialize() {} void listenIap() { final obs = Rx.combineLatest2, Tuple2>>( IapManager.instance.observableAvailable, IapManager.instance.observableAssetStore, (a, b) => Tuple2(a, b)); subscriptions.add(obs.listen((tuple) { final available = tuple.item1; final purchasedStore = tuple.item2; if (available && purchasedStore.isActive) { final tempIsNoAds = purchasedStore .existsAssets(GuruApp.instance.productProfile.noAdsCapIds); final isNoAds = isPurchasedNoAd; Log.i( "purchased store changed active! tempIsNoAds:$tempIsNoAds isNoAds:$isNoAds", syncFirebase: true); if (isNoAds != tempIsNoAds) { if (!tempIsNoAds) { GuruAnalytics.instance.logException(NoAdsException( "The payment system is abnormal, it shouldn't appear that the purchased item become unpurchased")); } setNoAds(tempIsNoAds); } } })); } static bool initializedSdk = false; Future initialize({SaasUser? saasUser}) async { _adsProfileSubject.addEx(GuruApp.instance.adsProfile); await initEnv(); final connected = await NetworkUtils.isNetworkConnected(); Log.d("adsManager initialize connected:$connected", tag: "Ads"); if (connected) { await initSdk( saasUser: saasUser, onInitialized: () { // loadAds(); adImpressionController.init(); checkAndPreload(); // GuruSettings.instance.totalLevelUp // .observe() // .throttleTime(const Duration(seconds: 1)) // .listen((count) { // checkAndPreload(); // }); Log.i("ADS Initialized", tag: "Ads", syncFirebase: true); }); } else { NetworkUtils.observableConnectivityTrack.listen((track) async { if (track.newResult != ConnectivityResult.none) { if (!initializedSdk) { initializedSdk = true; await initSdk(onInitialized: () { adImpressionController.init(); checkAndPreload(); Log.i("ADS Initialized", tag: "Ads", syncFirebase: true); }); } } if (track.newResult != track.oldResult) { Log.i("connectivity result changed! retry ads!", tag: "Ads"); setKeyword("connection", track.newResult.toString()); if (LifecycleManager.instance.isAppForeground()) { if (track.newResult == ConnectivityResult.none && track.oldResult != ConnectivityResult.none) { Log.i("connectivity changed! retry ads!", tag: "Ads"); retry(); } } } }); } subscriptions.add(RemoteConfigManager.instance.observeConfig().listen((_) { refreshAdsConfig(); }, onError: (error, stacktrace) { Log.i("init config error!", tag: "Ads", error: error, stackTrace: stacktrace); })); listenIap(); } // void initLifecycleConnectivity() { // StreamSubscription? streamSubscription; // LifecycleManager.instance.observableAppLifecycle.listen((foreground) { // if (foreground) { // streamSubscription = Connectivity() // .onConnectivityChanged // .listen((ConnectivityResult result) { // Log.i("Connectivity: $result", tag: "Connectivity"); // if (connectivityStatus == ConnectivityResult.none && // result != ConnectivityResult.none) { // Log.i("connectivity changed! retry ads!", tag: "Ads"); // retry(); // } // final changed = connectivityStatusSubject.addIfChanged(result); // if (changed) { // setKeyword("connection", result.toString()); // } // }); // } else { // streamSubscription?.cancel(); // streamSubscription = null; // } // }); // } void initAdsProfile() { final _hasAmazonBannerAds = hasAmazonBannerAds; final _hasAmazonInterstitialAds = hasAmazonInterstitialAds; final _hasAmazonAds = _hasAmazonBannerAds || _hasAmazonInterstitialAds; final defaultAdsProfile = GuruApp.instance.adsProfile; final strategyInterstitialIds = adsConfig.strategyAdsConfig.interstitialIds; final newAdsProfile = adsProfile.copyWith( amazonAppId: _hasAmazonAds ? defaultAdsProfile.amazonAppId : null, amazonBannerSlotId: _hasAmazonBannerAds ? defaultAdsProfile.amazonBannerSlotId : null, amazonInterstitialSlotId: _hasAmazonInterstitialAds ? defaultAdsProfile.amazonInterstitialSlotId : null, strategyInterstitialIds: strategyInterstitialIds); _adsProfileSubject.addEx(newAdsProfile); } Future initEnv() async { final adsPropertyBundle = await AppProperty.getInstance().loadValuesByTag(PropertyTags.ads); final isNoAds = adsPropertyBundle.getBool(PropertyKeys.isNoAds) ?? false; consentTestDeviceId = adsPropertyBundle.getString(PropertyKeys.admobConsentTestDeviceId); consentDebugGeography = adsPropertyBundle.getInt(PropertyKeys.admobConsentDebugGeography); noBannerAndInterstitialAdsSubject.addIfChanged(isNoAds); GuruAnalytics.instance .setUserProperty("user_type", isNoAds ? "noads" : "default"); setProperty("user_type", isNoAds ? "noads" : "default"); final result = await Connectivity().checkConnectivity().catchError((error) { Log.w("checkConnectivity error! $error"); }); // connectivityStatusSubject.addEx(result); setProperty("connectivityStatus", result.toString()); refreshAdsConfig(); initAdsProfile(); } Future initSdk( {SaasUser? saasUser, required VoidCallback onInitialized, Duration retryPeriod = const Duration(seconds: 15)}) async { final _adsProfile = adsProfile; bool initializeResult = false; if (GuruApp.instance.appSpec.deployment.adsCompliantInitialization && adsConfig.commonAdsConfig.compliantInitialization && Platform.isAndroid) { initializeResult = await GuruApplovinFlutter.instance .gatherConsentAndInitialize( userId: saasUser?.uid, amazonAppId: _adsProfile.amazonAppId?.id, pubmaticStoreUrl: adsProfile.pubmaticAppStoreUrl, testDeviceId: consentTestDeviceId, debugGeography: consentDebugGeography) .catchError((err) => false) ?? false; } else { initializeResult = await GuruApplovinFlutter.instance .initialize( userId: saasUser?.uid, amazonAppId: _adsProfile.amazonAppId?.id, pubmaticStoreUrl: adsProfile.pubmaticAppStoreUrl) .catchError((err) => false) ?? false; } _initializedSubject.addEx(initializeResult); Log.d("MAX sdk initialize result: $initializeResult"); if (initializeResult) { try { await initKeywords(); } catch (error, stacktrace) { Log.e("initKeywords error! $error $stacktrace", tag: "Ads"); } onInitialized.call(); } else { Future.delayed(retryPeriod, () { initSdk(onInitialized: onInitialized, retryPeriod: retryPeriod); }); Log.w("Ads Initialize error! retry", tag: "Ads", syncFirebase: true); } return initializeResult; } void checkAndPreload( {AdsValidator? rewardedValidator, AdsValidator? interstitialValidator}) async { final canPreloadReward = await adsConfig.rewardedConfig.canPreload(validator: rewardedValidator); if (canPreloadReward) { Log.d("preload reward canPreload!"); final reward = await getRewardsAds(); if (reward.loadCount <= 0) { reward.preload(); } } final canPreloadInterstitial = await adsConfig.interstitialConfig .canPreload(validator: interstitialValidator); if (!isPurchasedNoAd && canPreloadInterstitial) { Log.d("preload interstitial canPreload!"); final interstitial = await getInterstitialAds(); if (interstitial is AdsAudit && interstitial.loadCount <= 0) { interstitial.preload(); } } } void retry() async { final canPreloadReward = await adsConfig.rewardedConfig.canPreload(); if (canPreloadReward) { final reward = await getRewardsAds(); reward.retry(); } final canPreload = await adsConfig.interstitialConfig.canPreload(); if (canPreload) { Log.d("preload interstitial canPreload!"); final interstitial = await getInterstitialAds(); interstitial.retry(); } } static int _nearestLt(int low, int high, int lt) { if (low > high) { return -low; } while (low <= high) { final int mid = (low + high) >> 1; if (lt == ltSamples[mid]) { return mid; } else if (lt < ltSamples[mid]) { return _nearestLt(low, mid - 1, lt); } else { return _nearestLt(mid + 1, high, lt); } } return -low; } Future getKeywordLt() async { final latestLtDate = await AppProperty.getInstance().getLatestLtDate(); final dateNum = DateTimeUtils.yyyyMMddUtcNum; int lt = await AppProperty.getInstance().getLtDays(); if (dateNum != latestLtDate) { if (dateNum > latestLtDate) { lt = lt + 1; await AppProperty.getInstance().setLtDays(lt); } await AppProperty.getInstance().setLatestLtDate(dateNum); } final idx = _nearestLt(0, ltSamples.lastIndex, lt) .abs() .clamp(0, ltSamples.lastIndex); Log.d( "getKeywordLt: installTime:$latestLtDate now:$dateNum lt:$lt keywordLt:${ltSamples[idx]}"); return ltSamples[idx]; } Future getOSVersion() async { try { final deviceInfo = DeviceInfoPlugin(); if (Platform.isAndroid) { final info = await deviceInfo.androidInfo; return info.version.release; } else if (Platform.isIOS) { final info = await deviceInfo.iosInfo; return info.systemVersion; } } catch (error, stacktrace) { Log.w("getOSVersion error! $error"); } return "unknown"; } Future getConnection() async { try { final connectivity = await Connectivity().checkConnectivity(); return connectivity.toString(); } catch (error, stacktrace) { Log.w("getConnection error! $error"); } return "unknown"; } Future initKeywords() async { final paidUser = await AppProperty.getInstance().isPaidUser(); final version = Settings.get().version.get(); final lt = await getKeywordLt(); final osVersion = await getOSVersion(); final connection = await getConnection(); final keywords = { "app_version": version, "paid": paidUser ? "true" : "false", "lt": lt.toString(), "os_version": osVersion, "connection": connection }; keywordsSubject.stream.listen((keywords) { if (keywords.isNotEmpty) { Log.i("invoke setKeywords: $keywords", tag: "Ads"); GuruApplovinFlutter.instance.setKeywords(keywords); } }); keywordsSubject.addEx(keywords); } Future restoreKeywords(Map keywords) async { if (GuruSettings.instance.debugMode.get()) { final newKeywords = Map.of(keywords); keywordsSubject.addEx(newKeywords); } } void setKeyword(String key, String value, {bool debugForce = false}) { if (!GuruSettings.instance.debugMode.get() || !debugForce) { if (_reservedKeywords.contains(key)) { Log.w("setKeyword error! the key($key) is reserved and cannot be used!", tag: "Ads"); return; } if (key.isEmpty || key.length > 36 || key.indexOf(_alpha) != 0 || key.contains(_nonAlphaNumeric)) { Log.w( "setKeyword error! the key($key) must contain 1 to 36 alphanumeric characters.", tag: "Ads"); return; } } final newKeywords = Map.of(keywordsSubject.value); newKeywords[key] = value; keywordsSubject.addEx(newKeywords); } void removeKeyword(String key, {bool debugForce = false}) { if (!GuruSettings.instance.debugMode.get() || !debugForce) { if (_reservedKeywords.contains(key)) { Log.w( "removeKeyword error! the key($key) is reserved and cannot be used!", tag: "Ads"); return; } if (key.isEmpty || key.length > 36 || key.indexOf(_alpha) != 0 || key.contains(_nonAlphaNumeric)) { Log.w( "removeKeyword error! the key($key) must contain 1 to 36 alphanumeric characters.", tag: "Ads"); return; } } final newKeywords = Map.of(keywordsSubject.value); newKeywords.remove(key); keywordsSubject.addEx(newKeywords); } Future checkConsentDialogStatus() async { return await GuruApplovinFlutter.instance.checkConsentDialogStatus(); } Future afterAcceptPrivacy(bool consentResult) async { return await GuruApplovinFlutter.instance.afterAcceptPrivacy(consentResult); } bool testParseAdsDefaultConfig() { final iadsConfigString = RemoteConfigReservedConstants.getDefaultConfigString( RemoteConfigReservedConstants.iadsConfig) ?? ""; final radsConfigString = RemoteConfigReservedConstants.getDefaultConfigString( RemoteConfigReservedConstants.radsConfig) ?? ""; final badsConfigString = RemoteConfigReservedConstants.getDefaultConfigString( RemoteConfigReservedConstants.badsConfig) ?? ""; final iosAttConfigString = RemoteConfigReservedConstants.getDefaultConfigString( RemoteConfigReservedConstants.iosAttConfig) ?? ""; try { final adInterstitial = AdInterstitialConfig.fromJson(json.decode(iadsConfigString)); final adBanner = AdBannerConfig.fromJson(json.decode(badsConfigString)); final iosAttConfig = IOSAttConfig.fromJson(json.decode(iosAttConfigString)); Log.d("==== ADS AdsConfig ===="); Log.d(" ---> [INTERSTITIAL]: $iadsConfigString"); Log.d(" ---> [BANNER]: $badsConfigString"); Log.d(" ---> [IOSATT]: $iosAttConfigString"); Log.d("======================="); _adsConfigSubject.addEx(AdsConfig.build( interstitialConfig: adInterstitial, bannerConfig: adBanner, iosAttConfig: iosAttConfig)); return true; } catch (error, stacktrace) { Log.e("refreshAdsConfig error $error $stacktrace"); rethrow; } } bool refreshAdsConfig() { try { final commonAdsConfig = RemoteConfigManager.instance.getCommonAdsConfig(); final adInterstitial = RemoteConfigManager.instance.getIadsConfig(); final adReward = RemoteConfigManager.instance.getRadsConfig(); final adBanner = RemoteConfigManager.instance.getBadsConfig(); final strategyAdsConfig = RemoteConfigManager.instance.getStrategyAdsConfig(); final iosAttConfig = RemoteConfigManager.instance.getIOSAttConfig(); Log.d("==== ADS AdsConfig ====", tag: PropertyTags.ads); Log.d(" ---> [COMMON]: ${commonAdsConfig.toJson()}", tag: PropertyTags.ads); Log.d(" ---> [INTERSTITIAL]: ${adInterstitial.toJson()}", tag: PropertyTags.ads); Log.d(" ---> [REWARD]: ${adReward.toJson()}", tag: PropertyTags.ads); Log.d(" ---> [BANNER]: ${adBanner.toJson()}", tag: PropertyTags.ads); Log.d(" ---> [STRATEGY]: ${strategyAdsConfig.toJson()}", tag: PropertyTags.ads); Log.d(" ---> [IOSATT]: ${iosAttConfig.toJson()}", tag: PropertyTags.ads); Log.d("=======================", tag: PropertyTags.ads); _adsConfigSubject.addEx(AdsConfig.build( commonAdsConfig: commonAdsConfig, interstitialConfig: adInterstitial, rewardedConfig: adReward, bannerConfig: adBanner, strategyAdsConfig: strategyAdsConfig, iosAttConfig: iosAttConfig)); return true; } catch (error, stacktrace) { Log.e("refreshAdsConfig error $error $stacktrace"); rethrow; } } @override Future getInterstitialAds() async { final _adsProfile = adsProfile; final strategyInterstitialIds = adsProfile.strategyInterstitialIds ?? []; Ads? ad; if (strategyInterstitialIds.isNotEmpty) { if (strategyInterstitialIds.length > 1) { ad = interstitialAds[strategyInterstitialIds.first.adUnitId] ??= MaxStrategyInterstitialAds.create(strategyInterstitialIds)..init(); } else { ad = interstitialAds[strategyInterstitialIds.first.adUnitId] ??= ApplovinInterstitialAds.create( strategyInterstitialIds.first.adUnitId, strategyInterstitialIds.first.amazonAdSlotId) ..init(); } } else { ad = interstitialAds[_adsProfile.interstitialId] ??= ApplovinInterstitialAds.create( _adsProfile.interstitialId, _adsProfile.amazonInterstitialSlotId) ..init(); } return ad; } @override Future getRewardsAds() async { final _adsProfile = adsProfile; ApplovinRewardedAds? ad = rewardsAds[_adsProfile.rewardsId]; if (ad == null) { ad = ApplovinRewardedAds.create(_adsProfile.rewardsId, adAmazonSlotId: _adsProfile.amazonRewardedSlotId) ..init(); rewardsAds[_adsProfile.rewardsId] = ad; } return ad; } Future requestGdpr({int? debugGeography, String? testDeviceId}) async { Log.d( "requestGdpr! debugGeography:$debugGeography testDeviceId:$testDeviceId", tag: "Ads"); // adb logcat -s UserMessagingPlatform // Use new ConsentDebugSettings.Builder().addTestDeviceHashedId("xxxx") to set this as a debug device. final result = await GuruApplovinFlutter.instance.requestGdpr( debugGeography: debugGeography, testDeviceId: testDeviceId); final consentResult = await GuruAnalytics.instance.refreshConsents(); Log.d("requestGdpr result:$result consentResult:$consentResult"); return result; } Future resetGdpr() { return GuruApplovinFlutter.instance.resetGdpr(); } Future updateOrientation(int orientation) async { final result = await GuruApplovinFlutter.instance.updateOrientation(orientation); return result == true; } @override Future createBannerAds( {String? scene, AdsLifecycleObserver? observer}) async { final _adsProfile = adsProfile; return ApplovinBannerAds.create( _adsProfile.bannerId, _adsProfile.amazonBannerSlotId, scene: scene, observer: observer); } AdCause canShowInterstitial(String scene) { if (isPurchasedNoAd) { return AdCause.noAds; } final hiddenAt = AdsManager.instance.latestFullscreenAdsHiddenTimestamps; final now = DateTimeUtils.currentTimeInMillis(); final impGapInMillis = AdsManager.instance.adsConfig.interstitialConfig .getSceneImpGapInSeconds(scene) * 1000; Log.d( "canShowInterstitial($scene): now:$now latestFullscreenAdsHiddenTimestamps:$latestFullscreenAdsHiddenTimestamps hiddenAt:$hiddenAt impGapInMillis:$impGapInMillis", tag: "Ads"); if ((now - hiddenAt) < impGapInMillis) { Log.d("show ads too frequency", syncFirebase: true); return AdCause.tooFrequent; } return AdCause.success; } @override Future validateInterstitial(String? scene, {AdsValidator? validator}) { final interstitialConfig = adsConfig.interstitialConfig; return interstitialConfig.check(scene ?? "", validator: validator); } @override Future validateRewards(String? scene, {AdsValidator? validator}) { final rewardedConfig = adsConfig.rewardedConfig; return rewardedConfig.check(scene ?? "", validator: validator); } @override Future validateBanner(String? scene, {AdsValidator? validator}) { final rewardedConfig = adsConfig.bannerConfig; return rewardedConfig.check(scene ?? "", validator: validator); } @override dynamic getConfig(String type) { switch (type) { case "bannerAutoDisposeInterval": return adsConfig.bannerConfig.autoDisposeIntervalInMinutes; case "allowInterstitialAsAlternativeReward": return GuruApp .instance.appSpec.deployment.allowInterstitialAsAlternativeReward; case "showInternalAdsWhenBannerUnavailable": return GuruApp .instance.appSpec.deployment.showInternalAdsWhenBannerUnavailable; } } }