Skip to content

Commit 62f2f2b

Browse files
committed
fix: add microphone permission handling and user prompts in STTWidget
1 parent 4508c7e commit 62f2f2b

2 files changed

Lines changed: 132 additions & 47 deletions

File tree

ios/Runner/Info.plist

Lines changed: 49 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,51 +1,53 @@
11
<?xml version="1.0" encoding="UTF-8"?>
22
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
33
<plist version="1.0">
4-
<dict>
5-
<key>CFBundleDevelopmentRegion</key>
6-
<string>$(DEVELOPMENT_LANGUAGE)</string>
7-
<key>CFBundleDisplayName</key>
8-
<string>Interacting Tom</string>
9-
<key>CFBundleExecutable</key>
10-
<string>$(EXECUTABLE_NAME)</string>
11-
<key>CFBundleIdentifier</key>
12-
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
13-
<key>CFBundleInfoDictionaryVersion</key>
14-
<string>6.0</string>
15-
<key>CFBundleName</key>
16-
<string>interacting_tom</string>
17-
<key>CFBundlePackageType</key>
18-
<string>APPL</string>
19-
<key>CFBundleShortVersionString</key>
20-
<string>$(FLUTTER_BUILD_NAME)</string>
21-
<key>CFBundleSignature</key>
22-
<string>????</string>
23-
<key>CFBundleVersion</key>
24-
<string>$(FLUTTER_BUILD_NUMBER)</string>
25-
<key>LSRequiresIPhoneOS</key>
26-
<true/>
27-
<key>UILaunchStoryboardName</key>
28-
<string>LaunchScreen</string>
29-
<key>UIMainStoryboardFile</key>
30-
<string>Main</string>
31-
<key>UISupportedInterfaceOrientations</key>
32-
<array>
33-
<string>UIInterfaceOrientationPortrait</string>
34-
<string>UIInterfaceOrientationLandscapeLeft</string>
35-
<string>UIInterfaceOrientationLandscapeRight</string>
36-
</array>
37-
<key>UISupportedInterfaceOrientations~ipad</key>
38-
<array>
39-
<string>UIInterfaceOrientationPortrait</string>
40-
<string>UIInterfaceOrientationPortraitUpsideDown</string>
41-
<string>UIInterfaceOrientationLandscapeLeft</string>
42-
<string>UIInterfaceOrientationLandscapeRight</string>
43-
</array>
44-
<key>UIViewControllerBasedStatusBarAppearance</key>
45-
<false/>
46-
<key>CADisableMinimumFrameDurationOnPhone</key>
47-
<true/>
48-
<key>UIApplicationSupportsIndirectInputEvents</key>
49-
<true/>
50-
</dict>
4+
<dict>
5+
<key>CFBundleDevelopmentRegion</key>
6+
<string>$(DEVELOPMENT_LANGUAGE)</string>
7+
<key>CFBundleDisplayName</key>
8+
<string>Interacting Tom</string>
9+
<key>CFBundleExecutable</key>
10+
<string>$(EXECUTABLE_NAME)</string>
11+
<key>CFBundleIdentifier</key>
12+
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
13+
<key>CFBundleInfoDictionaryVersion</key>
14+
<string>6.0</string>
15+
<key>CFBundleName</key>
16+
<string>interacting_tom</string>
17+
<key>CFBundlePackageType</key>
18+
<string>APPL</string>
19+
<key>CFBundleShortVersionString</key>
20+
<string>$(FLUTTER_BUILD_NAME)</string>
21+
<key>CFBundleSignature</key>
22+
<string>????</string>
23+
<key>CFBundleVersion</key>
24+
<string>$(FLUTTER_BUILD_NUMBER)</string>
25+
<key>LSRequiresIPhoneOS</key>
26+
<true/>
27+
<key>UILaunchStoryboardName</key>
28+
<string>LaunchScreen</string>
29+
<key>UIMainStoryboardFile</key>
30+
<string>Main</string>
31+
<key>UISupportedInterfaceOrientations</key>
32+
<array>
33+
<string>UIInterfaceOrientationPortrait</string>
34+
<string>UIInterfaceOrientationLandscapeLeft</string>
35+
<string>UIInterfaceOrientationLandscapeRight</string>
36+
</array>
37+
<key>UISupportedInterfaceOrientations~ipad</key>
38+
<array>
39+
<string>UIInterfaceOrientationPortrait</string>
40+
<string>UIInterfaceOrientationPortraitUpsideDown</string>
41+
<string>UIInterfaceOrientationLandscapeLeft</string>
42+
<string>UIInterfaceOrientationLandscapeRight</string>
43+
</array>
44+
<key>UIViewControllerBasedStatusBarAppearance</key>
45+
<false/>
46+
<key>CADisableMinimumFrameDurationOnPhone</key>
47+
<true/>
48+
<key>UIApplicationSupportsIndirectInputEvents</key>
49+
<true/>
50+
<key>NSMicrophoneUsageDescription</key>
51+
<string>This app needs access to the microphone to listen to your voice and respond accordingly.</string>
52+
</dict>
5153
</plist>

lib/features/presentation/speech_to_text.dart

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import 'package:speech_to_text/speech_recognition_error.dart';
77
import 'package:speech_to_text/speech_recognition_result.dart';
88
import 'package:speech_to_text/speech_to_text.dart';
99
import 'package:collection/collection.dart';
10+
import 'package:permission_handler/permission_handler.dart';
1011

1112
class STTWidget extends ConsumerStatefulWidget {
1213
const STTWidget({super.key});
@@ -23,11 +24,13 @@ class _STTWidgetState extends ConsumerState<STTWidget>
2324
late AnimationController _idleAnimationController;
2425
late Animation<double> _idleAnimation;
2526
bool _isWaitingForResponse = false;
27+
bool _hasPermission = true; // Track permission status
2628
Timer? _responseTimeout;
2729
@override
2830
void initState() {
2931
super.initState();
3032
_initSpeech();
33+
_checkInitialPermissionStatus();
3134

3235
// Idle animation (gentle movement when not active)
3336
_idleAnimationController = AnimationController(
@@ -44,6 +47,14 @@ class _STTWidgetState extends ConsumerState<STTWidget>
4447
_idleAnimationController.repeat(reverse: true);
4548
}
4649

50+
/// Check initial permission status without requesting
51+
void _checkInitialPermissionStatus() async {
52+
var status = await Permission.microphone.status;
53+
setState(() {
54+
_hasPermission = status.isGranted;
55+
});
56+
}
57+
4758
@override
4859
void dispose() {
4960
_idleAnimationController.dispose();
@@ -72,12 +83,79 @@ class _STTWidgetState extends ConsumerState<STTWidget>
7283
_localeNames = await _speechToText.locales();
7384
}
7485

86+
/// Check and request microphone permission
87+
Future<bool> _checkMicrophonePermission() async {
88+
var status = await Permission.microphone.status;
89+
90+
if (status.isDenied) {
91+
// Request permission
92+
status = await Permission.microphone.request();
93+
}
94+
95+
if (status.isPermanentlyDenied) {
96+
// Show dialog to open app settings
97+
setState(() {
98+
_hasPermission = false;
99+
});
100+
await _showPermissionDialog();
101+
return false;
102+
}
103+
104+
bool granted = status.isGranted;
105+
setState(() {
106+
_hasPermission = granted;
107+
});
108+
109+
return granted;
110+
}
111+
112+
Future<void> _showPermissionDialog() async {
113+
if (!mounted) return;
114+
115+
return showDialog<void>(
116+
context: context,
117+
barrierDismissible: false,
118+
builder: (BuildContext context) {
119+
return AlertDialog(
120+
title: const Text('Microphone Permission Required'),
121+
content: const Text(
122+
'This app needs microphone access to listen to your voice. '
123+
'Please enable microphone permission in your device settings.',
124+
),
125+
actions: <Widget>[
126+
TextButton(
127+
child: const Text('Cancel'),
128+
onPressed: () {
129+
Navigator.of(context).pop();
130+
},
131+
),
132+
TextButton(
133+
child: const Text('Open Settings'),
134+
onPressed: () {
135+
Navigator.of(context).pop();
136+
openAppSettings();
137+
},
138+
),
139+
],
140+
);
141+
},
142+
);
143+
}
144+
75145
/// Each time to start a speech recognition session
76146
void _startListening() async {
77147
if (_speechToText.isListening) {
78148
print('Already listening');
79149
return;
80150
}
151+
152+
// Check microphone permission before starting
153+
bool hasPermission = await _checkMicrophonePermission();
154+
if (!hasPermission) {
155+
print('Microphone permission denied');
156+
return;
157+
}
158+
81159
ref.read(animationStateControllerProvider.notifier).updateHearing(true);
82160
final localeId = _getCurrentLocale();
83161
await _speechToText.listen(onResult: _onSpeechResult, localeId: localeId);
@@ -230,24 +308,29 @@ class _STTWidgetState extends ConsumerState<STTWidget>
230308

231309
IconData _getIconData() {
232310
if (_isWaitingForResponse) return Icons.hourglass_empty;
311+
if (!_hasPermission) return Icons.warning;
233312
return _isListening ? Icons.mic : Icons.mic_off;
234313
}
235314

236315
Color _getIconColor() {
237316
if (_isWaitingForResponse) return Colors.orange;
317+
if (!_hasPermission) return Colors.red;
238318
if (_isListening) return Colors.red;
239319
return Colors.grey[600]!;
240320
}
241321

242322
String _getDisplayText() {
243323
if (_isWaitingForResponse) return 'THINKING...';
244324
if (_isListening) return 'LISTENING...';
325+
if (!_hasPermission) return 'NEED MIC PERMISSION';
245326
return 'TAP TO SPEAK';
246327
}
247328

248329
String _getSemanticLabel() {
249330
if (_isWaitingForResponse) return 'Waiting for response, please wait';
250331
if (_isListening) return 'Currently listening, tap to stop';
332+
if (!_hasPermission)
333+
return 'Microphone permission required, tap to grant permission';
251334
return 'Tap to start speaking';
252335
}
253336
}

0 commit comments

Comments
 (0)