home_screen.dart 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649
  1. import 'dart:async';
  2. import 'dart:io';
  3. import 'dart:math';
  4. import 'package:app_tracking_transparency/app_tracking_transparency.dart';
  5. import 'package:applovin_max/applovin_max.dart';
  6. import 'package:firebase_crashlytics/firebase_crashlytics.dart';
  7. import 'package:firebase_messaging/firebase_messaging.dart';
  8. import 'package:flutter/material.dart';
  9. import 'package:flutter_svg/svg.dart';
  10. import 'package:fluttertoast/fluttertoast.dart';
  11. import 'package:logging/logging.dart';
  12. import 'package:lottie/lottie.dart';
  13. import 'package:provider/provider.dart';
  14. import 'package:puzzleweave/ads/applovin_ads_controller.dart';
  15. import 'package:puzzleweave/audio/jc_audio_controller.dart';
  16. import 'package:puzzleweave/collection/collection_screen.dart';
  17. import 'package:puzzleweave/config/config.dart';
  18. import 'package:puzzleweave/config/device.dart';
  19. import 'package:puzzleweave/firebase/adjust_helper.dart';
  20. import 'package:puzzleweave/homepage/home_board_play.dart';
  21. import 'package:puzzleweave/l10n/app_localizations.dart';
  22. import 'package:puzzleweave/models/cached_request.dart';
  23. import 'package:puzzleweave/models/data.dart';
  24. import 'package:puzzleweave/models/download.dart';
  25. import 'package:puzzleweave/models/items.dart';
  26. import 'package:puzzleweave/persistence/persistence.dart';
  27. import 'package:puzzleweave/platform/my_method_channel.dart';
  28. import 'package:puzzleweave/play/board_play.dart';
  29. import 'package:puzzleweave/settings/settings_screen.dart';
  30. import 'package:puzzleweave/skin/skin.dart';
  31. import 'package:puzzleweave/utils/mybutton.dart';
  32. import 'package:puzzleweave/utils/utils.dart';
  33. import '../ads/ads_state.dart';
  34. final Logger _log = Logger('home_screen');
  35. class HomeScreen extends StatefulWidget {
  36. const HomeScreen({super.key});
  37. @override
  38. State<StatefulWidget> createState() => _HomeScreen();
  39. }
  40. const int minimumRemoteLoadCount = 30; // 假设加载到 30 张图才算网络畅通
  41. class _HomeScreen extends AdsState<HomeScreen> with TickerProviderStateMixin {
  42. late Device device;
  43. late JcAudioController audio;
  44. late Data data;
  45. List<ListItem>? latest;
  46. late CachedRequest latestCachedRequest;
  47. late StreamSubscription? latestSubscription;
  48. // 自定义画布控制器(可选,用于控制画布绘制逻辑)
  49. final _canvasKey = GlobalKey<HomeBoardPlayState>();
  50. // !!! 新增:用于定位 Collection 按钮的 GlobalKey
  51. final GlobalKey _collectionKey = GlobalKey();
  52. bool isLoading = true;
  53. // !!! 新增:Collection 按钮的动画控制器和动画
  54. late AnimationController _collectionController; // 左上角 collection button 的动画控制器
  55. late Animation<double> _collectionAnimation; // 放大/缩小动画
  56. bool firstRun = false;
  57. @override
  58. void initState() {
  59. super.initState();
  60. _log.info("首页初始化");
  61. // 在组件绘制后检查 firstRun 并导航
  62. if (Persistence().firstRun) {
  63. firstRun = true;
  64. WidgetsBinding.instance.addPostFrameCallback((_) {
  65. // 仅当未跳转过时执行
  66. _handleFirstRunNavigation();
  67. });
  68. Persistence().firstRun = false;
  69. }
  70. device = context.read<Device>();
  71. audio = context.read<JcAudioController>();
  72. data = context.read<Data>();
  73. latestCachedRequest = data.latest;
  74. // 主动获取缓存数据(关键)
  75. final cachedData = latestCachedRequest.cachedData;
  76. if (cachedData != null) {
  77. _onLatestDataUpdate(cachedData);
  78. }
  79. latestSubscription = latestCachedRequest.stream.listen(_onLatestDataUpdate, onError: _onLatestDataError);
  80. // !!! 改造点 1: 初始化 Collection 按钮动画
  81. _collectionController =
  82. AnimationController(
  83. // 设定总时长
  84. duration: const Duration(milliseconds: 300),
  85. vsync: this,
  86. )..addStatusListener((status) {
  87. if (status == AnimationStatus.completed) {
  88. audio.playSfx(SfxType.pop);
  89. }
  90. });
  91. // !!! 改造点 2: 使用 TweenSequence 实现平滑的放大和缩小
  92. _collectionAnimation = TweenSequence<double>([
  93. // 阶段 1: 放大到 1.3 (占总时长的 50%)
  94. TweenSequenceItem(tween: Tween<double>(begin: 1.0, end: 1.4).chain(CurveTween(curve: Curves.easeOut)), weight: 40.0),
  95. // 阶段 2: 缩小回 1.0 (占总时长的 50%)
  96. TweenSequenceItem(tween: Tween<double>(begin: 1.4, end: 1.0).chain(CurveTween(curve: Curves.easeIn)), weight: 60.0),
  97. ]).animate(_collectionController);
  98. audio.startMusic();
  99. }
  100. // 首页初始化之后的跳转,首次运行直接进入play页面,上次从play页面退出有缓存存在也跳转到play页面
  101. void _handleFirstRunNavigation() async {
  102. _log.info('First run detected, navigating to initial play page.');
  103. final AssetItem initialItem = AssetItem(
  104. Config.firstId,
  105. '',
  106. 2000,
  107. 3000,
  108. 3,
  109. false,
  110. 'assets/builtin/${Config.firstId}.jpeg',
  111. 'assets/builtin/${Config.firstId}.jpeg',
  112. );
  113. return gotoPlay(initialItem, firstRun: true);
  114. }
  115. // 检查是否需要跳转到boardplay
  116. void checkGoPlay() async {
  117. if (currentItem != null) {
  118. final jsonFile = await localFile(currentItem!.jsonPath);
  119. final exists = await jsonFile.exists();
  120. // !!! 关键修复:检查当前组件是否还在组件树中
  121. if (!mounted) return;
  122. if (exists) {
  123. gotoPlay(currentItem!);
  124. }
  125. }
  126. }
  127. @override
  128. void dispose() {
  129. latestSubscription?.cancel();
  130. _collectionController.dispose();
  131. super.dispose();
  132. }
  133. _onLatestDataUpdate(datalist) {
  134. _log.info('_onLatestDataUpdate.... ');
  135. if (datalist != null) {
  136. bool check = false;
  137. if (currentItem == null && datalist != null && !firstRun) {
  138. check = true;
  139. }
  140. latest = datalist as List<ListItem>;
  141. isLoading = false;
  142. setState(() {});
  143. // 1. 检查数据量是否达到最低要求 (>= 30)
  144. final bool hasSufficientData = datalist.length >= minimumRemoteLoadCount;
  145. // 2. 检查数据是否来自最近一次成功的网络请求
  146. final bool isNetworkActive = latestCachedRequest.hasRecentSuccessfulFetch; // !!! 关键检查点
  147. if (hasSufficientData) {
  148. // 如果数据完整,无论是否是缓存数据,都尝试初始化第三方服务(因为主页已经可以显示了)
  149. if (!hasInit) {
  150. initThird();
  151. }
  152. if (!isNetworkActive) {
  153. // 如果是从缓存读取的,网络状态未知,静默刷新列表即可,不触发图片下载
  154. Future.delayed(Duration(seconds: 3), () => refresh());
  155. }
  156. } else {
  157. // 数据不足 (例如,只有内置图),无论是缓存还是远程失败,都需要重试
  158. _log.info('Data insufficient (only ${datalist.length} items). Attempting refresh in 3s.');
  159. Future.delayed(Duration(seconds: 3), () => refresh());
  160. }
  161. if (check) {
  162. checkGoPlay();
  163. }
  164. }
  165. }
  166. _onLatestDataError(error) {
  167. _log.info('_onLatestDataError.... $error');
  168. if (latest == null || latest!.isEmpty || latest!.length < 20) {
  169. // 列表数据如果少于20,说明只是内置图,仍然刷新远程请求
  170. _log.warning("_onLatestDataError, retry again");
  171. // refresh();
  172. Future.delayed(Duration(seconds: 3), () => refresh());
  173. }
  174. }
  175. Future<void> refresh() async {
  176. _log.info('refresh...');
  177. await latestCachedRequest.refresh();
  178. }
  179. // ListItem? get currentItem {
  180. // if (latest != null && latest!.isNotEmpty && data.currentLevel < latest!.length) {
  181. // // return latest![data.currentLevel]; // 原来的逻辑,太过简单,如果后台图片有调整顺序变了,用户可能会遇到重复的图
  182. // // todo... 改成从latest列表中查找首个 data.completedWorks 中不存在的图(即首个未完成图)
  183. // }
  184. // return null;
  185. // }
  186. ListItem? get currentItem {
  187. // 1. 确保 latest 数据已加载
  188. if (latest == null || latest!.isEmpty) {
  189. return null;
  190. }
  191. // 2. 获取已完成作品的唯一标识符集合,方便快速查找
  192. // 假设 ListItem 的 id/url/name 等属性是其唯一标识。
  193. // 我们使用 id 作为唯一标识符。
  194. final Set<String> completedIds = data.completedWorks.value.map((work) => work.id).toSet();
  195. // 3. 遍历 latest 列表,查找第一个未完成的 Item
  196. for (final item in latest!) {
  197. // 假设 ListItem 有一个唯一的 id 属性。
  198. // 如果 ListItem 没有 id,您需要使用其 URL 或其他唯一标识。
  199. // 这里我们假设 ListItem 是 RemoteItem/AssetItem 的基类,它们有一个 String 类型的 id 属性。
  200. final String itemId = item.id;
  201. // 检查这个 id 是否在已完成集合中
  202. if (!completedIds.contains(itemId)) {
  203. _log.info('Found current item: $itemId');
  204. return item; // 返回找到的第一个未完成的 Item
  205. }
  206. }
  207. // 4. 如果所有图片都完成了
  208. _log.info('All items in the latest list have been completed.');
  209. return null;
  210. }
  211. /// 预加载未来 N 张图片到磁盘,并最后触发当前关卡下载以最大化内存缓存命中率。
  212. void _preloadNextImages() async {
  213. // 预加载数量 (包括当前关卡在内,共 20 个)
  214. const int totalPreloadCount = 20;
  215. // 1. 确保 latest 数据已加载
  216. if (latest == null || latest!.isEmpty || latest!.length < minimumRemoteLoadCount) {
  217. _log.info('Preload failed: latest list is empty.');
  218. return;
  219. }
  220. // 2. 查找当前未完成的第一张图片的索引 (Index of currentItem)
  221. final Set<String> completedIds = data.completedWorks.value.map((work) => work.id).toSet();
  222. int startIndex = -1;
  223. for (int i = 0; i < latest!.length; i++) {
  224. if (!completedIds.contains(latest![i].id)) {
  225. startIndex = i;
  226. break;
  227. }
  228. }
  229. if (startIndex == -1) {
  230. _log.info('Preload: All images completed, nothing to preload.');
  231. return;
  232. }
  233. // 确定预加载范围 (从当前图片startIndex到 totalPreloadCount 个图片)
  234. final int endPreloadIndex = min(startIndex + totalPreloadCount, latest!.length);
  235. // 3. 准备要加载的列表 (从 startIndex 开始)
  236. final List<ListItem> itemsToLoad = latest!.sublist(startIndex, endPreloadIndex);
  237. if (itemsToLoad.isEmpty) {
  238. _log.info('Preload: No items found in the range.');
  239. return;
  240. }
  241. // 4. 将当前关卡 (第一个元素) 移动到列表的末尾
  242. final ListItem currentItemToLoad = itemsToLoad.removeAt(0);
  243. itemsToLoad.add(currentItemToLoad);
  244. _log.info('Preloading ${itemsToLoad.length} images. Current item: ${currentItemToLoad.id} will be loaded last.');
  245. // 5. 循环触发 ItemLoader 加载
  246. int preloadCount = 0;
  247. for (final item in itemsToLoad) {
  248. // 对远程图片进行预加载
  249. // 我们不关心返回值或 Future,只是触发下载
  250. if (item is RemoteItem) {
  251. try {
  252. // 使用静态 preload 方法,不再创建复杂的 Loader 实例
  253. ItemLoader.preload(item, device.suggestedQuality);
  254. // 稍微给一点延迟,避免瞬时并发 I/O 导致 UI 顿挫
  255. await Future.delayed(const Duration(milliseconds: 100));
  256. preloadCount++;
  257. } catch (e) {
  258. _log.warning('Failed to load item for preloading: ${item.id}, error: $e');
  259. }
  260. }
  261. }
  262. _log.info('Preload initiated for $preloadCount remote images, current item was last.');
  263. }
  264. @override
  265. Widget build(BuildContext context) {
  266. if (isLoading) return scrollableDummy;
  267. // 2. 计算画布尺寸(宽=屏幕宽-60,高=宽×3/2)
  268. // final canvasWidth = device.screenSize.width - 30 * 2; // 左右各30px
  269. // final canvasHeight = canvasWidth * 3 / 2;
  270. final double availableHeight = device.screenSize.height - device.appBarHeight - device.bannerHeight - 120;
  271. final double paddedWidth = device.screenSize.width - 2 * 30; // padding width 30
  272. final double paddedHeight = availableHeight;
  273. final double targetWidth = paddedWidth;
  274. final double targetHeight = targetWidth * device.aspectRatio;
  275. final double canvasWidth;
  276. final double canvasHeight;
  277. if (targetHeight > paddedHeight) {
  278. canvasHeight = paddedHeight;
  279. canvasWidth = paddedHeight / device.aspectRatio;
  280. } else {
  281. canvasWidth = targetWidth;
  282. canvasHeight = targetHeight;
  283. }
  284. return Scaffold(
  285. backgroundColor: SkinHelper.colorWhite,
  286. appBar: AppBar(
  287. backgroundColor: SkinHelper.colorWhite,
  288. // elevation: 1,
  289. centerTitle: true,
  290. leading: RepaintBoundary(
  291. // !!! 改造点 3: 添加 ScaleTransition
  292. key: _collectionKey, // 关联 GlobalKey
  293. child: ScaleTransition(
  294. scale: _collectionAnimation, // 使用定义的放大/缩小动画
  295. child: IconButton(
  296. onPressed: () {
  297. audio.playSfx(SfxType.click);
  298. Navigator.push(context, CollectionScreen.buildRoute());
  299. },
  300. icon: const Icon(Icons.collections, color: Colors.black87),
  301. ),
  302. ),
  303. ),
  304. // title: const Text(
  305. // 'Jigsort Solitaire',
  306. // style: TextStyle(color: Colors.black87, fontWeight: FontWeight.bold, fontSize: 24),
  307. // ),
  308. // 🚀 改造点:将 Text 标题替换为 SvgPicture
  309. title: SvgPicture.asset(
  310. 'assets/images/title.svg', // 替换为您的 SVG 文件路径
  311. height: 32, // 根据您的设计调整高度,确保它在 AppBar 中显示良好
  312. // colorFilter: const ColorFilter.mode(Colors.black87, BlendMode.srcIn), // 如果SVG是单色,可以设置颜色
  313. placeholderBuilder: (BuildContext context) => const Text(
  314. // 占位符,以防SVG加载失败
  315. 'Jigsort Solitaire',
  316. style: TextStyle(color: Colors.black87, fontWeight: FontWeight.bold, fontSize: 24),
  317. ),
  318. ),
  319. actions: [
  320. IconButton(
  321. onPressed: () {
  322. audio.playSfx(SfxType.click);
  323. // AppLovinMAX.showMediationDebugger();
  324. // Navigator.push(context, SettingsDialog.buildRoute());
  325. Navigator.push(context, SettingScreen.buildRoute());
  326. },
  327. icon: const Icon(Icons.settings, color: Colors.black87),
  328. ),
  329. ],
  330. ),
  331. body: Column(
  332. mainAxisAlignment: MainAxisAlignment.spaceBetween,
  333. children: [
  334. Expanded(
  335. child: Column(
  336. mainAxisAlignment: MainAxisAlignment.spaceEvenly,
  337. children: [
  338. // 2. 画布区域(固定尺寸)
  339. Padding(
  340. padding: const EdgeInsets.symmetric(horizontal: 30), // 左右30px
  341. child: SizedBox(
  342. width: canvasWidth,
  343. height: canvasHeight,
  344. child: ValueListenableBuilder(
  345. valueListenable: data.completedWorks,
  346. builder: (context, value, child) {
  347. return HomeBoardPlay(
  348. key: _canvasKey,
  349. canvasWidth: canvasWidth,
  350. canvasHeight: canvasHeight,
  351. collectionKey: _collectionKey,
  352. onCollectionDone: () {
  353. // collection unlocking 动画结束,启动collection button 的接收反馈动画
  354. _log.info('onCollectionDone, 启动合集收纳反馈动画');
  355. audio.playSfx(SfxType.appear);
  356. _collectionController.forward(from: 0.0);
  357. },
  358. );
  359. },
  360. ),
  361. ),
  362. ),
  363. playButton,
  364. ],
  365. ),
  366. ),
  367. SafeArea(
  368. child: SizedBox(
  369. // 始终预留高度,防止 Banner 出现时下方 UI 整体上跳(Layout Jitter)
  370. height: context.read<Device>().bannerHeight,
  371. width: double.infinity,
  372. child: isBannerVisible ? getBanner('home_bottom') : const SizedBox.shrink(), // 隐藏时完全不占位或保持留白
  373. ),
  374. ),
  375. ],
  376. ),
  377. );
  378. }
  379. void gotoPlay(ListItem item, {bool firstRun = false}) async {
  380. _log.info('goto play, firstRun = $firstRun');
  381. // !!! 增加保护
  382. if (!mounted) return;
  383. PageRouteBuilder? pageRouteBuilder = BoardPlay.buildRoute(item, firstRun: firstRun);
  384. final result = await Navigator.push(context, pageRouteBuilder);
  385. if (!mounted) return;
  386. if (result != null && result == true) {
  387. // 通关返回, 展示翻牌
  388. _canvasKey.currentState?.startFlipAnimation();
  389. final bool hasSufficientData = latest != null && latest!.length >= minimumRemoteLoadCount;
  390. final bool isNetworkActive = latestCachedRequest.hasRecentSuccessfulFetch;
  391. if (hasSufficientData) {
  392. // 1. 数据完整:如果网络活跃,立即顺延预加载。
  393. if (isNetworkActive) {
  394. _log.info('Game finished, data complete & Network Active. Triggering sequential preloading...');
  395. _preloadNextImages();
  396. } else {
  397. // 2. 数据完整但网络不活跃/状态未知:尝试刷新,让 _onLatestDataUpdate 负责后续处理
  398. _log.info('Game finished, data complete but Network inactive. Attempting refresh.');
  399. refresh();
  400. }
  401. } else {
  402. // 3. 数据不完整:无论如何都需要刷新,让 _onLatestDataUpdate 重新处理
  403. _log.info('Game finished, remote data incomplete. Attempting refresh...');
  404. refresh();
  405. }
  406. } else {
  407. // 非关卡通关返回,在这里播放插屏广告
  408. // showInterstitialAd("level_exit", currentItem!.id, data.currentLevel);
  409. }
  410. }
  411. Widget get playButton {
  412. return MyElevatedButton(
  413. width: device.isTablet ? 300 : 200,
  414. height: 70,
  415. borderRadius: BorderRadius.circular(20),
  416. gradient: LinearGradient(colors: [SkinHelper.coreBgColor, SkinHelper.slotBorderColor]),
  417. onPressed: () async {
  418. audio.playSfx(SfxType.click);
  419. if (currentItem != null) {
  420. gotoPlay(currentItem!);
  421. } else {
  422. Fluttertoast.showToast(
  423. msg: AppLocalizations.of(context)!.noMorePicture,
  424. toastLength: Toast.LENGTH_SHORT,
  425. gravity: ToastGravity.CENTER,
  426. timeInSecForIosWeb: 1,
  427. backgroundColor: SkinHelper.slotBorderColor,
  428. textColor: Colors.white,
  429. fontSize: 16.0,
  430. );
  431. }
  432. },
  433. child: Column(
  434. mainAxisAlignment: MainAxisAlignment.center,
  435. children: [
  436. Text(
  437. AppLocalizations.of(context)!.play,
  438. style: TextStyle(color: Colors.white, fontSize: 24, fontWeight: FontWeight.bold),
  439. ),
  440. ValueListenableBuilder<List<Work>>(
  441. valueListenable: data.completedWorks,
  442. builder: (context, isSoundOn, child) {
  443. return Text('${AppLocalizations.of(context)!.level} ${data.currentLevel + 1}', style: const TextStyle(color: Colors.white, fontSize: 16));
  444. },
  445. ),
  446. ],
  447. ),
  448. );
  449. }
  450. // Widget get scrollableDummy => Scaffold(
  451. // body: LayoutBuilder(
  452. // builder: (p0, p1) {
  453. // return SingleChildScrollView(
  454. // physics: const AlwaysScrollableScrollPhysics(),
  455. // child: SizedBox(
  456. // height: p1.maxHeight,
  457. // child: Center(child: ListView(shrinkWrap: true, children: [Lottie.asset('assets/lottie/loading.json', height: 100)])),
  458. // ),
  459. // );
  460. // },
  461. // ),
  462. // );
  463. Widget get scrollableDummy => Scaffold(
  464. backgroundColor: SkinHelper.colorWhite,
  465. body: Center(
  466. child: Column(
  467. mainAxisAlignment: MainAxisAlignment.center,
  468. children: [
  469. // 使用原生最轻量的进度指示器
  470. SizedBox(width: 40, height: 40, child: CircularProgressIndicator(strokeWidth: 3, valueColor: AlwaysStoppedAnimation<Color>(SkinHelper.coreBgColor))),
  471. const SizedBox(height: 20),
  472. // 可选:添加一个简单的文字,让用户知道在加载
  473. Text(
  474. "Loading...",
  475. style: TextStyle(color: SkinHelper.slotBorderColor.withOpacity(0.7), fontSize: 14, fontWeight: FontWeight.w500),
  476. ),
  477. ],
  478. ),
  479. ),
  480. );
  481. ///////////////////////// 初始化相关 /////////////////////////
  482. static bool hasInit = false;
  483. static MyMethodChannel platform = MyMethodChannel();
  484. // 在列表刷出来后才正式初始化admod等组件
  485. void initThird() async {
  486. if (hasInit) return;
  487. hasInit = true;
  488. // 有了UMP后, 这里的ATT就不需要了
  489. // bool auth = await initATT();
  490. // if (auth) {
  491. // await platform.setHasUserConsent(true);
  492. // await platform.setAdvertiserTrackingEnabled(true);
  493. // }
  494. // await initUMP(); // 征询欧洲用户同意 // applovin max 已经可以自动处理,这里不需要了
  495. TrackingStatus attStatus = await AppTrackingTransparency.trackingAuthorizationStatus;
  496. if (attStatus == TrackingStatus.authorized && Platform.isIOS) {
  497. // ATT 通过之后,ios需要调用相关的原生sdk接口做进一步的初始化
  498. // await platform.setHasUserConsent(true);
  499. // await platform.setAdvertiserTrackingEnabled(true);
  500. }
  501. initFCM(); // 消息推送许可弹窗
  502. initAd(); // admod 的广告加载安排在iOS ATT 之后,以便能够加载到个性化广告
  503. AdjustHelper.init(Persistence().uuid); // 初始化Adjust
  504. final idfa = await AppTrackingTransparency.getAdvertisingIdentifier();
  505. _log.info("idfa: $idfa");
  506. }
  507. /////////////////////////// ATT ///////////////////////////
  508. // Platform messages are asynchronous, so we initialize in an async method.
  509. Future<bool> initATT() async {
  510. TrackingStatus status = await AppTrackingTransparency.trackingAuthorizationStatus;
  511. _log.info('initATT111 $status');
  512. // If the system can show an authorization request dialog
  513. if (status == TrackingStatus.notDetermined) {
  514. // Show a custom explainer dialog before the system dialog
  515. // await showCustomTrackingDialog(context);
  516. // Wait for dialog popping animation
  517. // await Future.delayed(const Duration(milliseconds: 200));
  518. // Request system's tracking authorization dialog
  519. status = await AppTrackingTransparency.requestTrackingAuthorization();
  520. _log.info('initATT222 $status');
  521. }
  522. if (status == TrackingStatus.authorized) {
  523. return true;
  524. }
  525. return false;
  526. }
  527. // no need
  528. Future<void> showCustomTrackingDialog(BuildContext context) async => await showDialog<void>(
  529. context: context,
  530. builder: (context) => AlertDialog(
  531. title: const Text('Dear User'),
  532. content: const Text(
  533. 'We care about your privacy and data security. We keep this app free by showing ads. '
  534. 'Can we continue to use your data to tailor ads for you?\n\nYou can change your choice anytime in the app settings. '
  535. 'Our partners will collect data and use a unique identifier on your device to show you ads.',
  536. ),
  537. actions: [TextButton(onPressed: () => Navigator.pop(context), child: const Text('Continue'))],
  538. ),
  539. );
  540. /////////////////////////////////////////////////////////
  541. /// 初始化广告模块
  542. initAd() {
  543. _log.info('initAd');
  544. // AdsController adsController = context.read<AdsController>();
  545. // adsController.initialize();
  546. ApplovinAdsController applovinAdsController = context.read<ApplovinAdsController>();
  547. applovinAdsController.initialize();
  548. }
  549. /////////////////////////// FCM ///////////////////////////
  550. // 消息推送许可弹框
  551. initFCM() async {
  552. try {
  553. final fcmToken = await FirebaseMessaging.instance.getToken();
  554. _log.info("FCM Token: $fcmToken");
  555. FirebaseMessaging messaging = FirebaseMessaging.instance;
  556. NotificationSettings settings = await messaging.requestPermission(
  557. alert: true,
  558. announcement: false,
  559. badge: true,
  560. carPlay: false,
  561. criticalAlert: false,
  562. provisional: false,
  563. sound: true,
  564. );
  565. _log.warning('User granted permission: ${settings.authorizationStatus}');
  566. } catch (e) {
  567. FirebaseCrashlytics.instance.log("FCM FirebaseMessaging.instance.getToken error: $e");
  568. _log.warning(e);
  569. }
  570. }
  571. }