Dev Investigation Notes
Keeping the microphone on in an Android app: a pre-implementation checklist for boot startup and offline SpeechRecognizer
This is a translated article. Read the original in Korean ↗
The plan was to move a personal voice assistant (voice-assistant), which listened for a wake word on a PC and read responses aloud, to an always-listening app on an Android tablet docked on a charger. Before implementing it, I asked for a list of "what an always-on microphone needs." But checking it against the official documentation showed that two items were wrong as written: automatic startup on boot and built-in speech recognition = local.
This article is a design-stage checklist. There are no results from testing on a physical device; all factual judgments rely on the official Android documentation (checked on 2026-09-23). The target is Android 14 and later (targetSdk 34 or higher), and local recognition assumes devices running API 31 or higher. Devices below API 31 have no on-device recognizer, so they are excluded from support for apps that require local recognition. The app architecture—separating the ears and mouth from the brain, and using MockBrain—is covered separately in Connecting Claude Code to an Android tablet: where do you start with a voice assistant app?.
Conclusion
Things to check before building an always-listening app:
| # | Item | Verdict |
|---|---|---|
| 1 | A service with android:foregroundServiceType="microphone" + declarations for RECORD_AUDIO, FOREGROUND_SERVICE, and FOREGROUND_SERVICE_MICROPHONE | Required (targetSdk 34 and higher require declarations for the type and its type-specific permission) |
| 2 | Start the service from a visible Activity after obtaining microphone permission | Required. RECORD_AUDIO is a "while-in-use" permission, so starting a new service from the background can be blocked |
| 3 | Use RECEIVE_BOOT_COMPLETED to start the recording service automatically right after boot | Not allowed. A BOOT_COMPLETED receiver cannot launch a microphone-type FGS (starting with Android 14). Design around opening the app once after boot |
| 4 | POST_NOTIFICATIONS | Not a prerequisite for starting an FGS. Denying it only changes where the notification appears. The FGS still needs a notification |
| 5 | Battery optimization exemption, charging dock, and WAKE_LOCK | Helpful, but no guarantee of uninterrupted operation. Measure prolonged operation with the screen locked yourself |
| 6 | Use SpeechRecognizer.createSpeechRecognizer() for STT | Does not guarantee local recognition. It may send audio to a remote server. If offline operation is required, explicitly use the on-device recognizer available in API 31+ |
| 7 | Do wake-word detection (continuous AudioRecord) and STT use the microphone at the same time? | No guarantee. For the first implementation, switch sequentially: "detect wake word → release AudioRecord → STT → TTS → resume wake-word detection" |
The basic on-device recognition setup looks like this (API 31 and higher).
val recognizer = if (SpeechRecognizer.isOnDeviceRecognitionAvailable(context)) {
SpeechRecognizer.createOnDeviceSpeechRecognizer(context)
} else {
// If local recognition is required, show an error here instead of silently falling back to the standard recognizer
null
}
Even if isOnDeviceRecognitionAvailable() returns true, that does not mean the Korean model is ready. On API 33, follow these steps to check the Korean model.
- Pass a recognition intent containing
EXTRA_LANGUAGE="ko-KR"torecognizer.checkRecognitionSupport(intent, executor, callback). The callback'sRecognitionSupportprovides the list of installed on-device languages (installedOnDeviceLanguages) - If Korean is missing from the list, request a model download with
recognizer.triggerModelDownload(intent), then repeat step 1 after it finishes. If the download has not finished or has failed, show a "한국어 모델 없음" (Korean model unavailable) error - The final test is to try recognition with the network actually turned off
RecognizerIntent.EXTRA_PREFER_OFFLINE=true may be ignored depending on the implementation, so it must not be used as a guarantee of offline operation. (SpeechRecognizer, RecognizerIntent)
Situation
The planned app architecture was this: a Foreground Service (a service that keeps running while showing the user a notification) holds the microphone open. When an openWakeWord wake-word model (the same hey_jarvis used in the PC version) detects "Hey Jarvis," Android's SpeechRecognizer converts speech to text, and TextToSpeech reads the response aloud. The part that generates responses is initially a fake implementation called MockBrain.
The device would be a dedicated tablet left plugged into a charger, so the assumption was that most battery issues would disappear.
Symptom
The preparation list received before implementation looked like this.
- A
microphone-type Foreground Service + a persistent notification ("explicit type required on Android 14+") - Permissions:
RECORD_AUDIO,FOREGROUND_SERVICE,FOREGROUND_SERVICE_MICROPHONE,POST_NOTIFICATIONS - A battery optimization exemption, plus
WAKE_LOCKfor a dedicated docked device, makes Doze issues (the mode that puts devices to sleep when the screen is off) "virtually disappear" - Use
RECEIVE_BOOT_COMPLETEDfor automatic startup on boot - Use the built-in
SpeechRecognizerfor STT—"free and local. Offline operation is also possible with on-device recognition enabled"
The list makes it sound like a few declarations are all it takes. Build the app exactly this way, though, and recording may fail to start after boot, or recognition you believed was offline may actually send audio to a server.
Cause
The items on the list failed to distinguish "just declare it" from "it can be blocked depending on when you start it."
1. Automatic startup on boot. A microphone FGS that was started legitimately and continues recording after the app moves into the background is different from starting a new one in the background. RECORD_AUDIO is subject to while-in-use restrictions, and exemptions for starting an FGS in the background are separate from exemptions for microphone permission. An app targeting Android 14 or later can get a SecurityException if it launches a microphone FGS in violation of these conditions. At the moment the app receives the boot broadcast, it has no visible Activity. In fact, the Android 15 behavior changes documentation lists microphone among the FGS types that a BOOT_COMPLETED receiver cannot launch, and states that this restriction has been in place since Android 14. (FGS types, Background start restrictions, Android 15 behavior changes)
2. Built-in STT = local. The default createSpeechRecognizer() is not local-only. The official reference states that this recognizer may send audio to remote servers and is not designed for continuous recognition. Starting with API 31, on-device recognition must be checked with isOnDeviceRecognitionAvailable() and created separately with createOnDeviceSpeechRecognizer(). (SpeechRecognizer)
3. Two small corrections. POST_NOTIFICATIONS is permission to display notifications, not a prerequisite for starting an FGS. (Notification permission) Also, the microphone type itself already appears in Android 11's camera and microphone FGS requirements. What changed in Android 14 is that apps with targetSdk 34 or higher must declare the type and its type-specific permission. (Android 11 changes)
4. Missing from the list—simultaneous microphone use. Wake-word detection keeps the microphone open through AudioRecord, while SpeechRecognizer opens its own microphone input separately. Under the input-sharing policy introduced in Android 10, two ordinary apps cannot receive actual audio simultaneously, and one may receive silence. We cannot conclude that every combination of AudioRecord and a particular recognition service within the same app will fail, but there is no guarantee that they will work simultaneously, either. Audio focus arbitrates output, so it does not solve this problem. (Sharing audio input)
Fix
Revise the checklist as follows before implementation.
Startup flow
- Obtain the
RECORD_AUDIOruntime permission from the app screen (Activity) → start the service from that same screen and passServiceInfo.FOREGROUND_SERVICE_TYPE_MICROPHONEtostartForeground() - Make "open the app once to start listening" the default behavior after boot. Use the boot receiver only to show a notification prompting "눌러서 듣기 시작" (Tap to start listening)
- Tapping this notification to open an Activity and starting the service there does not run into the restriction. What is blocked is the boot receiver launching the microphone service directly; when the user taps and the service starts from a visible Activity, the app is in use and satisfies condition 2
- Create a notification because the FGS requires one. Obtaining
POST_NOTIFICATIONSis helpful, but its absence does not prevent the service from starting
Continuous listening
- For a battery optimization exemption, have the app request user approval through the
ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONSintent (requires theREQUEST_IGNORE_BATTERY_OPTIMIZATIONSpermission in the manifest), or direct the user to settings (ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS). The user decides whether to grant it - Leave it running on the charging dock, but use logs to check whether wake-word detection stays active with the screen locked, first for 1 hour and then for 8 hours. Record charging status, heat, and the time of any interruption together
STT
- Explicitly create an on-device recognizer and check the Korean model's status using steps 1–3 in the Conclusion
- If on-device recognition is unavailable in a mode that requires local recognition, show an error. Silently falling back to the standard recognizer creates the false impression that "it worked offline"
- Run recognition on the same sentences in airplane mode, tallying successful attempts and error codes separately
Microphone switching
- Use sequential switching as the initial baseline: detect wake word → stop and release
AudioRecordcapture → STT →MockBrainresponse and TTS → resume wake-word detection - Return to wake-word detection on every path through STT results, errors, and cancellation, and use a "recognition in progress" flag to prevent another wake-word detection from starting a new recognition session while one is already running
- Leave clipping of the first syllable between transitions as something to measure
- API 33's
RecognizerIntent.EXTRA_AUDIO_SOURCEalso provides a way to pass an already-open audio source to the recognizer, but recognizers that do not support it open their own microphone. First check whether the recognizer you use supports it (EXTRA_AUDIO_SOURCE)
Bonus — the limits of "it's a dedicated docked device, so battery isn't a concern"
Leaving it plugged into a charger eliminates battery drain as a problem. But issues 1–4 in this article (startup timing restrictions, the recognizer sending audio to a server, and microphone sharing) are unrelated to power. It is safest to read "dedicated docked device" as a condition that crosses off only the battery item.
Reporter's Notes — the list that looked like a few declarations would be enough
There has been no reproduction on a physical device yet. The four of them checked the preparation list against the official documentation, line by line.
RECEIVE_BOOT_COMPLETED for automatic startup on boot. Declare those and it should listen as soon as the power comes on. RECORD_AUDIO is only valid while the app is in use, so creating a new microphone service when the app isn't on screen can throw a SecurityException under the Android 14 conditions. And the list explicitly says a receiver handling the boot signal cannot launch a microphone-type service at all.SpeechRecognizer is Android's standard component for turning speech into text. It's free and runs on the device, so it should work offline too. createSpeechRecognizer() may send audio to a remote server. To keep it entirely on the device, we have to explicitly use createOnDeviceSpeechRecognizer(), available starting with API 31.checkRecognitionSupport(), and download it with triggerModelDownload() if it isn't. Then test it in airplane mode at the end.POST_NOTIFICATIONS. We need that before the service can start too, right? AudioRecord, and the recognizer opens its own microphone too. If both grab it at once, won't one get silence?AudioRecord when the wake word is detected, then return to wake-word detection when recognition finishes or fails.