292 lines
9.4 KiB
Dart
292 lines
9.4 KiB
Dart
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<String, dynamic> _fields = _defaultFields();
|
|
static bool _initialized = false;
|
|
static bool _monitoring = false;
|
|
static DateTime? _lastBatteryRefreshAt;
|
|
static StreamSubscription<ConnectivityResult>? _connectivitySubscription;
|
|
static StreamSubscription<BatteryState>? _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<void> 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 = <String, dynamic>{
|
|
'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<void> 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<void> refreshRuntimeObservationFields() async {
|
|
try {
|
|
_mergeFields(await runtime_observation.collectRuntimeObservationFields());
|
|
} catch (_) {}
|
|
}
|
|
|
|
/// 停止监听并清理订阅。
|
|
static Future<void> dispose() async {
|
|
_monitoring = false;
|
|
await _connectivitySubscription?.cancel();
|
|
await _batteryStateSubscription?.cancel();
|
|
await _runtimeObservationMonitor?.dispose();
|
|
_connectivitySubscription = null;
|
|
_batteryStateSubscription = null;
|
|
_runtimeObservationMonitor = null;
|
|
_trackNetworkStateChange = null;
|
|
}
|
|
|
|
/// 当前已缓存的环境快照字段。
|
|
static Map<String, dynamic> get fields => Map<String, dynamic>.from(_fields);
|
|
|
|
static void _mergeFields(Map<String, dynamic> fields) {
|
|
if (fields.isEmpty) return;
|
|
_fields = {
|
|
..._fields,
|
|
...fields,
|
|
};
|
|
SdkContext.updateEnvironmentFields(fields);
|
|
}
|
|
|
|
static Future<void> _handleNetworkChange(ConnectivityResult result) async {
|
|
final webConnectionFields =
|
|
kIsWeb ? await collectNetworkStateFields() : const <String, dynamic>{};
|
|
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<int> _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<void> 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<String, dynamic> _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,
|
|
};
|
|
}
|