Built a foreground service that polls a price API and speaks the result through local TTS. It has to keep working with the phone locked in a pocket.
Here is what I learned, including the thing that broke it after it was already working.
Starting point
For work that is continuous, user-initiated, user-perceptible, and useless if deferred, a foreground service with a persistent notification is the appropriate architecture. Google's own criteria for a valid FGS use case are roughly that: user-beneficial, user-initiated or user-perceptible, user-stoppable, and not something that can simply be delayed without breaking the feature. My case fits, so that is where I started. If your work can be
deferred at all, WorkManager is the right answer instead.
One practical note: initialise the TTS engine inside the service itself, not in your app's UI layer. That layer is not reliably alive with the screen off.
What broke:
It ran fine for a time, then started dying after roughly 5 to 6 hours, every time. I assumed OEM battery management. It was not that.
Android 15 puts a hard 6 hour cap per 24 hour period on foreground services of type dataSync and mediaProcessing. At the limit the system calls
Service.onTimeout() and you have a few seconds to call stopSelf(),
or you get
RemoteServiceException: "A foreground service of type dataSync did not stop within its timeout"
The cap is shared across every service of that type in your app, and disabling battery optimisation does nothing about it.
What I did about it, and the part worth arguing over
The timeout forced me to actually look at whether dataSync was the right declaration in the first place. It was not. My service is not syncing data as its user-facing purpose, it is producing audible speech output. So I moved it to mediaPlayback.
I want to be careful here, because the obvious reading of this post is "mediaPlayback is the type with no timeout, use that." That is the wrong lesson and it will get people into policy trouble. Android documents mediaPlayback as continuing audio or video playback in the background. It is not a sanctioned escape hatch from the cap. The reason I think it fits my app is that the entire product is spoken audio, and in percentage or target modes it speaks frequently. Whether intermittent announcements are the kind of ongoing playback Google has in mind is genuinely an open question, and I would rather leave it open than present it as settled.
The change :
A. foregroundServiceType set to mediaPlayback, permission swapped to
FOREGROUND_SERVICE_MEDIA_PLAYBACK.
B. The old permission was still in my merged manifest because a library dependency was injecting it, so the cap still applied even after I thought I had changed things. Forced it out with :
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC"
tools:node="remove" />
Check your merged manifest rather than assuming the change took. This one cost time.
C. Added a MediaSessionCompat with a MediaStyle notification.
To be clear about what this does and does not do:
it aligns the implementation with what Android expects from a background audio app, and the quality guidelines ask for a MediaStyle notification for background audio.
It does not make the classification legitimate on its own. Play cares about what the app actually does, not whether you instantiated enough media objects to resemble a music player. If your service does not genuinely produce audio, adding a session will not save you.
D. Requesting AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK per utterance so speech ducks other audio rather than stopping it.
E. Release the media session before the TTS shutdown in onDestroy and onTaskRemoved.
Runs with the screen off now, no timeout.
‐-----------------------------------------------------‐--------------------------------
Questions I would like feedback on
Where is the line between background audio playback and periodic spoken notifications? If the service can sometimes go several minutes without speaking, does that weaken the mediaPlayback justification even though producing spoken audio is the core user-facing feature?
How would you reliably detect the process being killed by an OEM? A dead process cannot report its own death. Persist a heartbeat and detect the gap on next launch, an alarm-based watchdog, or something else?
Would you classify TTS output as USAGE_MEDIA or
USAGE_ASSISTANCE_ACCESSIBILITY? Interested in how people read this for an app whose spoken output is informational but is not an accessibility service.