|
@@ -1,6 +1,9 @@
|
|
|
|
|
+import 'dart:async';
|
|
|
import 'dart:io';
|
|
import 'dart:io';
|
|
|
import 'dart:typed_data';
|
|
import 'dart:typed_data';
|
|
|
|
|
+import 'package:custom_notification/custom_notification.dart';
|
|
|
import 'package:electronic_assistant/module/record/record_task.dart';
|
|
import 'package:electronic_assistant/module/record/record_task.dart';
|
|
|
|
|
+import 'package:electronic_assistant/router/app_pages.dart';
|
|
|
import 'package:flutter/cupertino.dart';
|
|
import 'package:flutter/cupertino.dart';
|
|
|
import 'package:get/get.dart';
|
|
import 'package:get/get.dart';
|
|
|
import 'package:path_provider/path_provider.dart';
|
|
import 'package:path_provider/path_provider.dart';
|
|
@@ -9,20 +12,32 @@ import 'package:get/get_rx/src/rx_types/rx_types.dart';
|
|
|
import 'package:record/record.dart';
|
|
import 'package:record/record.dart';
|
|
|
import 'package:uuid/uuid.dart';
|
|
import 'package:uuid/uuid.dart';
|
|
|
import 'package:wakelock_plus/wakelock_plus.dart';
|
|
import 'package:wakelock_plus/wakelock_plus.dart';
|
|
|
|
|
+import '../../data/bean/talks.dart';
|
|
|
|
|
+import '../../data/consts/error_code.dart';
|
|
|
|
|
+import '../../data/consts/event_report_id.dart';
|
|
|
|
|
+import '../../data/repositories/talk_repository.dart';
|
|
|
import '../../resource/string.gen.dart';
|
|
import '../../resource/string.gen.dart';
|
|
|
|
|
+import '../../utils/http_handler.dart';
|
|
|
import '../../utils/mmkv_util.dart';
|
|
import '../../utils/mmkv_util.dart';
|
|
|
|
|
+import '../../utils/notification_util.dart';
|
|
|
import '../../utils/pcm_wav_converter.dart';
|
|
import '../../utils/pcm_wav_converter.dart';
|
|
|
import '../../utils/toast_util.dart';
|
|
import '../../utils/toast_util.dart';
|
|
|
|
|
+import '../talk/view.dart';
|
|
|
import 'constants.dart';
|
|
import 'constants.dart';
|
|
|
|
|
|
|
|
class RecordHandler {
|
|
class RecordHandler {
|
|
|
RecordHandler._();
|
|
RecordHandler._();
|
|
|
|
|
|
|
|
|
|
+ static const int minRecordDuration = 3;
|
|
|
|
|
+ static const String recordDone = 'done';
|
|
|
|
|
+ static const String recordPause = 'pause';
|
|
|
|
|
+ static const String recordRecording = 'recording';
|
|
|
|
|
+
|
|
|
static const String keyLastRecordId = "last_record_id";
|
|
static const String keyLastRecordId = "last_record_id";
|
|
|
|
|
|
|
|
final Rx<RecordStatus> currentStatus = RecordStatus.pending.obs;
|
|
final Rx<RecordStatus> currentStatus = RecordStatus.pending.obs;
|
|
|
final RxDouble currentDuration = 0.0.obs;
|
|
final RxDouble currentDuration = 0.0.obs;
|
|
|
- AudioRecorder? _record;
|
|
|
|
|
|
|
+ final AudioRecorder _record = AudioRecorder();
|
|
|
final RecordConfig _recordConfig = RecordConfig(
|
|
final RecordConfig _recordConfig = RecordConfig(
|
|
|
encoder: AudioEncoder.pcm16bits,
|
|
encoder: AudioEncoder.pcm16bits,
|
|
|
bitRate: 16000,
|
|
bitRate: 16000,
|
|
@@ -32,17 +47,66 @@ class RecordHandler {
|
|
|
|
|
|
|
|
String? _lastRecordId;
|
|
String? _lastRecordId;
|
|
|
|
|
|
|
|
|
|
+ final int _serviceId = 256;
|
|
|
|
|
+ final String _channelId = StringName.recordNotificationChannelId.tr;
|
|
|
|
|
+ final String _channelName = StringName.recordNotificationChannelName.tr;
|
|
|
|
|
+
|
|
|
String get lastRecordId => _lastRecordId ?? '';
|
|
String get lastRecordId => _lastRecordId ?? '';
|
|
|
|
|
+ StreamSubscription? _currentDurationListener;
|
|
|
|
|
+ StreamSubscription? _recordActionListener;
|
|
|
|
|
|
|
|
void init() {
|
|
void init() {
|
|
|
if (currentStatus.value != RecordStatus.recording) {
|
|
if (currentStatus.value != RecordStatus.recording) {
|
|
|
- _record = AudioRecorder();
|
|
|
|
|
_initLastRecordId();
|
|
_initLastRecordId();
|
|
|
_initLastRecordStatus();
|
|
_initLastRecordStatus();
|
|
|
_initForegroundService();
|
|
_initForegroundService();
|
|
|
|
|
+ _initRecordDurationStream();
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ void _initRecordDurationStream() {
|
|
|
|
|
+ _currentDurationListener?.cancel();
|
|
|
|
|
+ _currentDurationListener = currentDuration.listen((event) {
|
|
|
|
|
+ debugPrint('currentDuration: $event');
|
|
|
|
|
+ if (currentStatus.value == RecordStatus.pending) {
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ NotificationUtil.showRecordNotification(
|
|
|
|
|
+ _serviceId, currentStatus.value == RecordStatus.recording, event,
|
|
|
|
|
+ channelId: _channelId, channelName: _channelName);
|
|
|
|
|
+ });
|
|
|
|
|
+ _recordActionListener?.cancel();
|
|
|
|
|
+ _recordActionListener =
|
|
|
|
|
+ CustomNotification.recordActionStream().listen((action) {
|
|
|
|
|
+ if (action == recordDone) {
|
|
|
|
|
+ _recordNotificationDone();
|
|
|
|
|
+ } else if (action == recordPause) {
|
|
|
|
|
+ stopRecord();
|
|
|
|
|
+ } else if (action == recordRecording) {
|
|
|
|
|
+ startOrContinueRecord();
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ void _recordNotificationDone() {
|
|
|
|
|
+ saveCurrentRecord().then((talkInfo) {
|
|
|
|
|
+ if (Get.currentRoute == RoutePath.record) {
|
|
|
|
|
+ Get.back();
|
|
|
|
|
+ }
|
|
|
|
|
+ TalkPage.start(talkInfo, eventTag: EventId.id_001);
|
|
|
|
|
+ }).catchError((error) {
|
|
|
|
|
+ if (error is ServerErrorException) {
|
|
|
|
|
+ if (error.code == ErrorCode.errorCodeNoLogin) {
|
|
|
|
|
+ ToastUtil.showToast("录音已保存,请登录");
|
|
|
|
|
+ } else {
|
|
|
|
|
+ ToastUtil.showToast("${error.message}");
|
|
|
|
|
+ }
|
|
|
|
|
+ } else {
|
|
|
|
|
+ ToastUtil.showToast("录音已保存,请检查网络并重试");
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
void _initLastRecordId() {
|
|
void _initLastRecordId() {
|
|
|
String? lastRecordId = KVUtil.getString(keyLastRecordId, null);
|
|
String? lastRecordId = KVUtil.getString(keyLastRecordId, null);
|
|
|
if (lastRecordId == null || lastRecordId.isEmpty) {
|
|
if (lastRecordId == null || lastRecordId.isEmpty) {
|
|
@@ -58,8 +122,9 @@ class RecordHandler {
|
|
|
var fileLength = currentRecordFile.lengthSync();
|
|
var fileLength = currentRecordFile.lengthSync();
|
|
|
if (currentRecordFile.existsSync() && fileLength > 0) {
|
|
if (currentRecordFile.existsSync() && fileLength > 0) {
|
|
|
_changeRecordStatus(RecordStatus.paused);
|
|
_changeRecordStatus(RecordStatus.paused);
|
|
|
- currentDuration.value = _getPcmDuration(
|
|
|
|
|
|
|
+ double time = _getPcmDuration(
|
|
|
fileLength, _recordConfig.sampleRate, 16, _recordConfig.numChannels);
|
|
fileLength, _recordConfig.sampleRate, 16, _recordConfig.numChannels);
|
|
|
|
|
+ currentDuration.value = time;
|
|
|
} else {
|
|
} else {
|
|
|
currentDuration.value = 0;
|
|
currentDuration.value = 0;
|
|
|
}
|
|
}
|
|
@@ -81,17 +146,25 @@ class RecordHandler {
|
|
|
return file;
|
|
return file;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- Future<void> stopRecord() async {
|
|
|
|
|
|
|
+ Future<void> stopRecord({bool? isStopService}) async {
|
|
|
_releaseWakeLock();
|
|
_releaseWakeLock();
|
|
|
- _record
|
|
|
|
|
- ?.stop()
|
|
|
|
|
- .then((_) => _changeRecordStatus(RecordStatus.paused))
|
|
|
|
|
- .then((_) => FlutterForegroundTask.stopService());
|
|
|
|
|
- ;
|
|
|
|
|
|
|
+ if (await _record.isRecording()) {
|
|
|
|
|
+ await _record.stop();
|
|
|
|
|
+ }
|
|
|
|
|
+ _changeRecordStatus(RecordStatus.paused);
|
|
|
|
|
+ if (isStopService == true) {
|
|
|
|
|
+ await FlutterForegroundTask.stopService();
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
void _changeRecordStatus(RecordStatus status) {
|
|
void _changeRecordStatus(RecordStatus status) {
|
|
|
currentStatus.value = status;
|
|
currentStatus.value = status;
|
|
|
|
|
+ if (status == RecordStatus.pending) {
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ NotificationUtil.showRecordNotification(
|
|
|
|
|
+ _serviceId, status == RecordStatus.recording, currentDuration.value,
|
|
|
|
|
+ channelId: _channelId, channelName: _channelName);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
void _setWakeLock() {
|
|
void _setWakeLock() {
|
|
@@ -106,8 +179,8 @@ class RecordHandler {
|
|
|
WidgetsBinding.instance
|
|
WidgetsBinding.instance
|
|
|
.addPostFrameCallback((_) => FlutterForegroundTask.init(
|
|
.addPostFrameCallback((_) => FlutterForegroundTask.init(
|
|
|
androidNotificationOptions: AndroidNotificationOptions(
|
|
androidNotificationOptions: AndroidNotificationOptions(
|
|
|
- channelId: StringName.recordNotificationChannelId.tr,
|
|
|
|
|
- channelName: StringName.recordNotificationChannelName.tr,
|
|
|
|
|
|
|
+ channelId: _channelId,
|
|
|
|
|
+ channelName: _channelName,
|
|
|
channelDescription:
|
|
channelDescription:
|
|
|
StringName.recordNotificationChannelDescription.tr,
|
|
StringName.recordNotificationChannelDescription.tr,
|
|
|
channelImportance: NotificationChannelImportance.LOW,
|
|
channelImportance: NotificationChannelImportance.LOW,
|
|
@@ -132,10 +205,7 @@ class RecordHandler {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
Future<void> startOrContinueRecord() async {
|
|
Future<void> startOrContinueRecord() async {
|
|
|
- if (_record == null) {
|
|
|
|
|
- return;
|
|
|
|
|
- }
|
|
|
|
|
- bool hasPermission = await _record!.hasPermission();
|
|
|
|
|
|
|
+ bool hasPermission = await _record.hasPermission();
|
|
|
if (!hasPermission) {
|
|
if (!hasPermission) {
|
|
|
_onRecordPermissionDenied();
|
|
_onRecordPermissionDenied();
|
|
|
return;
|
|
return;
|
|
@@ -146,7 +216,7 @@ class RecordHandler {
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
File targetFile = await _getCurrentRecordFile();
|
|
File targetFile = await _getCurrentRecordFile();
|
|
|
- Stream<Uint8List> recordStream = await _record!.startStream(_recordConfig);
|
|
|
|
|
|
|
+ Stream<Uint8List> recordStream = await _record.startStream(_recordConfig);
|
|
|
_setWakeLock();
|
|
_setWakeLock();
|
|
|
_startForegroundService();
|
|
_startForegroundService();
|
|
|
if (currentStatus.value != RecordStatus.recording) {
|
|
if (currentStatus.value != RecordStatus.recording) {
|
|
@@ -183,29 +253,32 @@ class RecordHandler {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
Future<void> deleteCurrentRecord() async {
|
|
Future<void> deleteCurrentRecord() async {
|
|
|
- await stopRecord();
|
|
|
|
|
|
|
+ await stopRecord(isStopService: true);
|
|
|
File file = await _getCurrentRecordFile();
|
|
File file = await _getCurrentRecordFile();
|
|
|
if (file.existsSync()) {
|
|
if (file.existsSync()) {
|
|
|
file.deleteSync();
|
|
file.deleteSync();
|
|
|
}
|
|
}
|
|
|
KVUtil.putString(keyLastRecordId, "");
|
|
KVUtil.putString(keyLastRecordId, "");
|
|
|
- currentDuration.value = 0;
|
|
|
|
|
_changeRecordStatus(RecordStatus.pending);
|
|
_changeRecordStatus(RecordStatus.pending);
|
|
|
|
|
+ currentDuration.value = 0;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- Future<ServiceRequestResult> _startForegroundService() async {
|
|
|
|
|
- if (await FlutterForegroundTask.isRunningService) {
|
|
|
|
|
- return FlutterForegroundTask.restartService();
|
|
|
|
|
- } else {
|
|
|
|
|
- return FlutterForegroundTask.startService(
|
|
|
|
|
- serviceId: 256,
|
|
|
|
|
- notificationTitle: StringName.appName.tr,
|
|
|
|
|
- notificationText: StringName.recordStatusRecording.tr,
|
|
|
|
|
- notificationIcon: null,
|
|
|
|
|
- notificationButtons: [],
|
|
|
|
|
- callback: setRecordCallback,
|
|
|
|
|
- );
|
|
|
|
|
|
|
+ Future<void> _startForegroundService() async {
|
|
|
|
|
+ final isRunningService = await FlutterForegroundTask.isRunningService;
|
|
|
|
|
+ if (isRunningService) {
|
|
|
|
|
+ return;
|
|
|
}
|
|
}
|
|
|
|
|
+ await FlutterForegroundTask.startService(
|
|
|
|
|
+ serviceId: _serviceId,
|
|
|
|
|
+ notificationTitle: StringName.appName.tr,
|
|
|
|
|
+ notificationText: StringName.recordStatusRecording.tr,
|
|
|
|
|
+ notificationIcon: null,
|
|
|
|
|
+ notificationButtons: [],
|
|
|
|
|
+ callback: setRecordCallback,
|
|
|
|
|
+ );
|
|
|
|
|
+ // NotificationUtil.showRecordNotification(
|
|
|
|
|
+ // _serviceId, true, currentDuration.value,
|
|
|
|
|
+ // channelId: _channelId, channelName: _channelName);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/// 判断是否有未上传的录音
|
|
/// 判断是否有未上传的录音
|
|
@@ -219,11 +292,7 @@ class RecordHandler {
|
|
|
return await file.exists() && await file.length() > 0;
|
|
return await file.exists() && await file.length() > 0;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- void onClose() async {
|
|
|
|
|
- if (currentStatus.value != RecordStatus.recording) {
|
|
|
|
|
- _record?.dispose();
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ void onClose() async {}
|
|
|
|
|
|
|
|
Future<void> getConvertWavFile(String talkId) async {
|
|
Future<void> getConvertWavFile(String talkId) async {
|
|
|
File pcmFile = await _getCurrentRecordFile();
|
|
File pcmFile = await _getCurrentRecordFile();
|
|
@@ -243,6 +312,20 @@ class RecordHandler {
|
|
|
Directory documentDir = await getApplicationDocumentsDirectory();
|
|
Directory documentDir = await getApplicationDocumentsDirectory();
|
|
|
return File("${documentDir.path}/.atmob/record/$talkId.wav");
|
|
return File("${documentDir.path}/.atmob/record/$talkId.wav");
|
|
|
}
|
|
}
|
|
|
|
|
+
|
|
|
|
|
+ Future<TalkBean> saveCurrentRecord() async {
|
|
|
|
|
+ final currentDurationValue = currentDuration.value;
|
|
|
|
|
+ if (currentDurationValue < minRecordDuration) {
|
|
|
|
|
+ throw ServerErrorException(-1, "录音时长不足$minRecordDuration秒");
|
|
|
|
|
+ }
|
|
|
|
|
+ await recordHandler.stopRecord(isStopService: true);
|
|
|
|
|
+ return talkRepository
|
|
|
|
|
+ .talkCreate(recordHandler.lastRecordId, currentDuration.value.toInt())
|
|
|
|
|
+ .then((talkInfo) async {
|
|
|
|
|
+ await recordHandler.getConvertWavFile(talkInfo.id);
|
|
|
|
|
+ return talkInfo;
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
final recordHandler = RecordHandler._();
|
|
final recordHandler = RecordHandler._();
|