capacitor-community / capacitor-community/speech-recognition
fix(android): stop() never resolves; stale recognizer after partialResults breaks restart
- Dominant language
- Java
- Stars
- 130
- Forks
- 69
- PR merge metrics
- No merged PRs in 30d
Description
**Plugin version:** `@capacitor-community/speech-recognition@7.0.1`
**Capacitor:** `@capacitor/core` / `@capacitor/android` **8.3.3** (also expected on 7.x)
**Platform:** Android API **35** — **Pixel Tablet API 35** emulator and physical tablet
**Speech options:** `partialResults: true`, `popup: false`, `language: 'en-US'`
**App type:** Capacitor hybrid app with per-field mic buttons on forms (not the system speech popup)
**Verified after patch:** manual stop works; repeated start/stop; restart after speaking with partial text in the field; no every-other-press failure.
### User-visible behavior (UI flow)
We use a **mic button** beside text inputs (forms/modals) with `partialResults: true`, `popup: false`.
**Scenario A — works on fresh launch**
1. Open app → open a form → tap mic once.
2. Green system mic icon appears; speech-to-text works.
**Scenario B — every-other press (even with long waits)**
1. Tap mic → stop (or let session end) → tap mic again.
2. Sometimes the **2nd tap** shows the button active briefly, then off; **no green mic**; no listening.
3. **3rd tap** may work again (1st ✓, 2nd ✗, 3rd ✓ …).
4. Waiting several minutes between taps does **not** fix it — not a simple debounce issue.
**Scenario C — breaks after speech (most common report)**
1. Tap mic → **speak** (partial text appears in the field) → stop or auto-end.
2. Tap mic again to start a new utterance.
3. **Button turns on ~0.5s then off**; recognition never really starts.
4. Manual stop on an **active** session often works; failure is tied to **prior partial results / ended session**.
**Scenario D — stuck “listening” UI**
1. Tap mic to start, then tap again quickly to stop (or stop during startup).
2. Button can stay in the “active” state while the **system mic icon** is off (or vice versa).
**Scenario E — stop() from JavaScript**
- `await SpeechRecognition.stop()` **never returns** on Android (plugin never calls `call.resolve()`).
### Logcat / native
- `onError` with codes **5** (`ERROR_CLIENT`), **7** (`ERROR_NO_MATCH`), **11** (e.g. server disconnect / unmapped).
- Debugger may bounce between `stop()` and Capacitor `MessageHandler` / bridge.
- Related reports: #124 (every-other / error 11), #122 (`stop()`), #118 (`popup: false` + partial results).
### Root cause (in `SpeechRecognition.java`)
1. **`stop()`** calls `stopListening()` but never **`call.resolve()`**.
2. **`stopListening()`** only calls `speechRecognizer.stopListening()` when `listening == true` — does not cancel/destroy the instance.
3. **`onEndOfSpeech()`** sets `listening = false` but leaves a live `SpeechRecognizer`; the next `stop()` destroys it and emits a **late `listeningState: stopped`**, killing the next session.
4. **`onError()`** calls **`call.reject()`** after `start()` already resolved when `partialResults: true`, which breaks the Capacitor bridge.
### Proposed fix
- Resolve `stop()` after teardown.
- On stop/error/end-of-speech: `cancel()` + `destroy()` recognizer, clear `previousPartialResults`.
- On error with `partialResults: true`: notify `listeningState: stopped`, do not reject an already-resolved `start()` call (treat ERROR_NO_MATCH / ERROR_SPEECH_TIMEOUT / ERROR_CLIENT as benign for partial mode).
### Patch Gist
Java-only `patch-package` fix (README + patch file):
https://gist.github.com/jaredburks/e69a9df0571a2937678b1324e2121122
Copy the `patches/` folder from the gist into your project root (next to `package.json`), add `"postinstall": "patch-package"`, then `npm install` and rebuild Android.
### Patch ZIP if you prefer
- [speech-recognition-android-fix.zip](https://github.com/user-attachments/files/28179782/speech-recognition-android-fix.zip)
Happy to open a PR if maintainers want this approach. Note: #124 discusses error 11 / destroy-on-every-stop; this patch destroys on session end/error/stop but avoids rejecting partial-mode starts — may complement PR #125 if that is still open.
### Workaround for app developers (until upstream merge)
1. Do not `await SpeechRecognition.stop()` indefinitely — use a timeout or fire-and-forget + short delay.
2. Serialize start/stop (no overlapping calls).
3. Register `listeningState` listeners **after** `start()` resolves, and ignore `stopped` events for ~600ms while calling `stop()` before the next `start()`.
4. Apply the Java patch via `patch-package` (see attached README).
Contributor guide
Research direction
Start in SpeechRecognition.java, tracing stop(), stopListening(), and the onEndOfSpeech() and onError() callbacks; review the related reports #124, #122, and #118 for lifecycle context. Reproduce the partialResults=true, popup=false start/stop and restart flows on Android, then verify that stop() resolves, recognizers are torn down, and repeated sessions do not emit stale state or break the bridge.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- android, java
- Domain
- mobile-dev
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 55/100