Skip to main content

lib/mob_background.ex

defmodule MobBackground do
  @moduledoc """
  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`.

  ## 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 two `host_requirements` the native build warns about on
  every `mix mob.deploy --native` of the host — an Android `<service>`
  declaration and the iOS `UIBackgroundModes` plist key. See the
  *Requirements* sections below; without them `keep_alive/0` starts nothing.

  ## 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.

  ## 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 calls
  `startForeground/2` with 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 post a visible notification.
  The notification appears in the status bar and notification tray with the
  app name and the text "Running in background". It has `IMPORTANCE_LOW` so
  it produces no sound or vibration. There is no API to hide it — this is
  an OS-level constraint designed to inform users when apps are running in
  the background.

  ### Requirements

  The `FOREGROUND_SERVICE` permissions are added automatically when the plugin
  is activated. The host `AndroidManifest.xml` must additionally declare the
  service inside `<application>` (a `<service>` subclass can't be auto-injected):

      <service android:name="io.mob.background.BeamForegroundService"
          android:exported="false"
          android:foregroundServiceType="dataSync" />

  The `BeamForegroundService` source ships in this package under
  `priv/native/android/BeamForegroundService.kt` — copy it into your app's
  host package (the build copies only the bridge automatically).

  ### 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.
  """

  @doc """
  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.
  """
  @spec keep_alive() :: :ok
  def keep_alive do
    :mob_background_nif.background_keep_alive()
  end

  @doc """
  Stops the keep-alive and allows the OS to suspend the app normally when it
  goes to background.
  """
  @spec stop() :: :ok
  def stop do
    :mob_background_nif.background_stop()
  end
end