expo: Task Manager Job Doesn't Get Fired: Location stops updating after 5-10 minutes on Android (when using Foreground Service)

Summary

I’m testing Expo 42 with an ejected app (bare workflow). My app watches the current device location in foreground (when the app is open) and background (when the screen is turned off) using Android’s Foreground Service functionality, by permanently displaying a notification icon in the status bar.

Generally, it works as expected. However, after about 5 to 10 minutes, Android seems to put the device to “sleep” and that’s when my task (TaskManager.defineTask) doesn’t get fired periodically anymore. Instead, I only receive batch updates every couple of minutes, but not in real-time anymore, and thus useless for a real-time app relying on accurate location info. This is reproducible on both a real device as well as the Emulator running API 30.

I have spent about half a day debugging through Expo sources and finally found the culprit: At some point, when Android puts the device into a deeper sleep (aka “dozing”) after 5 minutes or so, the Expo Task Manager stops firing. Jobs are still scheduled but they aren’t run immediately anymore but batched and run only after a long delay of several minutes. I’m talking about this code in expo-task-manager/android/src/main/java/expo/modules/taskManager/TaskManagerUtils.java:

      JobInfo jobInfo = createJobInfo(context, task, newJobId, data);
      jobScheduler.schedule(jobInfo);

The job will simply not fire right away and subsequent jobs will get merged without getting fired either. These deferred jobs will apparently only run every 10 to 15 minutes or so.

I finally got a workaround going by increasing the priority of the job by using setImportantWhileForeground. I don’t really know the implications for other parts of Expo or on the battery usage, but for background location, this seems to work as expected now. Here’s a patch for TaskManagerUtils.java that I’m applying in my local repo using patch-package:

diff --git a/node_modules/expo-task-manager/android/src/main/java/expo/modules/taskManager/TaskManagerUtils.java b/node_modules/expo-task-manager/android/src/main/java/expo/modules/taskManager/TaskManagerUtils.java
index b40f2d1..7d47fd7 100644
--- a/node_modules/expo-task-manager/android/src/main/java/expo/modules/taskManager/TaskManagerUtils.java
+++ b/node_modules/expo-task-manager/android/src/main/java/expo/modules/taskManager/TaskManagerUtils.java
@@ -8,6 +8,7 @@ import android.content.ComponentName;
 import android.content.Context;
 import android.content.Intent;
 import android.net.Uri;
+import android.os.Build;
 import android.os.Bundle;
 import android.os.Parcelable;
 import android.os.PersistableBundle;
@@ -195,11 +196,16 @@ public class TaskManagerUtils implements TaskManagerUtilsInterface {
   }
 
   private JobInfo createJobInfo(int jobId, ComponentName jobService, PersistableBundle extras) {
-    return new JobInfo.Builder(jobId, jobService)
-      .setExtras(extras)
-      .setMinimumLatency(0)
-      .setOverrideDeadline(DEFAULT_OVERRIDE_DEADLINE)
-      .build();
+    JobInfo.Builder jobBuilder = new JobInfo.Builder(jobId, jobService)
+            .setExtras(extras);
+    if (Build.VERSION.SDK_INT < 28) {
+      jobBuilder.setMinimumLatency(1)
+              .setOverrideDeadline(DEFAULT_OVERRIDE_DEADLINE);
+    } else {
+      // FIXME: This method was deprecated in API level 31. Use setExpedited(boolean) instead.
+      jobBuilder.setImportantWhileForeground(true);
+    }
+    return jobBuilder.build();
   }
 
   private JobInfo createJobInfo(Context context, TaskInterface task, int jobId, List<PersistableBundle> data) {

This change ensures that Android keeps firing the job on time even if the device is sleeping.

Setting this to true indicates that this job is important while the scheduling app is in the foreground or on the temporary whitelist for background restrictions. This means that the system will relax doze restrictions on this job during this time. Apps should use this flag only for short jobs that are essential for the app to function properly in the foreground. Note that once the scheduling app is no longer whitelisted from background restrictions and in the background, or the job failed due to unsatisfied constraints, this job should be expected to behave like other jobs without this flag.

I’m also setting setMinimumLatency(1) as I’ve read somewhere that 0 might not always work as expected on some older Android versions when used in combination with setOverrideDeadline(). I didn’t bother testing it though 🤷 . Also, note that setOverrideDeadline has no effect if the device is dozing. Its value is simply ignored. But in any case, a deadline of 1 minute wouldn’t really be sufficient for real-time location updates anyway.

tl;dr.: Seems that the Expo Task Manager will stop running jobs such as the location updates once a device goes sleeping. The above patch avoids this by setting setImportantWhileForeground and making sure tasks keep firing.

Managed or bare workflow? If you have ios/ or android/ directories in your project, the answer is bare!

bare

What platform(s) does this occur on?

Android

SDK Version (managed workflow only)

No response

Environment

  Expo CLI 4.10.0 environment info:
    System:
      OS: macOS 11.5.2
      Shell: 5.8 - /bin/zsh
    Binaries:
      Node: 14.16.1 - ~/.nvm/versions/node/v14.16.1/bin/node
      npm: 7.20.3 - ~/.nvm/versions/node/v14.16.1/bin/npm
      Watchman: 2021.06.07.00 - /usr/local/bin/watchman
    Managers:
      CocoaPods: 1.10.2 - /usr/local/bin/pod
    SDKs:
      iOS SDK:
        Platforms: iOS 14.5, DriverKit 20.4, macOS 11.3, tvOS 14.5, watchOS 7.4
      Android SDK:
        API Levels: 29, 30
        Build Tools: 29.0.2, 29.0.3, 30.0.2, 30.0.3
        System Images: android-30 | Google APIs Intel x86 Atom, android-30 | Google Play Intel x86 Atom
    IDEs:
      Android Studio: 2020.3 AI-203.7717.56.2031.7583922
      Xcode: 12.5.1/12E507 - /usr/bin/xcodebuild
    npmPackages:
      expo: ^42.0.3 => 42.0.3 
      react: 17.0.1 => 17.0.1 
      react-dom: 17.0.1 => 17.0.1 
      react-native: ^0.64.1 => 0.64.2 
    npmGlobalPackages:
      expo-cli: 4.10.0
    Expo Workflow: bare

Reproducible demo or steps to reproduce from a blank project

None

About this issue

  • Original URL
  • State: closed
  • Created 3 years ago
  • Reactions: 5
  • Comments: 25 (2 by maintainers)

Most upvoted comments

Still an issue in 2023. Foreground working perfectly, but on Android (dont have IOS), still kicks in when the app is in the background, but location updates become more sporadic and then come in a huge batch (like 30 at a time, depending on the interval settings).

** After a lot of testing I found that battery optimization settings were stopping expo task-manager and location updates. After changing battery optimization to ‘unrestricted’, I now have consistent location updates with the app in background. I don’t think there’s a happy path for this solution, as telling the user they need to disable battery optimization may not be the best UX… 😭 Tested on Android 13 with Samsung S20 FE

I think this fix should be re-opened and merged in, maybe behind a flag so that it is off by default and only enabled for tasks that explicitly need priority (eg. when background location tracking is needed).

The following fix worked for me, I got timely updates for more than 20min with the screen locked, after forking and patching expo-task-manger. file: taskManager/TaskManagerUtils.java changed the function createJobInfo with the following:

 private JobInfo createJobInfo(int jobId, ComponentName jobService, PersistableBundle extras) {
    JobInfo.Builder jobBuilder = new JobInfo.Builder(jobId, jobService).setExtras(extras);
    if (Build.VERSION.SDK_INT < 28) {
      jobBuilder.setMinimumLatency(1).setOverrideDeadline(DEFAULT_OVERRIDE_DEADLINE);
      return jobBuilder.build();
    }
    if (Build.VERSION.SDK_INT < 31) {
      jobBuilder.setOverrideDeadline(DEFAULT_OVERRIDE_DEADLINE).setImportantWhileForeground(true);
      return jobBuilder.build();
    }
    jobBuilder.setExpedited(true);
    return jobBuilder.build();
  }

Hi, is there any solution or deadline for this problem to be fixed in the managed workflow? its really critical for our app and we really dont want to eject 😦

Expo Go seemed to perform fine in the background with this setup. @arabold mentioned having 0 as the minimum latency value for the actual native task code could make a difference on some phones. So I set it to 1ms where applicable and given a shorter timeout in case something was hanging and stopping the handler from firing. Logcat doesn’t give many hints that there’s something wrong.

const options = {
    accuracy: Location.Accuracy.BestForNavigation,
    distanceInterval: 1,
    timeInterval: 1,
    deferredUpdatesInterval: 1,
    deferredUpdatesTimeout: 1000,
    foregroundService: {
      notificationTitle: APP_NAME,
      notificationBody: "Location is used when user is running route",
    },
    activityType: Location.ActivityType.Fitness,
    showsBackgroundLocationIndicator: true,
    pausesUpdatesAutomatically: false,
}

@egm0121 No worries, appreciate your efforts!

Will chuck some logging statements in there and see what gets spat out. From the EAS build logs it definitely looked like your patch got applied. I’ll give your options setup a go now too. Cheers for that.

I’ll monkey around with the battery settings some more because it’s odd that it’s now totally fixed in Expo Go but not in production. Makes me think there’s a config setting somewhere that could be tweaked but I suppose Go is a bit of a special case.

My options for the background location api are:

`

accuracy: Location.Accuracy.Highest, timeInterval: 0, distanceInterval: 10, showsBackgroundLocationIndicator: true, activityType: Location.ActivityType.Fitness, deferredUpdatesInterval: 0, deferredUpdatesTimeout: 1000, foregroundService: { notificationTitle: ‘Activity Started’, notificationBody: ‘xxx now tracking your speed…’, notificationColor: ‘#2f95dc’ }

`

I haven’t tested my app yet with expo 49 so it is entirely possible that the patch no longer works…

But just to triple check, @rossparachute you can add some debug statement in TaskManagerUtils.java to verify with the android studio debugger that the patch is being applied for sure

Seems like the patch proposed by @egm0121 solves the problem. Thank you, Giulio!

This is so sad. Is there any way I can be of help to get this fixed in the codebase (implementing, testing, although I would only partly know what I’m doing there), so we can use the functionality also in managed workflows, or at least in bare workflows without patching stuff by hand?

Very disappointing 😦

This kind of error is super difficult to test and track. I was able to test it in the emulator in the context of my own app. But it’s rather complex and there are many things happening. Unfortunately, I can’t just create a proof of concept project or anything like that right now.

When implementing background location, I found the following tips helpful:

  1. The location task that you register with TaskManager.defineTask should have little to no dependencies on other JavaScript code. Keep it as lightweight as possible.
  2. Don’t implement any internal states in the task either. It is very hard to do right as the task isn’t necessarily started when you expect it to. Instead, check using Location.hasStartedLocationUpdatesAsync() if background location tracking is on or not.
  3. I would recommend implementing a LocationBackgroundService Singleton class that can be accessed from both the task as well as from the rest of your code. The same rule as before applies to this class: Avoid dependencies and complex internal states. Typical methods of the class would be startUpdates, stopUpdates, and a callback onLocationUpdate that is invoked from your task.
  4. If you do complex things when receiving a location, you might want to implement your own queue for processing the events. If your code takes too long to execute, the task will send updates while you’re still processing the previous one. If you’re using async methods and do some internal state handling, this can go wrong very quickly. A queue can be as simple as an array in that you push location updates into. Then implement serial processing of that array, ensuring that your processing code is never run multiple times in parallel. Remember that just because JS is single-threaded, doesn’t mean that there aren’t any possible race conditions!
  5. This should give you a robust starting point for testing and avoids many pitfalls I’ve encountered myself. Once all of that works, the patch above should fix the rest 🤞 I have tested my own app running in the background for several hours now. It works well on Android (although I still have some unrelated trouble in iOS).

There might be differences depending on the Android version you’re using. Are you on SDK Level 29+ or an earlier version, @dev-andremonteiro ? On a real device, e.g. a Samsung Galaxy, the OS might also simply decide to kill your app if you’re using too much memory or if the OS doesn’t seem your app worthy enough to keep around. Those issues are even worse to track 😞

I am also experiencing the same problem @arabold mentioned, my requirements need the background service to run on time even when the device is sleeping.

I have also tried your solution @arabold without much success, seems like we are getting updates in batches still. Let me confirm with you if what I followed the correct process, my project is running expo SDK 41 and expo-location background service btw. I have changed the file in /node_modules/expo-task-manager/android/src/main/java/expo/modules/taskManager/TaskManagerUtils.java with the changes you’ve posted above, I ran expo build:android on my project, installed the apk in the app with the changes and tested. Am I missing something here? Does it it work only with patch-package?

Sorry, of course this wouldn’t work because Expo installs packages in their cloud env so there’s no way for you to edit a file in node_modules and build with that. I had to eject to test this, once again I didn’t notice anything different, made my test that consist of using the emulator gps spoof in a route and turning off the phone screen, I got gps reads for the first 15 ~ 20 mins and then nothing. I have foreground sticky notification ON and had the changes @arabold listed above.