Pular para o conteúdo principal

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

ArquivoLocalizaçãoResponsabilidade
backgroundTasks.service.tssrc/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_ACTIVE
    • PERSISTENT_SHOULD_RUN
    • PERSISTENT_PAUSED
    • PERSISTENT_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:

  1. Pausa o serviço persistente (pausePersistentIfActive)
  2. Executa a task efêmera
  3. 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 = 5
  • DEFAULT_MAX_THRESHOLD_MINUTES = 3
  • DEFAULT_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:

CampoTipoDescrição
status'idle' | 'collecting' | 'paused' | 'finished'Estado da coleta
nroColetanumber | nullIdentificador da coleta
codVendstringVendedor
currentLatitude/currentLongitudestring | nullÚltima posição registrada
distanciaPercorrida/distanciaTotalnumberAtualização retornada do backend
lastUpdateTimenumberTimestamp 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:

  1. Obter GPS atual
  2. Enviar atualização para o backend
  3. Persistir no storage a última posição e métricas de distância

Fluxo (maybeRunGpsTick)

  • Sai se já estiver atualizando (gpsIsUpdating)
  • BasicGpsState do storage (readGpsState)
  • Valida:
    • status === 'collecting'
    • nroColeta e codVend válidos
  • Verifica se a coleta pertence ao dia atual:
    • Se tiver mudado o dia, tenta finalizarColeta automaticamente e limpa storage
  • Busca localização com Geolocation.getCurrentPosition (getCurrentLocationForGps)
  • Chama backend: GpsCollectionService.atualizarColeta({...})
  • Se sucesso:
    • Atualiza currentLatitude/currentLongitude, distanciaPercorrida/distanciaTotal, lastUpdateTime

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 item
  • TEMPMAXESP → minutos máximos

Caso sejam inválidos, aplica fallback:

  • DEFAULT_PER_ITEM_THRESHOLD_SECONDS
  • DEFAULT_MAX_THRESHOLD_MINUTES

Conversão segura via resolveNumericThreshold:

  • suporta number e string
  • 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
  • clear():
    • cancela timeouts
    • limpa mensagem do redux (clearUnstableConnectionMessage)
    • esconde banner no controller (ConnectionController.hideBanner)

Serviço Persistente — Controle de Lifecycle

Flags internas

FlagTipoDescrição
PERSISTENT_ACTIVEbooleanIndica que o serviço persistente está ativo
PERSISTENT_PAUSEDbooleanMarca que foi pausado e precisa retomar
PERSISTENT_RESUME_CONFIGobject | nullConfig para retomar após task efêmera
PERSISTENT_SHOULD_RUNbooleanControla 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 delay via setTimeout

Parada (stopPersistentService)

  • Seta PERSISTENT_SHOULD_RUN = false
  • Reseta flags e config
  • Aguarda pequena janela (100ms)
  • Se BackgroundJob.isRunning(), chama stop()

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)
  • 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):

  1. Calcula thresholds e gera monitor
  2. Liga monitor (monitor.start())
  3. Carrega quantidade de itens do pedido offline (getOfflineOrderItemsCount)
  4. Remove log offline do item (OfflineService.removeOfflineLogItem)
  5. Executa sync via SyncListService.submitChanges({ sendIndividualOrder: true, offlineOrderID: … })
  6. Trata cenários:
    • Duplicidade (hasDuplicatedOrder)
    • Sucesso / erro com toast
  7. Remove marcações de envio e ids de background (removeSalesBackgroundIDS, etc.)
  8. Sempre limpa monitor (monitor.clear()) em finally

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 currentId cria seu próprio monitor)
  • Retorno do envio do pedido principal determina se deve prosseguir com as bonificações (shouldSendBonuses === true)

Regras importantes:

  • Se shouldSend for false, 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

  1. Evitar concorrência no GPS

    • gpsIsUpdating impede sobreposição de ticks.
  2. Sempre limpar monitores

    • createUnstableConnectionMonitor().clear() deve rodar em finally.
  3. Não depender só do isRunning

    • Loop persistente é controlado por PERSISTENT_SHOULD_RUN.
  4. Isolar monitor por pedido

    • Em fluxos com múltiplos pedidos (bonificação), cada pedido precisa de seu próprio monitor (já implementado).
  5. 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.start está sendo chamado com taskIcon
  • Mensagens de conexão instável não aparecem:

    • Garantir que dispatch está sendo passado para a task
    • Verificar thresholds (TEMPESPITEM, TEMPMAXESP) no settings
  • GPS não atualiza:

    • Confirmar BasicGpsState.status === 'collecting'
    • Confirmar permissões de localização no aparelho
    • Ver logs GPS collection tick ...

Referências no Código

  • BackgroundTasks.startPersistentService / stopPersistentService
  • runWithForeground
  • pausePersistentIfActive / resumePersistentIfNeeded
  • createUnstableConnectionMonitor
  • maybeRunGpsTick