Files
MEIZU-Battery-Healthy/lib/pages/battery_health_page.dart
T
2026-07-07 12:32:47 +08:00

772 lines
24 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 '../models/device_matching.dart'; // ✅ 存在
import '../models/saf_permission_state.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'; // ✅ 存在(注意:不是 utils/)
import '../utils/time_utils.dart'; // ✅ 存在
import '../utils/charge_utils.dart'; // ✅ 存在
import '../widgets/trend_chart.dart'; // ⚠️ 需要确认文件内容完整
import '../widgets/battery_summary.dart'; // ⚠️ 需要确认文件内容完整
import '../widgets/battery_info_panel.dart'; // ⚠️ 需要确认文件内容完整
import '../widgets/charge_entry.dart'; // ⚠️ 需要确认文件内容完整
import '../widgets/charge_detail_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>
with SingleTickerProviderStateMixin {
// ---------- 数据 ----------
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;
// ---------- 趋势图数据 ----------
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;
// 动画控制
late AnimationController _chargeAnimationController;
late Animation<double> _bottomSlide;
late Animation<double> _entryFadeIn;
// 平台通道
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');
_initAnimations();
_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();
_chargeAnimationController.dispose();
_dbService.database.then((db) => db.close()).ignore();
super.dispose();
}
// ---------- 动画初始化 ----------
void _initAnimations() {
_chargeAnimationController = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 500),
);
_bottomSlide = Tween<double>(begin: 0, end: 24).animate(
CurvedAnimation(
parent: _chargeAnimationController,
curve: Curves.easeInOutCubic,
),
);
_entryFadeIn = Tween<double>(begin: 0, end: 1).animate(
CurvedAnimation(
parent: _chargeAnimationController,
curve: const Interval(0.0, 0.5, curve: Curves.easeIn),
),
);
}
// ---------- 设备信息 ----------
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;
_updateChargeState();
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;
_updateChargeState();
});
if (_isCharging) {
_fetchChargeDataFromLog();
}
}
// ---------- 读取当前数据 ----------
Future<void> readBatteryCapacity() async {
dev.log('🔴🔴🔴 readBatteryCapacity 被调用了 🔴🔴🔴', name: 'BatteryHealth');
_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();
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;
// 🟢 数据库目前不存温度,后续升级可扩展,暂时保留为 0
_maxTemp = 0;
_minTemp = 0;
_cycleCount = 0;
_hasData = true;
});
await _updateTrendChart();
} else {
dev.log('🔵 数据库无数据', name: 'BatteryHealth');
setState(() => _hasData = false);
}
if (_isCharging) _fetchChargeDataFromLog();
_chargeAnimationController.reset();
_updateChargeState();
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();
_chargeAnimationController.reset();
_updateChargeState();
return;
}
// 🟢 提取所有有效行(包含 tbat=)
final validLines = lines.where((line) => line.contains('tbat=')).toList();
if (validLines.isEmpty) {
dev.log('🔵 无有效数据行', name: 'BatteryHealth');
setState(() => _hasData = false);
_chargeAnimationController.reset();
_updateChargeState();
return;
}
// 🟢 1.4.7 核心改动:字段提取与容量提取解耦
// 无论 cap 是否为 null,都提取常规字段
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',
);
// 🟢 容量提取(可能为 null)
final cap = CapacityExtractor.extractCapacity(lines);
int? finalCap;
if (cap != null) {
// ✅ 今日有充满记录,直接使用
finalCap = cap;
dev.log('🔵 从日志提取容量: $cap', name: 'BatteryHealth');
} else {
// ⚠️ 今日无充满记录,尝试从数据库复制前一天容量
dev.log('🔵 今日无充满记录,尝试复制前一天容量', name: 'BatteryHealth');
final recent = await _dbService.queryRecentData(1);
if (recent.isNotEmpty) {
finalCap = recent.first['capacity'] as int;
dev.log('🔵 复制前一天容量: $finalCap', name: 'BatteryHealth');
} else {
finalCap = null;
dev.log('🔵 无历史数据可复制', name: 'BatteryHealth');
}
}
// 🟢 一次性 setState 更新所有字段
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();
_chargeAnimationController.reset();
_updateChargeState();
dev.log(
'🔵 ========== readBatteryCapacity 完成 ==========',
name: 'BatteryHealth',
);
} catch (e) {
dev.log('🔵 readBatteryCapacity 异常: $e', name: 'BatteryHealth');
setState(() => _hasData = false);
_chargeAnimationController.reset();
_updateChargeState();
}
}
// ---------- 辅助提取方法 ----------
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) {
// 静默降级
}
}
// ---------- 动画控制 ----------
void _startChargeAnimation() {
if (_chargeAnimationController.isAnimating) return;
_chargeAnimationController.forward();
}
void _stopChargeAnimation() {
if (_chargeAnimationController.isAnimating) return;
_chargeAnimationController.reverse();
}
void _updateChargeState() {
if (!mounted) return;
if (_isCharging) {
_startChargeAnimation();
} else {
_stopChargeAnimation();
}
}
// ---------- 显示辅助 ----------
String _getDisplayPower() {
if (!_isCharging) return '';
if (_isWireless) {
return ChargeUtils.getDisplayPowerWireless(_chargePower);
} else {
return ChargeUtils.getDisplayPowerWired(
_chargePower,
_wiredLevel,
_chargeType,
);
}
}
String _getChargeTypeLabel() {
return ChargeUtils.getChargeTypeLabel(_isWireless, _chargeType);
}
// ---------- Build ----------
@override
Widget build(BuildContext context) {
return AnnotatedRegion<SystemUiOverlayStyle>(
value: const SystemUiOverlayStyle(
statusBarColor: Colors.white,
statusBarIconBrightness: Brightness.dark,
statusBarBrightness: Brightness.dark,
),
child: Scaffold(
backgroundColor: Colors.white,
appBar: AppBar(
backgroundColor: Colors.white,
elevation: 0,
scrolledUnderElevation: 0,
shadowColor: Colors.transparent,
surfaceTintColor: Colors.transparent,
title: const Text(
'电池健康度',
style: TextStyle(
fontSize: 24,
fontWeight: FontWeight.w600,
color: Colors.black,
),
),
actions: [
IconButton(
icon: const Icon(Icons.refresh),
onPressed: readBatteryCapacity,
tooltip: '刷新数据',
color: Colors.black,
),
],
),
body: _hasData ? _buildDataLayout() : _buildEmptyState(),
),
);
}
Widget _buildEmptyState() {
return const Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.battery_unknown, size: 64, color: Colors.grey),
SizedBox(height: 16),
Text('暂无数据', style: TextStyle(fontSize: 18, color: Colors.grey)),
Text('点击右上角刷新尝试', style: TextStyle(fontSize: 14, color: Colors.grey)),
],
),
);
}
Widget _buildDataLayout() {
final healthPercent = (_designCap > 0)
? (_currentCap / _designCap) * 100
: 0;
String chargeSummary = '';
if (_isCharging) {
chargeSummary = '充电中 · ${_getChargeTypeLabel()} · ${_getDisplayPower()}';
}
return Padding(
padding: const EdgeInsets.all(20.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
_deviceModel,
style: const TextStyle(
fontSize: 14,
color: Colors.grey,
fontWeight: FontWeight.w400,
),
),
const SizedBox(height: 4),
BatterySummary(
healthPercent: healthPercent.toDouble(), // 显式转换
cycleCount: _cycleCount,
currentCap: _currentCap,
designCap: _designCap,
),
const SizedBox(height: 24),
TrendChart(data: _trendData, designCap: _designCap),
const SizedBox(height: 8),
AnimatedBuilder(
animation: _chargeAnimationController,
builder: (context, child) {
final entryHeight = 28 * _entryFadeIn.value;
final chargeEntry = Opacity(
opacity: _entryFadeIn.value,
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,
),
),
);
}
},
),
);
return Column(
children: [
SizedBox(height: entryHeight, child: chargeEntry),
Transform.translate(
offset: Offset(0, _bottomSlide.value),
child: BatteryInfoPanel(
batteryVolt: _batteryVolt,
batteryTemp: _batteryTemp,
maxVolt: _maxVolt,
minVolt: _minVolt,
maxTemp: _maxTemp,
minTemp: _minTemp,
),
),
],
);
},
),
const Spacer(),
],
),
);
}
}