Age jobs into runnability even in infrequent buckets

Actually use the "base heartbeats" of jobs when deciding runnability, so
that now we properly sweep up all of the patiently-waiting jobs for a
given bucket even if we can't quite get to them all within one heartbeat
duration.  Previously we would accidentally stop handling them until that
bucket's next evaluation point, which might be far in the future.  Now
we're fair: once the app has waited an appropriate time, we can "catch up"
all of its pending jobs, not just the ones we happen to fit into a
relatively short period of time.

Bug: 63527785
Test: manual
Change-Id: I6b49b1f151df53bda59a64ead0c6c2a834a0dfe9
diff --git a/services/core/java/com/android/server/job/JobSchedulerService.java b/services/core/java/com/android/server/job/JobSchedulerService.java
index 3f014b5..f2a1c47 100644
--- a/services/core/java/com/android/server/job/JobSchedulerService.java
+++ b/services/core/java/com/android/server/job/JobSchedulerService.java
@@ -1721,16 +1721,29 @@
 
         // If the app is in a non-active standby bucket, make sure we've waited
         // an appropriate amount of time since the last invocation
-        if (mHeartbeat < mNextBucketHeartbeat[job.getStandbyBucket()]) {
-            // TODO: log/trace that we're deferring the job due to bucketing if we hit this
-            if (job.getWhenStandbyDeferred() == 0) {
-                if (DEBUG_STANDBY) {
-                    Slog.v(TAG, "Bucket deferral: " + mHeartbeat + " < "
-                            + mNextBucketHeartbeat[job.getStandbyBucket()] + " for " + job);
+        final int bucket = job.getStandbyBucket();
+        if (mHeartbeat < mNextBucketHeartbeat[bucket]) {
+            // Only skip this job if it's still waiting for the end of its (initial) nominal
+            // bucket interval.  Once it's waited that long, we let it go ahead and clear.
+            // The final (NEVER) bucket is special; we never age those apps' jobs into
+            // runnability.
+            if (bucket >= mConstants.STANDBY_BEATS.length
+                    || (mHeartbeat < job.getBaseHeartbeat() + mConstants.STANDBY_BEATS[bucket])) {
+                // TODO: log/trace that we're deferring the job due to bucketing if we hit this
+                if (job.getWhenStandbyDeferred() == 0) {
+                    if (DEBUG_STANDBY) {
+                        Slog.v(TAG, "Bucket deferral: " + mHeartbeat + " < "
+                                + mNextBucketHeartbeat[job.getStandbyBucket()] + " for " + job);
+                    }
+                    job.setWhenStandbyDeferred(sElapsedRealtimeClock.millis());
                 }
-                job.setWhenStandbyDeferred(sElapsedRealtimeClock.millis());
+                return false;
+            } else {
+                if (DEBUG_STANDBY) {
+                    Slog.v(TAG, "Bucket deferred job aged into runnability at "
+                            + mHeartbeat + " : " + job);
+                }
             }
-            return false;
         }
 
         // The expensive check last: validate that the defined package+service is