Fix ConnectivityController running job counting.

Use a Set of running jobs so that ConnectivityController has a clear
understanding of how many jobs are running. In the process, also fix the
issue where controllers would think that a job was running even though
it had actually failed to start.

Bug: 184740668
Test: atest frameworks/base/services/tests/servicestests/src/com/android/server/job
Test: atest frameworks/base/services/tests/mockingservicestests/src/com/android/server/job
Test: atest CtsJobSchedulerTestCases
Change-Id: Iff142e63b2a3cb92c9467608f95401c7a0f287ee
diff --git a/apex/jobscheduler/service/java/com/android/server/job/JobConcurrencyManager.java b/apex/jobscheduler/service/java/com/android/server/job/JobConcurrencyManager.java
index d94d638..a6386d5 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/JobConcurrencyManager.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/JobConcurrencyManager.java
@@ -705,12 +705,16 @@
     private void startJobLocked(@NonNull JobServiceContext worker, @NonNull JobStatus jobStatus,
             @WorkType final int workType) {
         final List<StateController> controllers = mService.mControllers;
-        for (int ic = 0; ic < controllers.size(); ic++) {
+        final int numControllers = controllers.size();
+        for (int ic = 0; ic < numControllers; ic++) {
             controllers.get(ic).prepareForExecutionLocked(jobStatus);
         }
         if (!worker.executeRunnableJob(jobStatus, workType)) {
             Slog.e(TAG, "Error executing " + jobStatus);
             mWorkCountTracker.onStagedJobFailed(workType);
+            for (int ic = 0; ic < numControllers; ic++) {
+                controllers.get(ic).unprepareFromExecutionLocked(jobStatus);
+            }
         } else {
             mRunningJobs.add(jobStatus);
             mWorkCountTracker.onJobStarted(workType);
diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/ConnectivityController.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/ConnectivityController.java
index 11a8b3b..22f1ff6 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/controllers/ConnectivityController.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/ConnectivityController.java
@@ -147,7 +147,8 @@
             //   9. Enqueue time
             // TODO: maybe consider number of jobs
             // TODO: consider IMPORTANT_WHILE_FOREGROUND bit
-            final int runningPriority = prioritizeExistenceOver(0, us1.numRunning, us2.numRunning);
+            final int runningPriority = prioritizeExistenceOver(0,
+                    us1.runningJobs.size(), us2.runningJobs.size());
             if (runningPriority != 0) {
                 return runningPriority;
             }
@@ -254,7 +255,18 @@
         if (jobStatus.hasConnectivityConstraint()) {
             final UidStats uidStats =
                     getUidStats(jobStatus.getSourceUid(), jobStatus.getSourcePackageName(), true);
-            uidStats.numRunning++;
+            uidStats.runningJobs.add(jobStatus);
+        }
+    }
+
+    @GuardedBy("mLock")
+    @Override
+    public void unprepareFromExecutionLocked(JobStatus jobStatus) {
+        if (jobStatus.hasConnectivityConstraint()) {
+            final UidStats uidStats =
+                    getUidStats(jobStatus.getSourceUid(), jobStatus.getSourcePackageName(), true);
+            uidStats.runningJobs.remove(jobStatus);
+            postAdjustCallbacks();
         }
     }
 
@@ -270,12 +282,7 @@
             final UidStats uidStats =
                     getUidStats(jobStatus.getSourceUid(), jobStatus.getSourcePackageName(), true);
             uidStats.numReadyWithConnectivity--;
-            if (jobStatus.madeActive != 0) {
-                // numRunning would be 0 if the UidStats object didn't exist before this method
-                // was called. getUidStats() handles logging, so just make sure we don't save a
-                // negative value.
-                uidStats.numRunning = Math.max(0, uidStats.numRunning - 1);
-            }
+            uidStats.runningJobs.remove(jobStatus);
             maybeRevokeStandbyExceptionLocked(jobStatus);
             postAdjustCallbacks();
         }
@@ -1131,7 +1138,7 @@
     private static class UidStats {
         public final int uid;
         public int basePriority;
-        public int numRunning;
+        public final ArraySet<JobStatus> runningJobs = new ArraySet<>();
         public int numReadyWithConnectivity;
         public int numRequestedNetworkAvailable;
         public int numEJs;
@@ -1148,7 +1155,7 @@
             pw.print("UidStats{");
             pw.print("uid", uid);
             pw.print("pri", basePriority);
-            pw.print("#run", numRunning);
+            pw.print("#run", runningJobs.size());
             pw.print("#readyWithConn", numReadyWithConnectivity);
             pw.print("#netAvail", numRequestedNetworkAvailable);
             pw.print("#EJs", numEJs);
diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/ContentObserverController.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/ContentObserverController.java
index 8b0da34..e64233f 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/controllers/ContentObserverController.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/ContentObserverController.java
@@ -136,6 +136,29 @@
     }
 
     @Override
+    public void unprepareFromExecutionLocked(JobStatus taskStatus) {
+        if (taskStatus.hasContentTriggerConstraint()) {
+            if (taskStatus.contentObserverJobInstance != null) {
+                if (taskStatus.contentObserverJobInstance.mChangedUris == null) {
+                    taskStatus.contentObserverJobInstance.mChangedUris = taskStatus.changedUris;
+                } else {
+                    taskStatus.contentObserverJobInstance.mChangedUris
+                            .addAll(taskStatus.changedUris);
+                }
+                if (taskStatus.contentObserverJobInstance.mChangedAuthorities == null) {
+                    taskStatus.contentObserverJobInstance.mChangedAuthorities =
+                            taskStatus.changedAuthorities;
+                } else {
+                    taskStatus.contentObserverJobInstance.mChangedAuthorities
+                            .addAll(taskStatus.changedAuthorities);
+                }
+                taskStatus.changedUris = null;
+                taskStatus.changedAuthorities = null;
+            }
+        }
+    }
+
+    @Override
     public void maybeStopTrackingJobLocked(JobStatus taskStatus, JobStatus incomingJob,
             boolean forUpdate) {
         if (taskStatus.clearTrackingController(JobStatus.TRACKING_CONTENT)) {
diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/QuotaController.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/QuotaController.java
index d4ce437..aace645 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/controllers/QuotaController.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/QuotaController.java
@@ -34,7 +34,6 @@
 import android.annotation.UserIdInt;
 import android.app.ActivityManager;
 import android.app.AlarmManager;
-import android.app.AppGlobals;
 import android.app.IUidObserver;
 import android.app.usage.UsageEvents;
 import android.app.usage.UsageStatsManagerInternal;
@@ -677,27 +676,30 @@
     }
 
     @Override
-    public void maybeStopTrackingJobLocked(JobStatus jobStatus, JobStatus incomingJob,
-            boolean forUpdate) {
-        if (jobStatus.clearTrackingController(JobStatus.TRACKING_QUOTA)) {
-            Timer timer = mPkgTimers.get(jobStatus.getSourceUserId(),
-                    jobStatus.getSourcePackageName());
+    public void unprepareFromExecutionLocked(JobStatus jobStatus) {
+        Timer timer = mPkgTimers.get(jobStatus.getSourceUserId(), jobStatus.getSourcePackageName());
+        if (timer != null) {
+            timer.stopTrackingJob(jobStatus);
+        }
+        if (jobStatus.isRequestedExpeditedJob()) {
+            timer = mEJPkgTimers.get(jobStatus.getSourceUserId(), jobStatus.getSourcePackageName());
             if (timer != null) {
                 timer.stopTrackingJob(jobStatus);
             }
-            if (jobStatus.isRequestedExpeditedJob()) {
-                timer = mEJPkgTimers.get(jobStatus.getSourceUserId(),
-                        jobStatus.getSourcePackageName());
-                if (timer != null) {
-                    timer.stopTrackingJob(jobStatus);
-                }
-            }
+        }
+        mTopStartedJobs.remove(jobStatus);
+    }
+
+    @Override
+    public void maybeStopTrackingJobLocked(JobStatus jobStatus, JobStatus incomingJob,
+            boolean forUpdate) {
+        if (jobStatus.clearTrackingController(JobStatus.TRACKING_QUOTA)) {
+            unprepareFromExecutionLocked(jobStatus);
             ArraySet<JobStatus> jobs = mTrackedJobs.get(jobStatus.getSourceUserId(),
                     jobStatus.getSourcePackageName());
             if (jobs != null) {
                 jobs.remove(jobStatus);
             }
-            mTopStartedJobs.remove(jobStatus);
         }
     }
 
diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/StateController.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/StateController.java
index 334876f..f0fc3b0 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/controllers/StateController.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/StateController.java
@@ -76,6 +76,12 @@
     }
 
     /**
+     * Optionally implement logic here for when a job that was about to be executed failed to start.
+     */
+    public void unprepareFromExecutionLocked(JobStatus jobStatus) {
+    }
+
+    /**
      * Remove task - this will happen if the task is cancelled, completed, etc.
      */
     public abstract void maybeStopTrackingJobLocked(JobStatus jobStatus, JobStatus incomingJob,