Skip to content

Send new photos from iPhone with the app off screen - #1

Merged
PepoTechYT merged 1 commit into
mainfrom
ios-background-transfers
Sep 19, 2026
Merged

PepoTechYT merged 1 commit into
mainfrom
ios-background-transfers

Conversation

@PepoTechYT

@PepoTechYT PepoTechYT commented Sep 19, 2026 •

Copy link
Copy Markdown
Owner

En iPhone las fotos solo llegaban al PC con PepoConnect en pantalla. iOS suspende el proceso al salir, y no existe ninguna API que despierte una app suspendida al hacer una foto: PHPhotoLibraryChangeObserver solo entrega a un proceso vivo, las BGTask son oportunistas (minutos u horas) y BGContinuedProcessingTask de iOS 26 exige que el usuario toque algo en primer plano. La única salida es que el proceso no llegue a suspenderse.

Motor de segundo plano

ios/Runner/PepoBackground.swift:

  • KeepAlive — un bucle de 1 s de silencio con la sesión de audio en .playback + .mixWithOthers y sin now-playing. No se oye nada, no interrumpe la música de otras apps, no aparece indicador, y el motor Dart sigue corriendo igual que en primer plano: observador de fototeca, socket TLS de control y transferencias. Vigilante de 30 s más observadores de interrupción, cambio de ruta, reinicio de los servicios de medios y ciclo de vida (app y escena) para recuperar la sesión cuando una llamada u otra app se la lleva.
  • BackgroundHold — beginBackgroundTask mientras hay algo en vuelo, para que una transferencia que arranca justo al salir de pantalla termine.
  • PepoTasks — las dos BGTask registradas como red de seguridad para cuando el motor no corre, con timeout de 25 s si Dart no contesta.

UIBackgroundModes: audio, fetch, processing y BGTaskSchedulerPermittedIdentifiers. Ninguna de esas claves necesita entitlements, así que sobreviven a la refirma de AltStore o Sideloadly con un Apple ID gratuito.

Envío automático que no pierde nada

Mantener el proceso vivo no bastaba: el envío automático tenía tres agujeros propios.

  • MediaServer enviaba solo a quien estuviera conectado en ese instante, así que una foto con el PC dormido se descartaba y no se reintentaba nunca.
  • engine.setAutoSend se llamaba solo desde el interruptor de ajustes: tras reiniciar el proceso el interruptor decía «sí» y no se enviaba nada.
  • MediaSource toma una instantánea al arrancar, así que lo fotografiado con el proceso muerto no se notificaba jamás.

Ahora AutoSendQueue escribe cada pendiente a disco, lo entrega en cuanto el dispositivo es alcanzable y recuerda lo que ya salió; AppServices reaplica el ajuste en cada arranque y MediaServer.catchUp recoge lo que apareció mientras nadie escuchaba. Topes de edad, tamaño de cola y reintentos para que un móvil una semana sin red no inunde el PC. De paso queda conectado el gancho HEIC→JPEG (declarado pero nunca asignado) y se limpian las copias temporales que iOS exporta para poder enviar un original.

Bonjour/mDNS

El beacon UDP no puede correr en iPhone (broadcast necesita una autorización que Apple no da a cuentas gratuitas), así que un PC que se reiniciaba con otra IP desaparecía para siempre. BonjourDiscovery usa bonsoir, que ya estaba en pubspec.yaml sin usar, y anuncian las cinco plataformas. El manifiesto Flatpak necesitaba el nombre de Avahi en el bus de sistema, no en el de sesión.

Interfaz y CI

Los ajustes dicen la verdad en iPhone, con una fila de estado que muestra cuánto lleva vivo el proceso, y un aviso cuando el acceso a fotos está limitado a una selección (que oculta todas las fotos posteriores y dejaba el envío automático sin nada que enviar).

ci.yml gana un job ios en macos-15: el Swift no se compila en ningún otro sitio, y el job comprueba además que las claves de segundo plano siguen en el bundle construido. release.yml lo llama con ios: false porque ya compila iOS por su cuenta.

Comprobado

flutter analyze --fatal-infos, dart format, 191 tests de la app y 78 de pepo_core (cola de envío, MediaServer, motor iOS por canal simulado, mapeo de Bonjour, sección de ajustes), más un caso nuevo en integration_test/e2e_test.dart: foto con el PC desconectado → queda en cola → sale al reconectar.

El Swift solo lo compila este PR: no hay Mac en el equipo de desarrollo. La prueba en un iPhone real sigue pendiente (foto con el móvil bloqueado, vídeo largo, 30-60 min bloqueado, PC apagado y encendido, IP del PC cambiada, cerrar desde el selector de apps).

Lo que iOS sigue sin permitir

Cerrar la app desde el selector de apps mata el motor: las fotos se acumulan en la cola y salen al reabrirla, o antes si iOS concede una BGProcessingTask. Tras reiniciar el iPhone hay que abrir PepoConnect una vez. Y el motor gasta batería mientras está activo, igual que el servicio de Android.

On iPhone photos only ever reached the PC while PepoConnect was in front.
iOS suspends the process on the way out, and there is no API that wakes a
suspended app when a photo is taken: PHPhotoLibraryChangeObserver only
delivers to a running process, the BGTaskScheduler tasks run minutes to
hours later, and BGContinuedProcessingTask needs a foreground tap. The only
way a photo leaves on its own is for the process never to be suspended.

PepoBackground.swift keeps it alive with the audio background mode: one
second of silence on a loop, session .playback + .mixWithOthers, no
now-playing info. Nothing is audible, other apps keep their audio and no
indicator shows up, but the Dart engine keeps running exactly as it does in
the foreground. A watchdog and the interruption, route-change and
media-reset notifications win the session back when a call or another app
takes it. Transfers are wrapped in a beginBackgroundTask assertion, and
BGAppRefresh/BGProcessing are registered as the safety net for when the
engine is not running at all. None of those Info.plist keys needs an
entitlement, so they survive re-signing with a free Apple ID.

Staying alive is not enough on its own, because automatic sending had three
holes of its own:

- MediaServer sent only to whoever happened to be connected at that instant,
  so a photo taken with the PC asleep was dropped and never retried.
- engine.setAutoSend was called only from the settings switch, so after any
  restart the switch read on while nothing was ever sent.
- MediaSource takes a fresh snapshot when it starts, so whatever was
  photographed while the process was dead was never reported as new.

AutoSendQueue now writes every pending item to disk, hands it over as soon
as the device is reachable, and remembers what already went. AppServices
re-applies the setting at every start, and a catch-up pass picks up what
appeared while nothing was listening. Caps on age, backlog size and retries
keep a phone that was offline for a week from flooding the PC. HEIC
conversion is finally wired in, and the copies the platform exports to send
an original are cleaned up once nothing is queued or in flight.

Bonjour/mDNS discovery goes with it: the UDP beacon cannot run on iPhone
(broadcast needs an entitlement Apple does not hand out), so without it a PC
that reboots onto a new DHCP lease disappeared for good. Both ends announce
now. The Flatpak manifest needed the Avahi name on the system bus, not the
session one.

Settings tell the truth on iPhone now, including a background-state row that
shows how long the process has been alive, and a warning when photo access
is limited to a hand-picked selection, which hides every later photo.

ci.yml grows an iOS job: the Swift is only ever compiled on a macOS runner,
and it also checks the background keys survive into the built bundle.
@PepoTechYT
PepoTechYT merged commit fc2cc63 into main Sep 19, 2026
2 checks passed
@PepoTechYT
PepoTechYT deleted the ios-background-transfers branch September 19, 2026 13:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant