291 lines
9.6 KiB
Dart
291 lines
9.6 KiB
Dart
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=xxx;Android 其次读 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-Id:iOS 优先 IDFV(per-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;
|
||
}
|