Background Execution Limits for Android Devices

Written by: David Roman

By Andrii Savytskyi, Android Developer at WeAreBrain.


I recently faced a challenging task for one of my projects: how do I send push notifications to users if they haven’t been interacting with an application for more than 6+ days on any devices using Android LOLLIPOP through to Android N?

After I devoted time to carefully think about all the elements involved, I came up with a gameplan on how I would tackle this task. I separated this challenge into 2 subtasks:

  1. Building a continuous checking system which will monitor whether the user has been active on the application or not.
  2. How and when to send the notifications.

How does it work?

I decided to represent the process through descriptions of the main components’ responsibilities in the time diagram below.

MainActivity.kt

The launcher activity is responsible for setting the time of the last app launch and the start of the background service in the case of the first app launch.

lass MainActivity : AppCompatActivity() {

    val storage: Storage by inject()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        if (storage.isFirstRun()) {
            RecentRunService.enqueueWork()
            storage.setIsFirstRun(false)
        }

        storage.setLastRun(System.currentTimeMillis())
    }

    companion object {
        const val TAG = "MainActivity"
        fun getIntent(context: Context) = Intent(context, MainActivity::class.java)
    }
}

RecentRunService.kt

The background service is from JobIntentService. I decided to choose JobIntentService because the service can execute work from the background without any notifications. As you may know, Service and IntentService cannot work in the background on Android O and above, according to Background Execution Limits.

<service
            android:name=".services.RecentRunService"
            android:permission="android.permission.BIND_JOB_SERVICE" />

According to Developer.Android “Job services must be protected with this permission: android.permission.BIND_JOB_SERVICE. If a job service is declared in the manifest but not protected with this permission, that service will be ignored by the system”.

class RecentRunService : JobIntentService(), KoinComponent {

    val notificationManager: PushNotificationManager by inject()
    val alarmManager: AlarmManager by inject()
    val storage: Storage by inject()

    override fun onHandleWork(intent: Intent) {
        if (storage.getLastRun() < (System.currentTimeMillis() - DELAY)) {
            notificationManager.show(context)
        }
        alarmManager.setAlarm(context, DELAY)
        stopSelf(JOB_ID)
    }

    companion object {
        const val JOB_ID = 0x01
        const val TAG = "RecentRunService"
        private val DELAY = 10000L

        fun enqueueWork() {
            enqueueWork(context, RecentRunService::class.java, JOB_ID, Intent())
        }
    }
}

AlarmReceiver.kt

A broadcast receiver is registered in AndroidManifest.xml to receive explicit broadcasts.

<receiver
            android:name=".receivers.AlarmReceiver"
            android:enabled="true"
            android:exported="true">
            <intent-filter>
                <action android:name="com.savitskiy.reminder.intent.action.ALARM" />
            </intent-filter>
        </receiver>

According to Broadcast Limitations, “Apps that target Android 8.0 or higher can no longer register broadcast receivers for implicit broadcasts in their manifest. An implicit broadcast is a broadcast that does not target that app specifically. For example, ACTION_PACKAGE_REPLACED is an implicit broadcast, since it is sent to all registered listeners, letting them know that some package on the device was replaced. However, ACTION_MY_PACKAGE_REPLACED is not an implicit broadcast, since it is sent only to the app whose package was replaced, no matter how many other apps have registered listeners for that broadcast.”

class AlarmReceiver : BroadcastReceiver() {
    override fun onReceive(context: Context?, intent: Intent?) {
        RecentRunService.enqueueWork()
    }

    companion object {
        const val TAG = "AlarmReceiver"

        const val ACTION = "com.savitskiy.reminder.intent.action.ALARM"

        fun getIntent(context: Context?) = Intent(context, AlarmReceiver::class.java).apply {
            action = ACTION
        }

    }
}

BootReceiver.kt

BroadcastReceiver is registered in AndroidManifest.xml to receive a broadcast after the device is booted.

<receiver
            android:name=".receivers.BootReceiver"
            android:enabled="true"
            android:exported="true">
            <intent-filter>
                <action android:name="android.intent.action.BOOT_COMPLETED" />
                <action android:name="android.intent.action.LOCKED_BOOT_COMPLETED" />
            </intent-filter>
        </receiver>

“android.intent.action.BOOT_COMPLETED”, “android.intent.action.LOCKED_BOOT_COMPLETED” are implicit broadcasts which could be declared in AndroidManifest.xml, are exempted from Broadcasts limitations.

class BootReceiver : BroadcastReceiver() {

    override fun onReceive(context: Context?, intent: Intent?) {
        RecentRunService.enqueueWork()
    }

    companion object {
        const val TAG = "BootReceiver"
    }
}

There you have it. You can now send push notifications to devices which are dormant for 6+ days. I implemented a lot of what is contained in this article “Android Notifications — An elegant way to build and display” by Jovche Mitrejchevski.

Thanks for reading, I’d be very glad if my article helps someone and makes their life a bit easier. You can review all projects on my github.

(Visited 244 times, 1 visits today)
Last modified: April 17, 2020
Author info
David Roman
David is our content & social media specialist. He helps with the planning, creation, and distribution of all the social content at WeAreBrain. He also loves green tea, listening to good music and using his analogue camera.
Close