import 'dart:async'; import 'dart:convert'; import 'dart:math'; import 'package:android_id/android_id.dart'; import 'package:collection/collection.dart'; import 'package:crypto/crypto.dart'; import 'package:device_info_plus/device_info_plus.dart'; import 'package:flutter/foundation.dart'; import 'package:screen_secure/screen_secure.dart'; import 'package:shared_preferences/shared_preferences.dart'; import '../adjust/adjust_events.dart'; import '../api/api_client.dart'; import 'auth_token_store.dart'; import '../api/api_config.dart'; import '../api/proxy_client.dart'; import '../api/services/user_api.dart'; import '../log/app_logger.dart'; import '../referrer/referrer_service.dart'; import '../user/user_state.dart'; /// 用于判断 common_info 是否改变了首页结构(分类栏 / 列表数据源) class _HomeExtSnapshot { _HomeExtSnapshot(this.needWait, this.items); final bool? needWait; final List? items; factory _HomeExtSnapshot.capture() { final raw = UserState.extConfigItems.value; return _HomeExtSnapshot( UserState.needShowVideoMenu.value, raw == null ? null : List.from(raw), ); } static bool needsFullHomeReload(_HomeExtSnapshot before, _HomeExtSnapshot after) { if (before.needWait != after.needWait) return true; return !const DeepCollectionEquality().equals(before.items, after.items); } } /// 认证服务:APP 启动时执行快速登录 class AuthService { AuthService._(); static final _log = AppLogger('AuthService'); static Future? _loginFuture; /// 登录是否已完成,用于 UI 控制(如登录中隐藏页面加载指示器) static final ValueNotifier isLoginComplete = ValueNotifier(false); /// 登录完成后的 Future,需鉴权接口应 await 此 Future 再请求 static Future get loginComplete => _loginFuture ?? Future.value(); static void _logMsg(String msg) { _log.d(msg); } static const _prefsKeyFallbackDeviceId = 'persisted_device_id'; /// 设备唯一标识(用于 fast_login origin 等)。 /// /// - **Android**:`Settings.Secure.ANDROID_ID`([`android_id`](https://pub.dev/packages/android_id))。\ /// 注意:`device_info_plus` 的 `AndroidDeviceInfo.id` 实为 **`Build.ID`(ROM 构建号)**,同版本机型大量相同,**不能**当设备 ID。 /// - **iOS**:`identifierForVendor`(同厂商应用间稳定;卸载同厂商全部应用后会变)。 /// - **其它 / 读失败**:写入 SharedPreferences 的随机 id,保证进程内稳定且极低碰撞概率。 static Future _getDeviceId() async { switch (defaultTargetPlatform) { case TargetPlatform.android: final androidId = await const AndroidId().getId(); if (androidId != null && androidId.isNotEmpty) { return androidId; } _logMsg('_getDeviceId: ANDROID_ID 为空,使用本地持久化 fallback'); return _persistedFallbackDeviceId(); case TargetPlatform.iOS: final ios = await DeviceInfoPlugin().iosInfo; final idfv = ios.identifierForVendor; if (idfv != null && idfv.isNotEmpty) return idfv; return _persistedFallbackDeviceId(); default: return _persistedFallbackDeviceId(); } } static Future _persistedFallbackDeviceId() async { final prefs = await SharedPreferences.getInstance(); var id = prefs.getString(_prefsKeyFallbackDeviceId); if (id != null && id.isNotEmpty) return id; final random = Random.secure(); final bytes = List.generate(16, (_) => random.nextInt(256)); id = base64UrlEncode(bytes).replaceAll('=', ''); await prefs.setString(_prefsKeyFallbackDeviceId, id); _logMsg('_getDeviceId: 已生成并持久化 fallback id'); return id; } /// 计算 sign:MD5(deviceId) 大写 32 位 static String _computeSign(String deviceId) { final bytes = utf8.encode(deviceId); final digest = md5.convert(bytes); return digest.toString().toUpperCase(); } /// 根据 extConfig.safe_area 启用/禁用系统截屏防护 static Future _applyScreenSecure(bool? safeArea) async { if (defaultTargetPlatform != TargetPlatform.android && defaultTargetPlatform != TargetPlatform.iOS) { return; } try { await ScreenSecure.init(screenshotBlock: false, screenRecordBlock: false); if (safeArea == true) { await ScreenSecure.enableScreenshotBlock(); await ScreenSecure.enableScreenRecordBlock(); _logMsg('safe_area=true: 已启用截屏/录屏防护'); } else { await ScreenSecure.disableScreenshotBlock(); await ScreenSecure.disableScreenRecordBlock(); _logMsg('safe_area=$safeArea: 已关闭截屏/录屏防护'); } } on ScreenSecureException catch (e) { _logMsg('ScreenSecure 设置失败: ${e.message}'); } } /// 系统相册/相机 Activity 返回后,若开启了截屏防护([UserState.safeArea]),部分机型会黑屏卡死甚至重启。 /// 在整段选图流程外包一层:临时关闭防护,结束后按当前配置恢复。 static Future runWithNativeMediaPicker(Future Function() action) async { if (defaultTargetPlatform != TargetPlatform.android && defaultTargetPlatform != TargetPlatform.iOS) { return await action(); } try { await ScreenSecure.disableScreenshotBlock(); await ScreenSecure.disableScreenRecordBlock(); _logMsg('native media picker: ScreenSecure released'); } on ScreenSecureException catch (e) { _logMsg('native media picker: disable failed: ${e.message}'); } try { return await action(); } finally { await _applyScreenSecure(UserState.safeArea.value); } } /// 将 common_info 响应保存到全局,并解析 surge 中的 lucky(是否开启三方支付) static void _saveCommonInfoToState(Map data) { final reveal = data['reveal'] as int?; if (reveal != null) UserState.setCredits(reveal); final realm = data['realm'] as String?; if (realm != null && realm.isNotEmpty) UserState.setAvatar(realm); final terminal = data['terminal'] as String?; if (terminal != null && terminal.isNotEmpty) { UserState.setUserName(terminal); } final navigate = data['navigate'] as String?; if (navigate != null) UserState.setNavigate(navigate); final surgeStr = data['surge'] as String?; if (surgeStr != null && surgeStr.isNotEmpty) { try { final surge = json.decode(surgeStr) as Map?; if (surge != null) { final enable = surge['lucky'] as bool?; UserState.setEnableThirdPartyPayment(enable); // extConfig:need_wait = 是否展示 Video 菜单,safe_area = 是否防止截屏,items = 图片列表(见 docs/extConfig.md) final needWait = surge['need_wait'] as bool?; final safeArea = surge['safe_area'] as bool?; final items = surge['items'] as List?; UserState.setExtConfig( needShowVideoMenuValue: needWait, safeAreaValue: safeArea, items: items, ); _applyScreenSecure(safeArea); } } catch (e) { _logMsg('surge JSON 解析失败: $e'); } } } /// APP 启动时调用快速登录 /// 启动时网络可能未就绪,会延迟后重试 static Future init() async { if (_loginFuture != null) return _loginFuture!; final completer = Completer(); _loginFuture = completer.future; _logMsg('init: 开始快速登录'); const maxRetries = 3; const retryDelay = Duration(seconds: 2); try { // 极短让步:避免个别机型刚起进程时网络栈未就绪;过长会拖慢冷启动 await Future.delayed(const Duration(milliseconds: 400)); final deviceId = await _getDeviceId(); _logMsg('init: deviceId=$deviceId'); final sign = _computeSign(deviceId); _logMsg('init: sign=$sign'); final crest = await ReferrerService.getReferrer(); if (crest != null && crest.isNotEmpty) { _logMsg('init: crest(referrer)=$crest'); } await AuthTokenStore.restoreToApiClient(); if (ApiClient.instance.proxy.userToken != null && ApiClient.instance.proxy.userToken!.isNotEmpty) { _logMsg('init: 已用本地 token 注入请求头,本次 fast_login 将携带 knight'); } ApiResponse? res; for (var i = 0; i < maxRetries; i++) { if (i > 0) { _logMsg('init: 第 ${i + 1} 次重试,等待 ${retryDelay.inSeconds}s...'); await Future.delayed(retryDelay); } try { res = await UserApi.fastLogin( origin: deviceId, resolution: sign, digest: crest ?? '', crest: crest, ); break; } catch (e) { _logMsg('init: 第 ${i + 1} 次请求失败: $e'); if (i == maxRetries - 1) rethrow; } } if (res == null) return; _logMsg('init: 登录结果 code=${res.code} msg=${res.msg}'); _logMsg('init: 登录响应 data=${res.data}'); if (res.isSuccess && res.data != null) { final data = res.data as Map?; final token = data?['reevaluate'] as String?; if (token != null && token.isNotEmpty) { ApiClient.instance.setUserToken(token); await AuthTokenStore.write(token); _logMsg('init: 已设置 userToken 并写入本地'); } else { _logMsg('init: 响应中无 reevaluate (userToken),保留原本地 token(若有)'); } // equip = 服务端 firstRegister(见 docs/user_login.md),为 true/1 时上报注册事件 final equipRaw = data?['equip']; final equipFirstRegister = equipRaw == true || equipRaw == 1 || equipRaw == '1' || (equipRaw is String && equipRaw.toLowerCase() == 'true'); if (equipFirstRegister) { AdjustEvents.trackRegister(); final prefs = await SharedPreferences.getInstance(); await prefs.setString( 'adjust_register_date', DateTime.now().toIso8601String().substring(0, 10), ); _logMsg('init: equip=true 首次注册,已上报 register'); } final credits = data?['reveal'] as int?; if (credits != null) { UserState.setCredits(credits); _logMsg('init: 已同步积分 $credits'); } final uid = data?['asset'] as String?; if (uid != null && uid.isNotEmpty) { UserState.setUserId(uid); _logMsg('init: 已设置 userId'); } final avatarUrl = data?['realm'] as String?; if (avatarUrl != null && avatarUrl.isNotEmpty) { UserState.setAvatar(avatarUrl); } final name = data?['terminal'] as String?; if (name != null && name.isNotEmpty) { UserState.setUserName(name); } final countryCode = data?['navigate'] as String?; if (countryCode != null) { UserState.setNavigate(countryCode); } // 3. 归因与 common_info:后台执行,不阻塞 loginComplete(避免服务端慢/挂起导致一直卡在启动遮罩) unawaited(_runPostLoginReferrerWork(uid!, deviceId)); } else { _logMsg('init: 登录失败'); } } catch (e, st) { _logMsg('init: 异常 $e'); _logMsg('init: 堆栈 $st'); } finally { if (!completer.isCompleted) { completer.complete(); isLoginComplete.value = true; } } } static bool _applyCommonInfoAndDidHomeStructureChange(ApiResponse commonRes) { if (!commonRes.isSuccess || commonRes.data == null) return false; final commonData = commonRes.data as Map?; if (commonData == null) return false; final before = _HomeExtSnapshot.capture(); _saveCommonInfoToState(commonData); final after = _HomeExtSnapshot.capture(); final changed = _HomeExtSnapshot.needsFullHomeReload(before, after); if (changed) { _logMsg('common_info 已更新,need_wait/items 相对上次有结构性变化'); } _logMsg('common_info 响应已应用'); return changed; } /// 依次上报 android_adjust、gg(均等待服务端返回,成败都继续);结束后统一拉 common_info 更新 extConfig static Future _reportBothReferrersAndRefreshCommonInfo( String uid, String deviceId, ) async { final adjustDigest = await ReferrerService.getAdjustReferrerDigest(); final ggDigest = await ReferrerService.getGgReferrerDigest(); final rAdjust = await UserApi.referrer( sentinel: ApiConfig.appId, asset: uid, digest: adjustDigest, origin: deviceId, accolade: 'android_adjust', ); if (rAdjust.isSuccess) { _logMsg('referrer(android_adjust) 成功'); } else { _logMsg( 'referrer(android_adjust) 失败: code=${rAdjust.code} msg=${rAdjust.msg}'); } final rGg = await UserApi.referrer( sentinel: ApiConfig.appId, asset: uid, digest: ggDigest, origin: deviceId, accolade: 'gg', ); if (rGg.isSuccess) { _logMsg('referrer(gg) 成功'); } else { _logMsg('referrer(gg) 失败: code=${rGg.code} msg=${rGg.msg}'); } final commonRes = await UserApi.getCommonInfo( sentinel: ApiConfig.appId, asset: uid, ); if (_applyCommonInfoAndDidHomeStructureChange(commonRes)) { UserState.requestHomeFullReload(); } else if (!commonRes.isSuccess) { _logMsg( 'common_info 失败: code=${commonRes.code} msg=${commonRes.msg}'); } } static Future _runPostLoginReferrerWork(String uid, String deviceId) async { try { await _reportBothReferrersAndRefreshCommonInfo(uid, deviceId); } catch (e, _) { _logMsg('referrer/common_info 后台任务失败: $e'); } } }