Can this be run in the background?

Kirill Rozov

Android Development Team Leader at Tinkoff

In May 2023, the Yuztech Group team organized the Usetech Meetup “Mobile Development Trends” in Tomsk, where experts from the Russian IT market shared their experience. As a result of the event, we wrote a series of articles, each of which broadcasts the speech of one of the speakers. We started with a presentation by Mobile Developer Alexey Gladkov on the topic: The State of Kotlin Multiplatform. Let’s continue with the performance of Kirill Rozov.

Colleagues, welcome! My name is Kirill Rozov, I am the head of the Android development team at Tinkoff, and also the author YouTube channel “Android Broadcast”.

Android has more and more restrictions on launching and running tasks when apps are in the background. Today I will talk about different recipes and rules on how to get along (and not fight!) With the system and do work in the background. We will talk about WorkManager / JobScheduler, DownloadManager, Foreground Servise, Sync Adapter, AlarmManager, vendors, and how to choose an API for a task.

  1. Work Manager

If you ask how to run a task in the background, they will tell you that WorkManager is your everything. WorkManager is the class that will handle starting the work:

class UploadWorker(
    appContext: Context,
    workerParams: WorkerParameters
) : Worker(appContext, workerParams) {
    
    override fun doWork(): Result {
        // Делаем долгую работу
        doLongWork()
        // Отправляем результат выполнения работы
        return Result.success()
    }
}

At its core, this is something similar to services, only we describe not services, but Worker, we add context and some parameters there. This system works under the hood of JobScheduler. Of the benefits – support for Kotlin Coroutines:

class UploadWorker(
    appContext: Context,
    workerParams: WorkerParameters
) : CoroutineWorker(appContext, workerParams) {
    override suspend fun doWork(): Result {
        // Делаем долгую работу
        doLongWork()
        // Отправляем результат выполнения работы
        return Result.success()
    }
}

What else can WorkManager do? You can describe a one-time request for it, for example: “I need to do work X”, set certain conditions and the frequency of repetitions if the first attempt was unsuccessful:

val uploadWorkRequest: WorkRequest = 
    OneTimeWorkRequest.Builder(UploadWorker::class.java)
        .addTag(UPLOAD_WORK_TAG)
.setConstraints(uploadConstraints())
        .setBackoffCriteria(BackoffPolicy.EXPONENTIAL, Duration.ofMinutes(10))
        .build()
val operation: Operation = WorkManager.getInstance(context)
    .enqueue(uploadWorkRequest)

Along with launching one-time requests, it is also possible to launch periodic ones:

val uploadWorkRequest: WorkRequest =
    PeriodicWorkRequest.Builder(UploadWorker::class.java, Duration.ofHours(1))
        .addTag(UPLOAD_WORK_TAG)
 .setConstraints(uploadConstraints())
        .build()
val operation: Operation = WorkManager.getInstance(context)
    .enqueue(uploadWorkRequest)

There is an important nuance here: periodic operation does not mean launching every minute. The minimum interval between jobs is 15 minutes.

Let’s move on to the conditions: conditions can be set completely different. There are, in fact, a lot of them and they are replenished with each version update. It is now possible to set conditions related to the network, charging the device, performing work instantly or when the device is in rest mode:

Constraints(
    requiredNetworkType = NetworkType.CONNECTED, // Требования к сети
    requiresCharging = false, // зарядка
    requiresDeviceIdle = false, // устройство не используется
    requiresBatteryNotLow = false, // не низкий заряд батареи
    requiresStorageNotLow = true, // не мало свободной памяти
)

There is also a trigger option for content providers: if you have some data in a certain content provider that changes, WorkManager can also trigger and start, send synchronization:

Constraints(
    // Время в течение которого не должно быть изменений по заданному Uri
    contentTriggerUpdateDelayMillis = NO_DELAY,
    // Максимальная задержка запуска выполнения работы с первого изменения
    contentTriggerMaxDelayMillis = NO_DELAY,
    // Uri контента в ContentProvider
    contentUriTriggers = setOf(
Constraints.ContentUriTrigger(
contentUri, isTriggeredForDescendants = false
)
)
)

Let me give you an example: let’s say you store some data in a content provider. As soon as you save something, the content provider will trigger the changes and your Worker will work. But it is important to remember that we have 15 minutes, that is, this process occurs once every 15 minutes.

WorkManager can be prioritized. The Set Expedited Job feature has appeared, it means that the task must be completed right now:

val uploadWorkRequest: WorkRequest =
    PeriodicWorkRequest.Builder(UploadWorker::class.java, Duration.ofHours(1))
        .addTag(UPLOAD_WORK_TAG)
 .setConstraints(uploadConstraints())
 .setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST)
         .build()
val operation: Operation = WorkManager.getInstance(context)
    .enqueue(uploadWorkRequest)

Do not expect that you will do any work in Expedited and all processes will immediately start working. This is suitable for special occasions when you need to launch here and now. There is a limitation – the task should be short (up to a minute).

Also, in WorkManager, you can divide a task into subtasks, and, accordingly, combine them into chains. So, some subtasks can be executed in parallel, others can be dependent on each other:

Let’s summarize WorkManager:

  • It is connected as a separate library and takes into account the capabilities of all versions of Android;

  • Not all features appear at the same time as the JobScheduler (for example, Job priorities did not appear in the WorkManager);

  • The minimum interval for running periodic tasks is 15 minutes (you need to understand that this is a system mechanism for synchronization, and it is affected by all the operations that were done in Android to optimize power consumption. Therefore, it does not allow you to perform everything in a row and instantly. A 15-minute interval, in in turn, does not overload the system);

  • Work is limited by system mechanisms (App Standby, Doze Mode, Battery Saver, system execution quotas, etc.);

  • Additional features compared to JobScheduler (chains from Work, work in multiple processes, support for Coroutine and RxJava).

  1. Download Manager

Because WorkManager has limitations, you need to consider which ABIs you can use to minimize the complexity. The first and easiest option is DownloadManager.

DownloadManager is a simple system API, its task is to ask some kind of request directly and, accordingly, send it to download a file. You can set the data for the notification, as well as various parameters and the location where the specified file should be saved:

DownloadManager.Request(remoteFileUri)
 // Указываем будет ли показываться уведомление или нет
    .setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE)
    // Текст для уведомления
    .setTitle("Sample download")
    .setDescription("Download file")
 // Задаем требования для выполнения загрузки
    .setAllowedOverRoaming(false)
    .setAllowedOverMetered(true)
    .setRequiresCharging(false)
    .setRequiresDeviceIdle(false)
// Куда сохранять файл на устройстве
    .setDestinationInExternalFilesDir(context, Environment.DIRECTORY_DOWNLOADS, "")

Then you can set a request , take a system service and put your download there:

val request: DownloadManager.Request = …
val downloadManager: DownloadManager = context.getSystemService()
val downloadId: Long = downloadManager.enqueue(request)

You can then monitor via BroadcastReceiver when the download is complete:

class DownloadsBroadcastReceiver : BroadcastReceiver() {
    override fun onReceive(context: Context, intent: Intent) {
        val downloadInfo = DownloadInfo.fromDownloadIntent(context, intent)
        // Обрабатываем информацию о загрузке
    }
}

You can also get download information from DownloadManager:

val downloadId = intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, 0)
if (downloadId == 0L) return
val downloadManager: DownloadManager = checkNotNull(context.getSystemService())
val query = DownloadManager.Query().setFilterById(downloadId)
val downloadInfo: Cursor = downloadManager.query(query)
if (downloadInfo.count == 0) return
downloadInfo.moveToFirst()
DownloadInfo(
    status = downloadInfo.getInt(downloadInfo.getColumnIndex(DownloadManager.COLUMN_STATUS)),
    reason = downloadInfo.getInt(downloadInfo.getColumnIndex(DownloadManager.COLUMN_REASON)),
    localFileUri =
downloadInfo.getString(
downloadInfo.getColumnIndex(DownloadManager.COLUMN_LOCAL_URI)
).toUri()
)

* DownloadInfo – own class designed for convenience

Also you can register BroadcastReceiver from code:

context.registerReceiver(
    DownloadsBroadcastReceiver(),
    IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE),
    Context.RECEIVER_EXPORTED
)

What we have as a result with DownloadManager:

  • Can only download from simple HTTP/HTTPS links;

  • The file is saved only to public places;

  • You will have to work with Cursor;

  • Inability to receive progress updates through notifications.

  1. Foreground Service

Why Foreground? We have three types of services: Background, Foreground and Bound. Background are dead, let’s be honest. Bound is only needed if you are working between processes. And Foreground remained relevant.

What can Foreground Service do?

  • The only working option;

  • Need to declare permission;

  • You need to declare a task type;

  • Can be stopped via Task Manager.

What does working with Foreground Service look like? You start it (now, when you start the Foreground Service, a checkbox pops up every time asking for what purpose you start it):

class MediaService : Service() {
    override fun onCreate() {
        super.onCreate()
        startForeground(
            NOTIFICATION_ID,
            newForegroundNotification(),
       ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK
        )
    }
}

Then you will need to specify these flags when declaring service in the manifest:

<manifest>
    <application>
        <service
            android:name=".MediaService"
            android:foregroundServiceType="mediaPlayback"
            android:exported="false"
            />
    </application>
</manifest>

In Android 14, there was a change: all services are now required to declare the type of Foreground tasks for which they can run. It can be one task or several, but without this Foreground Service will not be able to start. Why do you think this is being done? Really, it’s all about control. All types of services clearly describe what tasks can be performed in the Foreground Service.

Foreground Service types in Android 14:

  • camera (video calls);

  • connectedDevice (connection with devices via Bluetooth, NFC, USB, etc.);

  • dataSync (transferring/receiving data over the network, local file processing, importing/exporting files);

  • health (fitness apps);

  • location (navigation, share location);

  • mediaPlayback (audio and video playback);

  • mediaProjection (sharing content to secondary or external display. Does not include ChromeCast SDK);

  • microphone (for applications with voice calls);

  • remoteMessaging (transfer of text messages between the user’s devices to continue working);

  • shortService (the end of a short important task for the user that cannot be interrupted or postponed);

  • specialUse (all other cases that are not covered by other types);

  • systemExempted (system applications and integrations that need to use the Foreground Service).

  1. Sync Adapter

If you use an Android smartphone, you most likely saw the ability to enable synchronization in the settings. This is a showcase of how the user sees the Sync Adapter. What is it used for?

To a greater extent, this is necessary in order to synchronize data between the client and server as quickly as possible. For example, this is how contacts work. You corrected something in the contact and the information was immediately sent to the server thanks to this mechanism.

This is how the implementation of the synchronization mechanism looks like:

As you can see, it’s not just one class. Far from alone. To implement this, we need:

  • Authenticator (user account in the service, can be stub);

  • ContentProvider – a data source that notifies about data changes (can be stub);

  • SyncAdapter is the mechanism that links all these parts;

  • Sync Service – which starts it all;

  • Sync Adapter XML;

  • Get special sync permissions.

If you need real time synchronization, then this is a good solution. If you need to execute something at a precise time, then you need an AlarmManager.

  1. AlarmManager

Let’s start with the basic facts about the AlarmManager:

  • allows you to set alarms at approximate time, exact time and intervals;

  • dispatches an Intent at the time it fires;

  • requires Android 12+ permission and Google Play approval.

How it all looks:

val alarmManager: AlarmManager = checkNotNull(context.getSystemService())
val intent = Intent(context, AlarmReceiver::class.java)
val alarmPendingIntent =
PendingIntent.getBroadcast(context, requestId, intent, FLAG_IMMUTABLE)
alarmManager.set(
    AlarmManager.ELAPSED_REALTIME_WAKEUP,
    SystemClock.elapsedRealtime() + 1.minutes().toMillis(),
    alarmPendingIntent
)

We can specify the time in different ways: absolute time, time since the device was loaded (as I indicated in the example above). In the example, I set the alarm in a minute. Will it work in a minute? No. It worked for me much later, and moreover randomly.

alarmManager.set(
    type = AlarmManager.ELAPSED_REALTIME_WAKEUP,
    triggerAt = SystemClock.elapsedRealtime() + 1.minutes().toMillis(),
    operation = alarmPendingIntent
)

Why is that? Because the .set method, as it were, sets the alarm, but in the approximate range of operation.

There is also a .setExact method. With it, there are already more guarantees for the alarm to go off on time, but also not a fact:

alarmManager.setExact(
    type = AlarmManager.ELAPSED_REALTIME_WAKEUP,
    triggerAt = SystemClock.elapsedRealtime() + 1.minutes().toMillis(),
    operation = alarmPendingIntent
)

That’s not all, because there is also .setExactAndAllowWhileIdle 🙂 This method appeared when Android introduced the deep sleep mode. When the phone is in deep sleep mode, it has activity windows. Set and setExact can only fire in these activity windows, while setExactAndAllowWhileIdle can fire in any mode and fires on time:

alarmManager.setExactAndAllowWhileIdle(
    type = AlarmManager.ELAPSED_REALTIME_WAKEUP,
    triggerAt = SystemClock.elapsedRealtime() + 1.minutes().toMillis(),
    operation = alarmPendingIntent
)

Why use the first two methods, I don’t know. Everyone only uses .setExactAndAllowWhileIdle.

But the methods do not end there – there is also .setAlarmClock. Its principle of operation is the same as that of .setExactAndAllowWhileIdle, but it is also intended for the case when your display should turn off:

alarmManager.setAlarmClock(
    info = AlarmClockInfo(
        triggerTime = System.currentTimeMillis() + 1.minutes().toMillis(),
        showIntent = alarmIntent
    ),
    operation = alarmIntent
)

There is also the .setWindow method, here you set the interval within which the alarm should go off:

alarmManager.setWindow(
    type = AlarmManager.ELAPSED_REALTIME_WAKEUP,
    windowStart = SystemClock.elapsedRealtime() + 1.minutes().toMillis()
    windowDuration = 5.minutes().toMillis(),
    action = alarmPendingIntent
)

It is important to remember that now it is mandatory to set permission for all alarms:

<manifest>
    <uses-permission
        android:name="android.permission.SCHEDULE_EXACT_ALARM"
        android:maxSdkVersion="32"
        />
    <!--  На Android 13+ (API Level 33+)  -->
    <uses-permission android:name="android.permission.USE_EXACT_ALARM" />
</manifest>

Why are there two?

Android 12 introduced permission.SCHEDULE_EXACT_ALARM (special access), and Android 13 introduced permission.USE_EXACT_ALARM (special access granted by Google Play, only apps that comply with the Google Play Policies and Rules can get it). USE_EXACT_ALARM actually gives the right to always set alarms without additional requests from Google Play.

SCHEDULE_EXACT_ALARM is the case where you can’t get USE_EXACT_ALARM, but as of Android 14 it won’t be issued anymore. Further, everything will be tied to Policies and your ability to explain to the user why he is given permission.

  1. System optimizations

On top of that, we also have system optimizations for all the background work. How Android reduces battery consumption:

  • dose mode;

  • App Standby;

  • battery saver;

  • Restrictions on running Services;

  • Stop background processes;

  • Vendor optimization.

There is a DontKillMyApp rating:

In fact, this is an anti-rating of how phones are optimized by aggressively killing various background processes. For a long time, Xiaomi was far behind in this rating, but Samsung took the palm.

Vendors do this because they want you to have a better experience as a user. They also create their own whitelist of applications that should work well: social networks, for example (as you probably need push notifications to arrive on time). It is possible to agree with the vendor on getting into this white list, but it is very difficult.

On that website you can see the developer stories:

You can find which optimizations are applied on which versions of Android by vendors:

And also find ways to tell users how to disable battery optimization on their devices (maybe in addition to the standard mechanism, there is also a proprietary vendor one):

You will be surprised how many ways to disable optimization:

If you want to disable optimizations, there is a standard way that was introduced in Android 12+. It’s available in the app’s settings and lets you choose from three settings:

Android also has Battery Optimization. You can ask the user to set “Ignore all optimizations”:

  1. How to choose an API for a task

So we got to the most important point: how to choose an API in all this variety?

First, you now need to beg the user to give you an exception and turn off optimizations if it is important for you to perform actions instantly.

What else is there from the ways? It is important for you to determine whether you really need to run the task exactly at the given time:

The next question that is important to ask yourself is: “Do we have an end result?”

Again, byagain, the main question you need to ask yourself is: “Will the user experience get significantly worse if the task runs later?”

Try to understand what will happen if you complete the given work not instantly, but in a minute, in 10, in 15 minutes. Will it affect the user experience?

All optimizations, API changes, and behavior changes should be planned with the importance of doing things instantly. Perhaps by doing the work a little later, you will make the user experience better and this will not critically affect the user. If you send data that needs to be synced every day during that day instead of instantly, it won’t be affected. You can, as an option, show a notification that we are waiting for Wi-Fi to appear.

There are different options, it is only important to understand the time window that you can afford to complete tasks.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *