Fix persisted job handling in bad-boot-RTC situations

Persisted jobs were inadvertently being discarded rather than
rescheduled with appropriate time constraints.  The underlying
bug is that the cancel-with-replacement code path was failing
to manage the replacement properly, and the other code path
leading to it was working around the bug by explicitly
rescheduling the job following cancellation.

Test: atest CtsJobSchedulerTestCases
Bug: 114292041
Bug: 117346686
Change-Id: I3a77e8a38c2f7028b61791bafd9c88b7a714ff6d
diff --git a/services/core/java/com/android/server/job/JobSchedulerService.java b/services/core/java/com/android/server/job/JobSchedulerService.java
index 389782a..dd993b8 100644
--- a/services/core/java/com/android/server/job/JobSchedulerService.java
+++ b/services/core/java/com/android/server/job/JobSchedulerService.java
@@ -877,14 +877,17 @@
             // This may throw a SecurityException.
             jobStatus.prepareLocked(ActivityManager.getService());
 
-            if (toCancel != null) {
-                cancelJobImplLocked(toCancel, jobStatus, "job rescheduled by app");
-            }
             if (work != null) {
                 // If work has been supplied, enqueue it into the new job.
                 jobStatus.enqueueWorkLocked(ActivityManager.getService(), work);
             }
-            startTrackingJobLocked(jobStatus, toCancel);
+
+            if (toCancel != null) {
+                // Implicitly replaces the existing job record with the new instance
+                cancelJobImplLocked(toCancel, jobStatus, "job rescheduled by app");
+            } else {
+                startTrackingJobLocked(jobStatus, null);
+            }
             StatsLog.write_non_chained(StatsLog.SCHEDULED_JOB_STATE_CHANGED,
                     uId, null, jobStatus.getBatteryName(),
                     StatsLog.SCHEDULED_JOB_STATE_CHANGED__STATE__SCHEDULED,
@@ -1013,6 +1016,12 @@
         }
     }
 
+    /**
+     * Cancel the given job, stopping it if it's currently executing.  If {@code incomingJob}
+     * is null, the cancelled job is removed outright from the system.  If
+     * {@code incomingJob} is non-null, it replaces {@code cancelled} in the store of
+     * currently scheduled jobs.
+     */
     private void cancelJobImplLocked(JobStatus cancelled, JobStatus incomingJob, String reason) {
         if (DEBUG) Slog.d(TAG, "CANCEL: " + cancelled.toShortString());
         cancelled.unprepareLocked(ActivityManager.getService());
@@ -1023,6 +1032,11 @@
         }
         // Cancel if running.
         stopJobOnServiceContextLocked(cancelled, JobParameters.REASON_CANCELED, reason);
+        // If this is a replacement, bring in the new version of the job
+        if (incomingJob != null) {
+            if (DEBUG) Slog.i(TAG, "Tracking replacement job " + incomingJob.toShortString());
+            startTrackingJobLocked(incomingJob, cancelled);
+        }
         reportActiveLocked();
     }