1.开始录制、结束录制增加

2. 增加电量检测、内存检查,是否低于 10%
This commit is contained in:
2026-06-04 18:25:58 +08:00
parent 124b4c1882
commit f49d208042
9 changed files with 267 additions and 0 deletions

View File

@@ -1,7 +1,11 @@
package com.qxy.dronex package com.qxy.dronex
import android.content.Context
import android.content.pm.ApplicationInfo import android.content.pm.ApplicationInfo
import android.os.BatteryManager
import android.os.Build import android.os.Build
import android.os.Environment
import android.os.StatFs
import androidx.camera.view.PreviewView import androidx.camera.view.PreviewView
import com.qxy.dronex.recording.RecordingPlatformHandler import com.qxy.dronex.recording.RecordingPlatformHandler
import com.qxy.dronex.recording.RecordingPreviewFactory import com.qxy.dronex.recording.RecordingPreviewFactory
@@ -33,6 +37,7 @@ class MainActivity : FlutterActivity() {
when (call.method) { when (call.method) {
"packageInfo" -> result.success(packageInfoMap()) "packageInfo" -> result.success(packageInfoMap())
"deviceInfo" -> result.success(deviceInfoMap()) "deviceInfo" -> result.success(deviceInfoMap())
"deviceHealth" -> result.success(deviceHealthMap())
else -> result.notImplemented() else -> result.notImplemented()
} }
} }
@@ -112,4 +117,29 @@ class MainActivity : FlutterActivity() {
"isPhysicalDevice" to !isEmulator, "isPhysicalDevice" to !isEmulator,
) )
} }
private fun deviceHealthMap(): Map<String, Any?> {
val batteryLevelPercent = readBatteryLevelPercent()
val storageAvailablePercent = readStorageAvailablePercent()
return mapOf(
"batteryLevelPercent" to batteryLevelPercent,
"storageAvailablePercent" to storageAvailablePercent,
)
}
private fun readBatteryLevelPercent(): Int? {
val batteryManager = getSystemService(Context.BATTERY_SERVICE) as? BatteryManager
?: return null
val level =
batteryManager.getIntProperty(BatteryManager.BATTERY_PROPERTY_CAPACITY)
return if (level in 0..100) level else null
}
private fun readStorageAvailablePercent(): Double {
val stat = StatFs(Environment.getDataDirectory().path)
val totalBytes = stat.totalBytes
if (totalBytes <= 0L) return 100.0
val availableBytes = stat.availableBytes
return availableBytes.toDouble() / totalBytes.toDouble() * 100.0
}
} }

View File

@@ -17,6 +17,8 @@ final class PlatformInfoPlugin: NSObject, FlutterPlugin {
result(packageInfoMap()) result(packageInfoMap())
case "deviceInfo": case "deviceInfo":
result(deviceInfoMap()) result(deviceInfoMap())
case "deviceHealth":
result(deviceHealthMap())
default: default:
result(FlutterMethodNotImplemented) result(FlutterMethodNotImplemented)
} }
@@ -32,6 +34,30 @@ final class PlatformInfoPlugin: NSObject, FlutterPlugin {
] ]
} }
private func deviceHealthMap() -> [String: Any?] {
let device = UIDevice.current
device.isBatteryMonitoringEnabled = true
var batteryLevelPercent: Int?
let batteryLevel = device.batteryLevel
if batteryLevel >= 0 {
batteryLevelPercent = Int((batteryLevel * 100).rounded())
}
var storageAvailablePercent = 100.0
if let attrs = try? FileManager.default.attributesOfFileSystem(forPath: NSHomeDirectory()),
let free = attrs[.systemFreeSize] as? NSNumber,
let total = attrs[.systemSize] as? NSNumber,
total.doubleValue > 0 {
storageAvailablePercent = free.doubleValue / total.doubleValue * 100.0
}
return [
"batteryLevelPercent": batteryLevelPercent,
"storageAvailablePercent": storageAvailablePercent,
]
}
private func deviceInfoMap() -> [String: Any] { private func deviceInfoMap() -> [String: Any] {
let device = UIDevice.current let device = UIDevice.current
return [ return [

View File

@@ -1,6 +1,7 @@
import 'dart:io'; import 'dart:io';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:recording_tool/core/platform/device_health_snapshot.dart';
class AppPackageInfo { class AppPackageInfo {
const AppPackageInfo({ const AppPackageInfo({
@@ -75,4 +76,11 @@ class AppPlatformInfo {
); );
return AppDeviceInfo.fromMap(result ?? const <Object?, Object?>{}); return AppDeviceInfo.fromMap(result ?? const <Object?, Object?>{});
} }
static Future<DeviceHealthSnapshot> deviceHealth() async {
final result = await _channel.invokeMapMethod<Object?, Object?>(
'deviceHealth',
);
return DeviceHealthSnapshot.fromMap(result ?? const <Object?, Object?>{});
}
} }

View File

@@ -0,0 +1,25 @@
import 'package:recording_tool/core/platform/device_health_snapshot.dart';
class DeviceHealthChecker {
DeviceHealthChecker._();
static const int thresholdPercent = 10;
static const String lowBatteryMessage = '电量低于10%,请充电';
static const String lowStorageMessage = '内存低于10%,请清理内存';
static List<String> warningLines(DeviceHealthSnapshot snapshot) {
final lines = <String>[];
final battery = snapshot.batteryLevelPercent;
if (battery != null && battery < thresholdPercent) {
lines.add(lowBatteryMessage);
}
if (snapshot.storageAvailablePercent < thresholdPercent) {
lines.add(lowStorageMessage);
}
return lines;
}
}

View File

@@ -0,0 +1,30 @@
class DeviceHealthSnapshot {
const DeviceHealthSnapshot({
this.batteryLevelPercent,
required this.storageAvailablePercent,
});
factory DeviceHealthSnapshot.fromMap(Map<Object?, Object?> map) {
final batteryRaw = map['batteryLevelPercent'];
int? batteryLevelPercent;
if (batteryRaw is int) {
batteryLevelPercent = batteryRaw;
} else if (batteryRaw is num) {
batteryLevelPercent = batteryRaw.round();
}
final storageRaw = map['storageAvailablePercent'];
final storageAvailablePercent = switch (storageRaw) {
final num value => value.toDouble(),
_ => 100.0,
};
return DeviceHealthSnapshot(
batteryLevelPercent: batteryLevelPercent,
storageAvailablePercent: storageAvailablePercent,
);
}
final int? batteryLevelPercent;
final double storageAvailablePercent;
}

View File

@@ -6,6 +6,8 @@ import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:permission_handler/permission_handler.dart'; import 'package:permission_handler/permission_handler.dart';
import 'package:recording_tool/core/platform/app_platform_info.dart';
import 'package:recording_tool/core/platform/device_health_checker.dart';
import 'package:recording_tool/core/utils/date_time_formatter.dart'; import 'package:recording_tool/core/utils/date_time_formatter.dart';
import 'package:recording_tool/features/recording/model/model_recording.dart'; import 'package:recording_tool/features/recording/model/model_recording.dart';
import 'package:recording_tool/features/recording/recording_display_name.dart'; import 'package:recording_tool/features/recording/recording_display_name.dart';
@@ -33,7 +35,20 @@ class _RecordingPageState extends ConsumerState<RecordingPage> {
WidgetsBinding.instance.addPostFrameCallback((_) => _bootstrap()); WidgetsBinding.instance.addPostFrameCallback((_) => _bootstrap());
} }
Future<void> _checkAndShowDeviceHealthAlerts() async {
final snapshot = await AppPlatformInfo.deviceHealth();
if (!mounted) return;
final lines = DeviceHealthChecker.warningLines(snapshot);
if (lines.isEmpty) return;
await AppDialog.deviceHealthAlert(context, lines: lines);
}
Future<void> _bootstrap() async { Future<void> _bootstrap() async {
await _checkAndShowDeviceHealthAlerts();
if (!mounted) return;
final clipboardResult = await ref final clipboardResult = await ref
.read(recordingViewModelProvider.notifier) .read(recordingViewModelProvider.notifier)
.getClipboardContent(); .getClipboardContent();
@@ -95,6 +110,8 @@ class _RecordingPageState extends ConsumerState<RecordingPage> {
await _showNoPlayerInfoDialog(); await _showNoPlayerInfoDialog();
return; return;
} }
await _checkAndShowDeviceHealthAlerts();
if (!mounted) return;
await ref.read(recordingSessionControllerProvider.notifier).startRecording(); await ref.read(recordingSessionControllerProvider.notifier).startRecording();
} }

View File

@@ -4,6 +4,7 @@ import 'dart:io';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:recording_tool/core/permission/permission_service.dart'; import 'package:recording_tool/core/permission/permission_service.dart';
import 'package:recording_tool/core/utils/rate_limiter.dart';
import 'package:recording_tool/features/recording/recording_display_name.dart'; import 'package:recording_tool/features/recording/recording_display_name.dart';
import 'package:recording_tool/features/recording/recording_platform.dart'; import 'package:recording_tool/features/recording/recording_platform.dart';
import 'package:recording_tool/features/recording/view-model/view_model_recording.dart'; import 'package:recording_tool/features/recording/view-model/view_model_recording.dart';
@@ -95,7 +96,12 @@ final recordingSessionControllerProvider =
); );
class RecordingSessionController extends Notifier<RecordingSessionState> { class RecordingSessionController extends Notifier<RecordingSessionState> {
static const Duration _recordingActionInterval = Duration(milliseconds: 300);
static const Object _startRecordingThrottleKey = 'recording.session.start';
static const Object _stopRecordingThrottleKey = 'recording.session.stop';
StreamSubscription<RecordingStatus>? _statusSubscription; StreamSubscription<RecordingStatus>? _statusSubscription;
final _rateLimit = RateLimitHub();
@override @override
RecordingSessionState build() { RecordingSessionState build() {
@@ -207,7 +213,21 @@ class RecordingSessionController extends Notifier<RecordingSessionState> {
return _galleryPermissions().isEmpty; return _galleryPermissions().isEmpty;
} }
bool _tryAcquireRecordingAction(Object key) {
var executed = false;
_rateLimit.throttle<void>(
key: key,
value: null,
duration: _recordingActionInterval,
options: const ThrottleOptions(leading: true, trailing: false),
onCallback: (_) => executed = true,
);
return executed;
}
Future<void> startRecording({bool enableDoNotDisturb = true}) async { Future<void> startRecording({bool enableDoNotDisturb = true}) async {
if (!_tryAcquireRecordingAction(_startRecordingThrottleKey)) return;
if (!state.isPreviewReady || if (!state.isPreviewReady ||
state.isRecording || state.isRecording ||
state.isStartingRecording) { state.isStartingRecording) {
@@ -239,6 +259,8 @@ class RecordingSessionController extends Notifier<RecordingSessionState> {
} }
Future<void> stopRecording() async { Future<void> stopRecording() async {
if (!_tryAcquireRecordingAction(_stopRecordingThrottleKey)) return;
if (!state.isRecording) return; if (!state.isRecording) return;
try { try {
@@ -302,6 +324,7 @@ class RecordingSessionController extends Notifier<RecordingSessionState> {
} }
Future<void> _dispose() async { Future<void> _dispose() async {
_rateLimit.clear();
await _statusSubscription?.cancel(); await _statusSubscription?.cancel();
} }
} }

View File

@@ -30,4 +30,24 @@ class AppDialog {
}, },
); );
} }
static Future<void> deviceHealthAlert(
BuildContext context, {
required List<String> lines,
}) {
return showDialog<void>(
context: context,
builder: (dialogContext) {
return AlertDialog(
content: Text(lines.join('\n')),
actions: [
TextButton(
onPressed: () => Navigator.of(dialogContext).pop(),
child: const Text('确定'),
),
],
);
},
);
}
} }

View File

@@ -0,0 +1,88 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:recording_tool/core/platform/device_health_checker.dart';
import 'package:recording_tool/core/platform/device_health_snapshot.dart';
void main() {
group('DeviceHealthChecker.warningLines', () {
test('returns empty when battery and storage are healthy', () {
const snapshot = DeviceHealthSnapshot(
batteryLevelPercent: 50,
storageAvailablePercent: 50,
);
expect(DeviceHealthChecker.warningLines(snapshot), isEmpty);
});
test('returns low battery message only', () {
const snapshot = DeviceHealthSnapshot(
batteryLevelPercent: 9,
storageAvailablePercent: 50,
);
expect(
DeviceHealthChecker.warningLines(snapshot),
[DeviceHealthChecker.lowBatteryMessage],
);
});
test('returns low storage message only', () {
const snapshot = DeviceHealthSnapshot(
batteryLevelPercent: 50,
storageAvailablePercent: 9.9,
);
expect(
DeviceHealthChecker.warningLines(snapshot),
[DeviceHealthChecker.lowStorageMessage],
);
});
test('returns both messages when battery and storage are low', () {
const snapshot = DeviceHealthSnapshot(
batteryLevelPercent: 5,
storageAvailablePercent: 5,
);
expect(
DeviceHealthChecker.warningLines(snapshot),
[
DeviceHealthChecker.lowBatteryMessage,
DeviceHealthChecker.lowStorageMessage,
],
);
});
test('does not warn at exactly threshold percent', () {
const snapshot = DeviceHealthSnapshot(
batteryLevelPercent: 10,
storageAvailablePercent: 10,
);
expect(DeviceHealthChecker.warningLines(snapshot), isEmpty);
});
test('skips battery warning when level is unknown', () {
const snapshot = DeviceHealthSnapshot(
batteryLevelPercent: null,
storageAvailablePercent: 5,
);
expect(
DeviceHealthChecker.warningLines(snapshot),
[DeviceHealthChecker.lowStorageMessage],
);
});
});
group('DeviceHealthSnapshot.fromMap', () {
test('parses native map fields', () {
final snapshot = DeviceHealthSnapshot.fromMap({
'batteryLevelPercent': 42,
'storageAvailablePercent': 12.5,
});
expect(snapshot.batteryLevelPercent, 42);
expect(snapshot.storageAvailablePercent, 12.5);
});
});
}