From 753992c263075bcd088ac6995c24fc9d6afb32c8 Mon Sep 17 00:00:00 2001 From: WangMing <2747639460@qq.com> Date: Fri, 16 Jan 2026 10:28:00 +0800 Subject: [PATCH] =?UTF-8?q?feat:"=E8=B0=83=E8=AF=95=E7=A9=BF=E5=B1=B1?= =?UTF-8?q?=E7=94=B2=E5=B9=BF=E5=91=8A=E5=8F=AF=E4=B8=8D=E5=BD=B1=E5=93=8D?= =?UTF-8?q?=E7=A8=8B=E5=BA=8F=E6=89=A7=E8=A1=8C=E6=88=90=E9=83=BD"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- android/app/src/main/AndroidManifest.xml | 15 +- .../kotlin/com/example/my_app/MainActivity.kt | 78 ++++++- android/gradle.properties | 1 + ios/Runner/Info.plist | 9 + lib/main.dart | 221 +++++++++++++++++- pubspec.lock | 8 + pubspec.yaml | 1 + 7 files changed, 328 insertions(+), 5 deletions(-) diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index a9a8f4d..7073b49 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -1,8 +1,19 @@ - + + + + + + + + + + + android:icon="@mipmap/ic_launcher" + android:usesCleartextTraffic="true" + tools:replace="android:label"> + when (call.method) { + "init" -> { + initMediationAdSdk(this) + result.success(null) + } + "initWithConfig" -> { + val args = call.arguments as? Map<*, *> + val appId = args?.get("appId") as? String ?: "" + val privacy = args?.get("privacy") as? Map<*, *> + initMediationAdSdk(this, appId, privacy) + result.success(null) + } + "showSplash" -> { + val args = call.arguments as? Map<*, *> + val codeId = args?.get("codeId") as? String ?: "" + SplashAdActivity.start(this, codeId) + result.success(null) + } + else -> result.notImplemented() + } + } + } + + private fun initMediationAdSdk(context: Context, appIdOverride: String? = null, privacy: Map<*, *>? = null) { + TTAdSdk.init(context, buildConfig(context, appIdOverride, privacy)) + TTAdSdk.start(object : TTAdSdk.Callback { + override fun success() { + } + + override fun fail(code: Int, msg: String?) { + } + }) + } + + private fun buildConfig(context: Context, appIdOverride: String? = null, privacy: Map<*, *>? = null): TTAdConfig { + val appName = context.applicationInfo.loadLabel(context.packageManager).toString() + val customController = object : com.bytedance.sdk.openadsdk.TTCustomController() { + override fun isCanUseLocation(): Boolean { + return privacy?.get("canUseLocation") as? Boolean ?: false + } + + override fun isCanUsePhoneState(): Boolean { + return privacy?.get("canUsePhoneState") as? Boolean ?: false + } + + override fun isCanUseWifiState(): Boolean { + return privacy?.get("canUseWifiState") as? Boolean ?: true + } + + override fun isCanUseWriteExternal(): Boolean { + return privacy?.get("canUseWriteExternal") as? Boolean ?: false + } + } + return TTAdConfig.Builder() + .appId(appIdOverride ?: "") + .appName(appName) + .useMediation(true) + .debug(true) + .supportMultiProcess(false) + .customController(customController) + .build() + } +} diff --git a/android/gradle.properties b/android/gradle.properties index 948a4fe..0e6fba3 100644 --- a/android/gradle.properties +++ b/android/gradle.properties @@ -1,3 +1,4 @@ org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError android.useAndroidX=true org.gradle.caching=true +android.enableJetifier=true \ No newline at end of file diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist index 038828e..6bf2cc9 100644 --- a/ios/Runner/Info.plist +++ b/ios/Runner/Info.plist @@ -66,5 +66,14 @@ UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight + + NSLocationWhenInUseUsageDescription + 用于获取位置信息以优化广告投放与推荐 + NSPhotoLibraryUsageDescription + 用于访问相册以支持上传或展示媒体内容 + NSCameraUsageDescription + 用于拍照或视频录制以便上传或广告素材使用 + NSUserTrackingUsageDescription + 用于广告个性化与跨应用跟踪(App Tracking Transparency) diff --git a/lib/main.dart b/lib/main.dart index 6345bca..dc8e30e 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,9 +1,11 @@ import 'package:flutter/material.dart'; +import 'dart:async'; import 'package:provider/provider.dart'; import 'providers/expense_provider.dart'; import 'screens/home_screen.dart'; - +import 'package:flutter_unionad/flutter_unionad.dart'; void main() { + WidgetsFlutterBinding.ensureInitialized(); runApp(const MyApp()); } @@ -22,7 +24,222 @@ class MyApp extends StatelessWidget { colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple), useMaterial3: true, ), - home: const HomeScreen(), + home: const SplashPage(), + ), + ); + } +} + +class SplashPage extends StatefulWidget { + const SplashPage({super.key}); + + @override + State createState() => _SplashPageState(); +} + +class _SplashPageState extends State { + bool _showAd = true; + Timer? _adWatchdog; + bool? _initSuccess; // null=loading, true=success, false=failure + + @override + void initState() { + super.initState(); + // 延后到首帧绘制后再初始化广告 SDK,避免在 Activity/Context 未就绪时调用导致 NPE + WidgetsBinding.instance.addPostFrameCallback((_) { + _initUnionAd(); + }); + // 如果广告或 SDK 未在合理时间内返回,使用回退跳转避免白屏 + _adWatchdog = Timer(const Duration(seconds: 6), () { + if (mounted) _goToHome(); + }); + } + + Future _initUnionAd() async { + // 注册并初始化 FlutterUnionad SDK + try { + await FlutterUnionad.register( + // 穿山甲广告 Android appid 必填 + androidAppId: "5779303", + // 穿山甲广告 ios appid 必填 + iosAppId: "5779303", + // appname 必填 + appName: "晴天记账", + // 使用聚合功能(true 使用 GroMore 下的广告位) + useMediation: false, + // 是否为计费用户 + paid: false, + // 用户画像关键词 + keywords: "", + // 是否允许 SDK 展示通知栏提示 + allowShowNotify: true, + // 是否显示 debug 日志 + debug: true, + // 是否支持多进程 + supportMultiProcess: false, + // 允许直接下载的网络状态集合 + directDownloadNetworkType: [ + FlutterUnionadNetCode.NETWORK_STATE_2G, + FlutterUnionadNetCode.NETWORK_STATE_3G, + FlutterUnionadNetCode.NETWORK_STATE_4G, + FlutterUnionadNetCode.NETWORK_STATE_WIFI, + ], + androidPrivacy: AndroidPrivacy( + isCanUseLocation: false, + lat: 0.0, + lon: 0.0, + isCanUsePhoneState: false, + imei: "", + isCanUseWifiState: false, + macAddress: "", + isCanUseWriteExternal: false, + oaid: "b69cd3cf68900323", + alist: false, + isCanUseAndroidId: false, + androidId: "", + isCanUsePermissionRecordAudio: false, + isLimitPersonalAds: false, + isProgrammaticRecommend: false, + userPrivacyConfig: {"mcod": "0"}, + ), + iosPrivacy: IOSPrivacy( + limitPersonalAds: false, + limitProgrammaticAds: false, + forbiddenCAID: false, + ), + // userInfo: UnionadUserInfo( + // userId: "unionad_123", + // age: 19, + // gender: 2, + // channel: "flutter", + // subChannel: "flutter_unionad", + // userValueGroup: "QQ", + // customInfos: {"QQ": "123", "WeChat": "456"}, + // ), + // localConfig: "site_config_5098580", + ); + debugPrint('FlutterUnionad.register: success'); + if (mounted) setState(() => _initSuccess = true); + } catch (e) { + debugPrint('FlutterUnionad.register error: $e'); + if (mounted) setState(() => _initSuccess = false); + } + try { + final sdkVersion = await FlutterUnionad.getSDKVersion(); + debugPrint('FlutterUnionad SDK Version: $sdkVersion'); + } catch (e) { + debugPrint('getSDKVersion error: $e'); + } + + FlutterUnionad.requestPermissionIfNecessary( + callBack: FlutterUnionadPermissionCallBack( + notDetermined: () { + debugPrint('权限未确定'); + }, + restricted: () { + debugPrint('权限限制'); + }, + denied: () { + debugPrint('权限拒绝'); + }, + authorized: () { + debugPrint('权限同意'); + }, + ), + ); + // 如果注册/初始化在合理时间内仍未完成,标记为失败以便调试 + Future.delayed(const Duration(seconds: 4), () { + if (!mounted) return; + if (_initSuccess == null) { + debugPrint('FlutterUnionad: init did not finish within 4s, marking as failed'); + setState(() => _initSuccess = false); + } + }); + } + + void _goToHome() { + if (!mounted) return; + _adWatchdog?.cancel(); + Navigator.of(context).pushReplacement( + MaterialPageRoute(builder: (_) => const HomeScreen()), + ); + } + + @override + void dispose() { + _adWatchdog?.cancel(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + body: Stack( + children: [ + // 后端主页面作为备用 + const Offstage(offstage: true, child: HomeScreen()), + // 开屏广告视图 + Positioned.fill( + child: _showAd + ? FlutterUnionadSplashAdView( + androidCodeId: "102729400", + iosCodeId: "102729400", + supportDeepLink: true, + width: MediaQuery.of(context).size.width, + height: MediaQuery.of(context).size.height, + hideSkip: false, + timeout: 3000, + callBack: FlutterUnionadSplashCallBack( + onShow: () { + debugPrint('开屏广告显示'); + setState(() => _showAd = true); + }, + onClick: () { + debugPrint('开屏广告点击'); + }, + onFail: (error) { + debugPrint('开屏广告失败 $error'); + _goToHome(); + }, + onFinish: () { + debugPrint('开屏广告倒计时结束'); + _goToHome(); + }, + onSkip: () { + debugPrint('开屏广告跳过'); + _goToHome(); + }, + onTimeOut: () { + debugPrint('开屏广告超时'); + _goToHome(); + }, + ), + ) + : const SizedBox.shrink(), + ), + // Debug: init 状态可视化,方便在真机上观察 + Positioned( + left: 12, + right: 12, + bottom: 24, + child: Align( + alignment: Alignment.bottomCenter, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + decoration: BoxDecoration( + color: Colors.black.withOpacity(0.6), + borderRadius: BorderRadius.circular(8), + ), + child: Text( + _initSuccess == null + ? 'SDK init: loading...' + : (_initSuccess == true ? 'SDK init: success' : 'SDK init: failed'), + style: const TextStyle(color: Colors.white), + ), + ), + ), + ), + ], ), ); } diff --git a/pubspec.lock b/pubspec.lock index f6099f4..daa1558 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -107,6 +107,14 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_unionad: + dependency: "direct main" + description: + name: flutter_unionad + sha256: "550773ff9ae43c8ba3d818d71cd2177259176d3f6c81b275e6537067de1cfa88" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.2.3" intl: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index c126527..083b1b1 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -36,6 +36,7 @@ dependencies: intl: ^0.19.0 fl_chart: ^0.66.0 uuid: ^4.3.3 + flutter_unionad: ^2.2.3 # The following adds the Cupertino Icons font to your application.