Background Work in Android Studio

Overview

You should not do blocking tasks on UI Thread. Such as
  • decoding bitmap
  • accessing storage
  • working on machine learning model
  • network request

Definition of background work

An app is running in the background when both the following conditions are satisfied:
  • No activities of an application are currently visible to the user.
  • When an application was visible to the user then that application didn't start any foreground services.

Type of background work

Approaches to background work

  • Below figure can be used to guide which approach is used for a particular type of work/task.

Immediate Work

  • Immediate work encompasses tasks which need to execute right away.
  • For persistent immediate work, you should use WorkManager with a OneTimeWorkRequest. Expedite a WorkRequest with setExpedited().
  • For impersistent immediate work, you should you use Kotlin coroutines. If your app uses the Java programming language, you should use RxJava or Guava. You can also use Executors.

Long-Running Work

  • Work is long running if it is likely to take more than ten minutes to complete.
  • WorkManager allows you to handle such tasks using a long-running Worker.

Deferrable Work

  • Deferrable work is any work that does not need to run right away.
  • Scheduling deferred work through WorkManager is the best way to handle tasks that don't need to run immediately but which ought to remain scheduled when the app closes or the device restarts.

Alarms

  • Alarms are a special use case that are not a part of background work.
  • You should only use AlarmManager only for scheduling exact alarms such as alarm clocks or calendar events.

Replacing Foreground Services

  • Android 12 restricts launching foreground services from the background. For most cases, you should use setForeground() from WorkManager rather than handle foreground services yourself. 
  • Some use cases of Foreground Services
    • Media Playback
    • Activity Tracking
    • Location Sharing
    • Voice or Video Calls

Running Android Tasks in Background

  • All Android apps use a main thread to handle UI operations. 
  • Calling long-running operations from this main thread can make your application freeze and unresponsive.
  • You can create additional background threads to handle long-running operations and leave the main thread to handle UI updates

Creating Multiple Threads

  • A thread pool is a managed collection of threads that run your tasks in parallel from a queue. New tasks are executed on some existing threads that becomes idle.
  • To send a task to a thread pool, use the ExecutorService interface. ExecutorService is not a Service
  • Creating threads is expensive, so you should create a thread pool only once as your app initializes.
 class MyApplication : Application() {
private var executorService: ExecutorService? = null
override fun onCreate() {
super.onCreate()
executorService = Executors.newFixedThreadPool(4)
}

companion object {
const val TAG: String = "MyApplication"
}
}

Executing in a Background Thread

  • Making a network request on the main thread causes the thread to wait, or block, until it receives a response. 
  • Since the thread is blocked, the OS can't call onDraw(), and your app freezes, potentially leading to an Application Not Responding (ANR) dialog.
  • The solution to this problem is to use thread pool.
  • Any thread in your app can run in parallel to other threads, including the main thread, so you should ensure that your code is thread-safe. For this purpose, you should avoid sharing mutable state between threads whenever possible.

Communicating with the main thread

  • We can use callbacks to communicate with the main thread. Make sure to add such callback methods in the execute() of Executor in ViewModel.
  • Note: To communicate with the View from the ViewModel layer, use LiveData as recommended in the Guide to app architecture. If the code is being executed on a background thread, you can call MutableLiveData.postValue() to communicate with the UI layer.

Using Handlers

  • Handler adds the action which is to be performed on different threads, in a queue. Now to specify which thread is to be used to run the action we can use Looper
  • A Looper is an object that runs the message loop for an associated thread.
  • To retrieve the main thread we can use the getMainLooper() method of Looper object.
  • Be sure to save the instance of the Handler either in your Application class or in a dependency injection container (where you have created the Executor earlier).
  • For best practices, we should inject the Handler into the repository and for more flexibility we can pass the Handler into each function too. In this way, we can directly modify the UI from the callback or use LiveData.setValue() to communicate with the UI.

Configuring a Thread Pool

  • You can create a thread pool in two ways
    • Using Executor
    • Using ThreadPoolExecutor
  • Below is the example of creating thread pool in both the ways
 class MyApplication : Application() {
private val NUMBER_OF_CORES = Runtime.getRuntime().availableProcessors()
private val workQueue: BlockingDeque<Runnable> = LinkedBlockingDeque<Runnable>()
private val KEEP_ALIVE_TIME = 1L
private val KEEP_ALIVE_TIME_UNIT = TimeUnit.SECONDS
private val threadPoolExecutor: ThreadPoolExecutor = ThreadPoolExecutor(
NUMBER_OF_CORES, NUMBER_OF_CORES, KEEP_ALIVE_TIME, KEEP_ALIVE_TIME_UNIT, workQueue
)
private var executorService: ExecutorService? = null
private val mainThreadHandler: Handler = HandlerCompat.createAsync(Looper.getMainLooper())
override fun onCreate() {
super.onCreate()
executorService = Executors.newFixedThreadPool(4)
threadPoolExecutor.execute {
Log.i(TAG,"Running a task in ThreadPool")
}
}

companion object {
const val TAG: String = "MyApplication"
}
}

Concurrency Libraries

Background Optimizations

  • Background processes can be memory- and battery-intensive. 
  • An implicit broadcast may start many background processes that have registered to listen for it. This can have a substantial impact on both device performance and user experience.

User-Initiated Restrictions

  • Beginning in Android 9 (API level 28), if an app exhibits some of the bad behaviors described in Android vitals, the system prompts the user to restrict that app's access to system resources.
  • If the system notices that an app is consuming excessive resources, it notifies the user, and gives the user the option of restricting the app's actions.

Schedule network jobs on unmetered conditions.

 private val MY_BACKGROUND_JOB = 0;
fun scheduleJob(context: Context) {
val jobScheduler = context.getSystemService(Context.JOB_SCHEDULER_SERVICE) as JobScheduler
val job = JobInfo.Builder(MY_BACKGROUND_JOB, ComponentName(context, MyJobService::class.java))
.setRequiredNetworkType(JobInfo.NETWORK_TYPE_UNMETERED)
.setRequiresCharging(true)
.build()
jobScheduler.schedule(job)
}
  • When the conditions for your job are met, your app receives a callback to run the onStartJob() method in the specified JobService.class. 
 class MyJobService: JobService() {
override fun onStartJob(parameters: JobParameters?): Boolean {
return true;
}

override fun onStopJob(parameters: JobParameters?): Boolean {
return true;
}
}

Monitor network connectivity while the application is running.

Restrictions on receiving image and video broadcasts

  • In Android 7.0 (API level 24), apps are not able to send or receive ACTION_NEW_PICTURE or ACTION_NEW_VIDEO broadcasts.
  • You can use JobInfo and JobParameters to support such features.
    • JobInfo API is used to trigger jobs on content URI, to learn more click here.
 private val MY_BACKGROUND_JOB = 0;
fun scheduleJob(context: Context) {
val jobScheduler = context.getSystemService(Context.JOB_SCHEDULER_SERVICE) as JobScheduler
val job =
JobInfo.Builder(MY_BACKGROUND_JOB, ComponentName(context, MyJobService::class.java))
.addTriggerContentUri(
JobInfo.TriggerContentUri(
MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
JobInfo.TriggerContentUri.FLAG_NOTIFY_FOR_DESCENDANTS
)
)
.build()
jobScheduler.schedule(job)
}
    • JobParameters API allow your application to receive useful information about what content authorities and URIs triggered the job, to learn more click here.
 class MyJobService : JobService() {
override fun onStartJob(parameters: JobParameters): Boolean {
StringBuilder().apply {
append("Media Content has changed: \n")
parameters.triggeredContentAuthorities?.also { authorities ->
append("Authorities : ${authorities.joinToString(", ")} \n")
append(parameters?.triggeredContentUris?.joinToString("\n"))
} ?: append("No Content")
Log.i(MyApplication.TAG, toString())
}
return true
}

override fun onStopJob(p0: JobParameters?): Boolean {
return true
}
}

Further Optimize your application

  • Optimizing your apps to run on low-memory devices, or in low-memory conditions, can improve performance and user experience.
  • To simulate conditions where implicit broadcasts and background services are unavailable
 $ adb shell cmd appops set <package_name> RUN_IN_BACKGROUND ignore
  • To re-enable implicit broadcasts and background services
$ adb shell cmd appops set <package_name> RUN_IN_BACKGROUND allow

Broadcasts Overview

  • Android apps can send or receive broadcast messages from the Android system and other Android apps.
  • Apps can register to receive specific broadcasts. When a broadcast is sent, the system automatically routes broadcasts to apps that have subscribed to receive that particular type of broadcast.

System Broadcasts

  • The system automatically sends broadcasts when various system events occur. These broadcasts are sent to all apps that are subscribed to receive the event.
  • The broadcast message itself is wrapped in an Intent object whose action string identifies the event that occurred.
  • For a complete list of system broadcast actions, see the BROADCAST_ACTIONS.TXT file in the Android SDK.

Changes to System Broadcasts

  • Beginning with Android 9 (API level 28), The NETWORK_STATE_CHANGED_ACTION broadcast doesn't receive information about the user's location or personally identifiable data.
  • If your app targets Android 8.0 or higher, you cannot use the manifest to declare a receiver for most implicit broadcasts. You can still use a context-registered receiver when the user is actively using your app.

Receiving Broadcasts

 <receiver
android:name=".broadcasts.MyBroadcast"
android:exported="false">
<intent-filter>
<action android:name="android.intent.action.AIRPLANE_MODE" />
</intent-filter>
</receiver>
      • Attribute andoird:name and android:exported is mandatory
      • The intent filters specify the broadcast actions your receiver subscribes to.
      • Create a subclass of BroadCastReceiver and overrice onReceive(Context, Intent)
 class MyBroadcast : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
if (intent?.action.equals(Intent.ACTION_AIRPLANE_MODE_CHANGED)) {
Toast.makeText(context, "Airplane Mode Changed", Toast.LENGTH_LONG).show()
}
}
}
      • The system package manager registers the receiver when the app is installed. 
      • The receiver then becomes a separate entry point into your app which means if your application is not running, the system can start the app and deliver the broadcast through that entry point.
      • The system creates a new BroadcastReceiver component object to handle each broadcast that it receives. This object is valid only for the duration of the call to onReceive(Context, Intent). Once your code returns from this method, the system considers the component no longer active (this means onRecieve() runs on main thread in foreground).
    • Context-registered Receivers
      • Create an instance of BroadcastReceiver in the application component (Activity or Fragment)
 val br: BroadcastReceiver = MyBroadcast()
 val filter = IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION).apply {
addAction(Intent.ACTION_AIRPLANE_MODE_CHANGED)
}
registerReceiver(br, filter)
      • If you register within an Activity context, you receive broadcasts as long as the activity is not destroyed.
      • If you register with the Application context, you receive broadcasts as long as the app is running.
    • To stop receiving broadcasts, call unregisterReceiver(android.content.BroadcastReceiver). Be sure to unregister the receiver when you no longer need it or the context is no longer valid.
    • If you register a receiver in onCreate(Bundle) using the activity's context, you should unregister it in onDestroy() to prevent leaking the receiver out of the activity context.
    • If you register a receiver in onResume(), you should unregister it in onPause() to prevent registering it multiple times.

Effects on process state

  • When a process executes a receiver (that is, currently running the code in its onReceive() method), it is considered to be a foreground process
  • Once your code returns from onReceive(), the BroadcastReceiver is no longer active.
  • Manifest-declared receiver is considered to be low-priority by the system and can be killed if required.
  • You should not start long running background threads from a broadcast receiver. After onReceive(), the system can kill the process at any time to reclaim memory, and in doing so, it terminates the spawned thread running in the process.
  • To avoid this you should use either goAsync() or schedule a JobService from the receiver using the JobScheduler.

Sending Broadcasts

  • Android provides three ways to send broadcasts from an application component (Activity / Fragment)
    • sendOrderedBroadcast (Intent, String)
      • This method sends broadcasts to one receiver at a time.
      • Each receiver executes in turn.
      • The data can propagate from one receiver to another receiver.
      • A receiver can abort other receivers.
      • The order receivers run in can be controlled with the android:priority attribute of the matching intent-filter; receivers with the same priority will be run in an arbitrary order.
 <intent-filter android:priority="@integer/material_motion_duration_short_1">
<action android:name="android.intent.action.AIRPLANE_MODE" />
</intent-filter>
    • sendBroadcast (Intent)
      • This method sends broadcasts to all receivers in an undefined order.
      • Receivers cannot receive data from another receivers.
      • The data cannot propagate from one receiver to another receiver. 
      • A receiver cannot abort other receivers.
 Intent().also { intent ->
intent.setAction("com.example.myapplication.MY_NOTIFICATION")
intent.putExtra("data", "Sent from MY_NOTIFICATION")
sendBroadcast(intent)
}
      • The intent's action string must provide the app's Java package name (e.g. "com.example.myapplication") syntax and uniquely identify the broadcast event.
    • LocalBroadcastManager.sendBroadcast
      • This method can send broadcasts to receivers that are in the same app as the sender.
      • If you don't need to send broadcasts across apps, use local broadcasts.
      • You don't need to worry about any security issues related to other apps being able to receive or send your broadcasts.

Requesting Broadcast with Permissions

  • You can enforce restrictions on either the sender or receiver of a broadcast with the help of permissions.
  • When you call sendBroadcast() or sendOrderedBroadcast(), you can specify a permission parameter.
  • Only receivers who have requested that permission with the tag in their manifest (and subsequently been granted the permission if it is dangerous) can receive the broadcast.
 sendBroadcast(intent, android.Manifest.permission.SEND_SMS)
  • To receive the broadcast, the receiving app must request the permission as shown below
 <uses-permission android:name="android.permission.SEND_SMS"/>
  • You can specify either an existing system permission like SEND_SMS or define a custom permission with the <permission> element.

Receiving with Permissions

  • If you specify a permission parameter when registering a broadcast receiver (either with registerReceiver(BroadcastReceiver, IntentFilter, String, Scheduler) or in <receiver> tag in your manifest), then only the broadcasters who have requested the permission with the <uses-permission> tag in their manifest can send an Intent to the receiver.
  • Permission in manifest-declared receiver.
 <receiver
android:name=".broadcasts.MyBroadcast"
android:exported="false"
android:permission="android.permission.SEND_SMS">
<intent-filter android:priority="@integer/material_motion_duration_short_1">
<action android:name="android.intent.action.AIRPLANE_MODE" />
</intent-filter>
</receiver>
  • Permission in context-registered receiver.
 registerReceiver(br, filter, android.Manifest.permission.SEND_SMS,null)
  • In order to send the broadcasts to the above receivers, the sending app must request the permission as shown below
 <uses-permission android:name="android.permission.SEND_SMS" />

Implicit Broadcast Exceptions

  • As part of the Android 8.0 (API level 26) Background Execution Limits, apps that target the API level 26 or higher can no longer register broadcast receivers for implicit broadcasts in their manifest.
  • To learn more please click here.

Manage Device Awake State

  • When an Android device is left idle, it will first dim, then turn off the screen, and ultimately turn off the CPU. This prevents the device's battery from quickly getting drained
    • Keep the Device Awake
      • Alternatives to using wake locks
        • If your app is performing long-running HTTP downloads, consider using DownloadManager.
        • If your app is synchronizing data from an external server, consider creating a sync adapter.
        • If your app relies on background services, consider using JobScheduler or Firebase Cloud Messaging to trigger these services at specific intervals.
        • If you need to keep your companion app running whenever a companion device is within range, use Companion Device Manager.
      • Keep the screen on
        • The best way to do this is to use the FLAG_KEEP_SCREEN_ON in your activity (and only in an activity, never in a service or other app component)
 override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
Log.d("MainActivity", "MyApplication :: MainActivity :: onCreate")
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
}
        • This approach doesn't require special permission.
        • You can also use android:keepScreenOn attribute in the xml file.
 <?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:keepScreenOn="true"
tools:context=".MainActivity">

</androidx.constraintlayout.widget.ConstraintLayout>
        • Though it doesn't require but if you want to clear the flag then you can use. 
 window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
      • Keep the CPU on
        • If you need to keep the CPU running in order to complete some work before the device goes to sleep, you can use Wake Locks (a feature of PowerManager).
        • You should use wake locks only when it is strictly necessary and use it as short as possible. You should never use a wake lock in an activity. Wake Locks will impact your battery life.
        • To use a wake lock we must first declare WAKE_LOCK in AndroidManifest.xml file
 <uses-permission android:name="android.permission.WAKE_LOCK"/>
        • Using a wake lock in broadcast receiver
 class MyBroadcast : BroadcastReceiver() {
var mContext: Context? = null
val wakeLock: PowerManager.WakeLock =
(mContext?.getSystemService(Context.POWER_SERVICE) as PowerManager).run {
newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "MyApp :: MyWakeLockTag").apply {
acquire(2 * 60 * 1000L /*2 minutes*/)
}
}

override fun onReceive(context: Context?, intent: Intent?) {
mContext = context
if (intent?.action.equals(Intent.ACTION_AIRPLANE_MODE_CHANGED)) {
Toast.makeText(context, "Airplane Mode Changed", Toast.LENGTH_LONG).show()
}
wakeLock.release()
}
}
 <receiver android:name=".broadcasts.MyWakefulReceiver"></receiver>
        • Then create an IntentService
 class WakefulIntentService : IntentService("WakefulIntentService") {
private val notificationManager: NotificationManager? = null
internal val builder: NotificationCompat.Builder? = null
override fun onHandleIntent(intent: Intent?) {
val extras: Bundle = intent?.extras!!
MyWakefulReceiver.completeWakefulIntent(intent) // This line is not working
}
}
        • Then create a subclass of WakefulBroadcastReceiver
 class MyWakefulReceiver : WakefulBroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
Intent(context, WakefulIntentService::class.java).also { service ->
startWakefulService(context, service)
}
}
}
    • Set a Repeating alarm
      • Repeating alarms allow the system to notify your app repeatedly.
      • A repeating alarm has the following characteristics
        • A alarm type.
        • A trigger time.
        • The alarm's interval.
        • A pending intent.
      • To create an instance of PendingIntent intent pass the flag FLAG_NO_CREATE to PendingIntent.getService().
 val alarmManager = context?.getSystemService(Context.ALARM_SERVICE) as AlarmManager
val pendingIntent =
PendingIntent.getService(context, 12, intent!!, PendingIntent.FLAG_NO_CREATE)
if (pendingIntent != null && alarmManager != null) {
alarmManager.cancel(pendingIntent)
}

Using a ListenableFuture

  • ListenableFuture is not a part of Android Framework and instead provided by Guava.
  • ListenableFuture is the result of an asynchronous computation: a computation that may or may not have finished producing a result yet.
  • ListenableFuture is used in Jetpack libraries such as CameraX or Health Services.
  • Required Libraries
 implementation "com.google.guava:guava:31.0.1-android"
// To use CallbackToFutureAdapter
implementation "androidx.concurrent:concurrent-futures:1.1.0"
// Kotlin
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-guava:1.6.0"

Do's and Don'ts

  • You should not do blocking tasks on UI Thread.
  • Apps cannot send or receive ACTION_NEW_PICTURE or ACTION_NEW_VIDEO broadcasts.
  • Apps targeting Android 7.0 (API level 24) and higher do not receive CONNECTIVITY_ACTION broadcasts if they declare their broadcast receiver in the manifest. Apps will still receive CONNECTIVITY_ACTION broadcasts if they register their BroadcastReceiver with Context.registerReceiver() and that context is still valid.
  • Do not unregister in onSaveInstanceState(Bundle), because this isn't called if the user moves back in the history stack.
  • You should not start long running background threads from a broadcast receiver.
  • If you don't need to send broadcasts across apps, use local broadcasts.
  • The most important topic of BroadcastReceiver is the Security Considerations and Best Practices.
  • You should use wake locks only when strictly necessary and hold them for as short a time as possible. Means, you should never need to use a wake lock in an activity.
  • If a PendingIntent is created with the flag FLAG_ONE_SHOT, it cannot be canceled.
  • We recommend to use Elapsed Real Time alarm if you can.

Reference



Comments

Popular posts from this blog

Architecture Components in Android

DataBinding in Android

SSLSocketFactory in Android