Files
2026-09-15 15:44:13 +07:00

324 lines
12 KiB
Dart
Raw Permalink 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:convert';
import 'dart:io';
import 'package:crypto/crypto.dart';
import 'package:device_identity/device_identity.dart';
import 'package:device_info_plus/device_info_plus.dart';
import 'package:flutter/services.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:hgdj/extension/extensions.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'package:path_provider/path_provider.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:uuid/uuid.dart';
import '../config/config.dart';
import '../hj_utils/light_model.dart';
import '../hj_utils/store_keys.dart';
import '../hj_utils/text_util.dart';
//设备信息管理类
class DeviceInfoService {
/// iOS Keychain 存 DevID。同一个 bundle id 卸载重装后 Keychain 项仍然存在(iOS
/// 沙盒清理不动 Keychain) → 同账号身份。**没声明 Keychain Sharing access group
/// 不同 bundle id 马甲互相读不到对方的 DevID(各自独立),这是产品取舍。**
static const _kKeychainDevIdKey = 'hgdj_device_id_v1';
static const _secureStorage = FlutterSecureStorage(
iOptions: IOSOptions(
accessibility: KeychainAccessibility.first_unlock,
),
);
static final DeviceInfoPlugin _infoPlugin = DeviceInfoPlugin();
static String? _brand;
static String? _model;
static String? _deviceId;
static String? _deviceOS;
static String? _systemVersion;
static String? _devType;
static String? _ua;
static String? _buildID;
//剪切板缓存(iOS 每次读取会弹系统提示,全局只读一次;缓存 Future 防并发重复触发)
static Future<String?>? _clipboardFuture;
/// 获取剪切板文本,全局只触发一次系统读取
static Future<String?> getClipboardTextCached() {
return _clipboardFuture ??= _readClipboardOnce();
}
static Future<String?> _readClipboardOnce() async {
try {
final data = await Clipboard.getData(Clipboard.kTextPlain);
return data?.text;
} catch (_) {
return null;
}
}
/// 获取品牌
static String get brand => _brand ?? "";
/// 获取型号
static String get model => _model ?? "";
/// 设备唯一 ID(APP 内保持不变)
static String get deviceId => _deviceId ?? "";
/// 设备平台
static String get deviceOS => _deviceOS ?? "";
//系统版本
static String get systemVersion => _systemVersion ?? "";
//拼接参数
static String get devType => _devType ?? "";
static String get ua => _ua ?? "";
/// 修正后的 pkgNameiOS 用 TF 内部包名常量,Android 用真实包名
static String get buildID => _buildID ?? "";
/// ios tf 的内部包名
/// note_this:不要修改这个值
static const iosBundleId = "yinse_opera";
static const naticeChannel = 'com.yinse/device'; //原生通道(设备/视频/编解码信息)
static const requestChannel =
'com.yinse/request'; //原生系统桥(现只剩换启动图标 changeIcon)
/// 外部调用入口
static Future<void> init() async {
// buildID 初始化一次,后续都用缓存
if (Platform.isIOS) {
_buildID = iosBundleId;
} else {
final pkg = await PackageInfo.fromPlatform();
_buildID = pkg.packageName;
}
// 设备ID:多重持久化(SP + 私有目录文件),任一存在即复用
_deviceId = await _resolveDeviceId();
if (Platform.isAndroid) {
final android = await _infoPlugin.androidInfo;
_brand = android.brand;
_model = android.model;
_deviceOS = "Android";
_systemVersion = android.version.release;
_devType = "${android.device}:${android.version.sdkInt}:id=${android.id}";
} else if (Platform.isIOS) {
final ios = await _infoPlugin.iosInfo;
_brand = "Apple";
_model = ios.utsname.machine; // 如 iPhone14,2
_deviceOS = "iOS";
_systemVersion = ios.utsname.release;
_devType = "${ios.systemName}:$_systemVersion";
} else {
_brand = "Unknown";
_model = "Unknown";
_deviceOS = Platform.operatingSystem;
_systemVersion = 'Unknown';
}
_ua = genUserAgent();
}
/// 拼装 UA。[deviceId] 可覆盖默认 deviceId
static String genUserAgent({String? deviceId}) {
final Map<String, String?> params = {
"DevID": (deviceId == null || deviceId.isEmpty)
? DeviceInfoService.deviceId
: deviceId,
"DevType": DeviceInfoService.devType,
"SysType": Platform.operatingSystem,
"Ver": Config.innerVersion,
"BuildID": DeviceInfoService.buildID,
'DeviceBrand': DeviceInfoService.brand,
'SystemName': DeviceInfoService.deviceOS,
'SystemVersion': DeviceInfoService.systemVersion,
'device_model': DeviceInfoService.model,
};
params.removeWhere((key, value) => value == null || value.isEmpty);
final ua = Uri.encodeComponent(
params.entries.map((e) => "${e.key}=${e.value}").join(";"));
return ua;
}
/// 获取带缓存的 UA。
/// - [deviceId] 为空:命中 KV 缓存直接返回;
/// - [deviceId] 非空:跳过缓存,按当前最新设备信息重新生成并写回 KV(用于扫码登录、设备切换等场景)。
/// 实际写入 UA 的 DevID 字段:deviceId 非空用它,否则 fallback 到 [DeviceInfoService.deviceId]。
static Future<String> getCachedUserAgent({String? deviceId}) async {
final oldUa = (await lightKV.getString(StoreKeys.UA_CACHE)) ?? '';
if (TextUtil.isEmpty(deviceId) && TextUtil.isNotEmpty(oldUa)) {
return oldUa;
}
final newUa = genUserAgent(deviceId: deviceId);
if (TextUtil.isNotEmpty(newUa))
lightKV.setString(StoreKeys.UA_CACHE, newUa);
return newUa;
}
/// 清除 UA 缓存
static Future clearCachedUserAgent() =>
lightKV.setString(StoreKeys.UA_CACHE, null);
/// 从剪切板 json 里取推广 tid,取不到返回空串
static Future<String> fetchTraceId() async {
final clipboardText = await getClipboardTextCached();
if (TextUtil.isEmpty(clipboardText)) return '';
try {
return jsonDecode(clipboardText!)['tid'] ?? '';
} catch (_) {
return '';
}
}
/// 已知 ANDROID_ID 脏值(厂商定制 ROM / 早期山寨机长期出现)
static const _dirtyAndroidIds = <String>{
'9774d56d682e549c', // Android 4.2 之前山寨机著名脏值
'0000000000000000',
'ffffffffffffffff',
'0123456789abcdef',
};
/// 解析设备 ID:按平台分两条独立路径,任一命中都回写到该平台的所有存储层。
///
/// iOS Keychain 是唯一**跨卸载 + 跨 bundle id** 持久的存储(配合 Keychain Sharing)。
/// SP/文件在 app 沙盒里,卸载就抹;放最高优先级用 Keychain 才能保住账号身份。
/// Android 没有等价机制,applicationId 一变就是新 app,账号无法共享(产品取舍)。
static Future<String> _resolveDeviceId() async {
final prefs = await SharedPreferences.getInstance();
return Platform.isIOS ? _resolveIOS(prefs) : _resolveAndroid(prefs);
}
/// iOSKeychain → SP → 文件 → 新生成,命中即回写三层(Keychain+SP+文件)
static Future<String> _resolveIOS(SharedPreferences prefs) async {
// 1. Keychain 优先(卸载重装、马甲切换都能拿到原 DevID)
try {
final kid = await _secureStorage.read(key: _kKeychainDevIdKey);
if (TextUtil.isNotEmpty(kid)) return _persistIOS(prefs, kid!);
} catch (_) {
// Keychain 偶发失败不阻塞启动,继续走 SP/文件兜底
}
// 2. SP 命中
final spId = prefs.getString(StoreKeys.DEVICE_ID);
if (TextUtil.isNotEmpty(spId)) return _persistIOS(prefs, spId!);
// 3. 文件命中
final fileId = await _readIdFile();
if (TextUtil.isNotEmpty(fileId)) return _persistIOS(prefs, fileId!);
// 4. 全新生成
return _persistIOS(prefs, await _generateDeviceId());
}
/// Android:SP → 文件 → 新生成,命中即回写两层(SP+文件),无 Keychain 等价机制
static Future<String> _resolveAndroid(SharedPreferences prefs) async {
// 1. SP 命中
final spId = prefs.getString(StoreKeys.DEVICE_ID);
if (TextUtil.isNotEmpty(spId)) return _persistAndroid(prefs, spId!);
// 2. 文件命中
final fileId = await _readIdFile();
if (TextUtil.isNotEmpty(fileId)) return _persistAndroid(prefs, fileId!);
// 3. 全新生成
return _persistAndroid(prefs, await _generateDeviceId());
}
/// iOS 回写三层,保证 Keychain/SP/文件一致
static Future<String> _persistIOS(SharedPreferences prefs, String id) async {
await prefs.setString(StoreKeys.DEVICE_ID, id);
await _writeIdFile(id);
await _trySaveKeychain(id);
return id;
}
/// Android 回写 SP + 文件
static Future<String> _persistAndroid(
SharedPreferences prefs, String id) async {
await prefs.setString(StoreKeys.DEVICE_ID, id);
await _writeIdFile(id);
return id;
}
/// 写 Keychain 失败不抛错(权限/entitlements 配错时降级走 SP+文件,不卡启动)
static Future<void> _trySaveKeychain(String id) async {
try {
await _secureStorage.write(key: _kKeychainDevIdKey, value: id);
} catch (_) {}
}
/// 生成新设备 ID
/// AndroidOAID → ANDROID_ID → UUID 兜底
/// iOSidentifierForVendorIDFV)→ UUID 兜底
static Future<String> _generateDeviceId() async {
String raw = '';
if (Platform.isAndroid) {
try {
await DeviceIdentity.register();
raw = await DeviceIdentity.oaid;
if (_isInvalidId(raw)) {
raw = await DeviceIdentity.androidId;
}
} catch (_) {}
} else if (Platform.isIOS) {
try {
final ios = await _infoPlugin.iosInfo;
raw = ios.identifierForVendor ?? '';
} catch (_) {}
}
if (_isInvalidId(raw)) raw = const Uuid().v4();
// 业务规则:hgdj_ 前缀,去空格,限制 50 字符(日志系统约束)
return 'hgdj_${raw.replaceAll(' ', '')}'.limit(50);
}
/// 脏值检测:空 / 太短 / 全 0 / 已知脏值
static bool _isInvalidId(String? id) {
if (TextUtil.isEmpty(id)) return true;
final v = id!.toLowerCase();
if (v.length < 8) return true;
if (v.hasManyZeros()) return true;
return _dirtyAndroidIds.contains(v);
}
/// 写入应用支持目录文件(零权限,卸载会丢,但与 SP 互为冗余)
static Future<void> _writeIdFile(String id) async {
try {
final dir = await getApplicationSupportDirectory();
final file = File('${dir.path}/.device_id');
await file.writeAsString(id, flush: true);
} catch (_) {}
}
/// 读取应用支持目录文件
static Future<String?> _readIdFile() async {
try {
final dir = await getApplicationSupportDirectory();
final file = File('${dir.path}/.device_id');
if (!await file.exists()) return null;
final v = (await file.readAsString()).trim();
return v.isEmpty ? null : v;
} catch (_) {
return null;
}
}
/// 获取加密后的设备id验签
static String getDevToken(String devId) {
String a = "g24p5VJ4fJ";
String b = "5P#at%Yu";
String c = "ZPRwQuKl8YlVIr";
String s = a + b + c + devId + a + b + c;
var data = utf8.encode(s);
var chiper = sha256.convert(data);
return base64Encode(chiper.bytes);
}
///获取渠道(非 Android 或取不到都返回空串)
static Future<String> getChannel() async {
if (!Platform.isAndroid) return "";
try {
return await const MethodChannel(naticeChannel)
.invokeMethod("getChannel") ??
"";
} on PlatformException {
return "";
}
}
}