1.开始录制、结束录制增加
2. 增加电量检测、内存检查,是否低于 10%
This commit is contained in:
@@ -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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 [
|
||||||
|
|||||||
@@ -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?>{});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
25
lib/core/platform/device_health_checker.dart
Normal file
25
lib/core/platform/device_health_checker.dart
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
30
lib/core/platform/device_health_snapshot.dart
Normal file
30
lib/core/platform/device_health_snapshot.dart
Normal 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;
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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('确定'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
88
test/core/platform/device_health_checker_test.dart
Normal file
88
test/core/platform/device_health_checker_test.dart
Normal 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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user