Files

747 lines
24 KiB
Dart
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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';
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';
// --- 潘通色定义 ---
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;
// ---------- 趋势图数据 ----------
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');
_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;
_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',
);
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(() {
_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);
}
}
// ---------- 辅助提取方法 ----------
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 double healthPercent = (_designCap > 0)
? (_currentCap / _designCap) * 100
: 0.0;
final String chargeSummary = _isCharging
? '充电中 · ${_getChargeTypeLabel()} · ${_getDisplayPower()}'
: '';
return AnnotatedRegion<SystemUiOverlayStyle>(
value: const SystemUiOverlayStyle(
statusBarColor: Color.fromRGBO(157, 212, 237, 0.1),
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,
),
PopupMenuButton<String>(
icon: const Icon(Icons.more_vert),
color: Colors.white,
onSelected: (value) {
switch (value) {
case 'changelog':
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const ChangelogPage(),
),
);
break;
case 'dark_mode':
break;
case 'about':
break;
}
},
itemBuilder: (context) => [
const PopupMenuItem(value: 'changelog', child: Text('更新日志')),
const PopupMenuItem(
value: 'dark_mode',
enabled: false,
child: Text('深色模式(开发中)'),
),
const PopupMenuItem(
value: 'about',
enabled: false,
child: Text('关于(开发中)'),
),
],
),
],
),
body: _hasData
? Column(
children: [
// 🟢 可滚动区域(带 RepaintBoundary
Expanded(
child: SingleChildScrollView(
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 用 RepaintBoundary 隔离
RepaintBoundary(
child: BatterySummary(
healthPercent: healthPercent,
cycleCount: _cycleCount,
currentCap: _currentCap,
designCap: _designCap,
),
),
const SizedBox(height: 24),
// 🟢 TrendChart 用 RepaintBoundary 隔离
RepaintBoundary(
child: TrendChart(
data: _trendData,
designCap: _designCap,
),
),
const SizedBox(height: 8),
// 🟢 充电条目 - 保留 AnimatedOpacity,但加 RepaintBoundary
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),
// 🟢 BatteryInfoPanel 也用 RepaintBoundary
RepaintBoundary(
child: BatteryInfoPanel(
batteryVolt: _batteryVolt,
batteryTemp: _batteryTemp,
maxVolt: _maxVolt,
minVolt: _minVolt,
maxTemp: _maxTemp,
minTemp: _minTemp,
),
),
],
),
),
),
],
)
: 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),
),
],
),
),
),
);
}
}