import 'dart:async'; import 'package:analytics_sdk/entity/network_state_change_event.dart'; import 'package:analytics_sdk/manager/network_state_fields_stub.dart' // ignore: uri_does_not_exist if (dart.library.html) 'package:analytics_sdk/manager/network_state_fields_web.dart'; import 'package:analytics_sdk/manager/sdk_context.dart'; import 'package:analytics_sdk/manager/session_manager.dart'; import 'package:battery_plus/battery_plus.dart'; import 'package:connectivity_plus/connectivity_plus.dart'; import 'package:flutter/foundation.dart'; import 'package:analytics_sdk/utils/network_type_resolver.dart'; import 'environment_info_native.dart' // ignore: uri_does_not_exist if (dart.library.html) 'environment_info_web.dart'; import 'runtime_observation_stub.dart' // ignore: uri_does_not_exist if (dart.library.io) 'runtime_observation_native.dart' as runtime_observation; /// 系统、网络、硬件和浏览器环境快照字段采集工具。 class EnvironmentInfoUtil { static const Duration _batteryRefreshInterval = Duration(seconds: 60); static final Battery _battery = Battery(); static final Connectivity _connectivity = Connectivity(); static Map _fields = _defaultFields(); static bool _initialized = false; static bool _monitoring = false; static DateTime? _lastBatteryRefreshAt; static StreamSubscription? _connectivitySubscription; static StreamSubscription? _batteryStateSubscription; static runtime_observation.RuntimeObservationMonitor? _runtimeObservationMonitor; static String _lastNetworkType = 'unknown'; static String _networkChangeCountSid = ''; static int _networkChangeCountInSid = 0; static void Function(dynamic event)? _trackNetworkStateChange; /// 初始化并缓存环境字段。失败时保留默认 unknown/-1 兜底值。 static Future initialize() async { if (_initialized) return; _initialized = true; try { _fields = { ..._defaultFields(), ...await collectEnvironmentInfo(), ...await runtime_observation.collectRuntimeObservationFields(), }; _lastNetworkType = _stringField(_fields['network_type'], 'unknown'); _lastBatteryRefreshAt = DateTime.now(); } catch (_) { _fields = _defaultFields(); _lastNetworkType = 'unknown'; } } /// 启动运行期字段监听,后续变化会同步更新内存公共字段。 static void startMonitoring({void Function(dynamic event)? track}) { if (track != null) _trackNetworkStateChange = track; if (_monitoring) return; _monitoring = true; _connectivitySubscription = _connectivity.onConnectivityChanged.listen( (result) => _handleNetworkChange(result), onError: (_) => _mergeFields({'network_type': 'unknown'}), ); _batteryStateSubscription = _battery.onBatteryStateChanged.listen( (state) async { final updates = { 'is_charging': _mapBatteryState(state), 'battery_state': _batteryStateName(state), }; final level = await _readBatteryLevel(); if (level >= 0) updates['battery_level'] = level; _mergeFields(updates); }, onError: (_) => _mergeFields({ 'is_charging': -1, 'battery_state': 'unknown', }), ); _runtimeObservationMonitor = runtime_observation.startRuntimeObservationMonitoring(_mergeFields); } /// 按最小间隔刷新电量百分比,避免为了字段新鲜度持续轮询。 static Future refreshBatteryLevelIfNeeded({bool force = false}) async { final now = DateTime.now(); final last = _lastBatteryRefreshAt; if (!force && last != null && now.difference(last) < _batteryRefreshInterval) { return; } final level = await _readBatteryLevel(); if (level < 0) return; _lastBatteryRefreshAt = now; _mergeFields({'battery_level': level}); } /// 主动刷新低频运行态快照,通常在前台恢复时调用。 static Future refreshRuntimeObservationFields() async { try { _mergeFields(await runtime_observation.collectRuntimeObservationFields()); } catch (_) {} } /// 停止监听并清理订阅。 static Future dispose() async { _monitoring = false; await _connectivitySubscription?.cancel(); await _batteryStateSubscription?.cancel(); await _runtimeObservationMonitor?.dispose(); _connectivitySubscription = null; _batteryStateSubscription = null; _runtimeObservationMonitor = null; _trackNetworkStateChange = null; } /// 当前已缓存的环境快照字段。 static Map get fields => Map.from(_fields); static void _mergeFields(Map fields) { if (fields.isEmpty) return; _fields = { ..._fields, ...fields, }; SdkContext.updateEnvironmentFields(fields); } static Future _handleNetworkChange(ConnectivityResult result) async { final webConnectionFields = kIsWeb ? await collectNetworkStateFields() : const {}; final toNetworkType = kIsWeb ? resolveWebNetworkType( connectionType: _stringField( webConnectionFields['connection_type'], 'unknown', ), effectiveType: _stringField( webConnectionFields['effective_type'], 'unknown', ), ) : _mapNetworkType(result); final fromNetworkType = _lastNetworkType; _mergeFields({'network_type': toNetworkType}); if (toNetworkType == fromNetworkType) return; _lastNetworkType = toNetworkType; final track = _trackNetworkStateChange; if (track == null) return; try { track(NetworkStateChangeEvent( networkType: toNetworkType, fromNetworkType: fromNetworkType, isOnline: isOnlineForNetworkType(toNetworkType), changeCountInSid: _nextNetworkChangeCountInSid(), carrier: kIsWeb ? '' : _stringField( SdkContext.getEnvironmentSnapshotFields()['carrier'], '', ), webConnectionFields: kIsWeb ? webConnectionFields : const {}, )); } catch (_) { // 网络变化是低优先级自动事件,采集失败不得影响业务。 } } static String _mapNetworkType(ConnectivityResult result) { switch (result) { case ConnectivityResult.wifi: return 'wifi'; case ConnectivityResult.ethernet: return 'ethernet'; case ConnectivityResult.mobile: return 'cellular'; case ConnectivityResult.none: return 'offline'; case ConnectivityResult.bluetooth: case ConnectivityResult.vpn: case ConnectivityResult.other: return 'unknown'; } } static int _mapBatteryState(BatteryState state) { if (state == BatteryState.charging || state == BatteryState.full) return 1; if (state == BatteryState.discharging) return 0; return -1; } static String _batteryStateName(BatteryState state) { switch (state) { case BatteryState.charging: return 'charging'; case BatteryState.discharging: return 'discharging'; case BatteryState.full: return 'full'; case BatteryState.unknown: return 'unknown'; } } static Future _readBatteryLevel() async { try { return await _battery.batteryLevel; } catch (_) { return -1; } } static int _isOnline(String networkType) { return isOnlineForNetworkType(networkType); } static int _nextNetworkChangeCountInSid() { final sid = SessionManager.instance.currentSessionId ?? ''; if (sid != _networkChangeCountSid) { _networkChangeCountSid = sid; _networkChangeCountInSid = 0; } _networkChangeCountInSid += 1; return _networkChangeCountInSid; } static String _stringField(dynamic value, String fallback) { final text = value?.toString().trim() ?? ''; return text.isEmpty ? fallback : text; } @visibleForTesting static Future handleNetworkChangeForTest( ConnectivityResult result, { required void Function(dynamic event) track, }) async { final previous = _trackNetworkStateChange; _trackNetworkStateChange = track; try { await _handleNetworkChange(result); } finally { _trackNetworkStateChange = previous; } } @visibleForTesting static void resetNetworkChangeStateForTest({ String networkType = 'unknown', String sid = '', }) { _lastNetworkType = networkType; _networkChangeCountSid = sid; _networkChangeCountInSid = 0; _trackNetworkStateChange = null; _mergeFields({'network_type': networkType}); } static Map _defaultFields() => { 'language': '', 'timezone': '', 'package_name': '', 'screen_width': -1, 'screen_height': -1, 'screen_dpi': -1, 'device_pixel_ratio': -1, 'orientation': '', 'screen_brightness': -1, 'volume': -1, 'is_screen_on': -1, 'network_type': 'unknown', 'battery_level': -1, 'is_charging': -1, 'battery_state': 'unknown', 'cpu_cores': -1, 'device_memory_gb': -1, 'browser_name': '', 'browser_version': '', 'cookie_enabled': -1, 'local_storage_enabled': -1, 'touch_support': -1, 'max_touch_points': -1, 'color_depth': -1, 'is_pwa': -1, }; }