@@ -7,6 +7,7 @@ import 'package:speech_to_text/speech_recognition_error.dart';
77import 'package:speech_to_text/speech_recognition_result.dart' ;
88import 'package:speech_to_text/speech_to_text.dart' ;
99import 'package:collection/collection.dart' ;
10+ import 'package:permission_handler/permission_handler.dart' ;
1011
1112class 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