Compare commits
2 Commits
745fdaf7e7
...
45c69c8ec4
| Author | SHA1 | Date | |
|---|---|---|---|
| 45c69c8ec4 | |||
| dff0f6fb6b |
@@ -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,16 +62,12 @@ android {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
// 如果有 productFlavors,可以在这里配置
|
|
||||||
// 如果没有,忽略即可
|
|
||||||
}
|
|
||||||
|
|
||||||
flutter {
|
flutter {
|
||||||
source = "../.."
|
source = "../.."
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
// 如果项目中有其他依赖,添加在这里
|
// 无需额外添加 Activity KTX,因为已改用 startActivityForResult
|
||||||
// 注意:sqflite、permission_handler 等 Flutter 插件会自动包含
|
|
||||||
}
|
}
|
||||||
@@ -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,256 @@
|
|||||||
|
package com.lxh.battery_health
|
||||||
|
|
||||||
|
import android.content.BroadcastReceiver
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.IntentFilter
|
||||||
|
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
|
||||||
|
|
||||||
|
// ---------- 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 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))
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- SAF 方法 ----------
|
||||||
|
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
|
||||||
|
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> {
|
||||||
|
val result = mutableListOf<String>()
|
||||||
|
val docId = getTreeDocId()
|
||||||
|
if (docId == null || docId.isEmpty()) 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}")
|
||||||
|
}
|
||||||
|
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) }
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- Flutter 通道配置 ----------
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SAF 通道
|
||||||
|
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, SAF_CHANNEL)
|
||||||
|
.setMethodCallHandler { call, result ->
|
||||||
|
when (call.method) {
|
||||||
|
"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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
@@ -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(),
|
||||||
|
|||||||
@@ -283,18 +283,23 @@ class _BatteryHealthPageState extends State<BatteryHealthPage>
|
|||||||
),
|
),
|
||||||
ElevatedButton(
|
ElevatedButton(
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
final ctx = context;
|
// 触发关闭
|
||||||
Navigator.pop(ctx);
|
Navigator.pop(context);
|
||||||
|
|
||||||
|
// 延迟确保动画完成
|
||||||
|
await Future.delayed(const Duration(milliseconds: 150));
|
||||||
|
|
||||||
final result = await _safService.requestPermission();
|
final result = await _safService.requestPermission();
|
||||||
if (result.success) {
|
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_safState = SafPermissionState.authorized;
|
_safState = SafPermissionState.authorized;
|
||||||
});
|
});
|
||||||
readBatteryCapacity();
|
readBatteryCapacity();
|
||||||
} else {
|
} else {
|
||||||
if (!mounted) return;
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
ScaffoldMessenger.of(ctx).showSnackBar(
|
|
||||||
SnackBar(
|
SnackBar(
|
||||||
content: Text(result.errorMessage ?? '授权失败,请重试'),
|
content: Text(result.errorMessage ?? '授权失败,请重试'),
|
||||||
backgroundColor: Colors.red,
|
backgroundColor: Colors.red,
|
||||||
@@ -303,7 +308,7 @@ class _BatteryHealthPageState extends State<BatteryHealthPage>
|
|||||||
_showSafAuthorizationDialog();
|
_showSafAuthorizationDialog();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
child: const Text('选择文件夹'), // 🟢 加这行
|
child: const Text('选择文件夹'),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -774,6 +779,7 @@ class _BatteryHealthPageState extends State<BatteryHealthPage>
|
|||||||
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
||||||
await _safService.init();
|
await _safService.init();
|
||||||
_safState = await _safService.getPermissionState();
|
_safState = await _safService.getPermissionState();
|
||||||
|
|
||||||
if (_safState == SafPermissionState.unauthorized ||
|
if (_safState == SafPermissionState.unauthorized ||
|
||||||
_safState == SafPermissionState.expired) {
|
_safState == SafPermissionState.expired) {
|
||||||
if (mounted) _showSafAuthorizationDialog();
|
if (mounted) _showSafAuthorizationDialog();
|
||||||
|
|||||||
@@ -1,65 +1,59 @@
|
|||||||
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';
|
||||||
import 'package:file_picker/file_picker.dart';
|
|
||||||
import '../models/saf_permission_state.dart';
|
import '../models/saf_permission_state.dart';
|
||||||
|
|
||||||
/// 使用 file_picker 实现文件夹访问
|
|
||||||
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();
|
||||||
|
|
||||||
static const String _prefKeySafPath = 'saf_storage_path';
|
static const MethodChannel _channel = MethodChannel(
|
||||||
String? _cachedPath;
|
'com.lxh.battery_health/saf',
|
||||||
|
);
|
||||||
|
static const String _prefKeySafUri = 'saf_storage_uri';
|
||||||
|
String? _cachedUri;
|
||||||
|
|
||||||
|
// 🟢 初始化时从 SharedPreferences 恢复 URI
|
||||||
Future<void> init() async {
|
Future<void> init() async {
|
||||||
|
try {
|
||||||
final prefs = await SharedPreferences.getInstance();
|
final prefs = await SharedPreferences.getInstance();
|
||||||
_cachedPath = prefs.getString(_prefKeySafPath);
|
_cachedUri = prefs.getString(_prefKeySafUri);
|
||||||
if (kDebugMode) {
|
if (kDebugMode) {
|
||||||
print('[SAF] 初始化,缓存路径: $_cachedPath');
|
print('[SAF] 初始化,缓存 URI: $_cachedUri');
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
if (kDebugMode) {
|
||||||
|
print('[SAF] 初始化失败: $e');
|
||||||
|
}
|
||||||
|
_cachedUri = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<SafPermissionState> getPermissionState() async {
|
Future<SafPermissionState> getPermissionState() async {
|
||||||
if (_cachedPath == null || _cachedPath!.isEmpty) {
|
if (_cachedUri == null || _cachedUri!.isEmpty) {
|
||||||
return SafPermissionState.unauthorized;
|
return SafPermissionState.unauthorized;
|
||||||
}
|
}
|
||||||
try {
|
// 🟢 简化:直接返回 authorized,相信本地缓存的 URI
|
||||||
final dir = Directory(_cachedPath!);
|
|
||||||
if (!await dir.exists()) {
|
|
||||||
_clearCache();
|
|
||||||
return SafPermissionState.expired;
|
|
||||||
}
|
|
||||||
return SafPermissionState.authorized;
|
return SafPermissionState.authorized;
|
||||||
} catch (e) {
|
|
||||||
_clearCache();
|
|
||||||
return SafPermissionState.expired;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<SafAuthorizationResult> requestPermission() async {
|
Future<SafAuthorizationResult> requestPermission() async {
|
||||||
try {
|
try {
|
||||||
// 使用 file_picker 选择文件夹
|
final String? uriString = await _channel.invokeMethod('selectFolder');
|
||||||
String? selectedPath = await FilePicker.platform.getDirectoryPath();
|
|
||||||
|
|
||||||
if (selectedPath == null || selectedPath.isEmpty) {
|
if (uriString == null || uriString.isEmpty) {
|
||||||
return SafAuthorizationResult.denied();
|
return SafAuthorizationResult.denied();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 验证路径是否包含 mbattery_charger(可选,提高容错)
|
_cachedUri = uriString;
|
||||||
if (!selectedPath.contains('mbattery_charger')) {
|
await _saveUri(uriString);
|
||||||
return SafAuthorizationResult.error('请选择 mbattery_charger 文件夹');
|
|
||||||
}
|
|
||||||
|
|
||||||
_cachedPath = selectedPath;
|
|
||||||
await _savePath(selectedPath);
|
|
||||||
|
|
||||||
if (kDebugMode) {
|
if (kDebugMode) {
|
||||||
print('[SAF] 授权成功,路径: $selectedPath');
|
print('[SAF] 授权成功,URI: $uriString');
|
||||||
}
|
}
|
||||||
|
|
||||||
return SafAuthorizationResult.success(selectedPath);
|
return SafAuthorizationResult.success(uriString);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (kDebugMode) {
|
if (kDebugMode) {
|
||||||
print('[SAF] 授权失败: $e');
|
print('[SAF] 授权失败: $e');
|
||||||
@@ -72,8 +66,11 @@ class SafStorageService {
|
|||||||
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 _channel.invokeMethod('listFiles');
|
||||||
return await file.exists();
|
if (files is List) {
|
||||||
|
return files.contains(fileName);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -83,9 +80,11 @@ class SafStorageService {
|
|||||||
final state = await getPermissionState();
|
final state = await getPermissionState();
|
||||||
if (state != SafPermissionState.authorized) return [];
|
if (state != SafPermissionState.authorized) return [];
|
||||||
try {
|
try {
|
||||||
final file = File('${_cachedPath!}/$fileName');
|
final String? content = await _channel.invokeMethod('readLogFile', {
|
||||||
if (!await file.exists()) return [];
|
'fileName': fileName,
|
||||||
return await file.readAsLines();
|
});
|
||||||
|
if (content == null || content.isEmpty) return [];
|
||||||
|
return content.split('\n');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (kDebugMode) {
|
if (kDebugMode) {
|
||||||
print('[SAF] 读取文件失败: $e');
|
print('[SAF] 读取文件失败: $e');
|
||||||
@@ -98,13 +97,11 @@ class SafStorageService {
|
|||||||
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
|
}
|
||||||
.whereType<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');
|
||||||
@@ -119,18 +116,30 @@ class SafStorageService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _clearCache() {
|
void _clearCache() {
|
||||||
_cachedPath = null;
|
_cachedUri = null;
|
||||||
_savePath(null);
|
_saveUri(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _savePath(String? path) async {
|
Future<void> _saveUri(String? uri) async {
|
||||||
|
try {
|
||||||
final prefs = await SharedPreferences.getInstance();
|
final prefs = await SharedPreferences.getInstance();
|
||||||
if (path != null && path.isNotEmpty) {
|
if (uri != null && uri.isNotEmpty) {
|
||||||
await prefs.setString(_prefKeySafPath, path);
|
await prefs.setString(_prefKeySafUri, uri);
|
||||||
|
if (kDebugMode) {
|
||||||
|
print('[SAF] 保存 URI 成功: $uri');
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
await prefs.remove(_prefKeySafPath);
|
await prefs.remove(_prefKeySafUri);
|
||||||
|
if (kDebugMode) {
|
||||||
|
print('[SAF] 清除 URI');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
if (kDebugMode) {
|
||||||
|
print('[SAF] 保存 URI 失败: $e');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
String? get cachedPath => _cachedPath;
|
String? get cachedUri => _cachedUri;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
+2
-2
@@ -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.3.0-dev.4
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ^3.9.2
|
sdk: ^3.9.2
|
||||||
@@ -38,7 +38,7 @@ 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
|
||||||
file_picker: ^6.0.0
|
|
||||||
|
|
||||||
# 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.
|
||||||
|
|||||||
Reference in New Issue
Block a user