Background execution keep-alive — a Mob plugin.
Keeps the BEAM node running when the screen locks or the app is backgrounded.
On iOS this is a silent AVAudioEngine session; on Android it is a
foreground service. Opt-in: add the dependency and activate it in mob.exs.
Which plugin do I actually want?
The background/wake/notify/push quartet gets mixed up regularly. Four distinct concerns:
| I want to… | Plugin |
|---|---|
| Keep my app alive continuously while backgrounded | mob_background (this plugin) |
| Run a handler only when the OS fires a scheduler / silent push | mob_wake (device-side) |
| Schedule local notifications / register for push | mob_notify (device-side) |
| Send a push from my server | mob_push (server-side; no device code) |
mob_backgroundvsmob_wake: background is a keep-alive — the app stays running while backgrounded so your own code (an active upload, a walking tracker, a music player) can keep going. Wake is event-driven — the OS (or a silent push) fires a specific handler, it runs to completion, the platform reports done. They stack: an app can use both. Cost/benefit:mob_backgroundneeds an ongoing platform justification (audio on iOS, persistent notification on Android);mob_wakedoesn't — but doesn't run continuously either.- When in doubt: if you're doing episodic work triggered by an
external event,
mob_wake. If you're doing continuous work the user has explicitly kicked off,mob_background. If you're supporting both patterns in the same app, activate both.
Installation
# mix.exs
{:mob_background, "~> 0.1"}
# mob.exs
config :mob, :plugins, [:mob_background]
config :mob, :trusted_plugins, %{mob_background: "ed25519:<fingerprint>"}mix mob.plugin.trust mob_background records the fingerprint, then
mix mob.deploy --native.
This plugin has one host_requirement the native build warns about on
every mix mob.deploy --native of the host: the iOS UIBackgroundModes
plist key (see iOS → Requirements below). The plugin deliberately
doesn't add it to Info.plist: Apple rejects the mode in apps with no
audio feature, so declaring it is the app author's call. The Android
service class and its <service> declaration are added by the build.
Usage
# Keep the app alive when the screen locks (e.g. in mount/2):
MobBackground.keep_alive()
# Allow suspension again when background execution is no longer needed:
MobBackground.stop()keep_alive/0 is idempotent — safe to call multiple times.
status/0 is a read-only check that keep-alive can work in this host
and whether it is on; MobBackground.SelfTest uses it for
mix mob.selftest.
iOS — silent audio session
iOS suspends apps when the screen locks unless they hold an active background
execution mode. keep_alive/0 starts a silent AVAudioEngine looping a
zero-filled buffer with AVAudioSessionCategoryOptionMixWithOthers — the OS
sees an active audio session and keeps the process running, the user hears
nothing, and any music already playing is undisturbed.
Requirements
The app's Info.plist must declare the audio background mode:
<key>UIBackgroundModes</key>
<array>
<string>audio</string>
</array>This is included in all projects generated by mix mob.new. For Xcode
projects, add it under Signing & Capabilities → Background Modes →
Audio, AirPlay, and Picture in Picture.
Coexistence with Mob.Audio
Playback (Mob.Audio.play/3): both sides use MixWithOthers, so they
mix transparently. The silent buffer is inaudible alongside real audio.
Recording (Mob.Audio.start_recording/2): recording switches the global
AVAudioSession category to PlayAndRecord, which sends an interruption to
the keep-alive engine. The engine stops — but the recording itself holds an
active audio session, so the app stays alive for the duration of the
recording. When stop_recording/1 is called and the session is released, iOS
fires AVAudioSessionInterruptionTypeEnded and the keep-alive engine restarts
automatically. No Elixir code is needed to handle this transition.
The same automatic restart applies to phone calls and any other event that temporarily takes the audio session away from the app.
Known limitation — observer cleanup
Internally, the NIF registers an NSNotificationCenter observer for
AVAudioSessionInterruptionNotification to handle the recording restart
described above. When stop/0 is called, it removes that observer by the
token it stored, so it only removes its own observer.
Apple's stance
Apple permits the audio background mode for apps that legitimately use
audio. Mob apps that use Mob.Audio recording or playback qualify. Apple
will reject apps that declare this mode without any audio feature — do not
add UIBackgroundModes: [audio] to an app that has no audio functionality.
Android — foreground service
On Android the OS equivalent of iOS background execution is a foreground
service. keep_alive/0 starts BeamForegroundService, which starts in
the foreground with type dataSync and a low-priority persistent notification. The OS
will not kill a foreground service under memory pressure and will not
pause the process when the screen locks.
Visible notification (required by Android)
Android requires every foreground service to have a notification. It uses
the app name and the text "Running in background", with IMPORTANCE_LOW
so it makes no sound or vibration. There is no API to hide it.
On Android 13+ (API 33) the notification is only shown in the status bar
and drawer when the app holds POST_NOTIFICATIONS — declared and granted
at runtime (activate mob_notify and request :notifications). Without
it the foreground service still runs (verified on Android 15) but users
only see it in the system's running-apps list.
Requirements
The build adds everything automatically when the plugin is activated: the
FOREGROUND_SERVICE permissions, the BeamForegroundService class (copied
into the app's sources next to the bridge) and its declaration inside
<application>:
<service android:name="io.mob.background.BeamForegroundService"
android:exported="false"
android:foregroundServiceType="dataSync" />If your app has its own copy of BeamForegroundService.kt (from plugin
0.1.x, which asked you to copy it in) outside
io/mob/background/, delete it: the build's copy now provides the class
and two would not compile. A copy at io/mob/background/BeamForegroundService.kt
is overwritten by the build. A <service> you declared by hand is left
as is.
Stop behaviour
stop/0 sends ACTION_STOP to the service, which calls stopForeground
and stopSelf. The OS removes the notification immediately. If the BEAM
node goes silent (no incoming distribution traffic) the OS may still
eventually kill the process — keep_alive/0 prevents aggressive
background process killing but not an eventual idle OOM kill after many
hours of complete inactivity.
Android 15 time limit
With targetSdk 35+, Android allows a dataSync foreground service at most
6 hours per 24 hours while the app is in the background. When the limit is
reached the system calls the service's onTimeout, which logs a warning
(tag MobBackground) and stops the service, so the app does not crash. The
BEAM is not notified. Call keep_alive/0 again once the app is back in
the foreground.
Summary
Functions
Starts the keep-alive (silent audio session on iOS, foreground service on Android) so the OS does not suspend the app when the screen locks. Idempotent — safe to call more than once.
Reports, without changing anything, whether keep-alive can work in this host and whether it is on.
Stops the keep-alive and allows the OS to suspend the app normally when it goes to background.
Functions
@spec keep_alive() :: :ok
Starts the keep-alive (silent audio session on iOS, foreground service on Android) so the OS does not suspend the app when the screen locks. Idempotent — safe to call more than once.
Reports, without changing anything, whether keep-alive can work in this host and whether it is on.
:idle— ready; keep-alive is off.:running— keep-alive is on (iOS:keep_alive/0was called and notstop/0; Android: the foreground service is started).{:error, :no_audio_background_mode}— iOS:Info.plistlacksUIBackgroundModesaudio.{:error, :bridge_not_registered}— Android:MobBackgroundBridgenever registered with the NIF (MobPluginBootstrap.registerAlldidn't run, or a method lookup failed).{:error, :no_activity}— Android: the bridge never received an Activity, sokeep_alive/0would do nothing.{:error, :service_not_declared}/{:error, :service_not_data_sync}— Android: the app manifest has noBeamForegroundService<service>, or declares it withoutforegroundServiceType="dataSync".{:error, :no_jni_env},{:error, :status_failed},{:error, {:unknown_status, code}}— Android: the query itself failed.
keep_alive/0 and stop/0 take effect asynchronously on iOS, so a
status read right after either may not reflect it yet.
@spec stop() :: :ok
Stops the keep-alive and allows the OS to suspend the app normally when it goes to background.