8 Commits

12 changed files with 588 additions and 464 deletions
+19 -22
View File
@@ -6,7 +6,7 @@ plugins {
android { android {
namespace = "com.lxh.battery_health" namespace = "com.lxh.battery_health"
compileSdk = flutter.compileSdkVersion compileSdk = 34
ndkVersion = flutter.ndkVersion ndkVersion = flutter.ndkVersion
compileOptions { compileOptions {
@@ -21,38 +21,39 @@ android {
defaultConfig { defaultConfig {
applicationId = "com.lxh.battery_health" applicationId = "com.lxh.battery_health"
minSdk = flutter.minSdkVersion minSdk = flutter.minSdkVersion
targetSdk = flutter.targetSdkVersion targetSdk = 34
versionCode = flutter.versionCode versionCode = flutter.versionCode
versionName = flutter.versionName versionName = flutter.versionName
multiDexEnabled = true multiDexEnabled = true
} }
signingConfigs { signingConfigs {
// 如果没有正式签名,可以用 debug 签名代替(测试用)
// 正式发布时请替换为自己的密钥
getByName("debug") {
// debug 签名已经存在,无需额外配置
}
create("release") { create("release") {
// 这里配置你正式发布的签名 // 如果有正式签名,在这里配置
// 如果还没有正式签名,debug 可以用 debug 签名替代 // keyAlias = "..."
keyAlias = "your_key_alias" // keyPassword = "..."
keyPassword = "your_key_password" // storeFile = file("...")
storeFile = file("your_keystore.jks") // storePassword = "..."
storePassword = "your_store_password"
} }
} }
signingConfigs {
// 如果没有正式签名,可以暂时用 debug 签名
// 正式发布时再替换
}
buildTypes { buildTypes {
debug { debug {
// 🟢 关键:debug 版本包名加 .dev 后缀
applicationIdSuffix = ".dev" applicationIdSuffix = ".dev"
signingConfig = signingConfigs.getByName("debug") signingConfig = signingConfigs.getByName("debug")
isDebuggable = true
isMinifyEnabled = false
isShrinkResources = false
} }
release { release {
applicationIdSuffix = "" // 保持原包名 // release 版本不加后缀,保持原包名
// 🟢 如果还没有正式签名,先用 debug 签名替代 signingConfig = signingConfigs.getByName("debug") // 若没有正式签名,先用 debug 签名
signingConfig = signingConfigs.getByName("debug")
isMinifyEnabled = true isMinifyEnabled = true
isShrinkResources = true isShrinkResources = true
proguardFiles( proguardFiles(
@@ -61,9 +62,6 @@ android {
) )
} }
} }
// 如果有 productFlavors,可以在这里配置
// 如果没有,忽略即可
} }
flutter { flutter {
@@ -71,6 +69,5 @@ flutter {
} }
dependencies { dependencies {
// 如果项目中有其他依赖,添加在这里 // 无需额外添加 Activity KTX,因为已改用 startActivityForResult
// 注意:sqflite、permission_handler 等 Flutter 插件会自动包含
} }
+4 -2
View File
@@ -5,13 +5,15 @@
android:label="@string/app_name" android:label="@string/app_name"
android:name="${applicationName}" android:name="${applicationName}"
android:icon="@drawable/logo" android:icon="@drawable/logo"
android:requestLegacyExternalStorage="true"> android:requestLegacyExternalStorage="true"
android:theme="@style/LaunchTheme"> <!-- 全局先用 LaunchTheme -->
<activity <activity
android:name=".MainActivity" android:name=".MainActivity"
android:exported="true" android:exported="true"
android:launchMode="singleTop" android:launchMode="singleTop"
android:taskAffinity="" android:taskAffinity=""
android:theme="@style/LaunchTheme" android:theme="@style/AppTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode" android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true" android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize"> android:windowSoftInputMode="adjustResize">
@@ -1,96 +0,0 @@
package com.lxh.battery_health
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.os.BatteryManager
import android.os.Bundle
import androidx.annotation.NonNull
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.EventChannel
import io.flutter.plugin.common.MethodChannel
class MainActivity : FlutterActivity() {
private val CHANNEL = "com.lxh.battery_health/charge"
private val EVENT_CHANNEL = "com.lxh.battery_health/charge_events"
private var eventSink: EventChannel.EventSink? = null
private val chargeReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
intent ?: return
when (intent.action) {
Intent.ACTION_POWER_CONNECTED -> sendChargeState(true)
Intent.ACTION_POWER_DISCONNECTED -> sendChargeState(false)
}
}
}
private fun sendChargeState(isCharging: Boolean) {
val chargeType = if (isCharging) {
val batteryIntent = registerReceiver(null, IntentFilter(Intent.ACTION_BATTERY_CHANGED))
val plug = batteryIntent?.getIntExtra(BatteryManager.EXTRA_PLUGGED, -1) ?: -1
android.util.Log.d("BatteryHealth", "plug value: $plug, isCharging: $isCharging")
when (plug) {
BatteryManager.BATTERY_PLUGGED_AC -> "AC"
BatteryManager.BATTERY_PLUGGED_USB -> "USB"
BatteryManager.BATTERY_PLUGGED_WIRELESS -> "WIRELESS" // 🟢 关键修复
else -> "UNKNOWN"
}
} else {
"NONE"
}
eventSink?.success(mapOf("isCharging" to isCharging, "chargeType" to chargeType))
}
override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL).setMethodCallHandler { call, result ->
if (call.method == "getChargeState") {
val isCharging = getBatteryStatus()
val chargeType = if (isCharging) {
val batteryIntent = registerReceiver(null, IntentFilter(Intent.ACTION_BATTERY_CHANGED))
val plug = batteryIntent?.getIntExtra(BatteryManager.EXTRA_PLUGGED, -1) ?: -1
when (plug) {
BatteryManager.BATTERY_PLUGGED_AC -> "AC"
BatteryManager.BATTERY_PLUGGED_USB -> "USB"
BatteryManager.BATTERY_PLUGGED_WIRELESS -> "WIRELESS" // 🟢 关键修复
else -> "UNKNOWN"
}
} else {
"NONE"
}
result.success(mapOf("isCharging" to isCharging, "chargeType" to chargeType))
} else {
result.notImplemented()
}
}
EventChannel(flutterEngine.dartExecutor.binaryMessenger, EVENT_CHANNEL).setStreamHandler(
object : EventChannel.StreamHandler {
override fun onListen(arguments: Any?, events: EventChannel.EventSink?) {
eventSink = events
val filter = IntentFilter().apply {
addAction(Intent.ACTION_POWER_CONNECTED)
addAction(Intent.ACTION_POWER_DISCONNECTED)
}
registerReceiver(chargeReceiver, filter)
}
override fun onCancel(arguments: Any?) {
eventSink = null
unregisterReceiver(chargeReceiver)
}
}
)
}
private fun getBatteryStatus(): Boolean {
val batteryIntent = registerReceiver(null, IntentFilter(Intent.ACTION_BATTERY_CHANGED))
val status = batteryIntent?.getIntExtra(BatteryManager.EXTRA_STATUS, -1) ?: -1
return status == BatteryManager.BATTERY_STATUS_CHARGING ||
status == BatteryManager.BATTERY_STATUS_FULL
}
}
@@ -0,0 +1,305 @@
package com.lxh.battery_health
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
import android.os.Bundle
import android.provider.DocumentsContract
import android.provider.OpenableColumns
import androidx.activity.ComponentActivity
import androidx.annotation.NonNull
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.EventChannel
import io.flutter.plugin.common.MethodChannel
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
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
when (intent.action) {
Intent.ACTION_POWER_CONNECTED -> sendChargeState(true)
Intent.ACTION_POWER_DISCONNECTED -> sendChargeState(false)
}
}
}
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))
val plug = batteryIntent?.getIntExtra(BatteryManager.EXTRA_PLUGGED, -1) ?: -1
android.util.Log.d("BatteryHealth", "plug value: $plug, isCharging: $isCharging")
when (plug) {
BatteryManager.BATTERY_PLUGGED_AC -> "AC"
BatteryManager.BATTERY_PLUGGED_USB -> "USB"
BatteryManager.BATTERY_PLUGGED_WIRELESS -> "WIRELESS"
else -> "UNKNOWN"
}
} else {
"NONE"
}
eventSink?.success(mapOf("isCharging" to isCharging, "chargeType" to chargeType))
}
private fun getBatteryStatus(): Boolean {
val batteryIntent = registerReceiver(null, IntentFilter(Intent.ACTION_BATTERY_CHANGED))
val status = batteryIntent?.getIntExtra(BatteryManager.EXTRA_STATUS, -1) ?: -1
return status == BatteryManager.BATTERY_STATUS_CHARGING ||
status == BatteryManager.BATTERY_STATUS_FULL
}
private fun openFolderPicker() {
try {
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE).apply {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
try {
val downloadsUri = DocumentsContract.buildDocumentUri(
"com.android.externalstorage.documents",
"primary:Downloads"
)
putExtra(DocumentsContract.EXTRA_INITIAL_URI, downloadsUri)
} catch (e: Exception) {
// 忽略
}
}
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
}
startActivityForResult(intent, FOLDER_PICKER_REQUEST)
} catch (e: Exception) {
android.util.Log.e("BatteryHealth", "打开文件夹选择器失败: ${e.message}")
safResult?.error("SELECT_FOLDER_FAILED", e.message, null)
safResult = null
}
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (requestCode == FOLDER_PICKER_REQUEST && resultCode == ComponentActivity.RESULT_OK) {
val uri = data?.data
if (uri != null) {
try {
val takeFlags = Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
contentResolver.takePersistableUriPermission(uri, takeFlags)
safUri = 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}")
safResult?.error("SAF_FAILED", e.message, null)
}
} else {
safResult?.success(null)
}
safResult = null
}
}
private fun getTreeDocId(): String? {
return safUri?.let { DocumentsContract.getTreeDocumentId(it) }
}
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()) {
android.util.Log.e("BatteryHealth", "docId 为空")
return result
}
try {
val childrenUri = DocumentsContract.buildChildDocumentsUriUsingTree(safUri!!, docId)
contentResolver.query(childrenUri, null, null, null, null)?.use { cursor ->
val nameIndex = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)
while (cursor.moveToNext()) {
val name = cursor.getString(nameIndex)
if (name != null) {
result.add(name)
}
}
}
} catch (e: Exception) {
android.util.Log.e("BatteryHealth", "列出文件失败: ${e.message}", e)
}
return result
}
private fun getFileUri(fileName: String): Uri? {
val docId = getTreeDocId() ?: return null
try {
val childrenUri = DocumentsContract.buildChildDocumentsUriUsingTree(safUri!!, docId)
contentResolver.query(childrenUri, null, null, null, null)?.use { cursor ->
val nameIndex = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)
val idIndex = cursor.getColumnIndex(DocumentsContract.Document.COLUMN_DOCUMENT_ID)
while (cursor.moveToNext()) {
val name = cursor.getString(nameIndex)
if (name == fileName) {
val fileDocId = cursor.getString(idIndex)
return DocumentsContract.buildDocumentUriUsingTree(safUri!!, fileDocId)
}
}
}
} catch (e: Exception) {
android.util.Log.e("BatteryHealth", "获取文件URI失败: ${e.message}")
}
return null
}
private fun readFile(uri: Uri): String? {
return try {
contentResolver.openInputStream(uri)?.use { inputStream ->
BufferedReader(InputStreamReader(inputStream)).use { reader ->
reader.readText()
}
}
} catch (e: Exception) {
android.util.Log.e("BatteryHealth", "读取文件失败: ${e.message}")
null
}
}
private fun readLogFile(fileName: String): String? {
val fileUri = getFileUri(fileName)
return fileUri?.let { readFile(it) }
}
override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHARGE_CHANNEL)
.setMethodCallHandler { call, result ->
if (call.method == "getChargeState") {
val isCharging = getBatteryStatus()
val chargeType = if (isCharging) {
val batteryIntent = registerReceiver(null, IntentFilter(Intent.ACTION_BATTERY_CHANGED))
val plug = batteryIntent?.getIntExtra(BatteryManager.EXTRA_PLUGGED, -1) ?: -1
when (plug) {
BatteryManager.BATTERY_PLUGGED_AC -> "AC"
BatteryManager.BATTERY_PLUGGED_USB -> "USB"
BatteryManager.BATTERY_PLUGGED_WIRELESS -> "WIRELESS"
else -> "UNKNOWN"
}
} else {
"NONE"
}
result.success(mapOf("isCharging" to isCharging, "chargeType" to chargeType))
} else {
result.notImplemented()
}
}
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()
}
"readLogFile" -> {
val fileName = call.argument<String>("fileName")
if (fileName != null) {
val content = readLogFile(fileName)
result.success(content)
} else {
result.error("INVALID_ARGUMENT", "fileName is required", null)
}
}
"listFiles" -> {
val files = listFiles()
result.success(files)
}
"getUri" -> {
result.success(safUri?.toString())
}
else -> result.notImplemented()
}
}
EventChannel(flutterEngine.dartExecutor.binaryMessenger, EVENT_CHANNEL)
.setStreamHandler(
object : EventChannel.StreamHandler {
override fun onListen(arguments: Any?, events: EventChannel.EventSink?) {
eventSink = events
val filter = IntentFilter().apply {
addAction(Intent.ACTION_POWER_CONNECTED)
addAction(Intent.ACTION_POWER_DISCONNECTED)
}
registerReceiver(chargeReceiver, filter)
}
override fun onCancel(arguments: Any?) {
eventSink = null
unregisterReceiver(chargeReceiver)
}
}
)
}
}
+5 -14
View File
@@ -1,18 +1,9 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off --> <style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
<style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar"> <!-- 状态栏背景白色 -->
<!-- Show a splash screen on the activity. Automatically removed when <item name="android:statusBarColor">@android:color/white</item>
the Flutter engine draws its first frame --> <!-- 状态栏图标亮色(深色图标) -->
<item name="android:windowBackground">@drawable/launch_background</item> <item name="android:windowLightStatusBar">true</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> </style>
</resources> </resources>
+4
View File
@@ -1,3 +1,7 @@
org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError
android.useAndroidX=true android.useAndroidX=true
android.enableJetifier=true android.enableJetifier=true
# This builtInKotlin flag was added automatically by Flutter migrator
android.builtInKotlin=false
# This newDsl flag was added automatically by Flutter migrator
android.newDsl=false
+3 -1
View File
@@ -20,7 +20,9 @@ class MyApp extends StatelessWidget {
title: '电池健康度', title: '电池健康度',
theme: ThemeData( theme: ThemeData(
fontFamily: 'HarmonyOS_Sans_SC', fontFamily: 'HarmonyOS_Sans_SC',
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple), colorScheme: ColorScheme.fromSeed(
seedColor: const Color.fromARGB(255, 255, 255, 255),
),
useMaterial3: true, useMaterial3: true,
), ),
home: const BatteryHealthPage(), home: const BatteryHealthPage(),
+123 -52
View File
@@ -1,5 +1,5 @@
import 'dart:io';
import 'dart:async'; import 'dart:async';
import 'dart:io';
import 'dart:developer' as dev; import 'dart:developer' as dev;
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
@@ -8,7 +8,7 @@ import 'package:sqflite/sqflite.dart';
import 'package:path_provider/path_provider.dart'; import 'package:path_provider/path_provider.dart';
import 'package:path/path.dart' as path; import 'package:path/path.dart' as path;
import 'package:device_info_plus/device_info_plus.dart'; import 'package:device_info_plus/device_info_plus.dart';
import 'package:shared_preferences/shared_preferences.dart'; //import 'package:shared_preferences/shared_preferences.dart';
import '../models/device_matching.dart'; import '../models/device_matching.dart';
import '../utils/charge_utils.dart'; import '../utils/charge_utils.dart';
@@ -85,8 +85,10 @@ class _BatteryHealthPageState extends State<BatteryHealthPage>
} }
Future<Database> _initDatabase() async { Future<Database> _initDatabase() async {
dev.log('🔵 _initDatabase 开始', name: 'BatteryHealth');
Directory appDocDir = await getApplicationDocumentsDirectory(); Directory appDocDir = await getApplicationDocumentsDirectory();
String dbPath = path.join(appDocDir.path, 'battery_health.db'); String dbPath = path.join(appDocDir.path, 'battery_health.db');
dev.log('🔵 数据库路径: $dbPath', name: 'BatteryHealth');
return await openDatabase( return await openDatabase(
dbPath, dbPath,
version: 2, version: 2,
@@ -137,15 +139,18 @@ class _BatteryHealthPageState extends State<BatteryHealthPage>
// ---------- 设备信息 ---------- // ---------- 设备信息 ----------
Future<void> _getDeviceInfo() async { Future<void> _getDeviceInfo() async {
dev.log('🔵 _getDeviceInfo 开始', name: 'BatteryHealth');
DeviceInfoPlugin deviceInfo = DeviceInfoPlugin(); DeviceInfoPlugin deviceInfo = DeviceInfoPlugin();
try { try {
AndroidDeviceInfo androidInfo = await deviceInfo.androidInfo; AndroidDeviceInfo androidInfo = await deviceInfo.androidInfo;
String model = androidInfo.model; String model = androidInfo.model;
dev.log('🔵 设备型号: $model', name: 'BatteryHealth');
setState(() { setState(() {
_deviceModel = DeviceMatching.getModelName(model); _deviceModel = DeviceMatching.getModelName(model);
_designCap = DeviceMatching.getDesignCapacity(model); _designCap = DeviceMatching.getDesignCapacity(model);
}); });
} catch (e) { } catch (e) {
dev.log('🔵 获取设备信息失败: $e', name: 'BatteryHealth');
setState(() { setState(() {
_deviceModel = '魅族21 Pro (默认)'; _deviceModel = '魅族21 Pro (默认)';
_designCap = 5050; _designCap = 5050;
@@ -153,23 +158,25 @@ class _BatteryHealthPageState extends State<BatteryHealthPage>
} }
} }
// ---------- 扫描目录 (SAF 模式) ---------- // ---------- 扫描目录 (SAF) ----------
Future<void> _scanAndUpdateDB() async { Future<void> _scanAndUpdateDB() async {
// 检查 SAF 授权状态 dev.log('🔵 _scanAndUpdateDB 开始', name: 'BatteryHealth');
_safState = await _safService.getPermissionState(); _safState = await _safService.getPermissionState();
if (_safState != SafPermissionState.authorized) { if (_safState != SafPermissionState.authorized) {
_showSafAuthorizationDialog(); dev.log('🔵 _scanAndUpdateDB: 未授权', name: 'BatteryHealth');
return; return;
} }
try { try {
final files = await _safService.listFiles(); final files = await _safService.listFiles();
dev.log('🔵 扫描文件列表: $files', name: 'BatteryHealth');
RegExp filePattern = RegExp(r'mbattery_charger_log_(\d{4}_\d{2}_\d{2})$'); RegExp filePattern = RegExp(r'mbattery_charger_log_(\d{4}_\d{2}_\d{2})$');
for (String fileName in files) { for (String fileName in files) {
Match? match = filePattern.firstMatch(fileName); Match? match = filePattern.firstMatch(fileName);
if (match != null) { if (match != null) {
String dateStr = match.group(1)!.replaceAll('_', '-'); String dateStr = match.group(1)!.replaceAll('_', '-');
dev.log('🔵 处理文件: $fileName, 日期: $dateStr', name: 'BatteryHealth');
try { try {
List<String> lines = await _safService.readLogFile(fileName); List<String> lines = await _safService.readLogFile(fileName);
if (lines.isEmpty) continue; if (lines.isEmpty) continue;
@@ -198,11 +205,14 @@ class _BatteryHealthPageState extends State<BatteryHealthPage>
if (maxVolt == null || minVolt == null) continue; if (maxVolt == null || minVolt == null) continue;
await _insertOrUpdate(dateStr, cap, maxVolt, minVolt); await _insertOrUpdate(dateStr, cap, maxVolt, minVolt);
} catch (_) {} dev.log('🔵 插入数据库: $dateStr, cap=$cap', name: 'BatteryHealth');
} catch (e) {
dev.log('🔵 处理文件 $fileName 异常: $e', name: 'BatteryHealth');
}
} }
} }
} catch (e) { } catch (e) {
dev.log('[_scanAndUpdateDB] ❌ 扫描异常: $e', name: 'BatteryHealth'); dev.log('🔵 _scanAndUpdateDB 异常: $e', name: 'BatteryHealth');
} }
} }
@@ -212,7 +222,9 @@ class _BatteryHealthPageState extends State<BatteryHealthPage>
String year = nowUtc.year.toString(); String year = nowUtc.year.toString();
String month = nowUtc.month.toString().padLeft(2, '0'); String month = nowUtc.month.toString().padLeft(2, '0');
String day = nowUtc.day.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;
} }
// ---------- 时间戳工具方法 ---------- // ---------- 时间戳工具方法 ----------
@@ -251,13 +263,10 @@ class _BatteryHealthPageState extends State<BatteryHealthPage>
// ---------- SAF 授权引导 ---------- // ---------- SAF 授权引导 ----------
void _showSafAuthorizationDialog() { void _showSafAuthorizationDialog() {
<<<<<<< HEAD dev.log('🔵 _showSafAuthorizationDialog 被调用', name: 'BatteryHealth');
=======
// 避免重复弹窗
>>>>>>> c5a85e05a3bb50cd4ea7de0da2db94c867eb7ddd
if (!mounted) return; if (!mounted) return;
showDialog( showDialog<String>(
context: context, context: context,
barrierDismissible: false, barrierDismissible: false,
builder: (context) => AlertDialog( builder: (context) => AlertDialog(
@@ -279,11 +288,8 @@ class _BatteryHealthPageState extends State<BatteryHealthPage>
actions: [ actions: [
TextButton( TextButton(
onPressed: () { onPressed: () {
Navigator.pop(context); dev.log('🔵 用户取消授权', name: 'BatteryHealth');
<<<<<<< HEAD Navigator.pop(context, 'cancel');
=======
// 用户取消后,保持当前状态,但不阻塞后续操作
>>>>>>> c5a85e05a3bb50cd4ea7de0da2db94c867eb7ddd
setState(() { setState(() {
_safState = SafPermissionState.denied; _safState = SafPermissionState.denied;
}); });
@@ -291,47 +297,44 @@ class _BatteryHealthPageState extends State<BatteryHealthPage>
child: const Text('取消'), child: const Text('取消'),
), ),
ElevatedButton( ElevatedButton(
onPressed: () async { onPressed: () {
<<<<<<< HEAD dev.log('🔵 用户点击选择文件夹', name: 'BatteryHealth');
final ctx = context; Navigator.pop(context, 'select');
Navigator.pop(ctx); },
final result = await _safService.requestPermission(); child: const Text('选择文件夹'),
if (result.success) { ),
],
),
).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 (!mounted) return;
=======
final ctx = context; // 保存引用 if (authResult.success) {
Navigator.pop(ctx); dev.log('🔵 授权成功,调用 readBatteryCapacity', name: 'BatteryHealth');
final result = await _safService.requestPermission();
if (result.success) {
if (!mounted) return; // 先检查 State 是否还在
>>>>>>> c5a85e05a3bb50cd4ea7de0da2db94c867eb7ddd
setState(() { setState(() {
_safState = SafPermissionState.authorized; _safState = SafPermissionState.authorized;
}); });
readBatteryCapacity(); readBatteryCapacity();
} else { } else {
if (!mounted) return; dev.log('🔵 授权失败: ${authResult.errorMessage}', name: 'BatteryHealth');
ScaffoldMessenger.of(ctx).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar( SnackBar(
content: Text(result.errorMessage ?? '授权失败,请重试'), content: Text(authResult.errorMessage ?? '授权失败,请重试'),
backgroundColor: Colors.red, backgroundColor: Colors.red,
), ),
); );
<<<<<<< HEAD
_showSafAuthorizationDialog(); _showSafAuthorizationDialog();
} }
},
child: const Text('选择文件夹'), // 🟢 加这行
=======
// 重新弹窗
_showSafAuthorizationDialog();
} }
}, });
>>>>>>> c5a85e05a3bb50cd4ea7de0da2db94c867eb7ddd
),
],
),
);
} }
// ---------- 核心:系统API定调 ---------- // ---------- 核心:系统API定调 ----------
@@ -539,10 +542,9 @@ class _BatteryHealthPageState extends State<BatteryHealthPage>
} }
} }
// 检查 SAF 授权
_safState = await _safService.getPermissionState(); _safState = await _safService.getPermissionState();
if (_safState != SafPermissionState.authorized) { if (_safState != SafPermissionState.authorized) {
if (mounted) _showSafAuthorizationDialog(); dev.log('[_fetchChargeDataFromLog] 未授权,跳过', name: 'BatteryHealth');
return; return;
} }
@@ -628,9 +630,17 @@ class _BatteryHealthPageState extends State<BatteryHealthPage>
// ---------- 读取当前数据 ---------- // ---------- 读取当前数据 ----------
Future<void> readBatteryCapacity() async { Future<void> readBatteryCapacity() async {
dev.log(
'🔵 ========== readBatteryCapacity 开始 ==========',
name: 'BatteryHealth',
);
// 检查 SAF 授权 // 检查 SAF 授权
_safState = await _safService.getPermissionState(); _safState = await _safService.getPermissionState();
dev.log('🔵 SAF状态: $_safState', name: 'BatteryHealth');
if (_safState != SafPermissionState.authorized) { if (_safState != SafPermissionState.authorized) {
dev.log('🔵 未授权,弹窗', name: 'BatteryHealth');
if (mounted) _showSafAuthorizationDialog(); if (mounted) _showSafAuthorizationDialog();
return; return;
} }
@@ -639,11 +649,16 @@ class _BatteryHealthPageState extends State<BatteryHealthPage>
await _scanAndUpdateDB(); await _scanAndUpdateDB();
final fileName = getTodayFileName(); final fileName = getTodayFileName();
dev.log('🔵 今日文件名: $fileName', name: 'BatteryHealth');
final exists = await _safService.logFileExists(fileName); final exists = await _safService.logFileExists(fileName);
dev.log('🔵 文件存在: $exists', name: 'BatteryHealth');
if (!exists) { if (!exists) {
dev.log('🔵 文件不存在,尝试从数据库读取', name: 'BatteryHealth');
List<Map<String, dynamic>> recent = await _queryRecentData(1); List<Map<String, dynamic>> recent = await _queryRecentData(1);
if (recent.isNotEmpty) { if (recent.isNotEmpty) {
dev.log('🔵 数据库有数据: ${recent.first}', name: 'BatteryHealth');
setState(() { setState(() {
_currentCap = recent.first['capacity'].toDouble(); _currentCap = recent.first['capacity'].toDouble();
_maxVolt = recent.first['volt_max'] ?? 0; _maxVolt = recent.first['volt_max'] ?? 0;
@@ -652,6 +667,7 @@ class _BatteryHealthPageState extends State<BatteryHealthPage>
}); });
await _updateTrendChart(); await _updateTrendChart();
} else { } else {
dev.log('🔵 数据库无数据', name: 'BatteryHealth');
setState(() => _hasData = false); setState(() => _hasData = false);
} }
if (_isCharging) _fetchChargeDataFromLog(); if (_isCharging) _fetchChargeDataFromLog();
@@ -662,7 +678,9 @@ class _BatteryHealthPageState extends State<BatteryHealthPage>
try { try {
final lines = await _safService.readLogFile(fileName); final lines = await _safService.readLogFile(fileName);
dev.log('🔵 读取到 ${lines.length}', name: 'BatteryHealth');
if (lines.isEmpty) { if (lines.isEmpty) {
dev.log('🔵 文件为空', name: 'BatteryHealth');
setState(() => _hasData = false); setState(() => _hasData = false);
if (_isCharging) _fetchChargeDataFromLog(); if (_isCharging) _fetchChargeDataFromLog();
_chargeAnimationController.reset(); _chargeAnimationController.reset();
@@ -670,12 +688,23 @@ class _BatteryHealthPageState extends State<BatteryHealthPage>
return; return;
} }
// 打印前几行
if (lines.isNotEmpty) {
dev.log('🔵 第一行: ${lines.first}', name: 'BatteryHealth');
if (lines.length > 1) {
dev.log('🔵 第二行: ${lines[1]}', name: 'BatteryHealth');
}
}
if (_isCharging) _parseChargeDataFromLog(lines); if (_isCharging) _parseChargeDataFromLog(lines);
List<String> validLines = lines List<String> validLines = lines
.where((line) => line.contains('tbat=')) .where((line) => line.contains('tbat='))
.toList(); .toList();
dev.log('🔵 有效行数 (含tbat): ${validLines.length}', name: 'BatteryHealth');
if (validLines.isEmpty) { if (validLines.isEmpty) {
dev.log('🔵 无有效数据行', name: 'BatteryHealth');
setState(() => _hasData = false); setState(() => _hasData = false);
_chargeAnimationController.reset(); _chargeAnimationController.reset();
_updateChargeState(); _updateChargeState();
@@ -683,6 +712,8 @@ class _BatteryHealthPageState extends State<BatteryHealthPage>
} }
String lastLine = validLines.last; String lastLine = validLines.last;
dev.log('🔵 最后一行: $lastLine', name: 'BatteryHealth');
RegExp capReg = RegExp(r'learned_cap=(\d+)'); RegExp capReg = RegExp(r'learned_cap=(\d+)');
RegExp cycleReg = RegExp(r'cycle=(\d+)'); RegExp cycleReg = RegExp(r'cycle=(\d+)');
RegExp tempReg = RegExp(r'tbat=(\d+)'); RegExp tempReg = RegExp(r'tbat=(\d+)');
@@ -693,7 +724,13 @@ class _BatteryHealthPageState extends State<BatteryHealthPage>
String? tempStr = tempReg.firstMatch(lastLine)?.group(1); String? tempStr = tempReg.firstMatch(lastLine)?.group(1);
String? voltStr = voltageReg.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) { if (capStr == null) {
dev.log('🔵 未找到 learned_cap', name: 'BatteryHealth');
setState(() => _hasData = false); setState(() => _hasData = false);
_chargeAnimationController.reset(); _chargeAnimationController.reset();
_updateChargeState(); _updateChargeState();
@@ -705,6 +742,11 @@ class _BatteryHealthPageState extends State<BatteryHealthPage>
double temp = double.parse(tempStr ?? '0'); double temp = double.parse(tempStr ?? '0');
double volt = double.parse(voltStr ?? '0') / 1000000; double volt = double.parse(voltStr ?? '0') / 1000000;
dev.log(
'🔵 解析结果: capMah=$capMah, cycle=$cycle, temp=$temp, volt=$volt',
name: 'BatteryHealth',
);
List<double> temps = []; List<double> temps = [];
for (String line in validLines) { for (String line in validLines) {
RegExpMatch? match = tempReg.firstMatch(line); RegExpMatch? match = tempReg.firstMatch(line);
@@ -739,10 +781,20 @@ class _BatteryHealthPageState extends State<BatteryHealthPage>
_hasData = true; _hasData = true;
}); });
dev.log(
'🔵 数据更新完成: _currentCap=$_currentCap, _cycleCount=$_cycleCount',
name: 'BatteryHealth',
);
await _updateTrendChart(); await _updateTrendChart();
_chargeAnimationController.reset(); _chargeAnimationController.reset();
_updateChargeState(); _updateChargeState();
dev.log(
'🔵 ========== readBatteryCapacity 完成 ==========',
name: 'BatteryHealth',
);
} catch (e) { } catch (e) {
dev.log('🔵 readBatteryCapacity 异常: $e', name: 'BatteryHealth');
setState(() => _hasData = false); setState(() => _hasData = false);
_chargeAnimationController.reset(); _chargeAnimationController.reset();
_updateChargeState(); _updateChargeState();
@@ -760,6 +812,7 @@ class _BatteryHealthPageState extends State<BatteryHealthPage>
@override @override
void initState() { void initState() {
super.initState(); super.initState();
dev.log('🔵 ========== initState 开始 ==========', name: 'BatteryHealth');
_chargeAnimationController = AnimationController( _chargeAnimationController = AnimationController(
vsync: this, vsync: this,
@@ -794,17 +847,29 @@ class _BatteryHealthPageState extends State<BatteryHealthPage>
} }
}); });
// 延迟初始化 SAF 并读取数据
WidgetsBinding.instance.addPostFrameCallback((_) async { WidgetsBinding.instance.addPostFrameCallback((_) async {
dev.log('🔵 initState -> addPostFrameCallback 开始', name: 'BatteryHealth');
await _safService.init(); await _safService.init();
dev.log(
'🔵 _safService.init() 完成,cachedUri: ${_safService.cachedUri}',
name: 'BatteryHealth',
);
_safState = await _safService.getPermissionState(); _safState = await _safService.getPermissionState();
dev.log('🔵 _safState: $_safState', name: 'BatteryHealth');
if (_safState == SafPermissionState.unauthorized || if (_safState == SafPermissionState.unauthorized ||
_safState == SafPermissionState.expired) { _safState == SafPermissionState.expired) {
dev.log('🔵 需要授权,弹窗', name: 'BatteryHealth');
if (mounted) _showSafAuthorizationDialog(); if (mounted) _showSafAuthorizationDialog();
} else { } else {
dev.log('🔵 已有授权,调用 readBatteryCapacity', name: 'BatteryHealth');
readBatteryCapacity(); readBatteryCapacity();
} }
}); });
dev.log('🔵 ========== initState 结束 ==========', name: 'BatteryHealth');
} }
@override @override
@@ -820,11 +885,17 @@ class _BatteryHealthPageState extends State<BatteryHealthPage>
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return AnnotatedRegion<SystemUiOverlayStyle>( return AnnotatedRegion<SystemUiOverlayStyle>(
value: SystemUiOverlayStyle.dark, value: SystemUiOverlayStyle(
statusBarColor: Colors.white,
statusBarIconBrightness: Brightness.dark,
statusBarBrightness: Brightness.dark,
),
child: Scaffold( child: Scaffold(
backgroundColor: Colors.white, // 🟢 页面背景色与 AppBar 统一
appBar: AppBar( appBar: AppBar(
backgroundColor: const Color.fromARGB(0, 255, 255, 255), backgroundColor: Colors.white,
elevation: 0, elevation: 0, // 🟢 去掉阴影
scrolledUnderElevation: 0, // 🟢 滚动时也不出现阴影
title: const Text( title: const Text(
'电池健康度', '电池健康度',
style: TextStyle( style: TextStyle(
+92 -182
View File
@@ -1,129 +1,56 @@
import 'dart:io';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
<<<<<<< HEAD
import 'package:file_picker/file_picker.dart';
import '../models/saf_permission_state.dart';
/// 使用 file_picker 实现文件夹访问
=======
import 'package:flutter_saf/flutter_saf.dart';
import '../models/saf_permission_state.dart'; import '../models/saf_permission_state.dart';
/// SAF 存储服务 /// SAF 存储服务
/// 负责管理通过 SAF 授权的文件夹访问 /// 负责管理通过 SAF 授权的文件夹访问
>>>>>>> c5a85e05a3bb50cd4ea7de0da2db94c867eb7ddd
class SafStorageService { class SafStorageService {
static final SafStorageService _instance = SafStorageService._internal(); static final SafStorageService _instance = SafStorageService._internal();
factory SafStorageService() => _instance; factory SafStorageService() => _instance;
SafStorageService._internal(); SafStorageService._internal();
<<<<<<< HEAD static const MethodChannel _channel = MethodChannel(
static const String _prefKeySafPath = 'saf_storage_path'; 'com.lxh.battery_health/saf',
String? _cachedPath; );
Future<void> init() async {
final prefs = await SharedPreferences.getInstance();
_cachedPath = prefs.getString(_prefKeySafPath);
if (kDebugMode) {
print('[SAF] 初始化,缓存路径: $_cachedPath');
}
}
Future<SafPermissionState> getPermissionState() async {
if (_cachedPath == null || _cachedPath!.isEmpty) {
return SafPermissionState.unauthorized;
}
try {
final dir = Directory(_cachedPath!);
if (!await dir.exists()) {
=======
static const String _prefKeySafUri = 'saf_storage_uri'; static const String _prefKeySafUri = 'saf_storage_uri';
static const String _prefKeySafPermission = 'saf_permission_uri'; String? _cachedUri;
/// 存储的授权 URI /// 初始化,从 SharedPreferences 恢复 URI
String? _cachedUriString;
/// 初始化,从 SharedPreferences 恢复授权状态
Future<void> init() async { Future<void> init() async {
try {
final prefs = await SharedPreferences.getInstance(); final prefs = await SharedPreferences.getInstance();
_cachedUriString = prefs.getString(_prefKeySafUri); _cachedUri = prefs.getString(_prefKeySafUri);
if (kDebugMode) { if (kDebugMode) {
print('[SAF] 初始化,缓存 URI: $_cachedUriString'); print('[SAF] 初始化,缓存 URI: $_cachedUri');
}
} catch (e) {
if (kDebugMode) {
print('[SAF] 初始化失败: $e');
}
_cachedUri = null;
} }
} }
/// 获取当前授权状态 /// 获取当前授权状态(简化版,直接根据缓存判断)
Future<SafPermissionState> getPermissionState() async { Future<SafPermissionState> getPermissionState() async {
if (_cachedUriString == null || _cachedUriString!.isEmpty) { if (_cachedUri == null || _cachedUri!.isEmpty) {
return SafPermissionState.unauthorized; return SafPermissionState.unauthorized;
} }
// 直接返回 authorized,相信缓存
try {
final uri = Uri.parse(_cachedUriString!);
// 检查 Uri 是否仍然有效
final isValid = await FlutterSaf.isDirectory(uri);
if (!isValid) {
>>>>>>> c5a85e05a3bb50cd4ea7de0da2db94c867eb7ddd
_clearCache();
return SafPermissionState.expired;
}
return SafPermissionState.authorized; return SafPermissionState.authorized;
} catch (e) {
_clearCache();
return SafPermissionState.expired;
}
} }
<<<<<<< HEAD
Future<SafAuthorizationResult> requestPermission() async {
try {
// 使用 file_picker 选择文件夹
String? selectedPath = await FilePicker.platform.getDirectoryPath();
if (selectedPath == null || selectedPath.isEmpty) {
return SafAuthorizationResult.denied();
}
// 验证路径是否包含 mbattery_charger(可选,提高容错)
if (!selectedPath.contains('mbattery_charger')) {
return SafAuthorizationResult.error('请选择 mbattery_charger 文件夹');
}
_cachedPath = selectedPath;
await _savePath(selectedPath);
if (kDebugMode) {
print('[SAF] 授权成功,路径: $selectedPath');
}
return SafAuthorizationResult.success(selectedPath);
=======
/// 请求 SAF 授权(引导用户选择文件夹) /// 请求 SAF 授权(引导用户选择文件夹)
/// 返回授权结果
Future<SafAuthorizationResult> requestPermission() async { Future<SafAuthorizationResult> requestPermission() async {
try { try {
// 调用系统文件夹选择器 final String? uriString = await _channel.invokeMethod('selectFolder');
// 引导用户选择 /storage/emulated/0/mbattery_charger/
// 或者用户可以选择任何目录,但我们建议选择具体路径
final uri = await FlutterSaf.openDocumentTree();
if (uri == null) { if (uriString == null || uriString.isEmpty) {
return SafAuthorizationResult.denied(); return SafAuthorizationResult.denied();
} }
// 获取持久化 URI 权限 _cachedUri = uriString;
await FlutterSaf.takePersistableUriPermission(uri);
// 验证是否为目录
final isValid = await FlutterSaf.isDirectory(uri);
if (!isValid) {
return SafAuthorizationResult.error('选择的路径不是有效目录');
}
// 保存 URI
final uriString = uri.toString();
_cachedUriString = uriString;
await _saveUri(uriString); await _saveUri(uriString);
if (kDebugMode) { if (kDebugMode) {
@@ -131,7 +58,6 @@ class SafStorageService {
} }
return SafAuthorizationResult.success(uriString); return SafAuthorizationResult.success(uriString);
>>>>>>> c5a85e05a3bb50cd4ea7de0da2db94c867eb7ddd
} catch (e) { } catch (e) {
if (kDebugMode) { if (kDebugMode) {
print('[SAF] 授权失败: $e'); print('[SAF] 授权失败: $e');
@@ -140,58 +66,63 @@ class SafStorageService {
} }
} }
<<<<<<< HEAD /// 🟢 将 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 { Future<bool> logFileExists(String fileName) async {
final state = await getPermissionState(); final state = await getPermissionState();
if (state != SafPermissionState.authorized) return false; if (state != SafPermissionState.authorized) return false;
try { try {
final file = File('${_cachedPath!}/$fileName'); final files = await listFiles();
return await file.exists(); return files.contains(fileName);
=======
/// 检查指定日期的日志文件是否存在
Future<bool> logFileExists(String fileName) async {
final state = await getPermissionState();
if (state != SafPermissionState.authorized) {
return false;
}
try {
final uri = Uri.parse(_cachedUriString!);
final fileUri = await FlutterSaf.getFile(uri, fileName);
return fileUri != null && await FlutterSaf.isFile(fileUri);
>>>>>>> c5a85e05a3bb50cd4ea7de0da2db94c867eb7ddd
} catch (e) { } catch (e) {
return false; return false;
} }
} }
<<<<<<< HEAD /// 读取日志文件内容(返回行列表)
Future<List<String>> readLogFile(String fileName) async { Future<List<String>> readLogFile(String fileName) async {
// 🟢 先尝试恢复 URI
await restoreUri();
final state = await getPermissionState(); final state = await getPermissionState();
if (state != SafPermissionState.authorized) return []; if (state != SafPermissionState.authorized) return [];
try {
final file = File('${_cachedPath!}/$fileName');
if (!await file.exists()) return [];
return await file.readAsLines();
=======
/// 读取日志文件内容
/// 返回文件行列表,如果文件不存在或读取失败返回空列表
Future<List<String>> readLogFile(String fileName) async {
final state = await getPermissionState();
if (state != SafPermissionState.authorized) {
return [];
}
try { try {
final uri = Uri.parse(_cachedUriString!); final String? content = await _channel.invokeMethod('readLogFile', {
final fileUri = await FlutterSaf.getFile(uri, fileName); 'fileName': fileName,
if (fileUri == null || !await FlutterSaf.isFile(fileUri)) { });
return []; if (content == null || content.isEmpty) return [];
}
final content = await FlutterSaf.readTextFile(fileUri);
return content.split('\n'); return content.split('\n');
>>>>>>> c5a85e05a3bb50cd4ea7de0da2db94c867eb7ddd
} catch (e) { } catch (e) {
if (kDebugMode) { if (kDebugMode) {
print('[SAF] 读取文件失败: $e'); print('[SAF] 读取文件失败: $e');
@@ -200,83 +131,62 @@ class SafStorageService {
} }
} }
<<<<<<< HEAD /// 获取文件夹中的文件列表
Future<List<String>> listFiles() async { Future<List<String>> listFiles() async {
// 🟢 先尝试恢复 URI
await restoreUri();
final state = await getPermissionState(); final state = await getPermissionState();
if (state != SafPermissionState.authorized) return []; if (state != SafPermissionState.authorized) return [];
try { try {
final dir = Directory(_cachedPath!); final result = await _channel.invokeMethod('listFiles');
if (!await dir.exists()) return []; if (result is List) {
final entities = await dir.list().toList(); return result.cast<String>();
return entities }
.where((e) => e is File) return [];
.map((e) => e.path.split(Platform.pathSeparator).last)
.toList();
} catch (e) { } catch (e) {
if (kDebugMode) { if (kDebugMode) {
print('[SAF] 列出文件失败: $e'); print('[SAF] 列出文件失败: $e');
} }
=======
/// 获取文件夹中的文件列表
Future<List<String>> listFiles() async {
final state = await getPermissionState();
if (state != SafPermissionState.authorized) {
return [];
}
try {
final uri = Uri.parse(_cachedUriString!);
final files = await FlutterSaf.listFiles(uri);
return files.where((f) => f.isFile).map((f) => f.name).toList();
} catch (e) {
>>>>>>> c5a85e05a3bb50cd4ea7de0da2db94c867eb7ddd
return []; return [];
} }
} }
<<<<<<< HEAD /// 验证授权是否有效(简化版)
=======
/// 检查授权是否有效,如果失效则清除缓存
>>>>>>> c5a85e05a3bb50cd4ea7de0da2db94c867eb7ddd
Future<bool> validatePermission() async { Future<bool> validatePermission() async {
final state = await getPermissionState(); final state = await getPermissionState();
return state == SafPermissionState.authorized; return state == SafPermissionState.authorized;
} }
<<<<<<< HEAD /// 清除缓存
void _clearCache() { void _clearCache() {
_cachedPath = null; _cachedUri = null;
_savePath(null);
}
Future<void> _savePath(String? path) async {
final prefs = await SharedPreferences.getInstance();
if (path != null && path.isNotEmpty) {
await prefs.setString(_prefKeySafPath, path);
} else {
await prefs.remove(_prefKeySafPath);
}
}
String? get cachedPath => _cachedPath;
=======
/// 清除授权缓存
void _clearCache() {
_cachedUriString = null;
_saveUri(null); _saveUri(null);
} }
/// 保存 URI 到 SharedPreferences /// 保存 URI 到 SharedPreferences
Future<void> _saveUri(String? uriString) async { Future<void> _saveUri(String? uri) async {
try {
final prefs = await SharedPreferences.getInstance(); final prefs = await SharedPreferences.getInstance();
if (uriString != null && uriString.isNotEmpty) { if (uri != null && uri.isNotEmpty) {
await prefs.setString(_prefKeySafUri, uriString); await prefs.setString(_prefKeySafUri, uri);
if (kDebugMode) {
print('[SAF] 保存 URI 成功: $uri');
}
} else { } else {
await prefs.remove(_prefKeySafUri); await prefs.remove(_prefKeySafUri);
if (kDebugMode) {
print('[SAF] 清除 URI');
}
}
} catch (e) {
if (kDebugMode) {
print('[SAF] 保存 URI 失败: $e');
}
} }
} }
/// 获取缓存的 URI(用于调试) /// 获取缓存的 URI(用于调试)
String? get cachedUri => _cachedUriString; String? get cachedUri => _cachedUri;
>>>>>>> c5a85e05a3bb50cd4ea7de0da2db94c867eb7ddd
} }
@@ -6,19 +6,11 @@ import FlutterMacOS
import Foundation import Foundation
import device_info_plus import device_info_plus
<<<<<<< HEAD
=======
import path_provider_foundation
>>>>>>> c5a85e05a3bb50cd4ea7de0da2db94c867eb7ddd
import shared_preferences_foundation import shared_preferences_foundation
import sqflite_darwin import sqflite_darwin
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin")) DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin"))
<<<<<<< HEAD
=======
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
>>>>>>> c5a85e05a3bb50cd4ea7de0da2db94c867eb7ddd
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin")) SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin"))
} }
-50
View File
@@ -113,14 +113,6 @@ packages:
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "7.0.1" version: "7.0.1"
file_picker:
dependency: "direct main"
description:
name: file_picker
sha256: "1bbf65dd997458a08b531042ec3794112a6c39c07c37ff22113d2e7e4f81d4e4"
url: "https://pub.flutter-io.cn"
source: hosted
version: "6.2.1"
flutter: flutter:
dependency: "direct main" dependency: "direct main"
description: flutter description: flutter
@@ -134,14 +126,6 @@ packages:
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "5.0.0" version: "5.0.0"
flutter_plugin_android_lifecycle:
dependency: transitive
description:
name: flutter_plugin_android_lifecycle
sha256: "3854fe5e3bff0b113c658f260b90c95dea17c92db0f2addeac2e343dd9969785"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.0.35"
flutter_svg: flutter_svg:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -416,7 +400,6 @@ packages:
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "2.1.8" version: "2.1.8"
<<<<<<< HEAD
pub_semver: pub_semver:
dependency: transitive dependency: transitive
description: description:
@@ -433,45 +416,28 @@ packages:
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "0.6.0" version: "0.6.0"
=======
>>>>>>> c5a85e05a3bb50cd4ea7de0da2db94c867eb7ddd
shared_preferences: shared_preferences:
dependency: "direct main" dependency: "direct main"
description: description:
name: shared_preferences name: shared_preferences
sha256: c3025c5534b01739267eb7d76959bbc25a6d10f6988e1c2a3036940133dd10bf sha256: c3025c5534b01739267eb7d76959bbc25a6d10f6988e1c2a3036940133dd10bf
<<<<<<< HEAD
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
=======
url: "https://pub.dev"
>>>>>>> c5a85e05a3bb50cd4ea7de0da2db94c867eb7ddd
source: hosted source: hosted
version: "2.5.5" version: "2.5.5"
shared_preferences_android: shared_preferences_android:
dependency: transitive dependency: transitive
description: description:
name: shared_preferences_android name: shared_preferences_android
<<<<<<< HEAD
sha256: "93ae5884a9df5d3bb696825bceb3a17590754548b5d740eba51500afc8d088f5" sha256: "93ae5884a9df5d3bb696825bceb3a17590754548b5d740eba51500afc8d088f5"
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "2.4.26" version: "2.4.26"
=======
sha256: e8d4762b1e2e8578fc4d0fd548cebf24afd24f49719c08974df92834565e2c53
url: "https://pub.dev"
source: hosted
version: "2.4.23"
>>>>>>> c5a85e05a3bb50cd4ea7de0da2db94c867eb7ddd
shared_preferences_foundation: shared_preferences_foundation:
dependency: transitive dependency: transitive
description: description:
name: shared_preferences_foundation name: shared_preferences_foundation
sha256: "4e7eaffc2b17ba398759f1151415869a34771ba11ebbccd1b0145472a619a64f" sha256: "4e7eaffc2b17ba398759f1151415869a34771ba11ebbccd1b0145472a619a64f"
<<<<<<< HEAD
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
=======
url: "https://pub.dev"
>>>>>>> c5a85e05a3bb50cd4ea7de0da2db94c867eb7ddd
source: hosted source: hosted
version: "2.5.6" version: "2.5.6"
shared_preferences_linux: shared_preferences_linux:
@@ -479,11 +445,7 @@ packages:
description: description:
name: shared_preferences_linux name: shared_preferences_linux
sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f" sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f"
<<<<<<< HEAD
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
=======
url: "https://pub.dev"
>>>>>>> c5a85e05a3bb50cd4ea7de0da2db94c867eb7ddd
source: hosted source: hosted
version: "2.4.1" version: "2.4.1"
shared_preferences_platform_interface: shared_preferences_platform_interface:
@@ -491,11 +453,7 @@ packages:
description: description:
name: shared_preferences_platform_interface name: shared_preferences_platform_interface
sha256: "649dc798a33931919ea356c4305c2d1f81619ea6e92244070b520187b5140ef9" sha256: "649dc798a33931919ea356c4305c2d1f81619ea6e92244070b520187b5140ef9"
<<<<<<< HEAD
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
=======
url: "https://pub.dev"
>>>>>>> c5a85e05a3bb50cd4ea7de0da2db94c867eb7ddd
source: hosted source: hosted
version: "2.4.2" version: "2.4.2"
shared_preferences_web: shared_preferences_web:
@@ -503,11 +461,7 @@ packages:
description: description:
name: shared_preferences_web name: shared_preferences_web
sha256: c49bd060261c9a3f0ff445892695d6212ff603ef3115edbb448509d407600019 sha256: c49bd060261c9a3f0ff445892695d6212ff603ef3115edbb448509d407600019
<<<<<<< HEAD
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
=======
url: "https://pub.dev"
>>>>>>> c5a85e05a3bb50cd4ea7de0da2db94c867eb7ddd
source: hosted source: hosted
version: "2.4.3" version: "2.4.3"
shared_preferences_windows: shared_preferences_windows:
@@ -515,11 +469,7 @@ packages:
description: description:
name: shared_preferences_windows name: shared_preferences_windows
sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1" sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1"
<<<<<<< HEAD
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
=======
url: "https://pub.dev"
>>>>>>> c5a85e05a3bb50cd4ea7de0da2db94c867eb7ddd
source: hosted source: hosted
version: "2.4.1" version: "2.4.1"
sky_engine: sky_engine:
+2 -6
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 # 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 # 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. # of the product and file versions while build-number is used as the build suffix.
version: 1.3.0-dev.2 version: 1.4.0
environment: environment:
sdk: ^3.9.2 sdk: ^3.9.2
@@ -38,12 +38,8 @@ dependencies:
path: ^1.9.0 path: ^1.9.0
flutter_svg: ^2.0.10+1 flutter_svg: ^2.0.10+1
shared_preferences: ^2.2.0 shared_preferences: ^2.2.0
<<<<<<< HEAD
file_picker: ^6.0.0
=======
flutter_saf: ^0.1.0
>>>>>>> c5a85e05a3bb50cd4ea7de0da2db94c867eb7ddd
# The following adds the Cupertino Icons font to your application. # The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons. # Use with the CupertinoIcons class for iOS style icons.