Transferência de dados iniciada pelo usuário

Se você precisar fazer uma transferência de dados que pode levar muito tempo, crie um job do JobScheduler e identifique-o como um job de transferência de dados iniciada pelo usuário (UIDT). Os jobs de UIDT são destinados a transferências de dados mais longas iniciadas pelo usuário do dispositivo, como o download de um arquivo de um servidor remoto. Os trabalhos de UIDT foram introduzidos no Android 14 (nível 34 da API).

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, sujeitos 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.

Programar jobs de transferência de dados iniciados pelo usuário

Para executar um job de transferência de dados iniciado pelo usuário, faça o seguinte:

  1. Verifique se o app declarou o JobService e as permissões associadas no manifesto:

    <service android:name="com.example.app.CustomTransferService"
            android:permission="android.permission.BIND_JOB_SERVICE"
            android:exported="false">
            ...
    </service>
    

    Além disso, defina uma subclasse concreta de JobService para sua transferência de dados:

    Kotlin

    class CustomTransferService : JobService() {
      ...
    }

    Java

    class CustomTransferService extends 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 método setUserInitiated() ao criar um objeto JobInfo. Esse método está disponível a partir do Android 14. Também é recomendável oferecer uma estimativa de tamanho de payload chamando setEstimatedNetworkBytes() ao criar seu job.

    Kotlin

    val networkRequestBuilder = NetworkRequest.Builder()
            // Add or remove capabilities based on your requirements.
            // For example, this code specifies that the job won't run
            // unless there's a connection to the internet (not just a local
            // network), and the connection doesn't charge per-byte.
            .addCapability(NET_CAPABILITY_INTERNET)
            .addCapability(NET_CAPABILITY_NOT_METERED)
            .build()
    
    val jobInfo = JobInfo.Builder(jobId,
                  ComponentName(mContext, CustomTransferService::class.java))
            // ...
            .setUserInitiated(true)
            .setRequiredNetwork(networkRequestBuilder)
            // Provide your estimate of the network traffic here
            .setEstimatedNetworkBytes(1024 * 1024 * 1024)
            // ...
            .build()

    Java

    NetworkRequest networkRequest = new NetworkRequest.Builder()
        // Add or remove capabilities based on your requirements.
        // For example, this code specifies that the job won't run
        // unless there's a connection to the internet (not just a local
        // network), and the connection doesn't charge per-byte.
        .addCapability(NET_CAPABILITY_INTERNET)
        .addCapability(NET_CAPABILITY_NOT_METERED)
        .build();
    
    JobInfo jobInfo = JobInfo.Builder(jobId,
            new ComponentName(mContext, CustomTransferService.class))
        // ...
        .setUserInitiated(true)
        .setRequiredNetwork(networkRequest)
        // Provide your estimate of the network traffic here
        .setEstimatedNetworkBytes(1024 * 1024 * 1024)
        // ...
        .build();
  4. Enquanto o job estiver sendo executado, chame setNotification() no objeto JobService. Chamar setNotification() informa 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.

    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.

    Kotlin

    class CustomTransferService: JobService() {
        private val scope = CoroutineScope(Dispatchers.IO)
    
        @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
        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)
            // Execute the work associated with this job asynchronously.
            scope.launch {
                doDownload(params)
            }
            return true
        }
    
        private suspend fun doDownload(params: JobParameters) {
            // Run the relevant async download task, then call
            // jobFinished once the task is completed.
            jobFinished(params, false)
        }
    
        // Called when the system stops the job.
        override fun onStopJob(params: JobParameters?): Boolean {
            // Asynchronously record job-related data, such as the
            // stop reason.
            return true // or return false if job should end entirely
        }
    }

    Java

    class CustomTransferService extends JobService{
        @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
        @Override
        public boolean onStartJob(JobParameters params) {
            Notification notification = Notification.Builder(getBaseContext(),
                                            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)
            // Execute the work associated with this job asynchronously.
            new Thread(() -> doDownload(params)).start();
            return true;
        }
    
        private void doDownload(JobParameters params) {
            // Run the relevant async download task, then call
            // jobFinished once the task is completed.
            jobFinished(params, false);
        }
    
        // Called when the system stops the job.
        @Override
        public boolean onStopJob(JobParameters params) {
            // Asynchronously record job-related data, such as the
            // stop reason.
            return true; // or return false if job should end entirely
        }
    }
  5. 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 for necessário atualizar o tamanho estimado da transferência, use a nova API, updateEstimatedNetworkBytes(), para atualizar o tamanho da transferência quando ele for conhecido.

Recomendações

Para executar jobs de UIDT de maneira eficaz, faça o seguinte:

  1. Defina claramente as restrições de rede e de execução do job para especificar quando ele deve ser executado.

  2. Execute a tarefa de forma assíncrona em onStartJob(). Por exemplo, você pode fazer isso usando uma corrotina. Se você não executar a tarefa de forma assíncrona, o trabalho será executado na linha de execução principal e poderá bloqueá-la, o que pode causar um ANR.

  3. Para evitar executar o job por mais tempo do que o necessário, chame jobFinished() quando a transferência terminar, seja ela bem-sucedida ou não. Assim, o job não é executado por mais tempo do que o necessário. Para descobrir por que um job foi interrompido, implemente o método de callback onStopJob() e chame JobParameters.getStopReason().

Compatibilidade com versões anteriores

目前还没有支持 UIDT 作业的 Jetpack 库。因此,我们建议您使用代码来限制更改,以验证您是否在 Android 14 或更高版本上运行。在较低的 Android 版本中,您可以将 WorkManager 的前台服务实现用作回退方法。

以下是检查相应系统版本的代码示例:

Kotlin

fun beginTask() {
    if (Build.VERSION.SDK_INT < Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
        scheduleDownloadFGSWorker(context)
    } else {
        scheduleDownloadUIDTJob(context)
    }
}

private fun scheduleDownloadUIDTJob(context: Context) {
    // build jobInfo
    val jobScheduler: JobScheduler =
        context.getSystemService(Context.JOB_SCHEDULER_SERVICE) as JobScheduler
    jobScheduler.schedule(jobInfo)
}

private fun scheduleDownloadFGSWorker(context: Context) {
    val myWorkRequest = OneTimeWorkRequest.from(DownloadWorker::class.java)
    WorkManager.getInstance(context).enqueue(myWorkRequest)
}

Java

public void beginTask() {
    if (Build.VERSION.SDK_INT < Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
        scheduleDownloadFGSWorker(context);
    } else {
        scheduleDownloadUIDTJob(context);
    }
}

private void scheduleDownloadUIDTJob(Context context) {
    // build jobInfo
    JobScheduler jobScheduler =
            (JobScheduler) context.getSystemService(Context.JOB_SCHEDULER_SERVICE);
    jobScheduler.schedule(jobInfo);
}

private void scheduleDownloadFGSWorker(Context context) {
    OneTimeWorkRequest myWorkRequest = OneTimeWorkRequest.from(DownloadWorker.class);
    WorkManager.getInstance(context).enqueue(myWorkRequest)
}

Interromper jobs da UIDT

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 o job não é mostrado no Gerenciador de tarefas.

Pelo sistema

与常规作业不同,用户发起的数据传输作业不受应用待机模式存储分区配额的影响。但是,如果出现以下任一情况,系统仍会停止作业:

  • 不再满足开发者定义的约束条件。
  • 系统确定该作业的运行时间超出了完成数据传输任务所需的时间。
  • 系统需要优先考虑系统运行状况,并因发热程度上升而停止作业。
  • 应用进程因设备内存不足而被终止。

如果系统因设备内存不足以外的原因停止作业,系统会调用 onStopJob(),并在系统认为最佳的时间重试作业。确保您的应用可以保留数据传输状态(即使未调用 onStopJob()),并且您的应用可以在再次调用 onStartJob() 时恢复此状态。

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

Os apps só podem 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:

  • 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 ser executado em um momento em que as condições necessárias não forem atendidas, 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 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()

Teste

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

Veja também

Outros recursos

Para mais informações sobre transferências de dados iniciadas pelo usuário, consulte os seguintes recursos adicionais: