5 Commits

6 changed files with 267 additions and 89 deletions
+4 -2
View File
@@ -5,13 +5,15 @@
android:label="@string/app_name"
android:name="${applicationName}"
android:icon="@drawable/logo"
android:requestLegacyExternalStorage="true">
android:requestLegacyExternalStorage="true"
android:theme="@style/LaunchTheme"> <!-- 全局先用 LaunchTheme -->
<activity
android:name=".MainActivity"
android:exported="true"
android:launchMode="singleTop"
android:taskAffinity=""
android:theme="@style/LaunchTheme"
android:theme="@style/AppTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize">
@@ -4,6 +4,7 @@ import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.content.SharedPreferences
import android.net.Uri
import android.os.BatteryManager
import android.os.Build
@@ -20,18 +21,17 @@ import java.io.BufferedReader
import java.io.InputStreamReader
class MainActivity : FlutterActivity() {
// ---------- 充电状态 ----------
private val CHARGE_CHANNEL = "com.lxh.battery_health/charge"
private val EVENT_CHANNEL = "com.lxh.battery_health/charge_events"
private var eventSink: EventChannel.EventSink? = null
// ---------- SAF 相关 ----------
private val SAF_CHANNEL = "com.lxh.battery_health/saf"
private var safResult: MethodChannel.Result? = null
private var safUri: Uri? = null
private val FOLDER_PICKER_REQUEST = 1001
private val PREF_NAME = "battery_health_prefs"
private val KEY_SAF_URI = "saf_uri"
// ---------- 充电广播接收器 ----------
private val chargeReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
intent ?: return
@@ -42,6 +42,46 @@ class MainActivity : FlutterActivity() {
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
android.util.Log.d("BatteryHealth", "onCreate 开始")
try {
val prefs: SharedPreferences = getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE)
val uriString = prefs.getString(KEY_SAF_URI, null)
android.util.Log.d("BatteryHealth", "SharedPreferences URI: $uriString")
if (uriString != null) {
safUri = Uri.parse(uriString)
android.util.Log.d("BatteryHealth", "恢复 SAF URI 成功: $safUri")
}
} catch (e: Exception) {
android.util.Log.e("BatteryHealth", "恢复 URI 异常: ${e.message}")
}
android.util.Log.d("BatteryHealth", "onCreate 结束,safUri = $safUri")
}
private fun saveUri(uri: Uri) {
try {
val prefs: SharedPreferences = getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE)
prefs.edit().putString(KEY_SAF_URI, uri.toString()).apply()
android.util.Log.d("BatteryHealth", "保存 SAF URI: $uri")
} catch (e: Exception) {
android.util.Log.e("BatteryHealth", "保存 URI 失败: ${e.message}")
}
}
private fun restoreUri(uriString: String) {
try {
safUri = Uri.parse(uriString)
android.util.Log.d("BatteryHealth", "通过 MethodChannel 恢复 URI: $safUri")
// 同时保存到 SharedPreferences
val prefs: SharedPreferences = getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE)
prefs.edit().putString(KEY_SAF_URI, uriString).apply()
} catch (e: Exception) {
android.util.Log.e("BatteryHealth", "restoreUri 失败: ${e.message}")
}
}
private fun sendChargeState(isCharging: Boolean) {
val chargeType = if (isCharging) {
val batteryIntent = registerReceiver(null, IntentFilter(Intent.ACTION_BATTERY_CHANGED))
@@ -66,7 +106,6 @@ class MainActivity : FlutterActivity() {
status == BatteryManager.BATTERY_STATUS_FULL
}
// ---------- SAF 方法 ----------
private fun openFolderPicker() {
try {
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE).apply {
@@ -100,7 +139,8 @@ class MainActivity : FlutterActivity() {
val takeFlags = Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
contentResolver.takePersistableUriPermission(uri, takeFlags)
safUri = uri
android.util.Log.d("BatteryHealth", "SAF URI: $uri")
saveUri(uri)
android.util.Log.d("BatteryHealth", "SAF 授权成功 URI: $uri")
safResult?.success(uri.toString())
} catch (e: Exception) {
android.util.Log.e("BatteryHealth", "SAF 授权失败: ${e.message}")
@@ -118,9 +158,13 @@ class MainActivity : FlutterActivity() {
}
private fun listFiles(): List<String> {
android.util.Log.d("BatteryHealth", "listFiles 被调用,safUri = $safUri")
val result = mutableListOf<String>()
val docId = getTreeDocId()
if (docId == null || docId.isEmpty()) return result
if (docId == null || docId.isEmpty()) {
android.util.Log.e("BatteryHealth", "docId 为空")
return result
}
try {
val childrenUri = DocumentsContract.buildChildDocumentsUriUsingTree(safUri!!, docId)
@@ -134,7 +178,7 @@ class MainActivity : FlutterActivity() {
}
}
} catch (e: Exception) {
android.util.Log.e("BatteryHealth", "列出文件失败: ${e.message}")
android.util.Log.e("BatteryHealth", "列出文件失败: ${e.message}", e)
}
return result
}
@@ -178,11 +222,9 @@ class MainActivity : FlutterActivity() {
return fileUri?.let { readFile(it) }
}
// ---------- Flutter 通道配置 ----------
override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
// 充电状态通道
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHARGE_CHANNEL)
.setMethodCallHandler { call, result ->
if (call.method == "getChargeState") {
@@ -205,10 +247,18 @@ class MainActivity : FlutterActivity() {
}
}
// SAF 通道
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, SAF_CHANNEL)
.setMethodCallHandler { call, result ->
when (call.method) {
"restoreUri" -> {
val uriString = call.argument<String>("uri")
if (uriString != null) {
restoreUri(uriString)
result.success(true)
} else {
result.error("INVALID_ARGUMENT", "uri is required", null)
}
}
"selectFolder" -> {
safResult = result
openFolderPicker()
@@ -233,7 +283,6 @@ class MainActivity : FlutterActivity() {
}
}
// 充电状态事件通道
EventChannel(flutterEngine.dartExecutor.binaryMessenger, EVENT_CHANNEL)
.setStreamHandler(
object : EventChannel.StreamHandler {
+5 -14
View File
@@ -1,18 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off -->
<style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when
the Flutter engine draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
Flutter UI initializes, as well as behind your Flutter UI while its
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
<style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
<!-- 状态栏背景白色 -->
<item name="android:statusBarColor">@android:color/white</item>
<!-- 状态栏图标亮色(深色图标) -->
<item name="android:windowLightStatusBar">true</item>
</style>
</resources>
+129 -40
View File
@@ -1,5 +1,5 @@
import 'dart:io';
import 'dart:async';
import 'dart:io';
import 'dart:developer' as dev;
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
@@ -8,6 +8,7 @@ import 'package:sqflite/sqflite.dart';
import 'package:path_provider/path_provider.dart';
import 'package:path/path.dart' as path;
import 'package:device_info_plus/device_info_plus.dart';
//import 'package:shared_preferences/shared_preferences.dart';
import '../models/device_matching.dart';
import '../utils/charge_utils.dart';
@@ -84,8 +85,10 @@ class _BatteryHealthPageState extends State<BatteryHealthPage>
}
Future<Database> _initDatabase() async {
dev.log('🔵 _initDatabase 开始', name: 'BatteryHealth');
Directory appDocDir = await getApplicationDocumentsDirectory();
String dbPath = path.join(appDocDir.path, 'battery_health.db');
dev.log('🔵 数据库路径: $dbPath', name: 'BatteryHealth');
return await openDatabase(
dbPath,
version: 2,
@@ -136,15 +139,18 @@ class _BatteryHealthPageState extends State<BatteryHealthPage>
// ---------- 设备信息 ----------
Future<void> _getDeviceInfo() async {
dev.log('🔵 _getDeviceInfo 开始', name: 'BatteryHealth');
DeviceInfoPlugin deviceInfo = DeviceInfoPlugin();
try {
AndroidDeviceInfo androidInfo = await deviceInfo.androidInfo;
String 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;
@@ -152,23 +158,25 @@ class _BatteryHealthPageState extends State<BatteryHealthPage>
}
}
// ---------- 扫描目录 (SAF 模式) ----------
// ---------- 扫描目录 (SAF) ----------
Future<void> _scanAndUpdateDB() async {
// 检查 SAF 授权状态
dev.log('🔵 _scanAndUpdateDB 开始', name: 'BatteryHealth');
_safState = await _safService.getPermissionState();
if (_safState != SafPermissionState.authorized) {
_showSafAuthorizationDialog();
dev.log('🔵 _scanAndUpdateDB: 未授权', name: 'BatteryHealth');
return;
}
try {
final files = await _safService.listFiles();
dev.log('🔵 扫描文件列表: $files', name: 'BatteryHealth');
RegExp filePattern = RegExp(r'mbattery_charger_log_(\d{4}_\d{2}_\d{2})$');
for (String fileName in files) {
Match? match = filePattern.firstMatch(fileName);
if (match != null) {
String dateStr = match.group(1)!.replaceAll('_', '-');
dev.log('🔵 处理文件: $fileName, 日期: $dateStr', name: 'BatteryHealth');
try {
List<String> lines = await _safService.readLogFile(fileName);
if (lines.isEmpty) continue;
@@ -197,11 +205,14 @@ class _BatteryHealthPageState extends State<BatteryHealthPage>
if (maxVolt == null || minVolt == null) continue;
await _insertOrUpdate(dateStr, cap, maxVolt, minVolt);
} catch (_) {}
dev.log('🔵 插入数据库: $dateStr, cap=$cap', name: 'BatteryHealth');
} catch (e) {
dev.log('🔵 处理文件 $fileName 异常: $e', name: 'BatteryHealth');
}
}
}
} catch (e) {
dev.log('[_scanAndUpdateDB] ❌ 扫描异常: $e', name: 'BatteryHealth');
dev.log('🔵 _scanAndUpdateDB 异常: $e', name: 'BatteryHealth');
}
}
@@ -211,7 +222,9 @@ class _BatteryHealthPageState extends State<BatteryHealthPage>
String year = nowUtc.year.toString();
String month = nowUtc.month.toString().padLeft(2, '0');
String day = nowUtc.day.toString().padLeft(2, '0');
return 'mbattery_charger_log_${year}_${month}_$day';
String fileName = 'mbattery_charger_log_${year}_${month}_$day';
dev.log('🔵 getTodayFileName: $fileName', name: 'BatteryHealth');
return fileName;
}
// ---------- 时间戳工具方法 ----------
@@ -250,9 +263,10 @@ class _BatteryHealthPageState extends State<BatteryHealthPage>
// ---------- SAF 授权引导 ----------
void _showSafAuthorizationDialog() {
dev.log('🔵 _showSafAuthorizationDialog 被调用', name: 'BatteryHealth');
if (!mounted) return;
showDialog(
showDialog<String>(
context: context,
barrierDismissible: false,
builder: (context) => AlertDialog(
@@ -274,7 +288,8 @@ class _BatteryHealthPageState extends State<BatteryHealthPage>
actions: [
TextButton(
onPressed: () {
Navigator.pop(context);
dev.log('🔵 用户取消授权', name: 'BatteryHealth');
Navigator.pop(context, 'cancel');
setState(() {
_safState = SafPermissionState.denied;
});
@@ -282,37 +297,44 @@ class _BatteryHealthPageState extends State<BatteryHealthPage>
child: const Text('取消'),
),
ElevatedButton(
onPressed: () async {
// 触发关闭
Navigator.pop(context);
// 延迟确保动画完成
await Future.delayed(const Duration(milliseconds: 150));
final result = await _safService.requestPermission();
if (!mounted) return;
if (result.success) {
setState(() {
_safState = SafPermissionState.authorized;
});
readBatteryCapacity();
} else {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(result.errorMessage ?? '授权失败,请重试'),
backgroundColor: Colors.red,
),
);
_showSafAuthorizationDialog();
}
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定调 ----------
@@ -520,10 +542,9 @@ class _BatteryHealthPageState extends State<BatteryHealthPage>
}
}
// 检查 SAF 授权
_safState = await _safService.getPermissionState();
if (_safState != SafPermissionState.authorized) {
if (mounted) _showSafAuthorizationDialog();
dev.log('[_fetchChargeDataFromLog] 未授权,跳过', name: 'BatteryHealth');
return;
}
@@ -609,9 +630,17 @@ class _BatteryHealthPageState extends State<BatteryHealthPage>
// ---------- 读取当前数据 ----------
Future<void> readBatteryCapacity() async {
dev.log(
'🔵 ========== readBatteryCapacity 开始 ==========',
name: 'BatteryHealth',
);
// 检查 SAF 授权
_safState = await _safService.getPermissionState();
dev.log('🔵 SAF状态: $_safState', name: 'BatteryHealth');
if (_safState != SafPermissionState.authorized) {
dev.log('🔵 未授权,弹窗', name: 'BatteryHealth');
if (mounted) _showSafAuthorizationDialog();
return;
}
@@ -620,11 +649,16 @@ class _BatteryHealthPageState extends State<BatteryHealthPage>
await _scanAndUpdateDB();
final fileName = getTodayFileName();
dev.log('🔵 今日文件名: $fileName', name: 'BatteryHealth');
final exists = await _safService.logFileExists(fileName);
dev.log('🔵 文件存在: $exists', name: 'BatteryHealth');
if (!exists) {
dev.log('🔵 文件不存在,尝试从数据库读取', name: 'BatteryHealth');
List<Map<String, dynamic>> recent = await _queryRecentData(1);
if (recent.isNotEmpty) {
dev.log('🔵 数据库有数据: ${recent.first}', name: 'BatteryHealth');
setState(() {
_currentCap = recent.first['capacity'].toDouble();
_maxVolt = recent.first['volt_max'] ?? 0;
@@ -633,6 +667,7 @@ class _BatteryHealthPageState extends State<BatteryHealthPage>
});
await _updateTrendChart();
} else {
dev.log('🔵 数据库无数据', name: 'BatteryHealth');
setState(() => _hasData = false);
}
if (_isCharging) _fetchChargeDataFromLog();
@@ -643,7 +678,9 @@ class _BatteryHealthPageState extends State<BatteryHealthPage>
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();
@@ -651,12 +688,23 @@ class _BatteryHealthPageState extends State<BatteryHealthPage>
return;
}
// 打印前几行
if (lines.isNotEmpty) {
dev.log('🔵 第一行: ${lines.first}', name: 'BatteryHealth');
if (lines.length > 1) {
dev.log('🔵 第二行: ${lines[1]}', name: 'BatteryHealth');
}
}
if (_isCharging) _parseChargeDataFromLog(lines);
List<String> validLines = lines
.where((line) => line.contains('tbat='))
.toList();
dev.log('🔵 有效行数 (含tbat): ${validLines.length}', name: 'BatteryHealth');
if (validLines.isEmpty) {
dev.log('🔵 无有效数据行', name: 'BatteryHealth');
setState(() => _hasData = false);
_chargeAnimationController.reset();
_updateChargeState();
@@ -664,6 +712,8 @@ class _BatteryHealthPageState extends State<BatteryHealthPage>
}
String lastLine = validLines.last;
dev.log('🔵 最后一行: $lastLine', name: 'BatteryHealth');
RegExp capReg = RegExp(r'learned_cap=(\d+)');
RegExp cycleReg = RegExp(r'cycle=(\d+)');
RegExp tempReg = RegExp(r'tbat=(\d+)');
@@ -674,7 +724,13 @@ class _BatteryHealthPageState extends State<BatteryHealthPage>
String? tempStr = tempReg.firstMatch(lastLine)?.group(1);
String? voltStr = voltageReg.firstMatch(lastLine)?.group(1);
dev.log(
'🔵 capStr=$capStr, cycleStr=$cycleStr, tempStr=$tempStr, voltStr=$voltStr',
name: 'BatteryHealth',
);
if (capStr == null) {
dev.log('🔵 未找到 learned_cap', name: 'BatteryHealth');
setState(() => _hasData = false);
_chargeAnimationController.reset();
_updateChargeState();
@@ -686,6 +742,11 @@ class _BatteryHealthPageState extends State<BatteryHealthPage>
double temp = double.parse(tempStr ?? '0');
double volt = double.parse(voltStr ?? '0') / 1000000;
dev.log(
'🔵 解析结果: capMah=$capMah, cycle=$cycle, temp=$temp, volt=$volt',
name: 'BatteryHealth',
);
List<double> temps = [];
for (String line in validLines) {
RegExpMatch? match = tempReg.firstMatch(line);
@@ -720,10 +781,20 @@ class _BatteryHealthPageState extends State<BatteryHealthPage>
_hasData = true;
});
dev.log(
'🔵 数据更新完成: _currentCap=$_currentCap, _cycleCount=$_cycleCount',
name: 'BatteryHealth',
);
await _updateTrendChart();
_chargeAnimationController.reset();
_updateChargeState();
dev.log(
'🔵 ========== readBatteryCapacity 完成 ==========',
name: 'BatteryHealth',
);
} catch (e) {
dev.log('🔵 readBatteryCapacity 异常: $e', name: 'BatteryHealth');
setState(() => _hasData = false);
_chargeAnimationController.reset();
_updateChargeState();
@@ -741,6 +812,7 @@ class _BatteryHealthPageState extends State<BatteryHealthPage>
@override
void initState() {
super.initState();
dev.log('🔵 ========== initState 开始 ==========', name: 'BatteryHealth');
_chargeAnimationController = AnimationController(
vsync: this,
@@ -775,18 +847,29 @@ class _BatteryHealthPageState extends State<BatteryHealthPage>
}
});
// 延迟初始化 SAF 并读取数据
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
@@ -802,11 +885,17 @@ class _BatteryHealthPageState extends State<BatteryHealthPage>
@override
Widget build(BuildContext context) {
return AnnotatedRegion<SystemUiOverlayStyle>(
value: SystemUiOverlayStyle.dark,
value: SystemUiOverlayStyle(
statusBarColor: Colors.white,
statusBarIconBrightness: Brightness.dark,
statusBarBrightness: Brightness.dark,
),
child: Scaffold(
backgroundColor: Colors.white, // 🟢 页面背景色与 AppBar 统一
appBar: AppBar(
backgroundColor: const Color.fromARGB(0, 255, 255, 255),
elevation: 0,
backgroundColor: Colors.white,
elevation: 0, // 🟢 去掉阴影
scrolledUnderElevation: 0, // 🟢 滚动时也不出现阴影
title: const Text(
'电池健康度',
style: TextStyle(
+53 -6
View File
@@ -3,6 +3,8 @@ import 'package:flutter/services.dart';
import 'package:shared_preferences/shared_preferences.dart';
import '../models/saf_permission_state.dart';
/// SAF 存储服务
/// 负责管理通过 SAF 授权的文件夹访问
class SafStorageService {
static final SafStorageService _instance = SafStorageService._internal();
factory SafStorageService() => _instance;
@@ -14,7 +16,7 @@ class SafStorageService {
static const String _prefKeySafUri = 'saf_storage_uri';
String? _cachedUri;
// 🟢 初始化从 SharedPreferences 恢复 URI
/// 初始化从 SharedPreferences 恢复 URI
Future<void> init() async {
try {
final prefs = await SharedPreferences.getInstance();
@@ -30,14 +32,16 @@ class SafStorageService {
}
}
/// 获取当前授权状态(简化版,直接根据缓存判断)
Future<SafPermissionState> getPermissionState() async {
if (_cachedUri == null || _cachedUri!.isEmpty) {
return SafPermissionState.unauthorized;
}
// 🟢 简化:直接返回 authorized,相信本地缓存的 URI
// 直接返回 authorized,相信缓存
return SafPermissionState.authorized;
}
/// 请求 SAF 授权(引导用户选择文件夹)
Future<SafAuthorizationResult> requestPermission() async {
try {
final String? uriString = await _channel.invokeMethod('selectFolder');
@@ -62,23 +66,57 @@ class SafStorageService {
}
}
/// 🟢 将 URI 传递给 Kotlin 端(用于恢复 safUri
Future<bool> restoreUri() async {
if (_cachedUri == null || _cachedUri!.isEmpty) {
if (kDebugMode) {
print('[SAF] restoreUri: 缓存 URI 为空');
}
return false;
}
try {
final bool? result = await _channel.invokeMethod('restoreUri', {
'uri': _cachedUri,
});
if (result == true) {
if (kDebugMode) {
print('[SAF] restoreUri 成功: $_cachedUri');
}
return true;
} else {
if (kDebugMode) {
print('[SAF] restoreUri 失败: 返回 false');
}
return false;
}
} catch (e) {
if (kDebugMode) {
print('[SAF] restoreUri 异常: $e');
}
return false;
}
}
/// 检查指定日期的日志文件是否存在
Future<bool> logFileExists(String fileName) async {
final state = await getPermissionState();
if (state != SafPermissionState.authorized) return false;
try {
final files = await _channel.invokeMethod('listFiles');
if (files is List) {
final files = await listFiles();
return files.contains(fileName);
}
return false;
} catch (e) {
return false;
}
}
/// 读取日志文件内容(返回行列表)
Future<List<String>> readLogFile(String fileName) async {
// 🟢 先尝试恢复 URI
await restoreUri();
final state = await getPermissionState();
if (state != SafPermissionState.authorized) return [];
try {
final String? content = await _channel.invokeMethod('readLogFile', {
'fileName': fileName,
@@ -93,9 +131,14 @@ class SafStorageService {
}
}
/// 获取文件夹中的文件列表
Future<List<String>> listFiles() async {
// 🟢 先尝试恢复 URI
await restoreUri();
final state = await getPermissionState();
if (state != SafPermissionState.authorized) return [];
try {
final result = await _channel.invokeMethod('listFiles');
if (result is List) {
@@ -110,16 +153,19 @@ class SafStorageService {
}
}
/// 验证授权是否有效(简化版)
Future<bool> validatePermission() async {
final state = await getPermissionState();
return state == SafPermissionState.authorized;
}
/// 清除缓存
void _clearCache() {
_cachedUri = null;
_saveUri(null);
}
/// 保存 URI 到 SharedPreferences
Future<void> _saveUri(String? uri) async {
try {
final prefs = await SharedPreferences.getInstance();
@@ -141,5 +187,6 @@ class SafStorageService {
}
}
/// 获取缓存的 URI(用于调试)
String? get cachedUri => _cachedUri;
}
+1 -1
View File
@@ -16,7 +16,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
# In Windows, build-name is used as the major, minor, and patch parts
# of the product and file versions while build-number is used as the build suffix.
version: 1.3.0-dev.4
version: 1.4.0
environment:
sdk: ^3.9.2