Files
jinzhi/lib/common/services/device_header_service.dart
T
2026-06-23 10:07:35 +08:00

291 lines
9.6 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import 'dart:io' show Platform;
import 'package:android_id/android_id.dart';
import 'package:android_oaid/android_oaid.dart';
import 'package:device_info_plus/device_info_plus.dart';
import 'package:flutter/foundation.dart' show kIsWeb;
import 'package:flutter_paid/flutter_paid.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'package:platform_metadata/platform_metadata.dart';
import 'package:uuid/uuid.dart';
import '../storage/storage_keys.dart';
import '../storage/storage_service.dart';
/// 请求公共 + 平台身份 header 来源(完全对齐研值契约)。
/// AuthInterceptor 每个请求注入 commonHeaders() + platformIdentityHeaders()
/// token 另由拦截器注入。空值 header 一律过滤。
///
/// 注:`User-Agent` 这里不表示浏览器 UA,而是沿用后端请求头契约中的平台标识,
/// 值同:iOS/Android/Web/Other。
///
/// 广告/设备标识(OAID/IMEI/MAC/IDFA/Paid/DeviceToken)由各自 SDK/ATT 流程
/// 写入 StorageService,这里读出注入;Android 标识仅在用户同意隐私后采集。
class DeviceHeaderService {
DeviceHeaderService._();
static final DeviceHeaderService to = DeviceHeaderService._();
static const _storage = FlutterSecureStorage();
static const _kDeviceIdKey = 'device_id';
// static const _platformMetadataChannel = MethodChannel('platform_metadata');
// 渠道:优先编译期 --dart-define=UMENG_CHANNEL=xxxAndroid 其次读 manifest
static const _umengChannel = String.fromEnvironment('UMENG_CHANNEL');
String _deviceId = '';
String _version = '';
String _build = '';
String _deviceModel = '';
String _userChannel = '';
String _iosIdfv = '';
String _iosPaid = '';
String _androidId = '';
String _androidOaid = '';
String _androidImei = '';
String _androidMac = '';
IosDeviceInfo? iosDeviceInfo;
AndroidDeviceInfo? androidDeviceInfo;
Future<void>? _baseInitialization;
Future<void>? _identityInitialization;
Future<void> ensureInitialized() async {
_baseInitialization ??= _initializeBase();
await _baseInitialization;
// Align with tradingagents-flutter-app: Android only initializes device
// information after the user has accepted the privacy policy.
if (!kIsWeb &&
Platform.isAndroid &&
!StorageService.to.getBool(storagePrivacyAgreed)) {
return;
}
_identityInitialization ??= _initializeIdentity();
await _identityInitialization;
}
Future<void> _initializeBase() async {
try {
final info = await PackageInfo.fromPlatform();
_version = info.version;
_build = info.buildNumber;
} catch (_) {}
_userChannel = await _resolveUserChannel();
}
Future<void> _initializeIdentity() async {
_deviceId = await _resolveDeviceId();
_androidOaid = StorageService.to.getString(storageAndroidOaid) ?? '';
_androidImei = StorageService.to.getString(storageAndroidImei) ?? '';
_androidMac = StorageService.to.getString(storageAndroidMac) ?? '';
_androidId = StorageService.to.getString(storageAndroidId) ?? '';
_iosPaid = StorageService.to.getString(storageIosPaid) ?? '';
await _resolvePlatformIdentity();
}
/// X-Device-IdiOS 优先 IDFVper-vendor 稳定),否则 UUIDv4
/// 首次生成后写入 secure_storage,后续复用。
Future<String> _resolveDeviceId() async {
final stored = await _storage.read(key: _kDeviceIdKey);
if (stored != null && stored.isNotEmpty) return stored;
var id = '';
if (!kIsWeb && Platform.isIOS) {
id = await _resolveIdfv();
}
if (id.isEmpty) id = const Uuid().v4();
await _storage.write(key: _kDeviceIdKey, value: id);
return id;
}
Future<String> _resolveIdfv() async {
try {
final idfv = (await DeviceInfoPlugin().iosInfo).identifierForVendor;
return idfv ?? '';
} catch (_) {
return '';
}
}
/// 设备型号 + 平台身份标识。Android 标识需用户同意隐私后才采集。
Future<void> _resolvePlatformIdentity() async {
if (kIsWeb) return;
try {
final plugin = DeviceInfoPlugin();
if (Platform.isAndroid) {
_deviceModel = (await plugin.androidInfo).model;
await _collectAndroidIdIfAgreed();
await _collectAndroidOaidIfAgreed();
await _collectAndroidImeiIfAgreed();
await _collectAndroidMacIfAgreed();
} else if (Platform.isIOS) {
_deviceModel = (await plugin.iosInfo).utsname.machine;
_iosIdfv = await _resolveIdfv();
await _collectIosPaidIfMissing();
}
await getDeviceInfo();
} catch (_) {}
}
Future<void> getDeviceInfo() async {
try {
final deviceInfoPlugin = DeviceInfoPlugin();
if (Platform.isIOS) {
iosDeviceInfo = await deviceInfoPlugin.iosInfo;
} else if (Platform.isAndroid) {
androidDeviceInfo = await deviceInfoPlugin.androidInfo;
}
} catch (e) {
print('$e');
}
}
Future<void> _collectAndroidIdIfAgreed() async {
if (_androidId.isNotEmpty) return;
if (!StorageService.to.getBool(storagePrivacyAgreed)) return; // 隐私同意后才采集
try {
_androidId = await const AndroidId().getId() ?? '';
if (_androidId.isNotEmpty) {
await StorageService.to.setString(storageAndroidId, _androidId);
}
} catch (_) {}
}
Future<void> _collectAndroidOaidIfAgreed() async {
if (_androidOaid.isNotEmpty) return;
if (!StorageService.to.getBool(storagePrivacyAgreed)) return;
try {
_androidOaid = await const AndroidOaidId().getOaid() ?? '';
if (_androidOaid.isNotEmpty) {
await StorageService.to.setString(storageAndroidOaid, _androidOaid);
}
} catch (_) {}
}
Future<void> _collectAndroidImeiIfAgreed() async {
if (_androidImei.isNotEmpty) return;
if (!StorageService.to.getBool(storagePrivacyAgreed)) return;
try {
_androidImei = await const AndroidId().getImei() ?? '';
if (_androidImei.isNotEmpty) {
await StorageService.to.setString(storageAndroidImei, _androidImei);
}
} catch (_) {}
}
Future<void> _collectAndroidMacIfAgreed() async {
if (_androidMac.isNotEmpty) return;
if (!StorageService.to.getBool(storagePrivacyAgreed)) return;
try {
_androidMac = await const AndroidId().getMaAddress() ?? '';
if (_androidMac.isNotEmpty) {
await StorageService.to.setString(storageAndroidMac, _androidMac);
}
} catch (_) {}
}
Future<void> _collectIosPaidIfMissing() async {
if (_iosPaid.isNotEmpty) return;
try {
_iosPaid = await FlutterPaid().paid() ?? '';
if (_iosPaid.isNotEmpty) {
await StorageService.to.setString(storageIosPaid, _iosPaid);
}
} catch (_) {}
}
/// 用户同意隐私协议后调用。设备信息由随后调用的 ensureInitialized 补采。
Future<void> markPrivacyAgreed() async {
await StorageService.to.setBool(storagePrivacyAgreed, true);
}
/// 是否需要首启隐私合规确认。**仅 Android**(合规要求:同意前不得采集设备
/// 标识)。由 go_router redirect 调用,未同意则强制跳隐私页。
bool get privacyConsentRequired =>
!kIsWeb &&
Platform.isAndroid &&
!StorageService.to.getBool(storagePrivacyAgreed);
/// iOS 首次启动页:仅在首次安装后展示一次。
bool get iosStartupRequired =>
!kIsWeb &&
Platform.isIOS &&
!StorageService.to.getBool(storageIosStartupSeen);
Future<void> markIosStartupSeen() async {
if (kIsWeb || !Platform.isIOS) return;
await StorageService.to.setBool(storageIosStartupSeen, true);
}
String get deviceId => _deviceId;
// User-Platform:平台标识(替代研值的 User-Agent)。
String _platform() {
if (kIsWeb) return 'Web';
if (Platform.isIOS) return 'iOS';
if (Platform.isAndroid) return 'Android';
return 'Other';
}
Future<String> _resolveUserChannel() async {
if (_umengChannel.isNotEmpty) return _umengChannel;
if (!kIsWeb && Platform.isAndroid) {
try {
final manifestChannel = await PlatformMetadata.getMetaDataValue(
'UMENG_CHANNEL',
);
if (manifestChannel != null && manifestChannel.isNotEmpty) {
return manifestChannel;
}
} catch (_) {}
}
if (kIsWeb) return 'web';
if (Platform.isIOS) return 'ios';
if (Platform.isAndroid) return 'android';
return 'other';
}
String _s(String key) => StorageService.to.getString(key) ?? '';
/// 公共 header(不含 token)。11
Map<String, String> commonHeaders() => _compact({
'X-Device-Id': _deviceId,
'User-Version': _version,
'User-BuildID': _build,
'User-Platform': _platform(),
'User-Channel': _userChannel,
'User-DeviceType': _deviceModel,
'User-DeviceToken': privacyConsentRequired ? '' : _s(storageDeviceToken),
});
/// 平台身份 / 广告标识 header(按平台)。
Map<String, String> platformIdentityHeaders() {
if (kIsWeb) return const {};
if (Platform.isAndroid) {
if (!StorageService.to.getBool(storagePrivacyAgreed)) return const {};
return _compact({
'User-Android-ID': _androidId,
'User-Android-OAID': _androidOaid,
'User-Android-IMEI': _androidImei,
'User-Android-Mac': _androidMac,
});
}
if (Platform.isIOS) {
return _compact({
'User-iOS-IDFV': _iosIdfv,
'User-iOS-IDFA': _s(storageIosIdfa),
'User-iOS-Paid': _iosPaid,
});
}
return const {};
}
}
Map<String, String> _compact(Map<String, String> headers) {
final out = <String, String>{};
headers.forEach((k, v) {
if (v.trim().isNotEmpty) out[k] = v;
});
return out;
}