import 'dart:async'; import 'dart:convert'; import 'dart:math'; import 'package:android_id/android_id.dart'; import 'package:client_proxy_framework/client_proxy_framework.dart'; import 'package:crypto/crypto.dart' show md5; 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 '../user/user_state.dart'; const _prefsKeyFallbackDeviceId = 'persisted_device_id'; 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); return id; } class AppAuthCallbacks implements AuthServiceCallbacks { /// 与 app_client 一致:Android 用 Settings.Secure.ANDROID_ID(android_id 包,非 Build.ID); /// iOS 用 identifierForVendor;失败或其它平台用 SharedPreferences 持久化随机 id。 @override Future getDeviceId() async { switch (defaultTargetPlatform) { case TargetPlatform.android: final androidId = await const AndroidId().getId(); if (androidId != null && androidId.isNotEmpty) { return androidId; } 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(); } } @override String computeSign(String deviceId) { return md5.convert(utf8.encode(deviceId)).toString().toUpperCase(); } @override void onLoginSuccess(FastLoginResponse data) { UserState.applyLogin( userId: data.userId, credits: data.credits, avatar: data.avatar, userName: data.userName, ); unawaited( AnalyticsEvents.trackRegisterIfNeeded( firstRegister: data.firstRegister == true, ), ); } @override void onCommonInfoLoaded(CommonInfoResponse data) { if (data.credits != null) UserState.setCredits(data.credits!); if (data.avatar != null) { final t = data.avatar!.trim(); UserState.setAvatar(t.isEmpty ? null : t); } if (data.userName != null) UserState.setUserName(data.userName!); } @override void onLoginFailed(String msg) { if (kDebugMode) { debugPrint('[AuthService] Login failed: $msg'); } } } /// 应用侧登录封装;Adjust 归因桥已由框架 [FrameworkAuthService.init] 默认注册。 class AuthService { static final _authCallbacks = AppAuthCallbacks(); static var _screenSecureListenerBound = false; /// [ExtConfigRuntime.data] 初次为 `null` 时触发的 `_applyScreenSecure(null)` 与后续 /// common_info 触发的 `_applyScreenSecure(…)` 均为异步:若前者后完成会错误地关闭防护,故用序号丢弃过期任务。 static int _screenSecureApplyGeneration = 0; static Future init() async { FrameworkAuthService.init(_authCallbacks); _bindScreenSecureToExtConfig(); await FrameworkAuthService.start(); } /// 与 [app_client] `AuthService.runWithNativeMediaPicker` 一致:系统相册/相机返回后若开启防截屏, /// 部分机型会黑屏;选图前后临时关闭防护,结束后再按 [ExtConfigRuntime] 恢复。 static Future runWithNativeMediaPicker(Future Function() action) async { if (defaultTargetPlatform != TargetPlatform.android && defaultTargetPlatform != TargetPlatform.iOS) { return await action(); } try { await ScreenSecure.disableScreenshotBlock(); await ScreenSecure.disableScreenRecordBlock(); } on ScreenSecureException catch (e) { if (kDebugMode) { debugPrint( '[AuthService] native media picker: disable failed: ${e.message}', ); } } try { return await action(); } finally { unawaited(_applyScreenSecure(ExtConfigRuntime.data.value)); } } static void _bindScreenSecureToExtConfig() { if (_screenSecureListenerBound) return; _screenSecureListenerBound = true; void listener() { unawaited(_applyScreenSecure(ExtConfigRuntime.data.value)); } ExtConfigRuntime.data.addListener(listener); // 勿在此处同步调用 listener():`data == null` 的 disable 若晚于 common_info 的 enable 完成,会把防截屏关掉。 } /// [ExtConfigData]:`screen` / `safe_area` 等键在 [ExtConfigKeySchema] 下解析为 [ExtConfigData.forbidScreenshot]; /// 为 `true` 时启用系统截屏/录屏防护(与 app_client `safe_area` 语义对齐)。 static Future _applyScreenSecure(ExtConfigData? ext) async { if (defaultTargetPlatform != TargetPlatform.android && defaultTargetPlatform != TargetPlatform.iOS) { return; } final gen = ++_screenSecureApplyGeneration; final block = ext?.forbidScreenshot == true; try { await ScreenSecure.init(screenshotBlock: false, screenRecordBlock: false); if (gen != _screenSecureApplyGeneration) return; if (block) { await ScreenSecure.enableScreenshotBlock(); await ScreenSecure.enableScreenRecordBlock(); } else { await ScreenSecure.disableScreenshotBlock(); await ScreenSecure.disableScreenRecordBlock(); } if (gen != _screenSecureApplyGeneration) return; if (kDebugMode && block) { debugPrint('[AuthService] ScreenSecure: enabled (forbidScreenshot)'); } } on ScreenSecureException catch (e) { if (kDebugMode) { debugPrint('[AuthService] ScreenSecure: ${e.message}'); } } } static Future get loginComplete => FrameworkAuthService.loginComplete; /// 登录流程是否已结束(含 fast_login + common_info 链路);用于遮罩,勿用 [loginComplete] 的 Future(首帧可能为 null)。 static ValueNotifier get isLoginComplete => FrameworkAuthService.isLoginComplete; /// 在 **登录成功**([UserState.userId] 非空)且 [isLoginComplete] 为 `true` 之后执行 [onReady]。 /// /// 若登录流程已结束但无用户(失败),调用一次 [onFailed]。若 [isLoginComplete] 已为 `true`,同步执行。 /// /// 返回在 [State.dispose] 中调用的取消函数,避免界面已销毁仍监听。 static VoidCallback whenLoginSucceeded({ required VoidCallback onReady, VoidCallback? onFailed, }) { void runOnce() { final uid = UserState.userId.value; if (uid != null && uid.isNotEmpty) { onReady(); } else { onFailed?.call(); } } void listener() { if (!isLoginComplete.value) return; isLoginComplete.removeListener(listener); runOnce(); } if (isLoginComplete.value) { runOnce(); return () {}; } isLoginComplete.addListener(listener); return () => isLoginComplete.removeListener(listener); } }