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 for a primeira vez que você declara uma API com o JobScheduler, declare a JobService e as permissões associadas no seu 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 da transferência estimado, 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

Apps can only start a user-initiated data transfer job if the app is in the visible window, or if the certain conditions are met. To determine when a user-initiated data transfer job can be scheduled, the system applies the same list of conditions that allow apps to start an activity from the background in special cases. Notably, this list of conditions are not the same as the set of exemptions for background-started foreground service restrictions.

The exceptions to the previous statement are the following:

  • If an app can launch activities from the background, they can also launch user-initiated data transfer jobs from the background.
  • If an app has an activity in the back stack of an existing task on the Recents screen, that alone doesn't allow a user-initiated data transfer job to run.

If the job is scheduled at some other time not listed in the allowed conditions list, the job fails and returns a RESULT_FAILURE error code.

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

To support jobs running at optimal points, Android offers the ability to assign constraints to each job type. These constraints are already available as of Android 13.

Note: The following table only compares the constraints that vary between each job type. See JobScheduler developer page or work constraints for all constraints.

The following table shows the different job types that support a given job constraint, as well as the set of job constraints that WorkManager supports. Use the search bar before the table to filter the table by the name of a job constraint method.

These are the constraints allowed with user-initiated data transfer jobs:

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

Testes

The following list shows some steps on how to test your app's jobs manually:

  • To get the job ID, get the value that is defined upon the job being built.
  • To run a job immediately, or to retry a stopped job, run the following command in a terminal window:

    adb shell cmd jobscheduler run -f APP_PACKAGE_NAME JOB_ID
    
  • To simulate the system force-stopping a job (due to system health or out-of-quota conditions), run the following command in a terminal window:

    adb shell cmd jobscheduler timeout TEST_APP_PACKAGE TEST_JOB_ID