Commit 6c23b545fdb0dbc00b8b36f597e8bcb5ae41ffb3

Authored by 吴启风
1 parent e26da9e0

fix: 修复友盟隐私合规初始化时机

android/app/src/main/kotlin/com/kouyuxingqiu/wow_english/MainActivity.kt
... ... @@ -11,8 +11,11 @@ import com.gyf.immersionbar.ImmersionBar
11 11 import com.kouyuxingqiu.wow_english.methodChannels.GameMethodChannel
12 12 import com.kouyuxingqiu.wow_english.methodChannels.SingSoungMethodChannel
13 13 import com.umeng.commonsdk.UMConfigure
14   -import io.flutter.embedding.android.FlutterActivity
15 14 import com.umeng.umcrash.UMCrash
  15 +import io.flutter.embedding.android.FlutterActivity
  16 +import io.flutter.embedding.engine.FlutterEngine
  17 +import io.flutter.plugin.common.BinaryMessenger
  18 +import io.flutter.plugin.common.MethodChannel
16 19  
17 20  
18 21 class MainActivity : FlutterActivity() {
... ... @@ -25,7 +28,11 @@ class MainActivity : FlutterActivity() {
25 28 SingSoungMethodChannel(this, it)
26 29 GameMethodChannel(this, it)
27 30 }
28   - initUmeng()
  31 + }
  32 +
  33 + override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
  34 + super.configureFlutterEngine(flutterEngine)
  35 + initUmengMethodChannel(flutterEngine.dartExecutor.binaryMessenger)
29 36 }
30 37  
31 38 override fun onResume() {
... ... @@ -60,11 +67,26 @@ class MainActivity : FlutterActivity() {
60 67 /**
61 68 * 友盟初始化
62 69 */
63   - private fun initUmeng() {
64   - // 友盟集成测试阶段获取UMID
65   -// android.util.Log.d("WQF UMConfigure", UMConfigure.getUMIDString(this))
66   - // 在application.onCreate内配置各模块开关并预初始化SDK
67   - // 重点关注:如果您还想采集Native 崩溃、ANR等日志可以参考下面设置
  70 + private var isUmengPrepared = false
  71 +
  72 + private fun initUmengMethodChannel(binaryMessenger: BinaryMessenger) {
  73 + MethodChannel(binaryMessenger, "wow_english/umeng_method_channel")
  74 + .setMethodCallHandler { call, result ->
  75 + if (call.method == "prepareAfterPrivacyConsent") {
  76 + prepareUmengAfterPrivacyConsent()
  77 + result.success(true)
  78 + } else {
  79 + result.notImplemented()
  80 + }
  81 + }
  82 + }
  83 +
  84 + private fun prepareUmengAfterPrivacyConsent() {
  85 + if (isUmengPrepared) return
  86 +
  87 + // APM 合规要求:preInit、Crash 配置和正式 init 均只能在用户同意后执行。
  88 + UMConfigure.setLogEnabled(false)
  89 + UMConfigure.preInit(this, "663b66b0b3362515012f4ea5", "official")
68 90 UMCrash.initConfig(Bundle().apply {
69 91 putBoolean(UMCrash.KEY_ENABLE_CRASH_JAVA, true)
70 92 putBoolean(UMCrash.KEY_ENABLE_CRASH_NATIVE, true)
... ... @@ -75,8 +97,6 @@ class MainActivity : FlutterActivity() {
75 97 putBoolean(UMCrash.KEY_ENABLE_H5PAGE, false)
76 98 putBoolean(UMCrash.KEY_ENABLE_POWER, false)
77 99 })
78   - //UM日志打印
79   - UMConfigure.setLogEnabled(false)
80   - UMConfigure.preInit(this, "663b66b0b3362515012f4ea5", "official")
  100 + isUmengPrepared = true
81 101 }
82 102 }
... ...
lib/app/splash_page.dart
... ... @@ -23,7 +23,6 @@ import '../common/core/module_cache.dart';
23 23 import '../common/widgets/webview_dialog.dart';
24 24 import '../common/utils/staticstring/userregister.dart';
25 25  
26   -
27 26 class SplashPage extends StatelessWidget {
28 27 const SplashPage({super.key});
29 28  
... ... @@ -43,10 +42,14 @@ class TransitionView extends StatefulWidget {
43 42 }
44 43  
45 44 class _TransitionViewState extends State<TransitionView> {
  45 + bool _isPrivacyDecisionInProgress = false;
  46 + bool _isInitializing = false;
  47 +
46 48 Future startTime() async {
47 49 // 判断是否登录
48 50 UserEntity? userEntity = UserUtil.getUser();
49   - Log.d('当前模式:kDebugMode=$kDebugMode, kProfileMode=$kProfileMode, kReleaseMode=$kReleaseMode');
  51 + Log.d(
  52 + '当前模式:kDebugMode=$kDebugMode, kProfileMode=$kProfileMode, kReleaseMode=$kReleaseMode');
50 53 Log.d('当前API地址:${RequestConfig.baseUrl}');
51 54 //BuildContext context = Navigator.of(context).context;
52 55 //var currentPageName = ModalRoute.of(Navigator.of(AppRouter.context))?.settings.name;
... ... @@ -63,89 +66,151 @@ class _TransitionViewState extends State&lt;TransitionView&gt; {
63 66 apartInMilliseconds = DateTime.now().difference(startTime).inMilliseconds;
64 67 }
65 68 int duration = max(1500 - apartInMilliseconds, 0);
66   - Log.d('Splash getUserInfo 耗时:${apartInMilliseconds}ms, 最终duration=$duration');
  69 + Log.d(
  70 + 'Splash getUserInfo 耗时:${apartInMilliseconds}ms, 最终duration=$duration');
67 71 Timer(Duration(milliseconds: duration), () {
68   - /*if (userEntity != null) {
69   - pushNamedAndRemoveUntil(AppRouteName.home, (route) => false);
70   - } else {
71   - pushNamedAndRemoveUntil(AppRouteName.login, (route) => false);
72   - }*/
73   - bool isAggreementAccepted = AppConfigHelper.getAgreementAccepted();
74   - if (isAggreementAccepted) {
75   - _initData();
76   - } else {
77   - showDialog(
78   - context: context,
79   - barrierDismissible: false,
80   - builder: (BuildContext context) {
81   - return PopScope(
82   - canPop: false,
83   - onPopInvoked: (didPop) {
84   - Log.d('WQF isAggreementAccepted onPopInvoked didPop=$didPop');
85   - },
86   - child: child(),
87   - );
  72 + pushNamedAndRemoveUntil(AppRouteName.home, (route) => false);
  73 + });
  74 + }
  75 +
  76 + void _showPrivacyDialog() {
  77 + showDialog(
  78 + context: context,
  79 + barrierDismissible: false,
  80 + builder: (BuildContext context) {
  81 + return PopScope(
  82 + canPop: false,
  83 + onPopInvoked: (didPop) {
  84 + Log.d('WQF privacy dialog onPopInvoked didPop=$didPop');
88 85 },
  86 + child: child(),
89 87 );
90   - }
91   - });
  88 + },
  89 + );
92 90 }
93 91  
94 92 Widget child() {
95 93 if (Platform.isIOS) {
96 94 return AlertDialog(
97   - title: const Center(
98   - child: Text("服务条款及隐私政策"),
99   - ),
100   - content: SizedBox(
  95 + title: const Center(
  96 + child: Text("服务条款及隐私政策"),
  97 + ),
  98 + content: SizedBox(
101 99 width: MediaQuery.of(context).size.height,
102 100 height: MediaQuery.of(context).size.height,
103   - child: SingleChildScrollView(
  101 + child: const SingleChildScrollView(
104 102 child: Text(UserRegisterString.userPrivacyPolicyStr),
105   - ),),
106   - actions: <Widget>[
107   - Row(
108   - mainAxisAlignment: MainAxisAlignment.spaceBetween,
109   - children: [
110   - TextButton(
111   - child: const Text("同意并继续",
112   - style: TextStyle(color: Color(0xFFFBB621))),
113   - onPressed: () {
114   - AppConfigHelper.saveAgreementAccepted(true);
115   - _initData();
116   - },
117   - ),
118   - TextButton(
119   - child: const Text("不同意,退出应用"),
120   - onPressed: () {
121   - // 退出应用
122   - AppConfigHelper.exitApp();
123   - },
124   - ),
125   - ],
126   - )
127   - ],);
  103 + ),
  104 + ),
  105 + actions: <Widget>[
  106 + Row(
  107 + mainAxisAlignment: MainAxisAlignment.spaceBetween,
  108 + children: [
  109 + TextButton(
  110 + onPressed: _acceptPrivacyAgreement,
  111 + child: const Text("同意并继续",
  112 + style: TextStyle(color: Color(0xFFFBB621))),
  113 + ),
  114 + TextButton(
  115 + onPressed: _rejectPrivacyAgreement,
  116 + child: const Text("不同意,退出应用"),
  117 + ),
  118 + ],
  119 + )
  120 + ],
  121 + );
128 122 }
129 123 return WebviewDialog(
130   - title: "服务条款及隐私政策",
131   - webUrl: AppConsts.userPrivacyPolicyUrl,
132   - leftButtonText: '同意并继续',
133   - rightButtonText: '不同意,退出应用',
134   - leftTap: () {
135   - AppConfigHelper.saveAgreementAccepted(true);
136   - _initData();
137   - },
138   - rightTap: () {
139   - // 退出应用
140   - AppConfigHelper.exitApp();
141   - },
142   - );
  124 + title: "服务条款及隐私政策",
  125 + webUrl: AppConsts.userPrivacyPolicyUrl,
  126 + leftButtonText: '同意并继续',
  127 + rightButtonText: '不同意,退出应用',
  128 + leftTap: _acceptPrivacyAgreement,
  129 + rightTap: _rejectPrivacyAgreement,
  130 + );
  131 + }
  132 +
  133 + Future<void> _acceptPrivacyAgreement() async {
  134 + if (_isPrivacyDecisionInProgress) return;
  135 + _isPrivacyDecisionInProgress = true;
  136 +
  137 + await AppConfigHelper.saveAgreementAccepted(true);
  138 + if (!mounted) return;
  139 +
  140 + // 先关闭隐私政策弹窗,再进行可能耗时的 SDK 和业务初始化。
  141 + Navigator.of(context, rootNavigator: true).pop();
  142 + await _startInitialization();
  143 + _isPrivacyDecisionInProgress = false;
  144 + }
  145 +
  146 + Future<void> _rejectPrivacyAgreement() async {
  147 + if (_isPrivacyDecisionInProgress) return;
  148 + _isPrivacyDecisionInProgress = true;
  149 +
  150 + // 显式撤销本地授权并等待写入完成,避免退出后残留 true。
  151 + await AppConfigHelper.saveAgreementAccepted(false);
  152 + AppConfigHelper.exitApp();
  153 + }
  154 +
  155 + Future<void> _startInitialization() async {
  156 + if (_isInitializing) return;
  157 + _isInitializing = true;
  158 + try {
  159 + await _initData();
  160 + } catch (error, stackTrace) {
  161 + Log.d('Splash初始化失败: $error\n$stackTrace');
  162 + _isInitializing = false;
  163 + if (mounted) {
  164 + _showInitializationErrorDialog();
  165 + }
  166 + }
  167 + }
  168 +
  169 + void _showInitializationErrorDialog() {
  170 + showDialog(
  171 + context: context,
  172 + barrierDismissible: false,
  173 + builder: (dialogContext) => AlertDialog(
  174 + title: const Text('温馨提示'),
  175 + content: const Text('初始化失败,请检查网络后重试'),
  176 + actions: [
  177 + const TextButton(
  178 + onPressed: AppConfigHelper.exitApp,
  179 + child: Text('退出'),
  180 + ),
  181 + TextButton(
  182 + onPressed: () {
  183 + Navigator.of(dialogContext).pop();
  184 + _startInitialization();
  185 + },
  186 + child: const Text('重试'),
  187 + ),
  188 + ],
  189 + ),
  190 + );
143 191 }
144 192  
145 193 ///初始化数据
146   - void _initData() {
147   - UmengCommonSdk.initCommon("663b66b0b3362515012f4ea5", "663b66ecf32cc41105ae74b7", "official");
148   - pushNamedAndRemoveUntil(AppRouteName.home, (route) => false);
  194 + Future<void> _initData() async {
  195 + // 用户同意后,先准备 Native APM,再正式初始化 Common SDK。
  196 + if (Platform.isAndroid) {
  197 + const MethodChannel umengMethodChannel =
  198 + MethodChannel('wow_english/umeng_method_channel');
  199 + await umengMethodChannel.invokeMethod('prepareAfterPrivacyConsent');
  200 + }
  201 + // Android 插件执行 initCommon 后不会回调 MethodChannel result,返回的
  202 + // Future 永远不结束,因此这里只触发初始化,不能 await。
  203 + unawaited(UmengCommonSdk.initCommon(
  204 + "663b66b0b3362515012f4ea5", "663b66ecf32cc41105ae74b7", "official")
  205 + .catchError((error, stackTrace) {
  206 + Log.d('友盟Common SDK初始化失败: $error\n$stackTrace');
  207 + }));
  208 + await AudioPlayerUtil.getInstance()
  209 + .playAudio(AudioPlayerUtilType.welcomeToWow)
  210 + .timeout(const Duration(seconds: 10), onTimeout: () {
  211 + Log.d('欢迎音频播放超时,继续进入首页');
  212 + });
  213 + await startTime();
149 214 }
150 215  
151 216 Future<void> fetchNecessaryData(String userToken,
... ... @@ -164,6 +229,12 @@ class _TransitionViewState extends State&lt;TransitionView&gt; {
164 229 completer.complete();
165 230 } catch (e) {
166 231 debugPrint('Splash获取用户信息异常:$e');
  232 + if (!mounted) {
  233 + if (!completer.isCompleted) {
  234 + completer.complete();
  235 + }
  236 + return;
  237 + }
167 238 //异常的话弹窗提示重试
168 239 showDialog(
169 240 context: context,
... ... @@ -219,17 +290,26 @@ class _TransitionViewState extends State&lt;TransitionView&gt; {
219 290 changeDevice();
220 291 await SpUtil.preInit();
221 292 ModuleCache.instance.init();
222   - await AudioPlayerUtil.getInstance().playAudio(AudioPlayerUtilType.welcomeToWow);
223   - startTime();
  293 + if (AppConfigHelper.getAgreementAccepted()) {
  294 + await _startInitialization();
  295 + } else {
  296 + WidgetsBinding.instance.addPostFrameCallback((_) {
  297 + if (mounted) {
  298 + _showPrivacyDialog();
  299 + }
  300 + });
  301 + }
224 302 }
225 303  
226 304 void changeDevice() async {
227 305 ///设置设备默认方向
228 306 WidgetsFlutterBinding.ensureInitialized();
229 307 if (Platform.isIOS) {
230   - await LimitingDirectionCsx.setScreenDirection(DeviceDirectionMask.Landscape);
  308 + await LimitingDirectionCsx.setScreenDirection(
  309 + DeviceDirectionMask.Landscape);
231 310 } else {
232   - await SystemChrome.setPreferredOrientations([DeviceOrientation.landscapeLeft, DeviceOrientation.landscapeRight]);
  311 + await SystemChrome.setPreferredOrientations(
  312 + [DeviceOrientation.landscapeLeft, DeviceOrientation.landscapeRight]);
233 313 }
234 314 await SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersiveSticky);
235 315 }
... ...
lib/common/core/app_config_helper.dart
1 1 import 'dart:io';
2 2  
3   -import 'package:flutter/cupertino.dart';
4 3 import 'package:flutter/foundation.dart';
5 4 import 'package:flutter/services.dart';
6 5 import 'package:package_info_plus/package_info_plus.dart';
... ... @@ -11,7 +10,6 @@ import &#39;../../utils/sp_util.dart&#39;;
11 10 import '../request/dao/system_dao.dart';
12 11  
13 12 class AppConfigHelper {
14   -
15 13 static const String _prefsKeyAgreementAccepted = "privacy_agreement_accepted";
16 14  
17 15 static AppConfigEntity? configEntityEntity;
... ... @@ -22,7 +20,7 @@ class AppConfigHelper {
22 20 static bool checkedUpdate = false;
23 21  
24 22 // 获取用户信息
25   - static Future<AppConfigEntity?> getAppConfig({ bool forceSync = false }) async {
  23 + static Future<AppConfigEntity?> getAppConfig({bool forceSync = false}) async {
26 24 if (forceSync == false && configEntityEntity != null) {
27 25 return configEntityEntity;
28 26 }
... ... @@ -38,7 +36,8 @@ class AppConfigHelper {
38 36 // 是否需要隐藏...
39 37 static bool shouldHidePay() {
40 38 return isIosPlatform() &&
41   - (configEntityEntity?.isAppReviewing() == true || UserUtil.getUser()?.phoneNum == "17730280759");
  39 + (configEntityEntity?.isAppReviewing() == true ||
  40 + UserUtil.getUser()?.phoneNum == "17730280759");
42 41 }
43 42  
44 43 // 获取app版本号
... ... @@ -51,22 +50,19 @@ class AppConfigHelper {
51 50 String versionCode = packageInfo.buildNumber; // 构建号
52 51 _versionCode = versionCode;
53 52  
54   - debugPrint('versionName=$versionName versionCode=$versionCode platForm=${Platform.operatingSystem}');
  53 + debugPrint(
  54 + 'versionName=$versionName versionCode=$versionCode platForm=${Platform.operatingSystem}');
55 55 return versionCode;
56 56 }
57 57  
58   - static void saveAgreementAccepted(bool accepted) {
59   - SpUtil.getInstance().setData(_prefsKeyAgreementAccepted, accepted);
  58 + static Future<void> saveAgreementAccepted(bool accepted) async {
  59 + await SpUtil.getInstance().setData(_prefsKeyAgreementAccepted, accepted);
60 60 }
61 61  
62 62 static bool getAgreementAccepted() {
63 63 return SpUtil.getInstance().get<bool>(_prefsKeyAgreementAccepted) ?? false;
64 64 }
65 65  
66   - static void _clearUserData() {
67   - SpUtil.getInstance().remove(_prefsKeyAgreementAccepted);
68   - }
69   -
70 66 static void exitApp() {
71 67 if (Platform.isIOS) {
72 68 exit(0);
... ...
lib/main.dart
... ... @@ -8,17 +8,6 @@ import &#39;package:wow_english/app/app.dart&#39;;
8 8  
9 9 import 'common/request/basic_config.dart';
10 10  
11   -// void main() {
12   -// ///设置设备默认方向
13   -// WidgetsFlutterBinding.ensureInitialized();
14   -// if (Platform.isAndroid) {
15   -// SystemChrome.setSystemUIOverlayStyle(const SystemUiOverlayStyle( //设置状态栏透明
16   -// statusBarColor: Colors.transparent,
17   -// ));
18   -// }
19   -// runApp(const App());
20   -// }
21   -
22 11 void main() {
23 12 ///设置设备默认方向
24 13 if (Platform.isAndroid) {
... ... @@ -28,8 +17,8 @@ void main() {
28 17 }
29 18  
30 19 final UmengApmSdk umengApmSdk = UmengApmSdk(
31   - name: "",
32   - bver: "",
  20 + name: '',
  21 + bver: '',
33 22  
34 23 // 是否开启SDK运行时日志输出
35 24 enableLog: BasicConfig.isTestDev,
... ... @@ -56,12 +45,15 @@ void main() {
56 45 },
57 46  
58 47 );
  48 +
  49 + // 此处只安装 Flutter 侧监控能力。Native APM/Common SDK 会保持未初始化状态,
  50 + // 直到用户同意隐私政策后才由 SplashPage 正式启动。
59 51 umengApmSdk.init(appRunner: (observer) async {
60 52 // 确保去掉原有的WidgetsFlutterBinding.ensureInitialized() ,以免出现重复初始化绑定的异常造成无法正常初始化,SDK内部已通过initFlutterBinding入参带入继承的WidgetsFlutterBinding实现初始化操作
61 53 // 依赖ensureInitialized()初始化的代码可在此调用
62 54 // 需要异步获取设置应用名称和版本号可在此回调中操作
63 55 // SDK实例化的设置可先将name和bver 为 "",然后通过以下方式进行设置
64   - PackageInfo packageInfo = await PackageInfo.fromPlatform();
  56 + final PackageInfo packageInfo = await PackageInfo.fromPlatform();
65 57 String packageName = packageInfo.packageName;
66 58 String buildNumber = packageInfo.buildNumber;
67 59 String version = packageInfo.version;
... ...
lib/utils/sp_util.dart
... ... @@ -28,17 +28,17 @@ class SpUtil {
28 28 return _instance!;
29 29 }
30 30  
31   - void setData<T>(String key, T data) {
  31 + Future<void> setData<T>(String key, T data) async {
32 32 if (data is String) {
33   - _prefs?.setString(key, data);
  33 + await _prefs?.setString(key, data);
34 34 } else if (data is double) {
35   - _prefs?.setDouble(key, data);
  35 + await _prefs?.setDouble(key, data);
36 36 } else if (data is int) {
37   - _prefs?.setInt(key, data);
  37 + await _prefs?.setInt(key, data);
38 38 } else if (data is bool) {
39   - _prefs?.setBool(key, data);
  39 + await _prefs?.setBool(key, data);
40 40 } else if (data is List<String>) {
41   - _prefs?.setStringList(key, data);
  41 + await _prefs?.setStringList(key, data);
42 42 }
43 43 }
44 44  
... ...
pubspec.yaml
... ... @@ -113,7 +113,7 @@ dependencies:
113 113 # 应用内更新 https://pub-web.flutter-io.cn/packages/flutter_app_update
114 114 flutter_app_update: ^3.0.4
115 115 # 友盟统计 https://pub-web.flutter-io.cn/packages/umeng_common_sdk
116   - umeng_common_sdk: ^1.2.8
  116 + umeng_common_sdk: ^1.3.1
117 117 # 友盟APM https://pub-web.flutter-io.cn/packages/umeng_apm_sdk
118 118 umeng_apm_sdk: ^2.3.2
119 119 # 嵌套滚动 https://pub.dev/packages/nested_scroll_views
... ...