capacitor-microphone 0.0.27 → 0.0.29

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -12,11 +12,14 @@ import com.getcapacitor.annotation.PermissionCallback;
12
12
  import com.getcapacitor.PermissionState;
13
13
 
14
14
  import android.content.Intent;
15
- import android.os.Bundle;
16
15
  import android.speech.RecognitionListener;
17
16
  import android.speech.RecognizerIntent;
18
17
  import android.speech.SpeechRecognizer;
19
18
 
19
+ import android.os.Handler;
20
+ import android.os.Looper;
21
+ import androidx.annotation.Nullable;
22
+
20
23
  import java.util.ArrayList;
21
24
 
22
25
  @CapacitorPlugin(
@@ -28,685 +31,271 @@ import java.util.ArrayList;
28
31
  )
29
32
  }
30
33
  )
31
- public class CapacitorMicrophonePlugin extends Plugin {
34
+ public class CapacitorMicrophonePlugin extends Plugin implements RecognitionListener{
32
35
 
33
36
  private SpeechRecognizer speechRecognizer;
34
- private PluginCall currentCall;
35
- private Intent speechIntent;
37
+ private Intent recognizerIntent;
38
+
36
39
  private boolean isListening = false;
37
40
  private boolean isDestroyed = false;
41
+
38
42
  private long lastStopTime = 0;
39
- private static final long RESTART_COOLDOWN_MS = 400;
43
+ private final long restartCooldownMs = 400;
44
+
45
+ private final Handler handler = new Handler(Looper.getMainLooper());
40
46
  private Runnable delayedStartRunnable;
41
- private Runnable silenceRunnable;
42
- private static final long SILENCE_TIMEOUT_MS = 3000;
43
- private boolean shouldClearOnNextResult = false;
44
- private StringBuilder accumulatedText = new StringBuilder();
45
- private String lastSentPartial = "";
46
- private boolean isInSpeech = false;
47
- private long lastSpeechTime = 0;
48
- private static final long SPEECH_PAUSE_THRESHOLD = 1500;
49
- private float lastRmsValue = 0;
50
- private static final float RMS_THRESHOLD = 2.0f;
51
- private boolean hasRealSpeech = false;
52
- private String pendingPartialText = "";
53
- private long lastPartialTime = 0;
54
-
55
- // Métodos de permisos (sin cambios)
56
- @PluginMethod
57
- public void checkPermission(PluginCall call) {
58
- PermissionState state = getPermissionState("microphone");
59
- JSObject result = new JSObject();
60
- result.put("granted", state == PermissionState.GRANTED);
61
- result.put("status",
62
- state == PermissionState.GRANTED ? "granted" :
63
- state == PermissionState.DENIED ? "denied" : "prompt"
64
- );
65
- result.put("details", "Android checkPermission");
66
- result.put("errorMessage", "");
67
- call.resolve(result);
68
- }
69
47
 
70
48
  @PluginMethod
71
- public void requestPermission(PluginCall call) {
72
- if (getPermissionState("microphone") == PermissionState.GRANTED) {
73
- JSObject result = new JSObject();
74
- result.put("granted", true);
75
- result.put("status", "granted");
76
- result.put("details", "Android permission already granted");
77
- result.put("errorMessage", "");
78
- call.resolve(result);
79
- return;
49
+ public void checkPermission(PluginCall call) {
50
+ PermissionState mic = getPermissionState("microphone");
51
+
52
+ boolean granted = mic == PermissionState.GRANTED;
53
+
54
+ JSObject ret = new JSObject();
55
+ ret.put("granted", granted);
56
+ ret.put("status", granted ? "granted" : "prompt");
57
+ ret.put("details", "Android checkPermission");
58
+ ret.put("errorMessage", "");
59
+
60
+ call.resolve(ret);
80
61
  }
81
- requestPermissionForAlias("microphone", call, "permissionCallback");
82
- }
83
62
 
84
- @PluginMethod
85
- public void checkRequestPermission(PluginCall call) {
86
- PermissionState state = getPermissionState("microphone");
87
- if (state == PermissionState.GRANTED) {
88
- JSObject result = new JSObject();
89
- result.put("granted", true);
90
- result.put("status", "granted");
91
- result.put("details", "Android microphone permission already granted");
92
- result.put("errorMessage", "");
93
- call.resolve(result);
94
- return;
63
+ @PluginMethod
64
+ public void requestPermission(PluginCall call) {
65
+ if (getPermissionState("microphone") == PermissionState.GRANTED) {
66
+ JSObject ret = new JSObject();
67
+ ret.put("granted", true);
68
+ ret.put("status", "granted");
69
+ ret.put("details", "Android permission already granted");
70
+ ret.put("errorMessage", "");
71
+ call.resolve(ret);
72
+ return;
73
+ }
74
+
75
+ requestPermissionForAlias("microphone", call, "permissionCallback");
95
76
  }
96
- requestPermissionForAlias("microphone", call, "permissionCallback");
97
- }
98
77
 
99
- @PermissionCallback
100
- private void permissionCallback(PluginCall call) {
101
- boolean granted = getPermissionState("microphone") == PermissionState.GRANTED;
102
- JSObject result = new JSObject();
103
- result.put("granted", granted);
104
- result.put("status", granted ? "granted" : "denied");
105
- result.put("details", "Android permission callback");
106
- result.put("errorMessage", granted ? "" : "User denied microphone permission");
107
- call.resolve(result);
108
- }
78
+ @PluginMethod
79
+ public void checkRequestPermission(PluginCall call) {
80
+ if (getPermissionState("microphone") == PermissionState.GRANTED) {
81
+ JSObject ret = new JSObject();
82
+ ret.put("granted", true);
83
+ ret.put("status", "granted");
84
+ ret.put("details", "Android microphone permission already granted");
85
+ ret.put("errorMessage", "");
86
+ call.resolve(ret);
87
+ } else {
88
+ requestPermission(call);
89
+ }
90
+ }
109
91
 
110
- @PluginMethod
111
- public void startListening(PluginCall call) {
112
- String lang = call.getString("lang", "es-MX");
113
-
114
- if (delayedStartRunnable != null) {
115
- getActivity().runOnUiThread(() -> {
116
- getActivity()
117
- .getWindow()
118
- .getDecorView()
119
- .removeCallbacks(delayedStartRunnable);
120
- });
121
- delayedStartRunnable = null;
92
+ @PermissionCallback
93
+ public void permissionCallback(PluginCall call) {
94
+ boolean granted = getPermissionState("microphone") == PermissionState.GRANTED;
95
+
96
+ JSObject ret = new JSObject();
97
+ ret.put("granted", granted);
98
+ ret.put("status", granted ? "granted" : "denied");
99
+ ret.put("details", "Android permission callback");
100
+ ret.put("errorMessage", granted ? "" : "User denied microphone permission");
101
+
102
+ call.resolve(ret);
122
103
  }
123
104
 
124
- long now = System.currentTimeMillis();
125
- long diff = now - lastStopTime;
105
+ @PluginMethod
106
+ public void startListening(PluginCall call) {
107
+ String lang = call.getString("lang", "es-MX");
126
108
 
127
- if (diff < RESTART_COOLDOWN_MS) {
128
- long delay = RESTART_COOLDOWN_MS - diff;
129
- delayedStartRunnable = () -> {
130
- if (!isDestroyed) {
131
- startListeningInternal(call, lang);
132
- } else {
133
- call.reject("Listening was cancelled");
134
- }
109
+ if (delayedStartRunnable != null) {
110
+ handler.removeCallbacks(delayedStartRunnable);
135
111
  delayedStartRunnable = null;
136
- };
137
-
138
- getActivity().runOnUiThread(() -> {
139
- getActivity()
140
- .getWindow()
141
- .getDecorView()
142
- .postDelayed(delayedStartRunnable, delay);
143
- });
144
- return;
145
- }
112
+ }
146
113
 
147
- startListeningInternal(call, lang);
148
- }
114
+ long now = System.currentTimeMillis();
115
+ long diff = now - lastStopTime;
149
116
 
150
- private void startListeningInternal(PluginCall call, String lang) {
151
- // Solo limpiar si es un inicio nuevo, no si es continuación
152
- if (!isListening) {
153
- accumulatedText.setLength(0);
154
- lastSentPartial = "";
155
- hasRealSpeech = false;
156
- pendingPartialText = "";
157
- // CORRECCIÓN: Asegurar que la bandera esté en false al iniciar
158
- shouldClearOnNextResult = false;
159
- } else {
160
- // Si ya estaba escuchando, NO limpiar el texto acumulado
161
- // y ASEGURAR que la bandera esté en false
162
- shouldClearOnNextResult = false;
163
- }
117
+ if (diff < restartCooldownMs) {
118
+ long delay = restartCooldownMs - diff;
119
+
120
+ delayedStartRunnable = () -> {
121
+ if (!isDestroyed) {
122
+ getActivity().runOnUiThread(() -> startListeningInternal(call, lang));
123
+ } else {
124
+ call.reject("Listening was cancelled");
125
+ }
126
+ };
164
127
 
165
- isDestroyed = false;
166
- isListening = true;
167
- isInSpeech = false;
168
- lastRmsValue = 0;
128
+ handler.postDelayed(delayedStartRunnable, delay);
129
+ return;
130
+ }
169
131
 
170
- if (getPermissionState("microphone") != PermissionState.GRANTED) {
171
- call.reject("Microphone permission not granted");
172
- return;
132
+ getActivity().runOnUiThread(() -> startListeningInternal(call, lang));
173
133
  }
174
134
 
175
- if (!SpeechRecognizer.isRecognitionAvailable(getContext())) {
176
- call.reject("Speech recognition not available");
177
- return;
178
- }
135
+ private void startListeningInternal(PluginCall call, String lang) {
136
+ if (getActivity() == null) {
137
+ call.reject("Activity is null");
138
+ return;
139
+ }
140
+
141
+ isDestroyed = false;
142
+ isListening = true;
143
+
144
+ if (getPermissionState("microphone") != PermissionState.GRANTED) {
145
+ call.reject("Microphone permission not granted");
146
+ return;
147
+ }
179
148
 
180
- if (speechRecognizer != null) {
181
149
  stopSpeech();
182
- }
183
150
 
184
- // Notificar inicio con texto acumulado (si hay)
185
- if (accumulatedText.length() > 0) {
186
- notifyListeners(
187
- "partialResult",
188
- new JSObject()
189
- .put("text", accumulatedText.toString())
190
- .put("isFinal", false)
191
- .put("hasSpeech", true)
151
+ if (!SpeechRecognizer.isRecognitionAvailable(getActivity())) {
152
+ call.reject("Speech recognizer not available");
153
+ return;
154
+ }
155
+
156
+ speechRecognizer = SpeechRecognizer.createSpeechRecognizer(getActivity());
157
+ speechRecognizer.setRecognitionListener(this);
158
+
159
+ recognizerIntent = new Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH);
160
+ recognizerIntent.putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL,
161
+ RecognizerIntent.LANGUAGE_MODEL_FREE_FORM);
162
+ recognizerIntent.putExtra(RecognizerIntent.EXTRA_PARTIAL_RESULTS, true);
163
+ recognizerIntent.putExtra(RecognizerIntent.EXTRA_LANGUAGE, lang);
164
+ recognizerIntent.putExtra(
165
+ RecognizerIntent.EXTRA_CALLING_PACKAGE,
166
+ getActivity().getPackageName()
192
167
  );
193
- } else {
194
- // Enviar evento vacío para indicar que está listo pero no hay habla
195
- notifyListeners(
196
- "partialResult",
197
- new JSObject()
198
- .put("text", "")
199
- .put("isFinal", false)
200
- .put("hasSpeech", false)
168
+ recognizerIntent.putExtra(
169
+ RecognizerIntent.EXTRA_PREFER_OFFLINE,
170
+ true
201
171
  );
202
- }
203
172
 
204
- this.currentCall = call;
205
-
206
- getActivity().runOnUiThread(() -> {
207
- try {
208
- speechRecognizer = SpeechRecognizer.createSpeechRecognizer(getActivity());
209
- speechRecognizer.setRecognitionListener(new RecognitionListener() {
210
-
211
- @Override
212
- public void onReadyForSpeech(Bundle params) {
213
- cancelSilenceTimeout();
214
- scheduleSilenceTimeout();
215
- // Indicar que está listo pero no hay habla aún
216
- if (!hasRealSpeech && accumulatedText.length() == 0) {
217
- notifyListeners(
218
- "partialResult",
219
- new JSObject()
220
- .put("text", "")
221
- .put("isFinal", false)
222
- .put("hasSpeech", false)
223
- );
224
- }
225
- }
173
+ speechRecognizer.startListening(recognizerIntent);
226
174
 
227
- @Override
228
- public void onBeginningOfSpeech() {
229
- isInSpeech = true;
230
- hasRealSpeech = true;
231
- lastSpeechTime = System.currentTimeMillis();
232
- cancelSilenceTimeout();
233
-
234
- // Notificar que comenzó el habla
235
- notifyListeners(
236
- "partialResult",
237
- new JSObject()
238
- .put("text", accumulatedText.length() > 0 ? accumulatedText.toString() : "")
239
- .put("isFinal", false)
240
- .put("hasSpeech", true)
241
- );
242
- }
175
+ call.resolve();
176
+ }
243
177
 
244
- @Override
245
- public void onRmsChanged(float rmsdB) {
246
- lastRmsValue = rmsdB;
247
-
248
- // Detectar si hay actividad de voz real
249
- if (rmsdB > RMS_THRESHOLD) {
250
- isInSpeech = true;
251
- hasRealSpeech = true;
252
- lastSpeechTime = System.currentTimeMillis();
253
- cancelSilenceTimeout();
254
- scheduleSilenceTimeout();
255
- } else if (rmsdB < 0.5f && !isInSpeech) {
256
- // Muy bajo RMS, probablemente silencio
257
- // No enviar eventos de habla si no hay texto
258
- if (!hasRealSpeech && pendingPartialText.isEmpty()) {
259
- notifyListeners(
260
- "partialResult",
261
- new JSObject()
262
- .put("text", "")
263
- .put("isFinal", false)
264
- .put("hasSpeech", false)
265
- );
266
- }
267
- }
268
- }
178
+ @PluginMethod
179
+ public void stopListening(PluginCall call) {
180
+ isListening = false;
181
+ isDestroyed = true;
269
182
 
270
- @Override
271
- public void onBufferReceived(byte[] buffer) {}
272
-
273
- @Override
274
- public void onEndOfSpeech() {
275
- isInSpeech = false;
276
- // Programar timeout más corto para pausas entre palabras
277
- cancelSilenceTimeout();
278
- getActivity()
279
- .getWindow()
280
- .getDecorView()
281
- .postDelayed(() -> {
282
- if (!isInSpeech && isListening && !isDestroyed) {
283
- scheduleSilenceTimeout();
284
- }
285
- }, 500);
286
- }
183
+ if (delayedStartRunnable != null) {
184
+ handler.removeCallbacks(delayedStartRunnable);
185
+ delayedStartRunnable = null;
186
+ }
287
187
 
288
- @Override
289
- public void onError(int error) {
290
- if (isDestroyed || !isListening) return;
291
-
292
- // Manejo diferente para cada tipo de error
293
- switch (error) {
294
- case SpeechRecognizer.ERROR_NO_MATCH:
295
- // No se reconoció voz - no es un error real
296
- if (hasRealSpeech) {
297
- // Había habla pero no se reconoció, mantener estado
298
- restartListeningQuietly();
299
- } else {
300
- // Nunca hubo habla, mantener estado silencioso
301
- notifyListeners(
302
- "partialResult",
303
- new JSObject()
304
- .put("text", "")
305
- .put("isFinal", false)
306
- .put("hasSpeech", false)
307
- );
308
- restartListeningQuietly();
309
- }
310
- break;
311
- case SpeechRecognizer.ERROR_SPEECH_TIMEOUT:
312
- // Timeout de habla - reiniciar silenciosamente
313
- restartListeningQuietly();
314
- break;
315
- case SpeechRecognizer.ERROR_RECOGNIZER_BUSY:
316
- // Esperar y reintentar
317
- getActivity()
318
- .getWindow()
319
- .getDecorView()
320
- .postDelayed(() -> restartListeningAfterError(), 300);
321
- break;
322
- default:
323
- // Otros errores, intentar reiniciar
324
- restartListeningAfterError();
325
- break;
326
- }
327
- }
188
+ stopSpeech();
189
+ call.resolve();
190
+ }
328
191
 
329
- @Override
330
- public void onResults(Bundle results) {
331
- // CORRECCIÓN: Verificar la bandera ANTES de procesar
332
- if (shouldClearOnNextResult) {
333
- android.util.Log.d("CapacitorMic", "Limpiando texto por shouldClearOnNextResult");
334
- shouldClearOnNextResult = false;
335
- accumulatedText.setLength(0);
336
- lastSentPartial = "";
337
- hasRealSpeech = false;
338
- pendingPartialText = "";
339
- restartListeningQuietly();
340
- return;
341
- }
342
-
343
- if (!isListening || isDestroyed) return;
344
-
345
- ArrayList<String> matches =
346
- results.getStringArrayList(SpeechRecognizer.RESULTS_RECOGNITION);
347
-
348
- if (matches != null && !matches.isEmpty()) {
349
- String finalText = matches.get(0);
350
-
351
- // Solo procesar si hay texto real
352
- if (finalText != null && !finalText.trim().isEmpty()) {
353
- // Actualizar texto acumulado
354
- if (!accumulatedText.toString().endsWith(finalText)) {
355
- // Agregar espacio si hay texto previo
356
- if (accumulatedText.length() > 0 && !accumulatedText.toString().endsWith(" ")) {
357
- accumulatedText.append(" ");
358
- }
359
- accumulatedText.append(finalText);
360
- }
361
-
362
- // Enviar resultado final
363
- notifyListeners(
364
- "partialResult",
365
- new JSObject()
366
- .put("text", accumulatedText.toString())
367
- .put("isFinal", true)
368
- .put("hasSpeech", true)
369
- );
370
-
371
- lastSentPartial = accumulatedText.toString();
372
- pendingPartialText = "";
373
- }
374
- } else {
375
- // No hay resultados - mantener estado actual
376
- if (accumulatedText.length() > 0) {
377
- notifyListeners(
378
- "partialResult",
379
- new JSObject()
380
- .put("text", accumulatedText.toString())
381
- .put("isFinal", false)
382
- .put("hasSpeech", true)
383
- );
384
- }
385
- }
386
-
387
- // Reiniciar para continuar escuchando
388
- restartListeningQuietly();
389
- }
192
+ @PluginMethod
193
+ public void restartListening(PluginCall call) {
194
+ String lang = call.getString("lang", "es-MX");
390
195
 
391
- @Override
392
- public void onPartialResults(Bundle partialResults) {
393
- if (!isListening || isDestroyed) return;
394
-
395
- ArrayList<String> matches =
396
- partialResults.getStringArrayList(SpeechRecognizer.RESULTS_RECOGNITION);
397
-
398
- if (matches != null && !matches.isEmpty()) {
399
- String partialText = matches.get(0);
400
- lastPartialTime = System.currentTimeMillis();
401
-
402
- // Filtrar resultados vacíos o muy cortos
403
- if (partialText != null && !partialText.trim().isEmpty() && partialText.length() > 1) {
404
- pendingPartialText = partialText;
405
- hasRealSpeech = true;
406
-
407
- // Construir texto completo: acumulado + nuevo parcial
408
- String currentText = accumulatedText.toString();
409
- String displayText;
410
-
411
- if (currentText.isEmpty()) {
412
- displayText = partialText;
413
- } else {
414
- // Si el texto parcial ya está contenido en el acumulado, usar solo acumulado
415
- if (currentText.contains(partialText)) {
416
- displayText = currentText;
417
- } else {
418
- // Sino, mostrar acumulado + nuevo
419
- displayText = currentText + " " + partialText;
420
- }
421
- }
422
-
423
- // Solo enviar si es diferente del último enviado
424
- if (!displayText.equals(lastSentPartial)) {
425
- notifyListeners(
426
- "partialResult",
427
- new JSObject()
428
- .put("text", displayText)
429
- .put("isFinal", false)
430
- .put("hasSpeech", true)
431
- );
432
- lastSentPartial = displayText;
433
- }
434
- } else if (hasRealSpeech) {
435
- // Hay habla previa pero el parcial está vacío
436
- // Mantener el último texto enviado
437
- notifyListeners(
438
- "partialResult",
439
- new JSObject()
440
- .put("text", accumulatedText.toString())
441
- .put("isFinal", false)
442
- .put("hasSpeech", true)
443
- );
444
- } else {
445
- // Nunca ha habido habla, enviar estado silencioso
446
- notifyListeners(
447
- "partialResult",
448
- new JSObject()
449
- .put("text", "")
450
- .put("isFinal", false)
451
- .put("hasSpeech", false)
452
- );
453
- }
454
-
455
- // Reiniciar timeout cuando recibimos resultados
456
- cancelSilenceTimeout();
457
- scheduleSilenceTimeout();
458
- }
459
- }
196
+ isListening = false;
197
+ isDestroyed = true;
460
198
 
461
- @Override
462
- public void onEvent(int eventType, Bundle params) {}
463
- });
464
-
465
- speechIntent = new Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH);
466
- speechIntent.putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL, RecognizerIntent.LANGUAGE_MODEL_FREE_FORM);
467
- speechIntent.putExtra(RecognizerIntent.EXTRA_LANGUAGE, lang);
468
- speechIntent.putExtra(RecognizerIntent.EXTRA_PARTIAL_RESULTS, true);
469
- speechIntent.putExtra(RecognizerIntent.EXTRA_MAX_RESULTS, 1);
470
- // Configuración para evitar timeout muy rápidos
471
- speechIntent.putExtra(RecognizerIntent.EXTRA_SPEECH_INPUT_MINIMUM_LENGTH_MILLIS, 10000);
472
- speechIntent.putExtra(RecognizerIntent.EXTRA_SPEECH_INPUT_COMPLETE_SILENCE_LENGTH_MILLIS, 4000);
473
- speechIntent.putExtra(RecognizerIntent.EXTRA_SPEECH_INPUT_POSSIBLY_COMPLETE_SILENCE_LENGTH_MILLIS, 3000);
474
-
475
- speechRecognizer.startListening(speechIntent);
476
- } catch (Exception e) {
477
- if (currentCall != null) {
478
- currentCall.reject(e.getMessage());
479
- currentCall = null;
480
- }
481
- }
482
- });
483
- }
199
+ stopSpeech();
200
+
201
+ handler.postDelayed(() -> {
202
+ isDestroyed = false;
203
+ isListening = true;
204
+ getActivity().runOnUiThread(() -> startListeningInternal(call, lang));
205
+ }, 250);
484
206
 
485
- private void restartListeningQuietly() {
486
- if (isDestroyed || !isListening || speechRecognizer == null || speechIntent == null) {
487
- return;
488
207
  }
489
208
 
490
- // CORRECCIÓN: Asegurar que NO se limpie el texto en reinicios automáticos
491
- shouldClearOnNextResult = false;
492
-
493
- getActivity().runOnUiThread(() -> {
494
- try {
495
- // Pequeña pausa antes de reiniciar
496
- getActivity()
497
- .getWindow()
498
- .getDecorView()
499
- .postDelayed(() -> {
500
- if (!isDestroyed && isListening && speechRecognizer != null) {
501
- try {
502
- speechRecognizer.startListening(speechIntent);
503
- } catch (Exception ignored) {}
504
- }
505
- }, 100);
506
- } catch (Exception ignored) {}
507
- });
508
- }
209
+ private void stopSpeech() {
210
+ lastStopTime = System.currentTimeMillis();
509
211
 
510
- private void restartListeningAfterError() {
511
- if (isDestroyed || !isListening) return;
512
-
513
- // CORRECCIÓN: Asegurar que NO se limpie el texto en reinicios por error
514
- shouldClearOnNextResult = false;
515
-
516
- getActivity().runOnUiThread(() -> {
517
- if (!isDestroyed && isListening && speechRecognizer != null && speechIntent != null) {
518
- try {
519
- // Pausa más larga después de un error
520
- getActivity()
521
- .getWindow()
522
- .getDecorView()
523
- .postDelayed(() -> {
524
- if (!isDestroyed && isListening && speechRecognizer != null) {
525
- try {
526
- speechRecognizer.startListening(speechIntent);
527
- } catch (Exception ignored) {}
528
- }
529
- }, 800);
530
- } catch (Exception ignored) {}
212
+ if (speechRecognizer != null) {
213
+ speechRecognizer.cancel();
214
+ speechRecognizer.destroy();
215
+ speechRecognizer = null;
531
216
  }
532
- });
533
- }
217
+ }
218
+
219
+ private void restartRecognition() {
220
+ if (!isListening || isDestroyed) return;
221
+
222
+ stopSpeech();
223
+
224
+ handler.postDelayed(() -> {
225
+ if (getActivity() == null) return;
534
226
 
535
- private void scheduleSilenceTimeout() {
536
- cancelSilenceTimeout();
537
-
538
- silenceRunnable = () -> {
539
- if (!isDestroyed && isListening) {
540
- // Silencio prolongado detectado
541
- // Si nunca hubo habla, mantener estado silencioso
542
- if (!hasRealSpeech) {
543
- notifyListeners(
544
- "partialResult",
545
- new JSObject()
546
- .put("text", "")
547
- .put("isFinal", false)
548
- .put("hasSpeech", false)
549
- );
227
+ if (isListening && !isDestroyed && recognizerIntent != null) {
228
+ speechRecognizer = SpeechRecognizer.createSpeechRecognizer(getActivity());
229
+ speechRecognizer.setRecognitionListener(this);
230
+ speechRecognizer.startListening(recognizerIntent);
550
231
  }
551
- // Reiniciar el reconocimiento
552
- restartListeningQuietly();
232
+ }, 200);
233
+ }
234
+
235
+ @Override public void onResults(android.os.Bundle results) {
236
+ handleResults(results, true);
553
237
  }
554
- };
555
238
 
556
- getActivity()
557
- .getWindow()
558
- .getDecorView()
559
- .postDelayed(silenceRunnable, SILENCE_TIMEOUT_MS);
560
- }
239
+ @Override public void onPartialResults(android.os.Bundle results) {
240
+ handleResults(results, false);
241
+ }
561
242
 
562
- private void cancelSilenceTimeout() {
563
- if (silenceRunnable != null) {
564
- getActivity().getWindow().getDecorView().removeCallbacks(silenceRunnable);
565
- silenceRunnable = null;
566
- }
567
- }
243
+ private void handleResults(android.os.Bundle results, boolean isFinal) {
244
+ if (!isListening || isDestroyed) return;
568
245
 
569
- @PluginMethod
570
- public void restartListening(PluginCall call) {
571
- String lang = call.getString("lang", "es-MX");
572
-
573
- // CORRECCIÓN: SOLO aquí activamos la bandera para limpiar
574
- android.util.Log.d("CapacitorMic", "restartListening llamado - activando shouldClearOnNextResult");
575
- shouldClearOnNextResult = true;
576
-
577
- // Limpiar inmediatamente el texto acumulado
578
- accumulatedText.setLength(0);
579
- lastSentPartial = "";
580
- hasRealSpeech = false;
581
- pendingPartialText = "";
582
-
583
- // Notificar limpieza (sin habla)
584
- notifyListeners(
585
- "partialResult",
586
- new JSObject()
587
- .put("text", "")
588
- .put("isFinal", false)
589
- .put("hasSpeech", false)
590
- );
591
-
592
- // Limpiar el estado actual
593
- isListening = false;
594
-
595
- if (delayedStartRunnable != null) {
596
- getActivity().runOnUiThread(() -> {
597
- getActivity()
598
- .getWindow()
599
- .getDecorView()
600
- .removeCallbacks(delayedStartRunnable);
601
- });
602
- delayedStartRunnable = null;
603
- }
246
+ if (results == null) return;
604
247
 
605
- // Parar y reiniciar
606
- stopSpeech();
248
+ ArrayList<String> matches =
249
+ results.getStringArrayList(SpeechRecognizer.RESULTS_RECOGNITION);
607
250
 
608
- getActivity().runOnUiThread(() -> {
609
- getActivity()
610
- .getWindow()
611
- .getDecorView()
612
- .postDelayed(() -> {
613
- isDestroyed = false;
614
- isListening = true;
615
- speechIntent = null;
251
+ if (matches == null || matches.isEmpty()) return;
616
252
 
617
- // Comenzar de nuevo
618
- startListeningInternal(call, lang);
619
- }, 300);
620
- });
253
+ String text = matches.get(0);
621
254
 
622
- call.resolve();
623
- }
255
+ JSObject data = new JSObject();
256
+ data.put("text", text);
257
+ data.put("isFinal", isFinal);
624
258
 
625
- @PluginMethod
626
- public void stopListening(PluginCall call) {
627
- isListening = false;
628
- isDestroyed = true;
629
-
630
- if (delayedStartRunnable != null) {
631
- getActivity().runOnUiThread(() -> {
632
- getActivity()
633
- .getWindow()
634
- .getDecorView()
635
- .removeCallbacks(delayedStartRunnable);
636
- });
637
- delayedStartRunnable = null;
638
- }
259
+ notifyListeners("partialResult", data);
639
260
 
640
- cancelSilenceTimeout();
641
- currentCall = null;
642
- stopSpeech();
643
-
644
- // Enviar resultado final con todo lo acumulado
645
- if (accumulatedText.length() > 0) {
646
- notifyListeners(
647
- "partialResult",
648
- new JSObject()
649
- .put("text", accumulatedText.toString())
650
- .put("isFinal", true)
651
- .put("hasSpeech", true)
652
- );
653
- }
261
+ if (isFinal) {
262
+ handler.postDelayed(this::restartRecognition, 100);
263
+ }
264
+ }
654
265
 
655
- call.resolve();
656
- }
266
+ @Override public void onError(int error) {
657
267
 
658
- private void stopSpeech() {
659
- try {
660
- lastStopTime = System.currentTimeMillis();
661
- cancelSilenceTimeout();
268
+ if (!isListening || isDestroyed) return;
662
269
 
663
- if (speechRecognizer != null) {
664
- speechRecognizer.cancel();
665
- speechRecognizer.setRecognitionListener(null);
270
+ if (error == SpeechRecognizer.ERROR_RECOGNIZER_BUSY) {
271
+ handler.postDelayed(this::restartRecognition, 300);
272
+ return;
273
+ }
666
274
 
667
- final SpeechRecognizer recognizerToDestroy = speechRecognizer;
668
- speechRecognizer = null;
275
+ // Retry on timeout / no match
276
+ if (error == SpeechRecognizer.ERROR_NO_MATCH ||
277
+ error == SpeechRecognizer.ERROR_SPEECH_TIMEOUT) {
669
278
 
670
- getActivity().runOnUiThread(() -> {
671
- getActivity()
672
- .getWindow()
673
- .getDecorView()
674
- .postDelayed(() -> {
675
- try {
676
- recognizerToDestroy.destroy();
677
- } catch (Exception ignored) {}
678
- }, 100);
679
- });
279
+ handler.postDelayed(this::restartRecognition, 100);
280
+ return;
281
+ }
282
+
283
+ stopSpeech();
680
284
  }
681
- } catch (Exception ignored) {}
682
- }
683
285
 
684
- @Override
685
- protected void handleOnDestroy() {
686
- isDestroyed = true;
687
- isListening = false;
688
-
689
- if (delayedStartRunnable != null) {
690
- getActivity().runOnUiThread(() -> {
691
- getActivity()
692
- .getWindow()
693
- .getDecorView()
694
- .removeCallbacks(delayedStartRunnable);
695
- });
696
- delayedStartRunnable = null;
697
- }
286
+ @Override
287
+ protected void handleOnDestroy() {
288
+ isDestroyed = true;
289
+ isListening = false;
290
+ stopSpeech();
291
+ }
698
292
 
699
- cancelSilenceTimeout();
700
- stopSpeech();
701
- }
293
+ // Unused callbacks
294
+ @Override public void onReadyForSpeech(android.os.Bundle params) {}
295
+ @Override public void onBeginningOfSpeech() {}
296
+ @Override public void onRmsChanged(float rmsdB) {}
297
+ @Override public void onBufferReceived(byte[] buffer) {}
298
+ @Override public void onEndOfSpeech() {}
299
+ @Override public void onEvent(int eventType, @Nullable android.os.Bundle params) {}
702
300
 
703
- private String mapError(int error) {
704
- switch (error) {
705
- case SpeechRecognizer.ERROR_AUDIO: return "Audio error";
706
- case SpeechRecognizer.ERROR_NETWORK: return "Network error";
707
- case SpeechRecognizer.ERROR_NO_MATCH: return "No speech recognized";
708
- case SpeechRecognizer.ERROR_SPEECH_TIMEOUT: return "Speech timeout";
709
- default: return "Unknown error";
710
- }
711
- }
712
301
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "capacitor-microphone",
3
- "version": "0.0.27",
3
+ "version": "0.0.29",
4
4
  "description": "plugin to use the microphone",
5
5
  "main": "dist/plugin.cjs.js",
6
6
  "module": "dist/esm/index.js",