889 lines
29 KiB
Dart
889 lines
29 KiB
Dart
import 'dart:async';
|
|
import 'dart:developer' as dev;
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter/services.dart';
|
|
import 'package:device_info_plus/device_info_plus.dart';
|
|
|
|
// --- 本地导入 ---
|
|
import '../main.dart';
|
|
import '../models/device_matching.dart';
|
|
import '../models/saf_permission_state.dart';
|
|
import '../models/data_abnormal_type.dart';
|
|
import '../services/database_service.dart';
|
|
import '../services/battery_data_service.dart';
|
|
import '../services/saf_storage_service.dart';
|
|
import '../logic/charge_parser.dart';
|
|
import '../logic/capacity_extractor.dart';
|
|
import '../utils/time_utils.dart';
|
|
import '../utils/charge_utils.dart';
|
|
import '../widgets/battery_summary.dart';
|
|
import '../widgets/battery_info_panel.dart';
|
|
import '../widgets/charge_entry.dart';
|
|
import '../widgets/charge_detail_page.dart';
|
|
import '../widgets/trend_chart.dart';
|
|
import '../pages/changelog_page.dart';
|
|
import '../pages/about_page.dart';
|
|
|
|
// --- 潘通色定义 ---
|
|
const Color kHealthGreen = Color(0xFF00B140);
|
|
const Color kHealthYellow = Color(0xFFEFDF00);
|
|
const Color kHealthOrange = Color(0xFFD75A16);
|
|
|
|
class BatteryHealthPage extends StatefulWidget {
|
|
const BatteryHealthPage({super.key});
|
|
|
|
@override
|
|
State<BatteryHealthPage> createState() => _BatteryHealthPageState();
|
|
}
|
|
|
|
class _BatteryHealthPageState extends State<BatteryHealthPage> {
|
|
// ---------- 数据 ----------
|
|
double _currentCap = 0;
|
|
int _cycleCount = 0;
|
|
double _batteryTemp = 0;
|
|
double _batteryVolt = 0;
|
|
int _designCap = 5050;
|
|
String _deviceModel = "魅族21 Pro (默认)";
|
|
double _maxTemp = 0;
|
|
double _minTemp = 0;
|
|
double _maxVolt = 0;
|
|
double _minVolt = 0;
|
|
bool _hasData = false;
|
|
|
|
// ---------- 🆕 1.7.0 异常状态标记 ----------
|
|
bool _hasAbnormalDataToday = false;
|
|
|
|
// ---------- 趋势图数据 ----------
|
|
List<Map<String, dynamic>> _trendData = [];
|
|
|
|
// ---------- 充电状态 ----------
|
|
bool _isCharging = false;
|
|
String _chargeType = '';
|
|
double _chargePower = 0.0;
|
|
double _chargeVolt = 0.0;
|
|
double _chargeCurr = 0.0;
|
|
int? _wiredLevel;
|
|
bool _isWireless = false;
|
|
DateTime? _chargingStartTime;
|
|
|
|
// ---------- SAF 状态 ----------
|
|
final SafStorageService _safService = SafStorageService();
|
|
final DatabaseService _dbService = DatabaseService();
|
|
final BatteryDataService _dataService = BatteryDataService();
|
|
SafPermissionState _safState = SafPermissionState.unauthorized;
|
|
|
|
// 平台通道
|
|
static const _methodChannel = MethodChannel('com.lxh.battery_health/charge');
|
|
static const _eventChannel = EventChannel(
|
|
'com.lxh.battery_health/charge_events',
|
|
);
|
|
StreamSubscription? _chargeEventSubscription;
|
|
|
|
// 轮询定时器(15秒)
|
|
Timer? _pollTimer;
|
|
|
|
// ---------- 生命周期 ----------
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
dev.log('🔵 ========== initState 开始 ==========', name: 'BatteryHealth');
|
|
|
|
_getInitialChargeState();
|
|
|
|
_chargeEventSubscription = _eventChannel.receiveBroadcastStream().listen((
|
|
event,
|
|
) {
|
|
final bool isCharging = event['isCharging'] ?? false;
|
|
final String systemType = event['chargeType'] ?? 'NONE';
|
|
_applySystemChargeState(isCharging, systemType);
|
|
});
|
|
|
|
_pollTimer = Timer.periodic(const Duration(seconds: 15), (timer) {
|
|
if (_isCharging) {
|
|
_fetchChargeDataFromLog();
|
|
}
|
|
});
|
|
|
|
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
|
dev.log('🔵 initState -> addPostFrameCallback 开始', name: 'BatteryHealth');
|
|
|
|
await _safService.init();
|
|
dev.log(
|
|
'🔵 _safService.init() 完成,cachedUri: ${_safService.cachedUri}',
|
|
name: 'BatteryHealth',
|
|
);
|
|
|
|
_safState = await _safService.getPermissionState();
|
|
dev.log('🔵 _safState: $_safState', name: 'BatteryHealth');
|
|
|
|
if (_safState == SafPermissionState.unauthorized ||
|
|
_safState == SafPermissionState.expired) {
|
|
dev.log('🔵 需要授权,弹窗', name: 'BatteryHealth');
|
|
if (mounted) _showSafAuthorizationDialog();
|
|
} else {
|
|
dev.log('🔵 已有授权,调用 readBatteryCapacity', name: 'BatteryHealth');
|
|
readBatteryCapacity();
|
|
}
|
|
});
|
|
|
|
dev.log('🔵 ========== initState 结束 ==========', name: 'BatteryHealth');
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_chargeEventSubscription?.cancel();
|
|
_pollTimer?.cancel();
|
|
_dbService.database.then((db) => db.close()).ignore();
|
|
super.dispose();
|
|
}
|
|
|
|
// ---------- 设备信息 ----------
|
|
Future<void> _getDeviceInfo() async {
|
|
dev.log('🔵 _getDeviceInfo 开始', name: 'BatteryHealth');
|
|
final deviceInfo = DeviceInfoPlugin();
|
|
try {
|
|
final androidInfo = await deviceInfo.androidInfo;
|
|
final model = androidInfo.model;
|
|
dev.log('🔵 设备型号: $model', name: 'BatteryHealth');
|
|
setState(() {
|
|
_deviceModel = DeviceMatching.getModelName(model);
|
|
_designCap = DeviceMatching.getDesignCapacity(model);
|
|
});
|
|
} catch (e) {
|
|
dev.log('🔵 获取设备信息失败: $e', name: 'BatteryHealth');
|
|
setState(() {
|
|
_deviceModel = '魅族21 Pro (默认)';
|
|
_designCap = 5050;
|
|
});
|
|
}
|
|
}
|
|
|
|
// ---------- SAF 授权引导 ----------
|
|
void _showSafAuthorizationDialog() {
|
|
dev.log('🔵 _showSafAuthorizationDialog 被调用', name: 'BatteryHealth');
|
|
if (!mounted) return;
|
|
|
|
showDialog<String>(
|
|
context: context,
|
|
barrierDismissible: false,
|
|
builder: (context) => AlertDialog(
|
|
title: const Text('需要文件夹访问权限'),
|
|
content: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: const [
|
|
Text('请授权访问日志文件夹,以读取电池数据。'),
|
|
SizedBox(height: 8),
|
|
Text(
|
|
'目标路径:\n/storage/emulated/0/mbattery_charger/',
|
|
style: TextStyle(fontSize: 12, color: Colors.grey),
|
|
),
|
|
SizedBox(height: 8),
|
|
Text('点击"选择文件夹"后,请在系统文件管理器中找到并选中上述文件夹。'),
|
|
],
|
|
),
|
|
actions: [
|
|
TextButton(
|
|
onPressed: () {
|
|
dev.log('🔵 用户取消授权', name: 'BatteryHealth');
|
|
Navigator.pop(context, 'cancel');
|
|
setState(() {
|
|
_safState = SafPermissionState.denied;
|
|
});
|
|
},
|
|
child: const Text('取消'),
|
|
),
|
|
ElevatedButton(
|
|
onPressed: () {
|
|
dev.log('🔵 用户点击选择文件夹', name: 'BatteryHealth');
|
|
Navigator.pop(context, 'select');
|
|
},
|
|
child: const Text('选择文件夹'),
|
|
),
|
|
],
|
|
),
|
|
).then((result) async {
|
|
dev.log('🔵 对话框关闭,result: $result', name: 'BatteryHealth');
|
|
if (result == 'select') {
|
|
dev.log('🔵 开始请求授权', name: 'BatteryHealth');
|
|
final authResult = await _safService.requestPermission();
|
|
dev.log(
|
|
'🔵 授权结果: success=${authResult.success}, state=${authResult.state}',
|
|
name: 'BatteryHealth',
|
|
);
|
|
|
|
if (!mounted) return;
|
|
|
|
if (authResult.success) {
|
|
dev.log('🔵 授权成功,调用 readBatteryCapacity', name: 'BatteryHealth');
|
|
setState(() {
|
|
_safState = SafPermissionState.authorized;
|
|
});
|
|
readBatteryCapacity();
|
|
} else {
|
|
dev.log('🔵 授权失败: ${authResult.errorMessage}', name: 'BatteryHealth');
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
SnackBar(
|
|
content: Text(authResult.errorMessage ?? '授权失败,请重试'),
|
|
backgroundColor: Colors.red,
|
|
),
|
|
);
|
|
_showSafAuthorizationDialog();
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
// ---------- 核心:系统API定调 ----------
|
|
void _applySystemChargeState(bool isCharging, String systemChargeType) {
|
|
dev.log(
|
|
'[_applySystemChargeState] isCharging: $isCharging, systemChargeType: $systemChargeType',
|
|
name: 'BatteryHealth',
|
|
);
|
|
|
|
setState(() {
|
|
_isCharging = isCharging;
|
|
|
|
if (!isCharging) {
|
|
dev.log('[_applySystemChargeState] 充电断开,重置状态', name: 'BatteryHealth');
|
|
_chargeType = '';
|
|
_isWireless = false;
|
|
_chargePower = 0.0;
|
|
_chargeVolt = 0.0;
|
|
_chargeCurr = 0.0;
|
|
_wiredLevel = null;
|
|
_chargingStartTime = null;
|
|
return;
|
|
}
|
|
|
|
if (_chargingStartTime == null) {
|
|
_chargingStartTime = DateTime.now().toUtc();
|
|
dev.log(
|
|
'[_applySystemChargeState] 充电开始时间: $_chargingStartTime',
|
|
name: 'BatteryHealth',
|
|
);
|
|
}
|
|
|
|
switch (systemChargeType) {
|
|
case "WIRELESS":
|
|
_isWireless = true;
|
|
_chargeType = "无线";
|
|
_wiredLevel = null;
|
|
break;
|
|
case "AC":
|
|
_isWireless = false;
|
|
_chargeType = "PPS";
|
|
break;
|
|
case "USB":
|
|
_isWireless = false;
|
|
_chargeType = "SDP";
|
|
break;
|
|
default:
|
|
_isWireless = false;
|
|
_chargeType = '';
|
|
break;
|
|
}
|
|
|
|
_chargePower = 0.0;
|
|
_chargeVolt = 0.0;
|
|
_chargeCurr = 0.0;
|
|
});
|
|
|
|
if (_isCharging) {
|
|
_fetchChargeDataFromLog();
|
|
}
|
|
}
|
|
|
|
// ---------- 读取当前数据 ----------
|
|
Future<void> readBatteryCapacity() async {
|
|
dev.log('🔴🔴🔴 readBatteryCapacity 被调用了 🔴🔴🔴', name: 'BatteryHealth');
|
|
|
|
// 🟢 每次读取时重置异常标记
|
|
_hasAbnormalDataToday = false;
|
|
|
|
_safState = await _safService.getPermissionState();
|
|
dev.log('🔵 SAF状态: $_safState', name: 'BatteryHealth');
|
|
|
|
if (_safState != SafPermissionState.authorized) {
|
|
dev.log('🔵 未授权,弹窗', name: 'BatteryHealth');
|
|
if (mounted) _showSafAuthorizationDialog();
|
|
return;
|
|
}
|
|
|
|
await _getDeviceInfo();
|
|
await _dataService.scanAndUpdateDB(designCap: _designCap);
|
|
|
|
final fileName = TimeUtils.getTodayFileName();
|
|
dev.log('🔵 今日文件名: $fileName', name: 'BatteryHealth');
|
|
|
|
final exists = await _safService.logFileExists(fileName);
|
|
dev.log('🔵 文件存在: $exists', name: 'BatteryHealth');
|
|
|
|
if (!exists) {
|
|
dev.log('🔵 文件不存在,尝试从数据库读取', name: 'BatteryHealth');
|
|
final recent = await _dbService.queryRecentData(1);
|
|
if (recent.isNotEmpty) {
|
|
dev.log('🔵 数据库有数据: ${recent.first}', name: 'BatteryHealth');
|
|
setState(() {
|
|
_currentCap = recent.first['capacity'].toDouble();
|
|
_maxVolt = recent.first['volt_max'] ?? 0;
|
|
_minVolt = recent.first['volt_min'] ?? 0;
|
|
_maxTemp = 0;
|
|
_minTemp = 0;
|
|
_cycleCount = 0;
|
|
_hasData = true;
|
|
});
|
|
await _updateTrendChart();
|
|
} else {
|
|
dev.log('🔵 数据库无数据', name: 'BatteryHealth');
|
|
setState(() => _hasData = false);
|
|
}
|
|
if (_isCharging) _fetchChargeDataFromLog();
|
|
return;
|
|
}
|
|
|
|
try {
|
|
final lines = await _safService.readLogFile(fileName);
|
|
dev.log('🔵 读取到 ${lines.length} 行', name: 'BatteryHealth');
|
|
|
|
if (lines.isEmpty) {
|
|
dev.log('🔵 文件为空', name: 'BatteryHealth');
|
|
setState(() => _hasData = false);
|
|
if (_isCharging) _fetchChargeDataFromLog();
|
|
return;
|
|
}
|
|
|
|
final validLines = lines.where((line) => line.contains('tbat=')).toList();
|
|
if (validLines.isEmpty) {
|
|
dev.log('🔵 无有效数据行', name: 'BatteryHealth');
|
|
setState(() => _hasData = false);
|
|
return;
|
|
}
|
|
|
|
final lastLine = validLines.last;
|
|
final cycle = _extractCycle(lastLine);
|
|
final temp = _extractTemp(lastLine);
|
|
final volt = _extractVolt(lastLine);
|
|
final (maxTemp, minTemp) = _extractTempExtremes(validLines);
|
|
final (maxVolt, minVolt) = _extractVoltExtremes(validLines);
|
|
|
|
dev.log(
|
|
'🔵 字段提取: cycle=$cycle, temp=$temp, volt=$volt',
|
|
name: 'BatteryHealth',
|
|
);
|
|
|
|
// 🆕 1.7.0 计算异常比例
|
|
final abnormalRatio = _calculateAbnormalRatio(lines, _designCap);
|
|
dev.log(
|
|
'🔵 异常比例: ${(abnormalRatio * 100).toStringAsFixed(1)}%',
|
|
name: 'BatteryHealth',
|
|
);
|
|
|
|
final cap = CapacityExtractor.extractCapacity(lines);
|
|
int? finalCap;
|
|
|
|
// 🆕 1.7.0 异常处理逻辑
|
|
if (cap != null && abnormalRatio < 0.5) {
|
|
// 短时尖刺:走校验
|
|
final validatedCap = _validateCapacity(cap, _designCap);
|
|
if (validatedCap != null) {
|
|
finalCap = validatedCap;
|
|
_hasAbnormalDataToday = false;
|
|
dev.log('🔵 短时尖刺校验通过: $cap', name: 'BatteryHealth');
|
|
} else {
|
|
finalCap = null;
|
|
_hasAbnormalDataToday = true;
|
|
dev.log('⚠️ 短时尖刺,已丢弃: $cap', name: 'BatteryHealth');
|
|
}
|
|
} else if (cap != null && abnormalRatio >= 0.5) {
|
|
// 长期偏离:放行(疑似扩容)
|
|
finalCap = cap;
|
|
_hasAbnormalDataToday = false;
|
|
dev.log(
|
|
'🔵 长期偏离(${(abnormalRatio * 100).toStringAsFixed(0)}% 记录 > 115%),疑似扩容电池,已放行: $cap',
|
|
name: 'BatteryHealth',
|
|
);
|
|
} else {
|
|
// cap == null,尝试复制前一天
|
|
dev.log('🔵 今日无充满记录,尝试复制前一天容量', name: 'BatteryHealth');
|
|
final recent = await _dbService.queryRecentData(1);
|
|
if (recent.isNotEmpty) {
|
|
finalCap = recent.first['capacity'] as int;
|
|
_hasAbnormalDataToday = false;
|
|
dev.log('🔵 复制前一天容量: $finalCap', name: 'BatteryHealth');
|
|
} else {
|
|
finalCap = null;
|
|
_hasAbnormalDataToday = false;
|
|
dev.log('🔵 无历史数据可复制', name: 'BatteryHealth');
|
|
}
|
|
}
|
|
|
|
setState(() {
|
|
_currentCap = (finalCap ?? _currentCap).toDouble();
|
|
_cycleCount = cycle;
|
|
_batteryTemp = temp;
|
|
_batteryVolt = volt;
|
|
_maxTemp = maxTemp;
|
|
_minTemp = minTemp;
|
|
_maxVolt = maxVolt;
|
|
_minVolt = minVolt;
|
|
_hasData = true;
|
|
});
|
|
|
|
if (_isCharging) {
|
|
final parseResult = ChargeParser.parse(lines, _chargingStartTime);
|
|
_applyParseResult(parseResult);
|
|
}
|
|
|
|
await _updateTrendChart();
|
|
dev.log(
|
|
'🔵 ========== readBatteryCapacity 完成 ==========',
|
|
name: 'BatteryHealth',
|
|
);
|
|
} catch (e) {
|
|
dev.log('🔵 readBatteryCapacity 异常: $e', name: 'BatteryHealth');
|
|
setState(() => _hasData = false);
|
|
}
|
|
}
|
|
|
|
// ---------- 🆕 1.7.0 统计当天超过 115% 的容量记录占比 ----------
|
|
double _calculateAbnormalRatio(List<String> lines, int designCap) {
|
|
final capReg = RegExp(r'(?:learned_cap|remaining_cap)=(\d+)');
|
|
int totalCount = 0;
|
|
int abnormalCount = 0;
|
|
|
|
for (String line in lines) {
|
|
if (!line.contains('tbat=')) continue;
|
|
|
|
final match = capReg.firstMatch(line);
|
|
if (match == null) continue;
|
|
|
|
final value = int.parse(match.group(1)!);
|
|
final capInMah = value > 100000 ? value ~/ 1000 : value;
|
|
|
|
totalCount++;
|
|
if (capInMah > designCap * 1.15) {
|
|
abnormalCount++;
|
|
}
|
|
}
|
|
|
|
if (totalCount == 0) return 0.0;
|
|
return abnormalCount / totalCount;
|
|
}
|
|
|
|
// ---------- 🆕 1.7.0 容量有效性校验 ----------
|
|
int? _validateCapacity(int? capacity, int designCap) {
|
|
if (capacity == null) return null;
|
|
|
|
if (capacity <= 0) {
|
|
dev.log('⚠️ 容量异常:<= 0,已忽略', name: 'BatteryHealth');
|
|
return null;
|
|
}
|
|
|
|
if (capacity < designCap * 0.6) {
|
|
dev.log(
|
|
'⚠️ 容量异常:低于设计容量 60%($capacity < ${designCap * 0.6}),已忽略',
|
|
name: 'BatteryHealth',
|
|
);
|
|
return null;
|
|
}
|
|
|
|
if (capacity > designCap * 1.15) {
|
|
dev.log(
|
|
'⚠️ 容量异常:高于设计容量 115%($capacity > ${designCap * 1.15}),已忽略',
|
|
name: 'BatteryHealth',
|
|
);
|
|
return null;
|
|
}
|
|
|
|
return capacity;
|
|
}
|
|
|
|
// ---------- 🆕 1.7.0 异常检测(供 UI 使用) ----------
|
|
DataAbnormalType _checkAbnormalType(
|
|
double currentCap,
|
|
int designCap,
|
|
double voltage,
|
|
bool hasAbnormalDataToday,
|
|
) {
|
|
// 🟢 优先:今天有异常数据被丢弃 → 强制显示红色
|
|
if (hasAbnormalDataToday) return DataAbnormalType.capacityTooHigh;
|
|
|
|
// 其次:当前值异常
|
|
if (currentCap > designCap * 1.15) return DataAbnormalType.capacityTooHigh;
|
|
if (currentCap < designCap * 0.6) return DataAbnormalType.capacityTooLow;
|
|
if (voltage < 3.0 || voltage > 5.0) return DataAbnormalType.voltageAbnormal;
|
|
|
|
return DataAbnormalType.none;
|
|
}
|
|
|
|
// ---------- 辅助提取方法 ----------
|
|
int _extractCycle(String line) {
|
|
final reg = RegExp(r'cycle=(\d+)');
|
|
final match = reg.firstMatch(line);
|
|
return match != null ? int.parse(match.group(1)!) : 0;
|
|
}
|
|
|
|
double _extractTemp(String line) {
|
|
final reg = RegExp(r'tbat=(\d+)');
|
|
final match = reg.firstMatch(line);
|
|
return match != null ? double.parse(match.group(1)!) : 0;
|
|
}
|
|
|
|
double _extractVolt(String line) {
|
|
final reg = RegExp(r'vbat=(\d+)');
|
|
final match = reg.firstMatch(line);
|
|
return match != null ? int.parse(match.group(1)!) / 1000000 : 0;
|
|
}
|
|
|
|
(double, double) _extractTempExtremes(List<String> lines) {
|
|
final reg = RegExp(r'tbat=(\d+)');
|
|
double? maxTemp, minTemp;
|
|
for (String line in lines) {
|
|
final match = reg.firstMatch(line);
|
|
if (match != null) {
|
|
final v = double.parse(match.group(1)!);
|
|
if (maxTemp == null || v > maxTemp) maxTemp = v;
|
|
if (minTemp == null || v < minTemp) minTemp = v;
|
|
}
|
|
}
|
|
return (maxTemp ?? 0, minTemp ?? 0);
|
|
}
|
|
|
|
(double, double) _extractVoltExtremes(List<String> lines) {
|
|
final reg = RegExp(r'vbat=(\d+)');
|
|
double? maxVolt, minVolt;
|
|
for (String line in lines) {
|
|
final match = reg.firstMatch(line);
|
|
if (match != null) {
|
|
final v = int.parse(match.group(1)!) / 1000000;
|
|
if (maxVolt == null || v > maxVolt) maxVolt = v;
|
|
if (minVolt == null || v < minVolt) minVolt = v;
|
|
}
|
|
}
|
|
return (maxVolt ?? 0, minVolt ?? 0);
|
|
}
|
|
|
|
void _applyParseResult(ParseResult result) {
|
|
setState(() {
|
|
_isWireless = result.isWireless;
|
|
_chargeType = result.chargeType;
|
|
_chargePower = result.power;
|
|
_chargeVolt = result.volt;
|
|
_chargeCurr = result.curr;
|
|
_wiredLevel = result.wiredLevel;
|
|
});
|
|
}
|
|
|
|
// ---------- 轮询读取日志 ----------
|
|
Future<void> _fetchChargeDataFromLog() async {
|
|
if (!_isCharging) {
|
|
dev.log('[_fetchChargeDataFromLog] 未充电,跳过读取', name: 'BatteryHealth');
|
|
return;
|
|
}
|
|
|
|
if (_chargingStartTime != null) {
|
|
final elapsed = DateTime.now().toUtc().difference(_chargingStartTime!);
|
|
if (elapsed.inMinutes > 5) {
|
|
dev.log(
|
|
'[_fetchChargeDataFromLog] 充电开始已超过 5 分钟,跳过本次轮询',
|
|
name: 'BatteryHealth',
|
|
);
|
|
return;
|
|
}
|
|
}
|
|
|
|
_safState = await _safService.getPermissionState();
|
|
if (_safState != SafPermissionState.authorized) {
|
|
dev.log('[_fetchChargeDataFromLog] 未授权,跳过', name: 'BatteryHealth');
|
|
return;
|
|
}
|
|
|
|
final fileName = TimeUtils.getTodayFileName();
|
|
final exists = await _safService.logFileExists(fileName);
|
|
if (!exists) {
|
|
dev.log(
|
|
'[_fetchChargeDataFromLog] ⚠️ 文件不存在: $fileName',
|
|
name: 'BatteryHealth',
|
|
);
|
|
return;
|
|
}
|
|
|
|
try {
|
|
final lines = await _safService.readLogFile(fileName);
|
|
if (lines.isEmpty) {
|
|
dev.log('[_fetchChargeDataFromLog] 文件为空', name: 'BatteryHealth');
|
|
return;
|
|
}
|
|
dev.log(
|
|
'[_fetchChargeDataFromLog] ✅ 读取成功,共 ${lines.length} 行',
|
|
name: 'BatteryHealth',
|
|
);
|
|
|
|
final result = ChargeParser.parse(lines, _chargingStartTime);
|
|
_applyParseResult(result);
|
|
} catch (e) {
|
|
dev.log('[_fetchChargeDataFromLog] ❌ 读取异常: $e', name: 'BatteryHealth');
|
|
}
|
|
}
|
|
|
|
// ---------- 趋势图 ----------
|
|
Future<void> _updateTrendChart() async {
|
|
final data = await _dbService.queryRecentData(15);
|
|
setState(() {
|
|
_trendData = data.reversed.toList();
|
|
});
|
|
}
|
|
|
|
// ---------- 平台通道 ----------
|
|
Future<void> _getInitialChargeState() async {
|
|
try {
|
|
final result = await _methodChannel.invokeMethod('getChargeState');
|
|
final isCharging = result['isCharging'] ?? false;
|
|
final systemType = result['chargeType'] ?? 'NONE';
|
|
_applySystemChargeState(isCharging, systemType);
|
|
} catch (e) {
|
|
// 静默降级
|
|
}
|
|
}
|
|
|
|
// ---------- 显示辅助 ----------
|
|
String _getDisplayPower() {
|
|
if (!_isCharging) return '';
|
|
if (_isWireless) {
|
|
return ChargeUtils.getDisplayPowerWireless(_chargePower);
|
|
} else {
|
|
return ChargeUtils.getMainDisplayPowerWired(
|
|
_chargePower,
|
|
_wiredLevel,
|
|
_chargeType,
|
|
);
|
|
}
|
|
}
|
|
|
|
String _getChargeTypeLabel() {
|
|
return ChargeUtils.getChargeTypeLabel(_isWireless, _chargeType);
|
|
}
|
|
|
|
// ---------- Build ----------
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final isDark = Theme.of(context).brightness == Brightness.dark;
|
|
|
|
final double healthPercent = (_designCap > 0)
|
|
? (_currentCap / _designCap) * 100
|
|
: 0.0;
|
|
|
|
final String chargeSummary = _isCharging
|
|
? '充电中 · ${_getChargeTypeLabel()} · ${_getDisplayPower()}'
|
|
: '';
|
|
|
|
final abnormalType = _checkAbnormalType(
|
|
_currentCap,
|
|
_designCap,
|
|
_batteryVolt,
|
|
_hasAbnormalDataToday,
|
|
);
|
|
|
|
return AnnotatedRegion<SystemUiOverlayStyle>(
|
|
value: SystemUiOverlayStyle(
|
|
statusBarColor: isDark
|
|
? const Color(0xFF1E1E1E)
|
|
: const Color.fromRGBO(157, 212, 237, 0.1),
|
|
statusBarIconBrightness: isDark ? Brightness.light : Brightness.dark,
|
|
statusBarBrightness: isDark ? Brightness.light : Brightness.dark,
|
|
),
|
|
child: Scaffold(
|
|
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
|
|
appBar: AppBar(
|
|
backgroundColor: Theme.of(context).appBarTheme.backgroundColor,
|
|
elevation: 0,
|
|
scrolledUnderElevation: 0,
|
|
shadowColor: Colors.transparent,
|
|
surfaceTintColor: Colors.transparent,
|
|
title: Text(
|
|
'电池健康度',
|
|
style: TextStyle(
|
|
fontSize: 24,
|
|
fontWeight: FontWeight.w600,
|
|
color: isDark ? Colors.white : Colors.black,
|
|
),
|
|
),
|
|
actions: [
|
|
IconButton(
|
|
icon: Icon(
|
|
Icons.refresh,
|
|
color: isDark ? Colors.white : Colors.black,
|
|
),
|
|
onPressed: readBatteryCapacity,
|
|
tooltip: '刷新数据',
|
|
),
|
|
PopupMenuButton<String>(
|
|
icon: Icon(
|
|
Icons.more_vert,
|
|
color: isDark ? Colors.white : Colors.black,
|
|
),
|
|
color: isDark ? const Color(0xFF2C2C2C) : Colors.white,
|
|
onSelected: (value) {
|
|
switch (value) {
|
|
case 'changelog':
|
|
Navigator.push(
|
|
context,
|
|
MaterialPageRoute(
|
|
builder: (context) => const ChangelogPage(),
|
|
),
|
|
);
|
|
break;
|
|
case 'about':
|
|
Navigator.push(
|
|
context,
|
|
MaterialPageRoute(
|
|
builder: (context) => const AboutPage(),
|
|
),
|
|
);
|
|
break;
|
|
case 'dark_mode':
|
|
// 循环切换:浅色 → 深色 → 跟随系统 → 浅色
|
|
final brightness = Theme.of(context).brightness;
|
|
ThemeMode next;
|
|
if (brightness == Brightness.light) {
|
|
next = ThemeMode.dark;
|
|
} else if (brightness == Brightness.dark) {
|
|
next = ThemeMode.system;
|
|
} else {
|
|
next = ThemeMode.light;
|
|
}
|
|
// 🆕 通过全局 key 调用 MyApp 的 switchTheme 方法
|
|
appKey.currentState?.switchTheme(next);
|
|
break;
|
|
}
|
|
},
|
|
itemBuilder: (context) => [
|
|
const PopupMenuItem(value: 'changelog', child: Text('更新日志')),
|
|
const PopupMenuItem(value: 'dark_mode', child: Text('夜间模式')),
|
|
const PopupMenuItem(value: 'about', child: Text('关于')),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
body: _hasData
|
|
? Column(
|
|
children: [
|
|
Expanded(
|
|
child: SingleChildScrollView(
|
|
padding: const EdgeInsets.all(20.0),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
_deviceModel,
|
|
style: TextStyle(
|
|
fontSize: 14,
|
|
color: isDark
|
|
? Colors.grey.shade400
|
|
: Colors.grey,
|
|
fontWeight: FontWeight.w400,
|
|
),
|
|
),
|
|
const SizedBox(height: 4),
|
|
|
|
RepaintBoundary(
|
|
child: BatterySummary(
|
|
healthPercent: healthPercent,
|
|
cycleCount: _cycleCount,
|
|
currentCap: _currentCap,
|
|
designCap: _designCap,
|
|
abnormalType: abnormalType,
|
|
),
|
|
),
|
|
|
|
const SizedBox(height: 24),
|
|
|
|
RepaintBoundary(
|
|
child: TrendChart(
|
|
data: _trendData,
|
|
designCap: _designCap,
|
|
),
|
|
),
|
|
|
|
const SizedBox(height: 8),
|
|
|
|
RepaintBoundary(
|
|
child: AnimatedOpacity(
|
|
opacity: _isCharging ? 1.0 : 0.0,
|
|
duration: const Duration(milliseconds: 300),
|
|
child: ChargeEntry(
|
|
summary: chargeSummary,
|
|
isCharging: _isCharging,
|
|
onTap: () {
|
|
if (_isCharging) {
|
|
Navigator.push(
|
|
context,
|
|
MaterialPageRoute(
|
|
builder: (context) => ChargeDetailPage(
|
|
chargeType: _chargeType,
|
|
isWireless: _isWireless,
|
|
power: _chargePower,
|
|
volt: _chargeVolt,
|
|
curr: _chargeCurr,
|
|
wiredLevel: _wiredLevel,
|
|
),
|
|
),
|
|
);
|
|
}
|
|
},
|
|
),
|
|
),
|
|
),
|
|
|
|
const SizedBox(height: 4),
|
|
|
|
RepaintBoundary(
|
|
child: BatteryInfoPanel(
|
|
batteryVolt: _batteryVolt,
|
|
batteryTemp: _batteryTemp,
|
|
maxVolt: _maxVolt,
|
|
minVolt: _minVolt,
|
|
maxTemp: _maxTemp,
|
|
minTemp: _minTemp,
|
|
abnormalType: abnormalType,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
],
|
|
)
|
|
: Center(
|
|
child: Column(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
Icon(
|
|
Icons.battery_unknown,
|
|
size: 64,
|
|
color: isDark ? Colors.grey.shade600 : Colors.grey,
|
|
),
|
|
const SizedBox(height: 16),
|
|
Text(
|
|
'暂无数据',
|
|
style: TextStyle(
|
|
fontSize: 18,
|
|
color: isDark ? Colors.grey.shade400 : Colors.grey,
|
|
),
|
|
),
|
|
Text(
|
|
'点击右上角刷新尝试',
|
|
style: TextStyle(
|
|
fontSize: 14,
|
|
color: isDark ? Colors.grey.shade500 : Colors.grey,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|