483 lines
14 KiB
Dart
483 lines
14 KiB
Dart
import 'dart:io' show Platform;
|
||
import 'package:flutter/services.dart';
|
||
import 'package:unity_levelplay_mediation/unity_levelplay_mediation.dart';
|
||
|
||
const String TAG = 'InterstitialAdManager';
|
||
|
||
|
||
const String APP_KEY_IOS = '24e816d5d';
|
||
|
||
|
||
enum InterstitialAdType {
|
||
first,
|
||
second,
|
||
third;
|
||
|
||
String get displayName {
|
||
switch (this) {
|
||
case InterstitialAdType.first:
|
||
return 'First Interstitial';
|
||
case InterstitialAdType.second:
|
||
return 'Second Interstitial';
|
||
case InterstitialAdType.third:
|
||
return 'Third Interstitial';
|
||
}
|
||
}
|
||
|
||
/// 获取广告的 adUnitId(广告位 ID)
|
||
String get adUnitId {
|
||
switch (this) {
|
||
case InterstitialAdType.first:
|
||
return 'ipgwvc8d6vg3bbf6';
|
||
case InterstitialAdType.second:
|
||
return '5b53pus0enyp87xw';
|
||
case InterstitialAdType.third:
|
||
return 'mnhvu9lj9vrdb4fz';
|
||
}
|
||
}
|
||
|
||
/// 获取广告的 placement 名称(返回广告 ID)
|
||
String get placementName => adUnitId;
|
||
}
|
||
|
||
/// Interstitial 广告管理器(单例)
|
||
class InterstitialAdManager with LevelPlayInitListener {
|
||
// 单例实例
|
||
static final InterstitialAdManager _instance =
|
||
InterstitialAdManager._internal();
|
||
|
||
// 工厂构造函数返回单例
|
||
factory InterstitialAdManager() {
|
||
return _instance;
|
||
}
|
||
|
||
// 私有构造函数
|
||
InterstitialAdManager._internal();
|
||
|
||
// 三个 Interstitial 广告实例
|
||
final Map<InterstitialAdType, LevelPlayInterstitialAd> _interstitialAds = {};
|
||
|
||
// adId 到 adType 的映射,用于在监听器中识别广告
|
||
final Map<String, InterstitialAdType> _adIdToTypeMap = {};
|
||
|
||
// 初始化状态
|
||
bool _isInitialized = false;
|
||
bool _isInitializing = false;
|
||
|
||
// 当前正在展示的广告(避免同时展示多个)
|
||
InterstitialAdType? _currentShowingAd;
|
||
|
||
// 广告完成回调映射
|
||
final Map<InterstitialAdType, VoidCallback?> _adCompletedCallbacks = {};
|
||
|
||
// --- 开屏广告专用回调和状态 ---
|
||
Function(InterstitialAdType adType)? _onSplashAdReadyCallback;
|
||
Function()? _onSplashAdAllFailedCallback;
|
||
Set<InterstitialAdType> _splashAdsToLoad = {};
|
||
int _splashAdsFailedCount = 0;
|
||
bool _isHandlingSplashAd = false;
|
||
|
||
/// 获取单例实例
|
||
static InterstitialAdManager get instance => _instance;
|
||
|
||
/// 获取广告实例
|
||
LevelPlayInterstitialAd? getAd(InterstitialAdType adType) {
|
||
return _interstitialAds[adType];
|
||
}
|
||
|
||
/// 初始化 LevelPlay SDK 和广告实例
|
||
Future<void> initialize() async {
|
||
if (_isInitialized || _isInitializing) {
|
||
logMethodName(
|
||
'Initialize',
|
||
'Info',
|
||
'Already initialized or initializing',
|
||
);
|
||
return;
|
||
}
|
||
|
||
_isInitializing = true;
|
||
|
||
try {
|
||
// 检查平台,只支持 iOS
|
||
if (!Platform.isIOS) {
|
||
logMethodName(
|
||
'Platform Check',
|
||
'Error',
|
||
'This app only supports iOS platform',
|
||
);
|
||
_isInitializing = false;
|
||
return;
|
||
}
|
||
|
||
// 初始化三个 Interstitial 广告实例,每个使用不同的 adUnitId
|
||
for (var adType in InterstitialAdType.values) {
|
||
final ad = LevelPlayInterstitialAd(adUnitId: adType.adUnitId);
|
||
_interstitialAds[adType] = ad;
|
||
ad.setListener(_InterstitialAdListenerWrapper(adType, this));
|
||
}
|
||
|
||
// 启用调试模式
|
||
// await enableDebug();
|
||
|
||
// 初始化 LevelPlay SDK
|
||
final initRequest = LevelPlayInitRequest.builder(APP_KEY_IOS).build();
|
||
await LevelPlay.init(initRequest: initRequest, initListener: this);
|
||
} on PlatformException catch (e) {
|
||
_isInitializing = false;
|
||
logMethodName('Initialize', 'PlatformException', e);
|
||
} catch (e) {
|
||
_isInitializing = false;
|
||
logMethodName('Initialize', 'Error', e);
|
||
}
|
||
}
|
||
|
||
/// iOS14 IDFA 访问权限检查
|
||
Future<void> checkATT() async {
|
||
try {
|
||
final currentStatus =
|
||
await ATTrackingManager.getTrackingAuthorizationStatus();
|
||
logMethodName(
|
||
'getTrackingAuthorizationStatus',
|
||
'ATTStatus:',
|
||
currentStatus,
|
||
);
|
||
if (currentStatus == ATTStatus.NotDetermined) {
|
||
final returnedStatus =
|
||
await ATTrackingManager.requestTrackingAuthorization();
|
||
logMethodName(
|
||
'requestTrackingAuthorizationATTStatus',
|
||
'ATTStatus returned:',
|
||
returnedStatus,
|
||
);
|
||
}
|
||
} catch (e) {
|
||
logMethodName('checkATT', 'Error', e);
|
||
}
|
||
}
|
||
|
||
/// 启用调试模式
|
||
Future<void> enableDebug() async {
|
||
try {
|
||
await LevelPlay.setAdaptersDebug(true);
|
||
LevelPlay.validateIntegration();
|
||
} catch (e) {
|
||
logMethodName('enableDebug', 'Error', e);
|
||
}
|
||
}
|
||
|
||
/// 加载开屏广告的专用方法 - 并行加载三个广告,任意一个成功就立即回调并跳转
|
||
/// 如果所有广告都失败,则调用 onAllAdsFailed
|
||
void loadInitialSplashAd({
|
||
required Function(InterstitialAdType adType) onAdReady,
|
||
required Function() onAllAdsFailed,
|
||
}) {
|
||
if (!_isInitialized) {
|
||
logMethodName(
|
||
'LoadInitialSplashAd',
|
||
'Warning',
|
||
'Manager not initialized yet. Call initialize() first.',
|
||
);
|
||
onAllAdsFailed();
|
||
return;
|
||
}
|
||
|
||
_onSplashAdReadyCallback = onAdReady;
|
||
_onSplashAdAllFailedCallback = onAllAdsFailed;
|
||
_splashAdsFailedCount = 0;
|
||
_isHandlingSplashAd = true;
|
||
|
||
_splashAdsToLoad = {
|
||
InterstitialAdType.first,
|
||
InterstitialAdType.second,
|
||
InterstitialAdType.third,
|
||
};
|
||
|
||
logMethodName('LoadInitialSplashAd', 'Info', '开始并行加载初始开屏广告...');
|
||
for (final adType in _splashAdsToLoad) {
|
||
loadAd(adType);
|
||
}
|
||
}
|
||
|
||
/// 清除开屏广告回调的私有方法
|
||
void _clearSplashCallbacks() {
|
||
_onSplashAdReadyCallback = null;
|
||
_onSplashAdAllFailedCallback = null;
|
||
_splashAdsToLoad.clear();
|
||
_splashAdsFailedCount = 0;
|
||
_isHandlingSplashAd = false;
|
||
}
|
||
|
||
/// 加载指定类型的广告
|
||
Future<void> loadAd(InterstitialAdType adType) async {
|
||
if (!_isInitialized) {
|
||
logMethodName(
|
||
'LoadAd',
|
||
'Warning',
|
||
'Manager not initialized yet. Call initialize() first.',
|
||
);
|
||
return;
|
||
}
|
||
|
||
final ad = _interstitialAds[adType];
|
||
if (ad != null) {
|
||
try {
|
||
await ad.loadAd();
|
||
// 加载后更新 adId 映射
|
||
if (ad.adId.isNotEmpty) {
|
||
_adIdToTypeMap[ad.adId] = adType;
|
||
}
|
||
logMethodName('LoadAd', 'Success', 'Loading ${adType.displayName}');
|
||
} catch (e) {
|
||
logMethodName(
|
||
'LoadAd',
|
||
'Error',
|
||
'Failed to load ${adType.displayName}: $e',
|
||
);
|
||
}
|
||
} else {
|
||
logMethodName(
|
||
'LoadAd',
|
||
'Error',
|
||
'Ad instance not found for ${adType.displayName}',
|
||
);
|
||
}
|
||
}
|
||
|
||
/// 显示指定类型的广告
|
||
/// [onAdCompleted] 广告完成回调(广告关闭或展示失败时调用)
|
||
Future<void> showAd(
|
||
InterstitialAdType adType, {
|
||
VoidCallback? onAdCompleted,
|
||
}) async {
|
||
if (!_isInitialized) {
|
||
logMethodName(
|
||
'ShowAd',
|
||
'Warning',
|
||
'Manager not initialized yet. Call initialize() first.',
|
||
);
|
||
return;
|
||
}
|
||
|
||
// 如果已有广告在展示,直接忽略新的展示请求,避免串台
|
||
if (_currentShowingAd != null) {
|
||
logMethodName(
|
||
'ShowAd',
|
||
'Info',
|
||
'Another interstitial is showing (${_currentShowingAd!.displayName}), skip ${adType.displayName}',
|
||
);
|
||
return;
|
||
}
|
||
|
||
final ad = _interstitialAds[adType];
|
||
if (ad != null) {
|
||
try {
|
||
final isReady = await ad.isAdReady();
|
||
if (isReady) {
|
||
_currentShowingAd = adType;
|
||
// 保存完成回调
|
||
_adCompletedCallbacks[adType] = onAdCompleted;
|
||
await ad.showAd(placementName: adType.placementName);
|
||
logMethodName('ShowAd', 'Success', 'Showing ${adType.displayName}');
|
||
} else {
|
||
logMethodName(
|
||
'ShowAd',
|
||
'Warning',
|
||
'${adType.displayName} is not ready yet',
|
||
);
|
||
// 如果未就绪,尝试重新加载
|
||
await loadAd(adType);
|
||
// 广告未就绪时也调用完成回调
|
||
onAdCompleted?.call();
|
||
}
|
||
} catch (e) {
|
||
logMethodName(
|
||
'ShowAd',
|
||
'Error',
|
||
'Failed to show ${adType.displayName}: $e',
|
||
);
|
||
// 避免异常导致状态悬挂
|
||
if (_currentShowingAd == adType) {
|
||
_currentShowingAd = null;
|
||
}
|
||
// 异常时调用完成回调
|
||
onAdCompleted?.call();
|
||
}
|
||
} else {
|
||
logMethodName(
|
||
'ShowAd',
|
||
'Error',
|
||
'Ad instance not found for ${adType.displayName}, try to load it.',
|
||
);
|
||
// 理论上初始化已创建实例;若缺失则尝试加载
|
||
await loadAd(adType);
|
||
// 广告实例不存在时也调用完成回调
|
||
onAdCompleted?.call();
|
||
}
|
||
}
|
||
|
||
/// 检查指定类型的广告是否已准备好
|
||
Future<bool> isAdReady(InterstitialAdType adType) async {
|
||
if (!_isInitialized) {
|
||
return false;
|
||
}
|
||
|
||
final ad = _interstitialAds[adType];
|
||
if (ad != null) {
|
||
try {
|
||
return await ad.isAdReady();
|
||
} catch (e) {
|
||
logMethodName(
|
||
'isAdReady',
|
||
'Error',
|
||
'Failed to check ${adType.displayName}: $e',
|
||
);
|
||
return false;
|
||
}
|
||
}
|
||
return false;
|
||
}
|
||
|
||
/// 检查是否已初始化
|
||
bool get isInitialized => _isInitialized;
|
||
|
||
/// LevelPlay Init listener
|
||
@override
|
||
void onInitFailed(LevelPlayInitError error) {
|
||
////初始化失败
|
||
logMethodName('❌InitListener', 'onInitFailed', error);
|
||
_isInitialized = false;
|
||
_isInitializing = false;
|
||
}
|
||
|
||
@override
|
||
void onInitSuccess(LevelPlayConfiguration configuration) {
|
||
//初始化成功
|
||
logMethodName('✅InitListener', 'onInitSuccess', configuration);
|
||
_isInitialized = true;
|
||
_isInitializing = false;
|
||
}
|
||
|
||
/// 内部方法:处理广告事件(由监听器包装类调用)
|
||
void _handleAdEvent(
|
||
InterstitialAdType adType,
|
||
String eventName,
|
||
dynamic data,
|
||
) {
|
||
logMethodName('${adType.displayName}', eventName, data);
|
||
}
|
||
|
||
/// 工具函数
|
||
void logMethodName(String adFormat, String methodName, dynamic data) {
|
||
print('$TAG: $adFormat - $methodName $data');
|
||
}
|
||
}
|
||
|
||
/// Interstitial 广告监听器包装类
|
||
/// 用于为每个广告实例创建独立的监听器,以便准确识别事件来源
|
||
class _InterstitialAdListenerWrapper with LevelPlayInterstitialAdListener {
|
||
final InterstitialAdType adType;
|
||
final InterstitialAdManager manager;
|
||
|
||
_InterstitialAdListenerWrapper(this.adType, this.manager);
|
||
|
||
@override
|
||
void onAdClicked(LevelPlayAdInfo adInfo) {
|
||
manager._handleAdEvent(adType, 'onAdClicked', adInfo);
|
||
}
|
||
|
||
@override
|
||
void onAdClosed(LevelPlayAdInfo adInfo) {
|
||
manager._handleAdEvent(adType, 'onAdClosed', adInfo);
|
||
|
||
// 调用完成回调
|
||
final callback = manager._adCompletedCallbacks[adType];
|
||
callback?.call();
|
||
manager._adCompletedCallbacks.remove(adType);
|
||
|
||
// 广告关闭后,自动重新加载该广告以便下次使用
|
||
manager.logMethodName(
|
||
'onAdClosed',
|
||
'Info',
|
||
'广告已关闭,开始重新加载: ${adType.displayName}',
|
||
);
|
||
// 清理当前展示状态
|
||
if (manager._currentShowingAd == adType) {
|
||
manager._currentShowingAd = null;
|
||
}
|
||
manager.loadAd(adType);
|
||
}
|
||
|
||
@override
|
||
void onAdDisplayFailed(LevelPlayAdError error, LevelPlayAdInfo adInfo) {
|
||
manager._handleAdEvent(adType, 'onAdDisplayFailed', '$error | $adInfo');
|
||
|
||
// 调用完成回调
|
||
final callback = manager._adCompletedCallbacks[adType];
|
||
callback?.call();
|
||
manager._adCompletedCallbacks.remove(adType);
|
||
|
||
// 广告展示失败后,也重新加载该广告
|
||
manager.logMethodName(
|
||
'onAdDisplayFailed',
|
||
'Info',
|
||
'广告展示失败,开始重新加载: ${adType.displayName}',
|
||
);
|
||
// 展示失败时清理当前展示状态
|
||
if (manager._currentShowingAd == adType) {
|
||
manager._currentShowingAd = null;
|
||
}
|
||
manager.loadAd(adType);
|
||
}
|
||
|
||
@override
|
||
void onAdDisplayed(LevelPlayAdInfo adInfo) {
|
||
manager._handleAdEvent(adType, 'onAdDisplayed', adInfo);
|
||
}
|
||
|
||
@override
|
||
void onAdInfoChanged(LevelPlayAdInfo adInfo) {
|
||
manager._handleAdEvent(adType, 'onAdInfoChanged', adInfo);
|
||
}
|
||
|
||
@override
|
||
void onAdLoadFailed(LevelPlayAdError error) {
|
||
manager._handleAdEvent(adType, 'onAdLoadFailed', error);
|
||
|
||
// 处理开屏广告失败逻辑
|
||
if (manager._isHandlingSplashAd &&
|
||
manager._splashAdsToLoad.contains(adType) &&
|
||
manager._onSplashAdAllFailedCallback != null) {
|
||
manager.logMethodName(
|
||
'LoadInitialSplashAd',
|
||
'Failed',
|
||
'❌ 广告加载失败: ${adType.displayName}, 原因: $error',
|
||
);
|
||
manager._splashAdsFailedCount++;
|
||
if (manager._splashAdsFailedCount >= manager._splashAdsToLoad.length) {
|
||
manager._onSplashAdAllFailedCallback!();
|
||
manager._clearSplashCallbacks();
|
||
}
|
||
}
|
||
}
|
||
|
||
@override
|
||
void onAdLoaded(LevelPlayAdInfo adInfo) {
|
||
manager._handleAdEvent(adType, 'onAdLoaded', adInfo);
|
||
|
||
// 处理开屏广告逻辑:任意一个加载成功就立即回调(只触发一次)
|
||
if (manager._isHandlingSplashAd &&
|
||
manager._splashAdsToLoad.contains(adType) &&
|
||
manager._onSplashAdReadyCallback != null) {
|
||
manager.logMethodName(
|
||
'LoadInitialSplashAd',
|
||
'Success',
|
||
'✅ 广告加载成功: ${adType.displayName}',
|
||
);
|
||
manager._onSplashAdReadyCallback!(adType);
|
||
// 清除回调状态,确保后续广告加载成功不会再次触发回调
|
||
manager._clearSplashCallbacks();
|
||
}
|
||
}
|
||
}
|