Migrar serviços em primeiro plano para jobs de transferência de dados iniciados pelo usuário

O Android 14 aplica regras rígidas sobre a possibilidade de apps usarem serviços em primeiro plano.

Além disso, estamos introduzindo no Android 14 uma nova API para especificar que um job precisa ser um job de transferência de dados iniciado pelo usuário. Essa API é útil para casos de uso que exigem uma transferência de dados iniciada pelo usuário mais longa, como o download de um arquivo de um servidor remoto. Esses tipos de tarefas precisam usar um job de transferência de dados iniciado pelo usuário.

Esses tipos de jobs de transferência de dados são iniciados pelo usuário. Eles exigem uma notificação, são iniciados imediatamente e podem ser executados por um período prolongado, conforme as condições do sistema permitirem. É possível executar vários jobs de transferência de dados iniciados pelo usuário ao mesmo tempo.

Os jobs iniciados pelo usuário precisam ser agendados enquanto o aplicativo está visível para o usuário (ou em uma das condições permitidas). Depois que todas as restrições forem atendidas, os jobs iniciados pelo usuário podem ser executados pelo SO, sujeito a restrições de integridade do sistema. O sistema também pode usar o tamanho de payload estimado para determinar o período de execução do job.

Permissão para jobs de transferência de dados iniciados pelo usuário

Os jobs de transferência de dados iniciados pelo usuário exigem uma nova permissão para serem executados: RUN_USER_INITIATED_JOBS. O sistema concede essa permissão automaticamente. Ele gera uma SecurityException quando você não declara a permissão no manifesto do app.

Processo para programar jobs de transferência de dados iniciados pelo usuário

Para executar um job iniciado pelo usuário, siga estas etapas:

  1. Se esta é a primeira vez que você declara uma API com o JobScheduler, declare o JobService e as permissões associadas no manifesto. Além disso, defina uma subclasse concreta de JobService para sua transferência de dados:

    <service android:name="com.example.app.CustomTransferService"
            android:permission="android.permission.BIND_JOB_SERVICE"
            android:exported="false">
            ...
    </service>
    
    class CustomTransferService : JobService() {
      ...
    }
    
  2. Declare a permissão RUN_USER_INITIATED_JOBS no manifesto:

    <manifest ...>
        <uses-permission android:name="android.permission.RUN_USER_INITIATED_JOBS" />
        <application ...>
            ...
        </application>
    </manifest>
    
  3. Chame o novo método setUserInitiated() ao criar um objeto JobInfo. Também é recomendável oferecer uma estimativa de tamanho de payload chamando setEstimatedNetworkBytes() ao criar seu job:

    val networkRequestBuilder = NetworkRequest.Builder()
            .addCapability(NET_CAPABILITY_INTERNET)
            .addCapability(NET_CAPABILITY_NOT_METERED)
            // Add or remove capabilities based on your requirements
            .build()
    
    val jobInfo = JobInfo.Builder()
            // ...
            .setUserInitiated(true)
            .setRequiredNetwork(networkRequestBuilder.build())
            .setEstimatedNetworkBytes(1024 * 1024 * 1024)
            // ...
            .build()
    
  4. Programe o job antes do início da transferência enquanto o aplicativo está visível ou na lista de condições permitidas:

    val jobScheduler: JobScheduler =
        context.getSystemService(Context.JOB_SCHEDULER_SERVICE) as JobScheduler
    jobScheduler.schedule(jobInfo)
    
  5. Quando o job estiver sendo executado, chame setNotification() no objeto JobService. Esse valor é usado para informar ao usuário que o job está em execução, tanto no gerenciador de tarefas quanto na área de notificação da barra de status:

    class CustomTransferService : JobService() {
      override fun onStartJob(params: JobParameters?): Boolean {
          val notification = Notification.Builder(applicationContext, NOTIFICATION_CHANNEL_ID)
                  .setContentTitle("My user-initiated data transfer job")
                  .setSmallIcon(android.R.mipmap.myicon)
                  .setContentText("Job is running")
                  .build()
    
          setNotification(params, notification.id, notification,
                  JobService.JOB_END_NOTIFICATION_POLICY_DETACH)
          // Do the job execution.
      }
    }
    
  6. Atualize a notificação periodicamente para manter o usuário informado sobre o status e o progresso do job. Se não for possível determinar o tamanho da transferência antes de programar o job ou se precisar atualizar o tamanho estimado da transferência, use a nova API, updateEstimatedNetworkBytes(), para atualizar o tamanho da transferência quando ele for conhecido.

  7. Quando a execução for concluída, chame jobFinished() para sinalizar ao sistema que o job foi concluído ou que ele precisa ser reagendado.

Os jobs de transferência de dados iniciados pelo usuário podem ser interrompidos

O usuário e o sistema podem interromper jobs de transferência iniciados pelo usuário.

Pelo usuário, no gerenciador de tarefas

O usuário pode interromper um job de transferência de dados iniciado pelo usuário que aparece no Gerenciador de tarefas.

Quando o usuário pressiona Stop, o sistema:

  • encerra o processo do app imediatamente, incluindo todos os outros jobs ou serviços em execução no primeiro plano;
  • não chama onStopJob() para nenhum job em execução;
  • impede que os jobs visíveis ao usuário sejam reprogramados.

Por esses motivos, é recomendável fornecer controles na notificação postada para o job para permitir uma interrupção e reprogramação normais.

Em circunstâncias especiais, o botão Stop não aparece ao lado do job no Gerenciador de tarefas, ou então o job não é mostrado no gerenciador de tarefas.

Pelo sistema

Diferente dos jobs normais, os jobs de transferência de dados iniciados pelo usuário não são afetados pelas cotas de buckets de apps em espera. No entanto, o sistema ainda interromperá o job se alguma das condições a seguir ocorrer:

  • Uma restrição definida pelo desenvolvedor não é mais atendida.
  • O sistema determina que o job foi executado por mais tempo que o necessário para concluir a tarefa de transferência de dados.
  • O sistema precisa priorizar a integridade e interromper os jobs devido ao aumento do estado térmico.
  • O processo do app é encerrado devido à pouca memória do dispositivo.

Quando o job é interrompido pelo sistema (não pelo caso de pouca memória), o sistema chama onStopJob() e tenta executar o job novamente em um momento que considera ideal. Confira se o app pode manter o estado de transferência de dados, mesmo que onStopJob() não seja chamado, e se ele pode restaurar esse estado quando onStartJob() é chamado novamente.

Condições permitidas para programar jobs de transferência de dados iniciados pelo usuário

Os apps só poderão iniciar um job de transferência de dados iniciado pelo usuário se estiverem na janela visível ou se determinadas condições forem atendidas. Para determinar quando um job de transferência de dados iniciado pelo usuário pode ser programado, o sistema aplica a mesma lista de condições que permitem que os apps iniciem uma atividade em segundo plano em casos especiais. Essa lista de condições não é igual ao conjunto de isenções para restrições de serviço em primeiro plano iniciadas em segundo plano.

Confira algumas exceções:

  • Se um app puder iniciar atividades em segundo plano, ele também poderá iniciar jobs de transferência de dados iniciados pelo usuário em segundo plano.
  • Se um app tiver uma atividade na backstack de uma tarefa na tela Recentes, isso, por si só, não permitirá que um job de transferência de dados iniciado pelo usuário seja executado.

Se o job estiver programado para algum outro momento não listado nas condições permitidas, ele falhará e retornará um código de erro RESULT_FAILURE.

Restrições permitidas para jobs de transferência de dados iniciados pelo usuário

Para oferecer suporte a jobs em execução nos pontos ideais, no Android é possível atribuir restrições a cada tipo de job. Essas restrições já estão disponíveis desde o Android 13.

Observação: a tabela a seguir compara somente as restrições que variam entre cada tipo de job. Consulte a Página do desenvolvedor do JobScheduler ou as restrições de trabalho para conferir todas as restrições.

A tabela a seguir mostra os diferentes tipos de job que aceitam determinada restrição, bem como o conjunto de restrições de jobs com suporte ao WorkManager. Use a barra de pesquisa antes da tabela para filtrá-la pelo nome de um método de restrição de job.

Estas são as restrições permitidas com jobs de transferência de dados iniciados pelo usuário:

  • setBackoffCriteria(JobInfo.BACKOFF_POLICY_EXPONENTIAL)
  • setClipData()
  • setEstimatedNetworkBytes()
  • setMinimumNetworkChunkBytes()
  • setPersisted()
  • setNamespace()
  • setRequiredNetwork()
  • setRequiredNetworkType()
  • setRequiresBatteryNotLow()
  • setRequiresCharging()
  • setRequiresStorageNotLow()

Testes

A lista a seguir mostra algumas etapas para testar os jobs do app manualmente:

  • Para encontrar o ID do job, acesse o valor definido após a criação dele.
  • Para executar um job imediatamente ou repetir um job interrompido, execute o comando a seguir em uma janela do terminal:

    adb shell cmd jobscheduler run -f APP_PACKAGE_NAME JOB_ID
    
  • Para simular o fechamento forçado de um job (devido a uma condição de integridade do sistema ou falta de cotas), execute o seguinte comando em uma janela do terminal:

    adb shell cmd jobscheduler timeout TEST_APP_PACKAGE TEST_JOB_ID