Initial commit: 91dj android client dev_1.0.0

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
谢宇宁
2026-10-08 11:37:25 +07:00
co-authored by Claude Opus 5.5
commit 97dd6a739a
1262 changed files with 119813 additions and 0 deletions
@@ -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,
};
}