Serviço de Segundo Plano (Background Tasks / Foreground Service)
Visão Geral
O app utiliza um serviço de segundo plano (no Android, com foreground service) para executar tarefas críticas mesmo quando o usuário troca de app, bloqueia a tela ou o sistema tenta reduzir prioridade do processo.
Este módulo resolve principalmente:
- Envio de pedidos em segundo plano (sincronização), exibindo notificação do Android durante a execução;
- Monitoramento de lentidão/instabilidade de conexão durante o envio (mensagens/banners);
- Serviço persistente (keep-alive) para manter o processo com maior prioridade;
- Coleta de GPS em loop quando uma coleta está ativa, com persistência em storage;
- Um mecanismo de pausar/retomar o serviço persistente quando uma tarefa efêmera precisa rodar com prioridade.
Arquivo e Responsabilidades
| Arquivo | Localização | Responsabilidade |
|---|---|---|
backgroundTasks.service.ts | src/services/ | Orquestra tasks em segundo plano (efêmeras e persistentes), monitor de conexão e ticks de GPS |
Conceitos
1) Task Efêmera
Task “curta” e com objetivo específico. Exemplos:
- Enviar um pedido individual em background (
BackgroundOrderTask) - Enviar pedido principal + bonificações (
sendOrderAndBonusBackgroundOrderTask) - Excluir venda (
deleteBackgroundOrderTask) - Sincronizar/baixar dados (
sync)
Características:
- Normalmente executa via
BackgroundJob.start(task, options); - Pode (ou não) usar foreground service dependendo do estado do serviço persistente;
- Deve sempre limpar timers/monitores ao finalizar.
2) Serviço Persistente (Keep-alive)
Loop “leve” em foreground para manter prioridade do app. Ele executa periodicamente um tick mínimo, e opcionalmente o tick de GPS.
Características:
- Mantém notificação ativa no Android;
- Não depende exclusivamente de
BackgroundJob.isRunning()para controlar o loop; - Controlado por flags internas:
PERSISTENT_ACTIVEPERSISTENT_SHOULD_RUNPERSISTENT_PAUSEDPERSISTENT_RESUME_CONFIG
3) Política de concorrência: Pausar/Retomar
Quando uma task efêmera precisa executar com foreground service e o persistente está ativo, o sistema:
- Pausa o serviço persistente (
pausePersistentIfActive) - Executa a task efêmera
- Retoma o serviço persistente (
resumePersistentIfNeeded)
Isso evita conflito de uso do foreground service e garante previsibilidade.
Constantes e Configurações
Mensagens de conexão
UNSTABLE_CONNECTION_MESSAGE: "Aguarde… Estamos enviando seu pedido"MAX_DELAY_CONNECTION_MESSAGE: "Aguarde… Envio de pedido demorando mais que o esperado."
Thresholds padrão
DEFAULT_PER_ITEM_THRESHOLD_SECONDS = 5DEFAULT_MAX_THRESHOLD_MINUTES = 3DEFAULT_PER_ITEM_GAP_SECONDS = 10
GPS
GPS_STORAGE_KEY = 'gps_collection_data'GPS_COLLECTION_INTERVAL = 10000(10s)
Observação: o intervalo do loop persistente pode ser configurado via
startPersistentService({ delayMs }). O tick de GPS também respeita controles internos para não rodar concorrente (gpsIsUpdating).
Persistência e Estado (GPS)
Estrutura armazenada (BasicGpsState)
Campos principais:
| Campo | Tipo | Descrição |
|---|---|---|
status | 'idle' | 'collecting' | 'paused' | 'finished' | Estado da coleta |
nroColeta | number | null | Identificador da coleta |
codVend | string | Vendedor |
currentLatitude/currentLongitude | string | null | Última posição registrada |
distanciaPercorrida/distanciaTotal | number | Atualização retornada do backend |
lastUpdateTime | number | Timestamp do último tick |
Storage auxiliar por coleta
Além do estado principal, o código também usa uma chave por coleta:
coleta_${codVend}_${nroColeta}
Essa chave é usada para recuperar dados (ex.: date, latitude, longitude, codEmp) e decidir finalização automática por mudança de dia.
Algoritmo — Tick de GPS
Objetivo
Quando uma coleta está em andamento (status = collecting), o serviço persistente executa um tick periódico para:
- Obter GPS atual
- Enviar atualização para o backend
- Persistir no storage a última posição e métricas de distância
Fluxo (maybeRunGpsTick)
- Sai se já estiver atualizando (
gpsIsUpdating) - Lê
BasicGpsStatedo storage (readGpsState) - Valida:
status === 'collecting'nroColetaecodVendválidos
- Verifica se a coleta pertence ao dia atual:
- Se tiver mudado o dia, tenta
finalizarColetaautomaticamente e limpa storage
- Se tiver mudado o dia, tenta
- Busca localização com
Geolocation.getCurrentPosition(getCurrentLocationForGps) - Chama backend:
GpsCollectionService.atualizarColeta({...}) - Se sucesso:
- Atualiza
currentLatitude/currentLongitude,distanciaPercorrida/distanciaTotal,lastUpdateTime
- Atualiza
Fluxograma (alto nível)
Monitor de Conexão Instável (Pedidos)
Objetivo
Enquanto o pedido está sendo enviado/sincronizado, o usuário deve receber feedback se:
- está demorando “por item” além do esperado;
- ultrapassou um máximo absoluto de tempo.
Origem dos thresholds
O app tenta obter valores configuráveis do Redux (store.getState().settings):
TEMPESPITEM→ segundos por itemTEMPMAXESP→ minutos máximos
Caso sejam inválidos, aplica fallback:
DEFAULT_PER_ITEM_THRESHOLD_SECONDSDEFAULT_MAX_THRESHOLD_MINUTES
Conversão segura via resolveNumericThreshold:
- suporta
numberestring - ignora negativos e
NaN
Cálculo de atraso “por item”
perItemDelayMs = itemsCount * perItemThresholdMs- Atraso final inclui um “gap” fixo:
totalPerItemDelayMs = perItemDelayMs + perItemGapMs
Comportamento do monitor (createUnstableConnectionMonitor)
start()cria timeouts:- Per-item → dispara banner e redux message (
setUnstableConnectionMessage) - Max → dispara banner e redux message
- Per-item → dispara banner e redux message (
clear():- cancela timeouts
- limpa mensagem do redux (
clearUnstableConnectionMessage) - esconde banner no controller (
ConnectionController.hideBanner)
Serviço Persistente — Controle de Lifecycle
Flags internas
| Flag | Tipo | Descrição |
|---|---|---|
PERSISTENT_ACTIVE | boolean | Indica que o serviço persistente está ativo |
PERSISTENT_PAUSED | boolean | Marca que foi pausado e precisa retomar |
PERSISTENT_RESUME_CONFIG | object | null | Config para retomar após task efêmera |
PERSISTENT_SHOULD_RUN | boolean | Controla o loop interno (fonte da verdade) |
Início (startPersistentService)
Responsabilidades:
- Evitar duplicação (se já está ativo e
BackgroundJob.isRunning()) - Solicitar permissão de notificação no Android 13+ (
ensurePostNotificationsPermission) - Salvar
PERSISTENT_RESUME_CONFIG - Iniciar background job com loop controlado por
PERSISTENT_SHOULD_RUN
Loop:
- Enquanto
PERSISTENT_SHOULD_RUN === true:- tenta executar tick de GPS (se aplicável)
- aguarda
delayviasetTimeout
Parada (stopPersistentService)
- Seta
PERSISTENT_SHOULD_RUN = false - Reseta flags e config
- Aguarda pequena janela (100ms)
- Se
BackgroundJob.isRunning(), chamastop()
Execução de Tasks Efêmeras com Segurança
runWithForeground(task, options)
Wrapper para decidir como executar a task:
- Se
PERSISTENT_ACTIVE:- executa
task()sem foreground service (para não conflitar)
- executa
- Caso contrário:
- pausa persistent se necessário
- roda via
BackgroundJob.start(task, options) - na saída, retoma persistent se tinha sido pausado
stopIfEphemeral()
Garante que o BackgroundJob.stop() só será chamado quando não estiver em modo persistente.
Tarefas Implementadas
1) BackgroundOrderTask
Objetivo: enviar um pedido offline individual.
Passos (alta fidelidade):
- Calcula thresholds e gera monitor
- Liga monitor (
monitor.start()) - Carrega quantidade de itens do pedido offline (
getOfflineOrderItemsCount) - Remove log offline do item (
OfflineService.removeOfflineLogItem) - Executa sync via
SyncListService.submitChanges({ sendIndividualOrder: true, offlineOrderID: … }) - Trata cenários:
- Duplicidade (
hasDuplicatedOrder) - Sucesso / erro com toast
- Duplicidade (
- Remove marcações de envio e ids de background (
removeSalesBackgroundIDS, etc.) - Sempre limpa monitor (
monitor.clear()) emfinally
2) sendOrderAndBonusBackgroundOrderTask
Objetivo: enviar um pedido principal e, caso seja permitido, enviar pedidos de bonificação.
Características:
- Possui logs extensos (
[BONUS_DEBUG]) para rastrear execução e falhas - Isola monitor por pedido (cada
currentIdcria seu próprio monitor) - Retorno do envio do pedido principal determina se deve prosseguir com as bonificações (
shouldSendBonuses === true)
Regras importantes:
- Se
shouldSendforfalse, pula o pedido explicitamente - Em duplicidade do pedido principal, pode continuar (tratado como sucesso para fluxo de bônus)
3) deleteBackgroundOrderTask
Objetivo: excluir uma venda, com feedback de sucesso/erro.
- Remove log offline (
OfflineService.removeOfflineLogItem) - Executa exclusão em background
- Atualiza lista de vendas e filtros via dispatch após sucesso
4) sync
Objetivo: sincronização geral/atualização de dados.
- Executa task de sync em background
- Notifica usuário ao final via
NotificationService.notify
Permissões (Android 13+)
Para Android API 33+ é solicitada permissão:
POST_NOTIFICATIONS
Implementação: ensurePostNotificationsPermission().
Importante: sem essa permissão, o foreground service pode falhar em exibir notificação/rodar corretamente dependendo da implementação do sistema.
Observações e Boas Práticas
-
Evitar concorrência no GPS
gpsIsUpdatingimpede sobreposição de ticks.
-
Sempre limpar monitores
createUnstableConnectionMonitor().clear()deve rodar emfinally.
-
Não depender só do isRunning
- Loop persistente é controlado por
PERSISTENT_SHOULD_RUN.
- Loop persistente é controlado por
-
Isolar monitor por pedido
- Em fluxos com múltiplos pedidos (bonificação), cada pedido precisa de seu próprio monitor (já implementado).
-
Bateria/ANR
- O loop persistente deve permanecer “leve” e com intervalos razoáveis.
Checklist de Troubleshooting
-
Foreground service não aparece no Android:
- Verificar permissão
POST_NOTIFICATIONS(Android 13+) - Verificar se
BackgroundJob.startestá sendo chamado comtaskIcon
- Verificar permissão
-
Mensagens de conexão instável não aparecem:
- Garantir que
dispatchestá sendo passado para a task - Verificar thresholds (
TEMPESPITEM,TEMPMAXESP) nosettings
- Garantir que
-
GPS não atualiza:
- Confirmar
BasicGpsState.status === 'collecting' - Confirmar permissões de localização no aparelho
- Ver logs
GPS collection tick ...
- Confirmar
Referências no Código
BackgroundTasks.startPersistentService/stopPersistentServicerunWithForegroundpausePersistentIfActive/resumePersistentIfNeededcreateUnstableConnectionMonitormaybeRunGpsTick