Skip to main content

How to detect when your Android app has been backgrounded

Eric Romero
Eric Romero
Jun 20 - 5 min read

Thinking globally with Android

One of the great things about developing for Android as a platform is the application model. Instead of a central code execution paradigm, Android breaks the code initiation down into components such as Activities, Services, etc. Unfortunately this means some global functionality might not always be available; even a simple onAppBackground() callback remains elusive on Android.

There may be situations when your app needs to perform certain tasks upon background as to not disrupt the user experience when the app is foregrounded. For this reason we will share a simple solution to achieve this behavior.

A real world scenario

If you are familiar with Salesforce for Android, you may have noticed that some of your data will be pre-cached for offline use. The service that performs the data caching actually lives on the web layer of our hybrid app. Given the threading limitations of a Chromium-based WebView, if we want to pre-cache data without disrupting the user experience then we need to perform the caching in the background. While we’d like to schedule a task like this for later, our need for a bootstrapped and authenticated WebView would become a major obstacle. This is where a global onAppBackground() callback would be very useful!

Potential candidates

Our first instinct might be to inspect the list of tasks running and simply detect if our task is at the top. After all, ActivityManager did provide us with a convenient getRunningTasks() method, but unfortunately this approach is a non-starter. If you are hoping to develop for any device running Lollipop or higher, you should note that this method was deprecated long ago and is in fact no longer available.

Alternatively, Android provides a useful interface for listening to Activity-level callbacks, aptly named ActivityLifecycleCallbacks, that can be registered at the application level. Sounds promising, right? Not so fast though, because unless your app is single-Activity based, how will you know that the application is actually paused, and not just transitioning between your activities? We need something more global. Maybe something like a Service so that it is agnostic of which Activity is on top.

A solution emerges…

Enter the JobIntentService. JobIntentService is a great feature of the Android Support Library that takes the compatibility headaches out of scheduling jobs. When running Android Oreo or later, the work is dispatched as a job via JobScheduler.enqueue(), and on older versions it will use Context.startService(). The important thing is that it can be triggered outside of any Activity lifecycle.

That’s great, but how does it help?

This is where we get our hands a little dirty. The TL;DR is that we can trigger JobIntentService to run upon every Activity.onPause() callback, and cancel it every time we get Activity.onResume(), regardless of which of our Activities is transitioning. When the job begins, we simply wait for a short time, and if we did not receive word of another activity resuming, we can safely assume that our app is now backgrounded.

Let’s take a look at the code. First we construct our JobIntentService

public class BackgroundJobService extends JobIntentService {
    @Override
    protected void onHandleWork(@NonNull Intent intent) {}
}

In your Application.onCreate() method, register your lifecycle callbacks to listen for onActivityPause() and onActivityResume() events. For every pause, we want to enqueue work on the JobIntentService, and for every resume we will cancel it:

public class DetectBackgroundApp extends Application {
    @Override
    public void onCreate() {
        super.onCreate();
        registerActivityLifecycleCallbacks(
                new ActivityLifecycleCallbacks() {
                    @Override
                    public void onActivityPaused(Activity activity) {
                        BackgroundJobService.enqueueWork(activity);
                    }
                    @Override
                    public void onActivityResumed(Activity activity) {
                        BackgroundJobService.stopWork();
                    }
                    ... rest of the callbacks ...
                });
    }
}

Don’t forget to register the JobServiceIntent it in your AndroidManifest.xml, along with the permission for WAKE_LOCK:

<application
        android:name=".DetectBackgroundApp"
        ...
        >
        ...
       <service
            android:name=".BackgroundJobService"
            android:permission="android.permission.BIND_JOB_SERVICE" />
        ...
    </application>
    <uses-permission android:name="android.permission.WAKE_LOCK" />

Back in the JobIntentService, we can now use a simple Java lock to wait for a specified amount of time. When stopWork() is called, we can set a local variable to indicate a cancel before notifying the lock. When the lock is woken up and the cancel flag has not been set, we can be almost certain that the app is in the background. Time to do some work!

public class BackgroundJobService extends JobIntentService {
    public static final int JOB_ID = 1000;
    public static volatile boolean shouldContinue = false;
    private static final Object lock = new Object();
    private static final long BACKGROUND_TIMEOUT_WAIT_MS = 5000;
    final Handler mHandler = new Handler();
    
    public static void enqueueWork(Context context) {
        Intent bgJobService = new Intent(context, BackgroundJobService.class);
        BackgroundJobService.enqueueWork(
                context, BackgroundJobService.class, BackgroundJobService.JOB_ID, bgJobService);
    }
    
    public static void stopWork() {
        synchronized (lock) {
            shouldContinue = false;
            lock.notifyAll();
        }
    }
    
    @Override
    protected void onHandleWork(@NonNull Intent intent) {
        shouldContinue = true;
        try {
            synchronized (lock) {
                lock.wait(BACKGROUND_TIMEOUT_WAIT_MS);
                if (shouldContinue) {
                    // Do the background work here
                }
            }
        } catch (InterruptedException ex) {}
    }
}

Conclusion

By using a JobIntentService, we can ensure that not only are our activities indeed all paused, but also that anything left in-memory is still available to utilize. This solution has allowed us to unobtrusively pre-cache data for millions of users. It has withstood the test of time, and according to our analytics our WebView almost always survives the entire pre-caching process without being cleaned up by the operating system. (Before Android Oreo, we had used a normal IntentService for the same solution.)

One (or two) last thing(s)…

In certain situations, we’d like to access an external activity but remain within our task. An example would be launching the Camera app. In these cases we don’t want to trigger any background jobs. Since ActivityLifecycleCallbacks does not track external activities, our global onActivityResume() will not get triggered to prevent JobIntentService from completing. Fortunately, this problem can be easily remedied by attaching a flag to the current activity before launching the external one.

// Flag the current activity to skip background check upon pause
activity.getIntent().putExtra(BackgroundJobService.SKIP_BG_CHECK, true);

// Launch the Camera app to take a picture
Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
activity.startActivityForResult(intent, ActivityRequests.TAKE_PICTURE_REQUEST);

Before we launch the JobIntentService, we can easily check for this flag and prevent the service from running if it has been set.

@Override
public void onActivityPaused(Activity activity) {
    if (activity.getIntent() == null
            || !activity.getIntent().getBooleanExtra(BackgroundJobService.SKIP_BG_CHECK, false)) {
        BackgroundJobService.enqueueWork(activity);
    }
}

Lastly, you might find your app is throwing a SecurityException if running on Android Oreo or later. This error is currently being tracked by Google here and here. The good news is that this can be avoided by setting mJobImpl in your JobIntentService as follows:

public abstract class SafeJobIntentService extends JobIntentService {
    @Override
    public void onCreate() {
        super.onCreate();
        if (Build.VERSION.SDK_INT >= 26) {
            mJobImpl = new androidx.core.app.SafeJobServiceEngineImpl(this);
        } else {
            mJobImpl = null;
        }
    }
}

Demo

If you would like to see the application background solution in action, check out our demo here: https://github.com/eric-romero/detectbackground


Interested in solving more problems like this? We’re hiring!

Related General Engineering Articles

View all