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

Android 14 applies strict rules on when apps are allowed to use foreground services.

Also in Android 14 we are introducing a new API to specify that a job must be a user-initiated data transfer job. This API is helpful for use cases that require longer-duration, user-initiated transferring of data, such as downloading a file from a remote server. These types of tasks should use a user-initiated data transfer job.

User-initiated data transfer jobs are started by the user. These jobs require a notification, start immediately, and may be able to run for an extended period of time as system conditions allow. You can run several user-initiated data transfer jobs concurrently.

User initiated jobs must be scheduled while the application is visible to the user (or in one of the allowed conditions). After all constraints are met, user initiated jobs can be executed by the OS, subject to system health restrictions. The system may also use the provided estimated payload size to determine how long the job executes.

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

Unlike regular jobs, user-initiated data transfer jobs are unaffected by App Standby Buckets quotas. However, the system still stops the job if any of the following conditions occur:

  • A developer-defined constraint is no longer met.
  • The system determines that the job has run for longer than necessary to complete the data transfer task.
  • The system needs to prioritize system health and stop jobs due to increased thermal state.
  • The app process is killed due to low device memory.

When the job is stopped by the system (not by the low-memory case), the system calls onStopJob(), and the system retries the job at a time that the system deems to be optimal. Check that your app can persist data transfer state, even if onStopJob() isn't called, and that your app can restore this state when onStartJob() is called again.

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