You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
544 lines
19 KiB
544 lines
19 KiB
import 'dart:async';
|
|
import 'dart:developer' as developer;
|
|
import 'dart:io';
|
|
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter/services.dart';
|
|
import 'package:get/get.dart';
|
|
import 'package:permission_handler/permission_handler.dart';
|
|
import 'package:device_info_plus/device_info_plus.dart';
|
|
import 'package:path_provider/path_provider.dart';
|
|
|
|
import '../../data/services/voice_recognition_service.dart';
|
|
|
|
class SpeechDemoPage extends StatefulWidget {
|
|
const SpeechDemoPage({Key? key}) : super(key: key);
|
|
|
|
@override
|
|
State<SpeechDemoPage> createState() => _SpeechDemoPageState();
|
|
}
|
|
|
|
class _SpeechDemoPageState extends State<SpeechDemoPage> {
|
|
final VoiceRecognitionService _recognitionService = Get.find<VoiceRecognitionService>();
|
|
final RxBool _permissionsGranted = false.obs;
|
|
final RxBool _isInitializing = false.obs;
|
|
final RxBool _isRecognizing = false.obs;
|
|
final RxString _errorMessage = ''.obs;
|
|
final RxString _recognizedText = ''.obs;
|
|
final RxString _status = ''.obs;
|
|
|
|
// 添加原生权限通道 - 只用于处理Android 11+的MANAGE_EXTERNAL_STORAGE权限
|
|
static const MethodChannel _permissionChannel = MethodChannel('com.example.deep_voice/permissions');
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_setupPermissionListener();
|
|
_checkPermissions();
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_recognitionService.stopRecognition();
|
|
super.dispose();
|
|
}
|
|
|
|
// 设置权限监听器 - 只监听MANAGE_EXTERNAL_STORAGE权限结果
|
|
void _setupPermissionListener() {
|
|
_permissionChannel.setMethodCallHandler((call) async {
|
|
if (call.method == 'manageExternalStorageResult') {
|
|
developer.log('Received manageExternalStorageResult from native: ${call.arguments}');
|
|
|
|
// 更新权限状态
|
|
await _checkPermissions();
|
|
}
|
|
return null;
|
|
});
|
|
}
|
|
|
|
// 检查所有必要的权限
|
|
Future<void> _checkPermissions() async {
|
|
developer.log('Checking permissions');
|
|
|
|
try {
|
|
// 检查基本权限
|
|
final micStatus = await Permission.microphone.status;
|
|
final storageStatus = await Permission.storage.status;
|
|
|
|
developer.log('=== 详细权限状态 ===');
|
|
developer.log('麦克风权限: $micStatus');
|
|
developer.log('存储权限: $storageStatus');
|
|
|
|
// 对于Android 11+,检查MANAGE_EXTERNAL_STORAGE权限
|
|
bool manageStorageGranted = true;
|
|
if (Platform.isAndroid) {
|
|
if (await _isAndroid11OrHigher()) {
|
|
// 使用原生方法检查MANAGE_EXTERNAL_STORAGE权限
|
|
try {
|
|
manageStorageGranted = await _permissionChannel.invokeMethod('isExternalStorageManager') ?? false;
|
|
} catch (e) {
|
|
developer.log('Error checking MANAGE_EXTERNAL_STORAGE: $e');
|
|
manageStorageGranted = await Permission.manageExternalStorage.isGranted;
|
|
}
|
|
developer.log('管理所有文件权限: $manageStorageGranted');
|
|
}
|
|
}
|
|
|
|
// 记录其他权限状态
|
|
developer.log('读取外部存储权限: ${await Permission.audio.status}');
|
|
developer.log('写入外部存储权限: ${await Permission.photos.status}');
|
|
developer.log('=====================');
|
|
|
|
// 更新权限状态
|
|
final bool basicPermissionsGranted = micStatus.isGranted && storageStatus.isGranted;
|
|
|
|
// 修改逻辑:如果是Android 11+且已授予MANAGE_EXTERNAL_STORAGE权限,则忽略普通存储权限状态
|
|
bool effectiveStoragePermission = storageStatus.isGranted;
|
|
if (Platform.isAndroid && await _isAndroid11OrHigher() && manageStorageGranted) {
|
|
developer.log('已授予MANAGE_EXTERNAL_STORAGE权限,忽略普通存储权限状态');
|
|
effectiveStoragePermission = true;
|
|
}
|
|
|
|
_permissionsGranted.value = micStatus.isGranted && (effectiveStoragePermission || manageStorageGranted);
|
|
|
|
developer.log('所有权限已授予: ${_permissionsGranted.value}');
|
|
|
|
// 如果权限已授予,初始化引擎
|
|
if (_permissionsGranted.value && !_isInitializing.value) {
|
|
_initializeEngine();
|
|
} else if (!_permissionsGranted.value) {
|
|
_errorMessage.value = '需要麦克风和存储权限才能使用语音识别功能';
|
|
}
|
|
} catch (e) {
|
|
developer.log('检查权限时出错: $e');
|
|
_errorMessage.value = '检查权限时出错: $e';
|
|
}
|
|
}
|
|
|
|
// 检查是否为Android 11或更高版本
|
|
Future<bool> _isAndroid11OrHigher() async {
|
|
if (Platform.isAndroid) {
|
|
final androidInfo = await DeviceInfoPlugin().androidInfo;
|
|
return androidInfo.version.sdkInt >= 30; // Android 11 is API 30
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// 记录详细的权限状态
|
|
Future<void> _logPermissionDetails() async {
|
|
developer.log('=== 详细权限状态 ===');
|
|
developer.log('麦克风权限: ${await Permission.microphone.status}');
|
|
developer.log('存储权限: ${await Permission.storage.status}');
|
|
|
|
if (Platform.isAndroid) {
|
|
if (await _isAndroid11OrHigher()) {
|
|
bool manageStorageGranted = false;
|
|
try {
|
|
manageStorageGranted = await _permissionChannel.invokeMethod('isExternalStorageManager') ?? false;
|
|
} catch (e) {
|
|
developer.log('Error checking MANAGE_EXTERNAL_STORAGE: $e');
|
|
manageStorageGranted = await Permission.manageExternalStorage.isGranted;
|
|
}
|
|
developer.log('管理所有文件权限: $manageStorageGranted');
|
|
}
|
|
}
|
|
|
|
developer.log('读取外部存储权限: ${await Permission.audio.status}');
|
|
developer.log('写入外部存储权限: ${await Permission.photos.status}');
|
|
developer.log('=====================');
|
|
}
|
|
|
|
// 请求所有必要的权限
|
|
Future<void> _requestPermissions() async {
|
|
developer.log('请求权限');
|
|
|
|
try {
|
|
// 对于Android 11+,优先请求MANAGE_EXTERNAL_STORAGE权限
|
|
if (Platform.isAndroid && await _isAndroid11OrHigher()) {
|
|
bool isManager = false;
|
|
try {
|
|
isManager = await _permissionChannel.invokeMethod('isExternalStorageManager') ?? false;
|
|
} catch (e) {
|
|
developer.log('检查MANAGE_EXTERNAL_STORAGE时出错: $e');
|
|
isManager = await Permission.manageExternalStorage.isGranted;
|
|
}
|
|
|
|
if (!isManager) {
|
|
developer.log('请求MANAGE_EXTERNAL_STORAGE权限');
|
|
// 使用原生方法请求MANAGE_EXTERNAL_STORAGE权限
|
|
await _permissionChannel.invokeMethod('requestManageExternalStorage');
|
|
|
|
// 等待一段时间,让用户有机会授予权限
|
|
await Future.delayed(const Duration(seconds: 1));
|
|
|
|
// 再次检查权限状态
|
|
try {
|
|
isManager = await _permissionChannel.invokeMethod('isExternalStorageManager') ?? false;
|
|
developer.log('MANAGE_EXTERNAL_STORAGE权限状态: $isManager');
|
|
} catch (e) {
|
|
developer.log('检查MANAGE_EXTERNAL_STORAGE时出错: $e');
|
|
}
|
|
}
|
|
}
|
|
|
|
// 请求基本权限
|
|
Map<Permission, PermissionStatus> statuses = await [
|
|
Permission.microphone,
|
|
Permission.storage,
|
|
].request();
|
|
|
|
// 记录结果
|
|
statuses.forEach((permission, status) {
|
|
developer.log('权限 $permission: $status');
|
|
});
|
|
|
|
// 检查权限状态
|
|
await _checkPermissions();
|
|
} catch (e) {
|
|
developer.log('请求权限时出错: $e');
|
|
_errorMessage.value = '请求权限时出错: $e';
|
|
}
|
|
}
|
|
|
|
// 打开应用设置
|
|
Future<void> _openAppSettings() async {
|
|
developer.log('打开应用设置');
|
|
try {
|
|
// 使用permission_handler打开设置
|
|
final opened = await openAppSettings();
|
|
if (!opened) {
|
|
// 如果permission_handler失败,使用原生方法
|
|
await _permissionChannel.invokeMethod('openAppSettings');
|
|
}
|
|
} catch (e) {
|
|
developer.log('打开应用设置时出错: $e');
|
|
// 使用原生方法作为备选
|
|
try {
|
|
await _permissionChannel.invokeMethod('openAppSettings');
|
|
} catch (e2) {
|
|
developer.log('通过原生通道打开应用设置时出错: $e2');
|
|
}
|
|
}
|
|
}
|
|
|
|
// 初始化语音引擎
|
|
Future<void> _initializeEngine() async {
|
|
if (_isInitializing.value) return;
|
|
|
|
_isInitializing.value = true;
|
|
_errorMessage.value = '';
|
|
_status.value = '正在初始化语音引擎...';
|
|
|
|
try {
|
|
// 再次检查权限状态
|
|
await _checkPermissions();
|
|
|
|
if (!_permissionsGranted.value) {
|
|
developer.log('权限未授予,无法初始化引擎');
|
|
_errorMessage.value = '权限未授予,无法初始化语音引擎';
|
|
_isInitializing.value = false;
|
|
return;
|
|
}
|
|
|
|
// 记录权限详情
|
|
await _logPermissionDetails();
|
|
|
|
// 测试存储权限
|
|
String? availableDir = await _getAvailableStorageDirectory();
|
|
if (availableDir != null) {
|
|
developer.log('找到可用的存储目录: $availableDir');
|
|
} else {
|
|
developer.log('警告: 未找到可用的存储目录,可能会影响语音引擎初始化');
|
|
}
|
|
|
|
// 初始化语音引擎
|
|
developer.log('初始化语音识别引擎');
|
|
await _recognitionService.initialize();
|
|
|
|
developer.log('语音识别引擎初始化成功');
|
|
_status.value = '语音引擎初始化成功,可以开始识别';
|
|
_isInitializing.value = false;
|
|
} catch (e, stackTrace) {
|
|
developer.log('初始化语音识别引擎时出错: $e');
|
|
developer.log('堆栈跟踪: $stackTrace');
|
|
|
|
// 解析错误代码
|
|
String errorDetail = '未知错误';
|
|
if (e is PlatformException) {
|
|
developer.log('PlatformException 代码: ${e.code}, 消息: ${e.message}');
|
|
|
|
if (e.code == 'INIT_ERROR') {
|
|
if (e.message?.contains('-7') == true) {
|
|
errorDetail = '无法创建日志目录 (错误码: -7),可能是存储权限问题';
|
|
} else {
|
|
errorDetail = '初始化错误: ${e.message}';
|
|
}
|
|
}
|
|
}
|
|
|
|
_errorMessage.value = '初始化语音引擎失败: $errorDetail';
|
|
_status.value = '初始化失败';
|
|
_isInitializing.value = false;
|
|
|
|
// 显示错误提示和重试选项
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
SnackBar(
|
|
content: Text('初始化失败: $errorDetail'),
|
|
action: SnackBarAction(
|
|
label: '重试',
|
|
onPressed: _initializeEngine,
|
|
),
|
|
duration: const Duration(seconds: 10),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
Future<void> _startRecognition() async {
|
|
developer.log('开始识别');
|
|
_errorMessage.value = '';
|
|
_isRecognizing.value = true;
|
|
_status.value = '正在识别...';
|
|
|
|
try {
|
|
await _recognitionService.startRecognition();
|
|
|
|
// 设置监听器接收识别结果
|
|
_recognitionService.recognizedText.listen((text) {
|
|
_recognizedText.value = text;
|
|
});
|
|
|
|
} catch (e) {
|
|
developer.log('开始识别时出错: $e');
|
|
_errorMessage.value = '启动识别失败: $e';
|
|
_isRecognizing.value = false;
|
|
_status.value = '识别启动失败';
|
|
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
SnackBar(
|
|
content: Text('启动识别失败: $e'),
|
|
duration: const Duration(seconds: 3),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
Future<void> _stopRecognition() async {
|
|
developer.log('停止识别');
|
|
|
|
try {
|
|
await _recognitionService.stopRecognition();
|
|
_isRecognizing.value = false;
|
|
_status.value = '识别已停止';
|
|
|
|
// 保存最终识别结果
|
|
_recognizedText.value = _recognitionService.recognizedText.value;
|
|
|
|
} catch (e) {
|
|
developer.log('停止识别时出错: $e');
|
|
_errorMessage.value = '停止识别失败: $e';
|
|
_isRecognizing.value = false;
|
|
_status.value = '识别停止失败';
|
|
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
SnackBar(
|
|
content: Text('停止识别失败: $e'),
|
|
duration: const Duration(seconds: 3),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
// 获取可用的存储目录
|
|
Future<String?> _getAvailableStorageDirectory() async {
|
|
try {
|
|
List<Directory> directoriesToTry = [];
|
|
|
|
// 应用专用外部存储目录
|
|
if (Platform.isAndroid) {
|
|
directoriesToTry.add(Directory('/storage/emulated/0/Android/data/com.example.deep_voice/files'));
|
|
}
|
|
|
|
// 应用内部存储目录
|
|
try {
|
|
final appDir = await getApplicationDocumentsDirectory();
|
|
directoriesToTry.add(appDir);
|
|
} catch (e) {
|
|
developer.log('获取应用内部存储目录失败: $e');
|
|
}
|
|
|
|
// 应用缓存目录
|
|
try {
|
|
final cacheDir = await getTemporaryDirectory();
|
|
directoriesToTry.add(cacheDir);
|
|
} catch (e) {
|
|
developer.log('获取应用缓存目录失败: $e');
|
|
}
|
|
|
|
// 尝试每个目录
|
|
for (var directory in directoriesToTry) {
|
|
try {
|
|
developer.log('测试目录可用性: ${directory.path}');
|
|
|
|
if (!await directory.exists()) {
|
|
await directory.create(recursive: true);
|
|
}
|
|
|
|
final testFile = File('${directory.path}/test_write.tmp');
|
|
await testFile.writeAsString('Test write at ${DateTime.now()}');
|
|
await testFile.readAsString(); // 验证可读
|
|
await testFile.delete();
|
|
|
|
developer.log('找到可用目录: ${directory.path}');
|
|
return directory.path;
|
|
} catch (e) {
|
|
developer.log('目录 ${directory.path} 不可用: $e');
|
|
}
|
|
}
|
|
|
|
developer.log('未找到可用的存储目录');
|
|
return null;
|
|
} catch (e) {
|
|
developer.log('获取可用存储目录时出错: $e');
|
|
return null;
|
|
}
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Scaffold(
|
|
appBar: AppBar(
|
|
title: const Text('语音识别演示'),
|
|
),
|
|
body: Padding(
|
|
padding: const EdgeInsets.all(16.0),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
children: [
|
|
// 权限请求部分
|
|
Obx(() => !_permissionsGranted.value
|
|
? Card(
|
|
color: Colors.amber.shade100,
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(16.0),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
const Text(
|
|
'需要麦克风和存储权限',
|
|
style: TextStyle(
|
|
fontWeight: FontWeight.bold,
|
|
fontSize: 16,
|
|
),
|
|
),
|
|
const SizedBox(height: 8),
|
|
const Text(
|
|
'注意: 在Android 11及以上版本,您需要在设置中手动授予"管理所有文件"权限',
|
|
style: TextStyle(
|
|
fontSize: 14,
|
|
),
|
|
),
|
|
const SizedBox(height: 16),
|
|
Wrap(
|
|
spacing: 8.0,
|
|
runSpacing: 8.0,
|
|
alignment: WrapAlignment.center,
|
|
children: [
|
|
ElevatedButton(
|
|
onPressed: _requestPermissions,
|
|
child: const Text('授予权限'),
|
|
),
|
|
ElevatedButton(
|
|
onPressed: _openAppSettings,
|
|
child: const Text('打开设置'),
|
|
),
|
|
ElevatedButton(
|
|
onPressed: _logPermissionDetails,
|
|
child: const Text('检查权限'),
|
|
),
|
|
ElevatedButton(
|
|
onPressed: _initializeEngine,
|
|
child: const Text('强制初始化'),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
),
|
|
)
|
|
: const SizedBox.shrink()),
|
|
|
|
// 状态显示
|
|
Obx(() => _status.value.isNotEmpty
|
|
? Padding(
|
|
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
|
child: Text(
|
|
'状态: ${_status.value}',
|
|
style: const TextStyle(
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
)
|
|
: const SizedBox.shrink()),
|
|
|
|
// 错误消息
|
|
Obx(() => _errorMessage.value.isNotEmpty
|
|
? Padding(
|
|
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
|
child: Text(
|
|
'错误: ${_errorMessage.value}',
|
|
style: const TextStyle(
|
|
color: Colors.red,
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
)
|
|
: const SizedBox.shrink()),
|
|
|
|
// 识别结果显示区域
|
|
Expanded(
|
|
child: Card(
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(16.0),
|
|
child: SingleChildScrollView(
|
|
child: Obx(() => Text(
|
|
_recognizedText.value.isEmpty
|
|
? '识别结果将显示在这里...'
|
|
: _recognizedText.value,
|
|
style: const TextStyle(
|
|
fontSize: 18.0,
|
|
),
|
|
)),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
|
|
// 控制按钮
|
|
Padding(
|
|
padding: const EdgeInsets.symmetric(vertical: 16.0),
|
|
child: Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
|
children: [
|
|
Obx(() => ElevatedButton(
|
|
onPressed: _permissionsGranted.value && !_isRecognizing.value && !_isInitializing.value
|
|
? _startRecognition
|
|
: null,
|
|
child: const Text('开始识别'),
|
|
)),
|
|
Obx(() => ElevatedButton(
|
|
onPressed: _isRecognizing.value ? _stopRecognition : null,
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: Colors.red,
|
|
),
|
|
child: const Text('停止识别'),
|
|
)),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|