Skip to content

Instantly share code, notes, and snippets.

@tyrauber
Created December 17, 2025 16:51
Show Gist options
  • Select an option

  • Save tyrauber/9d798fd281ff6509f2fef80857d246cb to your computer and use it in GitHub Desktop.

Select an option

Save tyrauber/9d798fd281ff6509f2fef80857d246cb to your computer and use it in GitHub Desktop.
expo-task-manager patch: Fix Android job scheduling ANR and duplicate data #41688
diff --git a/android/build.gradle b/android/build.gradle
index bdecf20e6f9f5e7449abae9b8e0c4f75e6c140bb..2e24862b7bb85a07a69e787c8a33cecc0dbb8f6d 100644
--- a/android/build.gradle
+++ b/android/build.gradle
@@ -15,6 +15,8 @@ android {
}
dependencies {
- implementation project(':unimodules-app-loader')
+ // PATCH: Changed from 'implementation project(':unimodules-app-loader')' to use expo-modules-core
+ // The apploader classes (AppLoaderProvider, HeadlessAppLoader) are now part of expo-modules-core
+ compileOnly project(':expo-modules-core')
api "androidx.core:core:1.0.0"
}
diff --git a/android/src/main/java/expo/modules/taskManager/TaskJobService.java b/android/src/main/java/expo/modules/taskManager/TaskJobService.java
index abc6d9017e021c9c202a0957d128c7467e6f5728..597e88c9301e9529a911bc64952a625b8a6488db 100644
--- a/android/src/main/java/expo/modules/taskManager/TaskJobService.java
+++ b/android/src/main/java/expo/modules/taskManager/TaskJobService.java
@@ -3,21 +3,55 @@ package expo.modules.taskManager;
import android.app.job.JobParameters;
import android.app.job.JobService;
import android.content.Context;
+import android.os.PersistableBundle;
+import android.util.Log;
public class TaskJobService extends JobService {
+ private static final String TAG = "TaskJobService";
+
+ // PATCH: Helper function to log to native logcat with consistent prefix
+ private static void patchLog(String message) {
+ Log.i(TAG, "[EXPO-TASK-MANAGER] " + message);
+ }
+
@Override
public boolean onStartJob(JobParameters params) {
+ PersistableBundle extras = params.getExtras();
+ String taskName = extras.getString("taskName", "unknown");
+ int dataSize = extras.getInt("dataSize", 0);
+ int jobId = params.getJobId();
+
+ patchLog("onStartJob CALLED - jobId=" + jobId + ", task=" + taskName + ", dataSize=" + dataSize);
+ long startTime = System.currentTimeMillis();
+
Context context = getApplicationContext();
TaskService taskService = new TaskService(context);
- return taskService.handleJob(this, params);
+ boolean isAsync = taskService.handleJob(this, params);
+
+ long duration = System.currentTimeMillis() - startTime;
+ patchLog("onStartJob COMPLETE - jobId=" + jobId + ", isAsync=" + isAsync + ", duration=" + duration + "ms");
+
+ return isAsync;
}
@Override
public boolean onStopJob(JobParameters params) {
+ PersistableBundle extras = params.getExtras();
+ String taskName = extras.getString("taskName", "unknown");
+ int dataSize = extras.getInt("dataSize", 0);
+ int jobId = params.getJobId();
+
+ patchLog("onStopJob CALLED - jobId=" + jobId + ", task=" + taskName + ", dataSize=" + dataSize
+ + " (job being stopped by system!)");
+
Context context = getApplicationContext();
TaskService taskService = new TaskService(context);
- return taskService.cancelJob(this, params);
+ boolean shouldReschedule = taskService.cancelJob(this, params);
+
+ patchLog("onStopJob COMPLETE - jobId=" + jobId + ", shouldReschedule=" + shouldReschedule);
+
+ return shouldReschedule;
}
}
diff --git a/android/src/main/java/expo/modules/taskManager/TaskManagerUtils.java b/android/src/main/java/expo/modules/taskManager/TaskManagerUtils.java
index f31dd3d4e1a96827920d6c1e1df8d8e8099bc2bf..ae99a25ae5ffe6d0aa31e754ce48e5495d8548b2 100644
--- a/android/src/main/java/expo/modules/taskManager/TaskManagerUtils.java
+++ b/android/src/main/java/expo/modules/taskManager/TaskManagerUtils.java
@@ -15,15 +15,11 @@ import android.os.PersistableBundle;
import android.util.Log;
import java.util.ArrayList;
-import java.util.Collections;
-import java.util.Comparator;
import java.util.List;
import java.util.Map;
-import java.util.Set;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
-import androidx.collection.ArraySet;
import expo.modules.interfaces.taskManager.TaskExecutionCallback;
import expo.modules.interfaces.taskManager.TaskInterface;
@@ -31,18 +27,30 @@ import expo.modules.interfaces.taskManager.TaskManagerUtilsInterface;
public class TaskManagerUtils implements TaskManagerUtilsInterface {
- // Key that every job created by the task manager must contain in its extras bundle.
+ // Key that every job created by the task manager must contain in its extras
+ // bundle.
private static final String EXTRAS_REQUIRED_KEY = "expo.modules.taskManager";
private static final String TAG = "TaskManagerUtils";
+ // PATCH: Helper function to log to native logcat with consistent prefix
+ private static void patchLog(String message) {
+ Log.i(TAG, "[EXPO-TASK-MANAGER] " + message);
+ }
+
// Request code number used for pending intents created by this module.
private static final int PENDING_INTENT_REQUEST_CODE = 5055;
private static final int DEFAULT_OVERRIDE_DEADLINE = 60 * 1000; // 1 minute
- private static final Set<TaskInterface> sTasksReschedulingJob = new ArraySet<>();
+ // Leave buffer before Android's job limit to avoid edge cases.
+ private static final int JOB_LIMIT_BUFFER = 10;
+
+ private static int getJobLimit() {
+ // Android 12 (API 31+) increased limit from 100 to 150
+ return Build.VERSION.SDK_INT >= Build.VERSION_CODES.S ? 150 : 100;
+ }
- //region TaskManagerUtilsInterface
+ // region TaskManagerUtilsInterface
@Override
public PendingIntent createTaskIntent(Context context, TaskInterface task) {
@@ -61,8 +69,11 @@ public class TaskManagerUtils implements TaskManagerUtilsInterface {
@Override
public void scheduleJob(Context context, @NonNull TaskInterface task, List<PersistableBundle> data) {
if (task == null) {
+ patchLog("ERROR: scheduleJob called with null task!");
Log.e(TAG, "Trying to schedule job for null task!");
} else {
+ int dataSize = data != null ? data.size() : 0;
+ patchLog("scheduleJob called - task: " + task.getName() + ", dataSize: " + dataSize);
updateOrScheduleJob(context, task, data);
}
}
@@ -78,11 +89,14 @@ public class TaskManagerUtils implements TaskManagerUtilsInterface {
@Override
public void cancelScheduledJob(Context context, int jobId) {
+ patchLog("cancelScheduledJob - jobId: " + jobId);
JobScheduler jobScheduler = (JobScheduler) context.getSystemService(Context.JOB_SCHEDULER_SERVICE);
if (jobScheduler != null) {
jobScheduler.cancel(jobId);
+ patchLog("cancelScheduledJob - job cancelled successfully");
} else {
+ patchLog("ERROR: cancelScheduledJob - Job scheduler not found!");
Log.e(this.getClass().getName(), "Job scheduler not found!");
}
}
@@ -92,6 +106,9 @@ public class TaskManagerUtils implements TaskManagerUtilsInterface {
PersistableBundle extras = params.getExtras();
List<PersistableBundle> data = new ArrayList<>();
int dataSize = extras.getInt("dataSize", 0);
+ String taskName = extras.getString("taskName", "unknown");
+
+ patchLog("extractDataFromJobParams - task: " + taskName + ", dataSize: " + dataSize);
for (int i = 0; i < dataSize; i++) {
data.add(extras.getPersistableBundle(String.valueOf(i)));
@@ -99,72 +116,77 @@ public class TaskManagerUtils implements TaskManagerUtilsInterface {
return data;
}
- //endregion TaskManagerUtilsInterface
- //region static helpers
-
- static boolean notifyTaskJobCancelled(TaskInterface task) {
- boolean isRescheduled = sTasksReschedulingJob.contains(task);
-
- if (isRescheduled) {
- sTasksReschedulingJob.remove(task);
- }
- return isRescheduled;
- }
-
- //endregion static helpers
- //region private helpers
+ // endregion TaskManagerUtilsInterface
+ // region private helpers
private void updateOrScheduleJob(Context context, TaskInterface task, List<PersistableBundle> data) {
+ String taskName = task.getName();
+ int dataSize = data != null ? data.size() : 0;
+ patchLog("updateOrScheduleJob START - task: " + taskName + ", dataSize: " + dataSize);
+
JobScheduler jobScheduler = (JobScheduler) context.getSystemService(Context.JOB_SCHEDULER_SERVICE);
if (jobScheduler == null) {
+ patchLog("ERROR: updateOrScheduleJob - Job scheduler not found!");
Log.e(this.getClass().getName(), "Job scheduler not found!");
return;
}
List<JobInfo> pendingJobs = jobScheduler.getAllPendingJobs();
+ if (pendingJobs == null) {
+ pendingJobs = new ArrayList<>();
+ }
- Collections.sort(pendingJobs, new Comparator<JobInfo>() {
- @Override
- public int compare(JobInfo a, JobInfo b) {
- return Integer.compare(a.getId(), b.getId());
- }
- });
+ patchLog("updateOrScheduleJob - found " + pendingJobs.size() + " pending jobs in system");
- // We will be looking for the lowest number that is not being used yet.
+ // Find newest job for this task (for merging if needed) and next available ID
int newJobId = 0;
+ JobInfo newestJob = null;
for (JobInfo jobInfo : pendingJobs) {
int jobId = jobInfo.getId();
-
+ if (jobId >= newJobId) {
+ newJobId = jobId + 1;
+ }
if (isJobInfoRelatedToTask(jobInfo, task)) {
- JobInfo mergedJobInfo = createJobInfoByAddingData(jobInfo, data);
-
- // Add the task to the list of rescheduled tasks.
- sTasksReschedulingJob.add(task);
-
- try {
- // Cancel jobs with the same ID to let them be rescheduled.
- jobScheduler.cancel(jobId);
-
- // Reschedule job for given task.
- jobScheduler.schedule(mergedJobInfo);
- } catch (IllegalStateException e) {
- Log.e(this.getClass().getName(), "Unable to reschedule a job: " + e.getMessage());
+ if (newestJob == null || jobId > newestJob.getId()) {
+ newestJob = jobInfo;
}
- return;
}
- if (newJobId == jobId) {
- newJobId++;
+ }
+
+ // At Android's job limit? Merge into newest job for this task.
+ if (pendingJobs.size() >= getJobLimit() - JOB_LIMIT_BUFFER && newestJob != null) {
+ patchLog("updateOrScheduleJob - Approaching job limit (" + pendingJobs.size() + "). Merging data for task '" + taskName + "'.");
+ Log.i(TAG, "Approaching job limit (" + pendingJobs.size() + "). Merging data for task '" + taskName + "'.");
+ try {
+ JobInfo mergedJobInfo = createJobInfoByAddingData(newestJob, data);
+ jobScheduler.cancel(newestJob.getId());
+ jobScheduler.schedule(mergedJobInfo);
+ patchLog("updateOrScheduleJob - MERGED into job " + newestJob.getId());
+ } catch (IllegalStateException e) {
+ patchLog("ERROR: updateOrScheduleJob - Unable to merge job: " + e.getMessage());
+ Log.e(this.getClass().getName(), "Unable to merge job: " + e.getMessage());
}
+ return;
}
+ // Under limit: schedule as new job. Don't touch existing jobs.
try {
- // Given task doesn't have any pending jobs yet, create a new JobInfo and schedule it then.
+ patchLog("updateOrScheduleJob - SCHEDULING NEW JOB with id=" + newJobId + ", dataSize=" + dataSize);
+ long startTime = System.currentTimeMillis();
JobInfo jobInfo = createJobInfo(context, task, newJobId, data);
- jobScheduler.schedule(jobInfo);
+ int result = jobScheduler.schedule(jobInfo);
+ long duration = System.currentTimeMillis() - startTime;
+ if (result == JobScheduler.RESULT_SUCCESS) {
+ patchLog("updateOrScheduleJob - JOB SCHEDULED SUCCESSFULLY in " + duration + "ms (id=" + newJobId + ")");
+ } else {
+ patchLog("ERROR: updateOrScheduleJob - JOB SCHEDULE FAILED in " + duration + "ms (id=" + newJobId + ", result="
+ + result + ")");
+ }
} catch (IllegalStateException e) {
- Log.e(this.getClass().getName(), "Unable to schedule a new job: " + e.getMessage());
+ patchLog("ERROR: updateOrScheduleJob - IllegalStateException: " + e.getMessage());
+ Log.e(this.getClass().getName(), "Unable to schedule job: " + e.getMessage());
}
}
@@ -191,13 +213,14 @@ public class TaskManagerUtils implements TaskManagerUtilsInterface {
// query param is called appId for legacy reasons
Uri dataUri = new Uri.Builder()
- .appendQueryParameter("appId", appScopeKey)
- .appendQueryParameter("taskName", taskName)
- .build();
+ .appendQueryParameter("appId", appScopeKey)
+ .appendQueryParameter("taskName", taskName)
+ .build();
intent.setData(dataUri);
- // We're defaulting to the behaviour prior API 31 (mutable) even though Android recommends immutability
+ // We're defaulting to the behaviour prior API 31 (mutable) even though Android
+ // recommends immutability
int mutableFlag = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S ? PendingIntent.FLAG_MUTABLE : 0;
return PendingIntent.getBroadcast(context, PENDING_INTENT_REQUEST_CODE, intent, flags | mutableFlag);
}
@@ -210,11 +233,24 @@ 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)
+ .setPersisted(true)
+ .setRequiresDeviceIdle(false);
+
+ if (Build.VERSION.SDK_INT < 28) {
+ // For Android versions below 28 (Android 9 and below)
+ jobBuilder.setMinimumLatency(0)
+ .setOverrideDeadline(DEFAULT_OVERRIDE_DEADLINE);
+ } else if (Build.VERSION.SDK_INT < 31) {
+ // For Android 9 (API 28) to Android 11 (API 30)
+ jobBuilder.setImportantWhileForeground(true);
+ } else {
+ // For Android 12 (API 31) and above
+ jobBuilder.setExpedited(true);
+ }
+
+ return jobBuilder.build();
}
private JobInfo createJobInfo(Context context, TaskInterface task, int jobId, List<PersistableBundle> data) {
@@ -255,8 +291,8 @@ public class TaskManagerUtils implements TaskManagerUtilsInterface {
return false;
}
- //endregion private helpers
- //region converting map to bundle
+ // endregion private helpers
+ // region converting map to bundle
@SuppressWarnings("unchecked")
static Bundle mapToBundle(Map<String, Object> map) {
@@ -332,5 +368,5 @@ public class TaskManagerUtils implements TaskManagerUtilsInterface {
return arrayList;
}
- //endregion converting map to bundle
+ // endregion converting map to bundle
}
diff --git a/android/src/main/java/expo/modules/taskManager/TaskService.java b/android/src/main/java/expo/modules/taskManager/TaskService.java
index 2b6c2fa60a8c52dda8a41641d416563db6e15725..1db9323d3882c0ba4897ee9ca0107a65e445eac3 100644
--- a/android/src/main/java/expo/modules/taskManager/TaskService.java
+++ b/android/src/main/java/expo/modules/taskManager/TaskService.java
@@ -51,10 +51,16 @@ public class TaskService implements SingletonModule, TaskServiceInterface {
private static final String SHARED_PREFERENCES_NAME = "TaskManagerModule";
private static final int MAX_TASK_EXECUTION_TIME_MS = 15000; // 15 seconds
+ // PATCH: Helper function to log to native logcat with consistent prefix
+ private static void patchLog(String message) {
+ Log.i(TAG, "[EXPO-TASK-MANAGER] " + message);
+ }
+
private WeakReference<Context> mContextRef;
private TaskManagerUtilsInterface mTaskManagerUtils;
- // Map with task managers of running (foregrounded) apps. { "<appScopeKey>": WeakReference(TaskManagerInterface) }
+ // Map with task managers of running (foregrounded) apps. { "<appScopeKey>":
+ // WeakReference(TaskManagerInterface) }
private static final Map<String, WeakReference<TaskManagerInterface>> sTaskManagers = new HashMap<>();
// Same as above but for headless (backgrounded) apps.
@@ -65,7 +71,8 @@ public class TaskService implements SingletonModule, TaskServiceInterface {
private TasksAndEventsRepository mTasksAndEventsRepository;
- // Map of callbacks for task execution events. Schema: { "<eventId>": TaskExecutionCallback }
+ // Map of callbacks for task execution events. Schema: { "<eventId>":
+ // TaskExecutionCallback }
private static final Map<String, TaskExecutionCallback> sTaskCallbacks = new HashMap<>();
public TaskService(Context context) {
@@ -83,7 +90,7 @@ public class TaskService implements SingletonModule, TaskServiceInterface {
return "TaskService";
}
- //region TaskServiceInterface
+ // region TaskServiceInterface
@Override
public boolean hasRegisteredTask(String taskName, String appScopeKey) {
@@ -92,7 +99,8 @@ public class TaskService implements SingletonModule, TaskServiceInterface {
}
@Override
- public void registerTask(String taskName, String appScopeKey, String appUrl, Class consumerClass, Map<String, Object> options) throws TaskRegisteringFailedException {
+ public void registerTask(String taskName, String appScopeKey, String appUrl, Class consumerClass,
+ Map<String, Object> options) throws TaskRegisteringFailedException {
TaskInterface task = getTask(taskName, appScopeKey);
Class unversionedConsumerClass = unversionedClassForClass(consumerClass);
@@ -107,7 +115,8 @@ public class TaskService implements SingletonModule, TaskServiceInterface {
}
@Override
- public void unregisterTask(String taskName, String appScopeKey, Class consumerClass) throws TaskNotFoundException, InvalidConsumerClassException {
+ public void unregisterTask(String taskName, String appScopeKey, Class consumerClass)
+ throws TaskNotFoundException, InvalidConsumerClassException {
TaskInterface task = getTask(taskName, appScopeKey);
Class unversionedConsumerClass = unversionedClassForClass(consumerClass);
@@ -206,7 +215,8 @@ public class TaskService implements SingletonModule, TaskServiceInterface {
if (appEvents.size() == 0) {
sEvents.remove(appScopeKey);
- // Invalidate app record but after 2 seconds delay so we can still take batched events.
+ // Invalidate app record but after 2 seconds delay so we can still take batched
+ // events.
Handler handler = new Handler();
handler.postDelayed(new Runnable() {
@Override
@@ -257,7 +267,8 @@ public class TaskService implements SingletonModule, TaskServiceInterface {
mTasksAndEventsRepository.removeEvents(appScopeKey);
if (!isHeadless) {
- // Maybe update app url in user defaults. It might change only in non-headless mode.
+ // Maybe update app url in user defaults. It might change only in non-headless
+ // mode.
maybeUpdateAppUrlForAppScopeKey(appUrl, appScopeKey);
}
}
@@ -316,71 +327,96 @@ public class TaskService implements SingletonModule, TaskServiceInterface {
}
public boolean handleJob(final JobService jobService, final JobParameters params) {
+ long startTime = System.currentTimeMillis();
+ int jobId = params.getJobId();
PersistableBundle extras = params.getExtras();
- // persistable bundle extras param name is appId for legacy reasons
String appScopeKey = extras.getString("appId");
String taskName = extras.getString("taskName");
+ // PATCH: Log job handling start with details
+ int keyCount = extras.keySet() != null ? extras.keySet().size() : 0;
+ patchLog("handleJob START - jobId=" + jobId + ", taskName=" + taskName + ", appScopeKey=" + appScopeKey
+ + ", extrasKeyCount=" + keyCount);
+
TaskConsumerInterface consumer = getTaskConsumer(taskName, appScopeKey);
if (consumer == null) {
+ patchLog("handleJob FAILED - consumer not found for task '" + taskName + "', returning false");
Log.w(TAG, "Task or consumer not found.");
return false;
}
Log.i(TAG, "Handling job with task name '" + taskName + "' for app with scoping identifier '" + appScopeKey + "'.");
+ patchLog("handleJob - consumer found: " + consumer.getClass().getSimpleName() + ", executing...");
- // executes task
boolean isAsyncJob = consumer.didExecuteJob(jobService, params);
+ long executionTime = System.currentTimeMillis() - startTime;
+ patchLog("handleJob - didExecuteJob returned isAsyncJob=" + isAsyncJob + ", executionTime=" + executionTime + "ms");
if (isAsyncJob) {
- // Make sure the task doesn't take more than 15 seconds
+ patchLog("handleJob - scheduling timeout callback in " + MAX_TASK_EXECUTION_TIME_MS + "ms");
finishJobAfterTimeout(jobService, params, MAX_TASK_EXECUTION_TIME_MS);
}
+ patchLog("handleJob END - jobId=" + jobId + ", taskName=" + taskName + ", isAsyncJob=" + isAsyncJob + ", totalTime="
+ + executionTime + "ms");
return isAsyncJob;
}
public boolean cancelJob(JobService jobService, JobParameters params) {
+ int jobId = params.getJobId();
PersistableBundle extras = params.getExtras();
- // persistable bundle extras param name is appId for legacy reasons
String appScopeKey = extras.getString("appId");
String taskName = extras.getString("taskName");
+ // PATCH: Log job cancellation
+ patchLog("cancelJob START - jobId=" + jobId + ", taskName=" + taskName + ", appScopeKey=" + appScopeKey);
+
TaskInterface task = getTask(taskName, appScopeKey);
- // `notifyTaskJobCancelled` notifies TaskManagerUtils about a job for task being cancelled.
- // It returns `true` if the job has been intentionally cancelled to be rescheduled,
- // in that case we don't want to inform the consumer about cancellation.
- if (task != null && !TaskManagerUtils.notifyTaskJobCancelled(task)) {
+ if (task != null) {
TaskConsumerInterface consumer = task.getConsumer();
if (consumer == null) {
+ patchLog("cancelJob - task found but consumer is null, returning false");
return false;
}
Log.i(TAG, "Job for task '" + taskName + "' has been cancelled by the system.");
+ patchLog("cancelJob - job cancelled by system, calling didCancelJob on consumer: "
+ + consumer.getClass().getSimpleName());
- // cancels task
- return consumer.didCancelJob(jobService, params);
+ boolean result = consumer.didCancelJob(jobService, params);
+ patchLog("cancelJob END - jobId=" + jobId + ", consumer.didCancelJob returned=" + result);
+ return result;
}
+ // PATCH: Log when task not found
+ patchLog("cancelJob END - task not found, returning false (no reschedule)");
+
// `false` = don't reschedule the job.
return false;
}
public void executeTask(TaskInterface task, Bundle data, Error error, TaskExecutionCallback callback) {
+ String taskName = task.getName();
+ String appScopeKey = task.getAppScopeKey();
+
+ // PATCH: Log task execution start
+ int dataSize = data != null ? data.keySet().size() : 0;
+ patchLog("executeTask START - taskName=" + taskName + ", appScopeKey=" + appScopeKey + ", dataKeyCount=" + dataSize
+ + ", hasError=" + (error != null) + ", hasCallback=" + (callback != null));
+
TaskManagerInterface taskManager = getTaskManager(task.getAppScopeKey());
Bundle body = createExecutionEventBody(task, data, error);
Bundle executionInfo = body.getBundle("executionInfo");
if (executionInfo == null) {
- // it should never happen, just to suppress warnings :)
+ patchLog("executeTask FAILED - executionInfo is null, aborting");
return;
}
String eventId = executionInfo.getString("eventId");
- String appScopeKey = task.getAppScopeKey();
if (callback != null) {
sTaskCallbacks.put(eventId, callback);
@@ -397,12 +433,18 @@ public class TaskService implements SingletonModule, TaskServiceInterface {
}
if (taskManager != null) {
+ patchLog("executeTask - taskManager available, executing immediately with eventId=" + eventId);
taskManager.executeTaskWithBody(body);
return;
}
+ // PATCH: Log when task needs to be queued
+ patchLog(
+ "executeTask - taskManager not available, queueing event with eventId=" + eventId + " for later execution");
+
// The app is not fully loaded as its task manager is not there yet.
- // We need to add event's body to the queue from which events will be executed once the task manager is ready.
+ // We need to add event's body to the queue from which events will be executed
+ // once the task manager is ready.
if (!mTasksAndEventsRepository.hasEvents(appScopeKey)) {
mTasksAndEventsRepository.putEvents(appScopeKey, new ArrayList<>());
}
@@ -431,8 +473,8 @@ public class TaskService implements SingletonModule, TaskServiceInterface {
}
}
- //endregion
- //region helpers
+ // endregion
+ // region helpers
private HeadlessAppLoader getAppLoader() {
if (mContextRef.get() != null) {
@@ -442,7 +484,8 @@ public class TaskService implements SingletonModule, TaskServiceInterface {
}
}
- private void internalRegisterTask(String taskName, String appScopeKey, String appUrl, Class<TaskConsumerInterface> consumerClass, Map<String, Object> options) throws TaskRegisteringFailedException {
+ private void internalRegisterTask(String taskName, String appScopeKey, String appUrl,
+ Class<TaskConsumerInterface> consumerClass, Map<String, Object> options) throws TaskRegisteringFailedException {
Constructor<?> consumerConstructor;
TaskConsumerInterface consumer;
Context context = mContextRef.get();
@@ -460,7 +503,9 @@ public class TaskService implements SingletonModule, TaskServiceInterface {
Task task = new Task(taskName, appScopeKey, appUrl, consumer, options, this);
- Map<String, TaskInterface> appTasks = mTasksAndEventsRepository.hasTasks(appScopeKey) ? mTasksAndEventsRepository.getTasks(appScopeKey) : new HashMap<String, TaskInterface>();
+ Map<String, TaskInterface> appTasks = mTasksAndEventsRepository.hasTasks(appScopeKey)
+ ? mTasksAndEventsRepository.getTasks(appScopeKey)
+ : new HashMap<String, TaskInterface>();
appTasks.put(taskName, task);
mTasksAndEventsRepository.putTasks(appScopeKey, appTasks);
@@ -530,16 +575,17 @@ public class TaskService implements SingletonModule, TaskServiceInterface {
appConfig.put("appUrl", appUrl);
preferences
- .edit()
- .putString(appScopeKey, new JSONObject(appConfig).toString())
- .apply();
+ .edit()
+ .putString(appScopeKey, new JSONObject(appConfig).toString())
+ .apply();
}
}
}
@SuppressWarnings("unchecked")
private void restoreTasks() {
- Map<String, TasksAndEventsRepository.AppConfig> apps = mTasksAndEventsRepository.readPersistedTasks(getSharedPreferences());
+ Map<String, TasksAndEventsRepository.AppConfig> apps = mTasksAndEventsRepository
+ .readPersistedTasks(getSharedPreferences());
for (Map.Entry<String, TasksAndEventsRepository.AppConfig> entry : apps.entrySet()) {
String appScopeKey = entry.getKey();
@@ -561,13 +607,15 @@ public class TaskService implements SingletonModule, TaskServiceInterface {
Map<String, Object> options = (HashMap<String, Object>) taskConfig.get("options");
try {
- // register the task using internal method which doesn't change shared preferences.
+ // register the task using internal method which doesn't change shared
+ // preferences.
internalRegisterTask(taskName, appScopeKey, appUrl, consumerClass, options);
} catch (TaskRegisteringFailedException e) {
Log.e(TAG, e.getMessage());
}
} else {
- Log.w(TAG, "Task consumer '" + consumerClassString + "' has version '" + currentConsumerVersion + "' that is not compatible with the saved version '" + previousConsumerVersion + "'.");
+ Log.w(TAG, "Task consumer '" + consumerClassString + "' has version '" + currentConsumerVersion
+ + "' that is not compatible with the saved version '" + previousConsumerVersion + "'.");
}
} catch (ClassNotFoundException | NullPointerException e) {
Log.e(TAG, e.getMessage());
@@ -587,7 +635,8 @@ public class TaskService implements SingletonModule, TaskServiceInterface {
}
/**
- * Returns task manager for given appScopeKey. Task managers initialized in non-headless contexts have precedence over headless one.
+ * Returns task manager for given appScopeKey. Task managers initialized in
+ * non-headless contexts have precedence over headless one.
*/
@Nullable
private TaskManagerInterface getTaskManager(String appScopeKey) {
@@ -610,11 +659,22 @@ public class TaskService implements SingletonModule, TaskServiceInterface {
}
private void finishJobAfterTimeout(final JobService jobService, final JobParameters params, long timeout) {
+ final int jobId = params.getJobId();
+ final String taskName = params.getExtras().getString("taskName");
+
+ // PATCH: Log timeout scheduling
+ patchLog("finishJobAfterTimeout - scheduling jobFinished callback for jobId=" + jobId + ", taskName=" + taskName
+ + " in " + timeout + "ms");
+
Handler handler = new Handler();
handler.postDelayed(new Runnable() {
@Override
public void run() {
+ // PATCH: Log when timeout fires
+ patchLog(
+ "finishJobAfterTimeout FIRED - calling jobFinished(false) for jobId=" + jobId + ", taskName=" + taskName);
jobService.jobFinished(params, false);
+ patchLog("finishJobAfterTimeout - jobFinished completed for jobId=" + jobId);
}
}, timeout);
}
diff --git a/android/src/main/java/expo/modules/taskManager/Utils.java b/android/src/main/java/expo/modules/taskManager/Utils.java
index 36b4e573409a80aac0087fe713992e69ba698c62..548d821432771e19e626df5c9724e290fd3c9b67 100644
--- a/android/src/main/java/expo/modules/taskManager/Utils.java
+++ b/android/src/main/java/expo/modules/taskManager/Utils.java
@@ -51,7 +51,8 @@ public class Utils {
}
/**
- * Method that unversions class names, so we can always use unversioned task consumer classes.
+ * Method that unversions class names, so we can always use unversioned task
+ * consumer classes.
*/
public static String unversionedClassNameForClass(Class versionedClass) {
String className = versionedClass.getName();
@@ -59,7 +60,8 @@ public class Utils {
}
/**
- * Returns task consumer's version. Defaults to 0 if `VERSION` static field is not implemented.
+ * Returns task consumer's version. Defaults to 0 if `VERSION` static field is
+ * not implemented.
*/
public static int getConsumerVersion(Class consumerClass) {
try {
@@ -103,7 +105,10 @@ public class Utils {
for (int i = 0; i < json.length(); i++) {
Object value = json.get(i);
- if (value instanceof JSONArray) {
+ // Handle JSONObject.NULL - convert to actual Java null
+ if (value == JSONObject.NULL) {
+ value = null;
+ } else if (value instanceof JSONArray) {
value = jsonToList((JSONArray) value);
} else if (value instanceof JSONObject) {
value = jsonToMap((JSONObject) value);
@@ -117,6 +122,12 @@ public class Utils {
}
public static Object jsonObjectToObject(Object json) {
+ // Handle JSONObject.NULL - convert to actual Java null
+ // JSONObject.NULL is returned when a JSON key has a null value
+ // and it's an instance of a special singleton class (JSONObject$1)
+ if (json == null || json == JSONObject.NULL) {
+ return null;
+ }
if (json instanceof JSONObject) {
return jsonToMap((JSONObject) json);
}
diff --git a/expo-module.config.json b/expo-module.config.json
index b2698a3815fd5e5e8db3ee597e3ba8ec160028ef..e3ed27b1a8c4563cc300353d0a978cbfbd401e54 100644
--- a/expo-module.config.json
+++ b/expo-module.config.json
@@ -1,12 +1,6 @@
{
"platforms": ["apple", "android"],
"android": {
- "modules": ["expo.modules.taskManager.TaskManagerModule"],
- "publication": {
- "groupId": "host.exp.exponent",
- "artifactId": "expo.modules.taskmanager",
- "version": "14.0.7",
- "repository": "local-maven-repo"
- }
+ "modules": ["expo.modules.taskManager.TaskManagerModule"]
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment