Initial commit: 91dj android client dev_1.0.0
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,291 @@
|
||||
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,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user