OSDN Git Service

The job scheduler now backs off jobs based on standby bucketing
authorChristopher Tate <ctate@google.com>
Fri, 27 Oct 2017 00:26:53 +0000 (17:26 -0700)
committerAmith Yamasani <yamasani@google.com>
Wed, 22 Nov 2017 22:07:06 +0000 (22:07 +0000)
The default parameters here translate to roughly this rate limiting:

  ACTIVE:   run jobs whenever
  WORKING:  ~ hourly
  FREQUENT: ~ every 6 hours
  RARE:     ~ daily

Bug: 63527785
Test: cts & manual (WIP)
      atest CtsJobSchedulerTestCases
Change-Id: I58f8e53e5bdf40601823e5a10a9f2383a6f67ae5

14 files changed:
core/java/android/app/job/JobService.java
core/java/android/app/usage/UsageStatsManagerInternal.java
core/java/android/content/pm/PackageManagerInternal.java
services/core/java/com/android/server/job/JobSchedulerInternal.java
services/core/java/com/android/server/job/JobSchedulerService.java
services/core/java/com/android/server/job/JobServiceContext.java
services/core/java/com/android/server/job/JobStore.java
services/core/java/com/android/server/job/controllers/AppIdleController.java
services/core/java/com/android/server/job/controllers/JobStatus.java
services/core/java/com/android/server/pm/PackageManagerService.java
services/tests/servicestests/src/com/android/server/job/JobStoreTest.java
services/usage/java/com/android/server/usage/AppIdleHistory.java
services/usage/java/com/android/server/usage/AppStandbyController.java
services/usage/java/com/android/server/usage/UsageStatsService.java

index 69afed2..61afada 100644 (file)
@@ -72,6 +72,33 @@ public abstract class JobService extends Service {
     }
 
     /**
+     * Call this to inform the JobScheduler that the job has finished its work.  When the
+     * system receives this message, it releases the wakelock being held for the job.
+     * <p>
+     * You can request that the job be scheduled again by passing {@code true} as
+     * the <code>wantsReschedule</code> parameter. This will apply back-off policy
+     * for the job; this policy can be adjusted through the
+     * {@link android.app.job.JobInfo.Builder#setBackoffCriteria(long, int)} method
+     * when the job is originally scheduled.  The job's initial
+     * requirements are preserved when jobs are rescheduled, regardless of backed-off
+     * policy.
+     * <p class="note">
+     * A job running while the device is dozing will not be rescheduled with the normal back-off
+     * policy.  Instead, the job will be re-added to the queue and executed again during
+     * a future idle maintenance window.
+     * </p>
+     *
+     * @param params The parameters identifying this job, as supplied to
+     *               the job in the {@link #onStartJob(JobParameters)} callback.
+     * @param wantsReschedule {@code true} if this job should be rescheduled according
+     *     to the back-off criteria specified when it was first scheduled; {@code false}
+     *     otherwise.
+     */
+    public final void jobFinished(JobParameters params, boolean wantsReschedule) {
+        mEngine.jobFinished(params, wantsReschedule);
+    }
+
+    /**
      * Called to indicate that the job has begun executing.  Override this method with the
      * logic for your job.  Like all other component lifecycle callbacks, this method executes
      * on your application's main thread.
@@ -127,31 +154,4 @@ public abstract class JobService extends Service {
      * to end the job entirely.  Regardless of the value returned, your job must stop executing.
      */
     public abstract boolean onStopJob(JobParameters params);
-
-    /**
-     * Call this to inform the JobScheduler that the job has finished its work.  When the
-     * system receives this message, it releases the wakelock being held for the job.
-     * <p>
-     * You can request that the job be scheduled again by passing {@code true} as
-     * the <code>wantsReschedule</code> parameter. This will apply back-off policy
-     * for the job; this policy can be adjusted through the
-     * {@link android.app.job.JobInfo.Builder#setBackoffCriteria(long, int)} method
-     * when the job is originally scheduled.  The job's initial
-     * requirements are preserved when jobs are rescheduled, regardless of backed-off
-     * policy.
-     * <p class="note">
-     * A job running while the device is dozing will not be rescheduled with the normal back-off
-     * policy.  Instead, the job will be re-added to the queue and executed again during
-     * a future idle maintenance window.
-     * </p>
-     *
-     * @param params The parameters identifying this job, as supplied to
-     *               the job in the {@link #onStartJob(JobParameters)} callback.
-     * @param wantsReschedule {@code true} if this job should be rescheduled according
-     *     to the back-off criteria specified when it was first scheduled; {@code false}
-     *     otherwise.
-     */
-    public final void jobFinished(JobParameters params, boolean wantsReschedule) {
-        mEngine.jobFinished(params, wantsReschedule);
-    }
 }
index 29e7439..9954484 100644 (file)
@@ -16,6 +16,7 @@
 
 package android.app.usage;
 
+import android.app.usage.AppStandby.StandbyBuckets;
 import android.content.ComponentName;
 import android.content.res.Configuration;
 
@@ -91,6 +92,19 @@ public abstract class UsageStatsManagerInternal {
     public abstract boolean isAppIdle(String packageName, int uidForAppId, int userId);
 
     /**
+     * Returns the app standby bucket that the app is currently in.  This accessor does
+     * <em>not</em> obfuscate instant apps.
+     *
+     * @param packageName
+     * @param userId
+     * @param nowElapsed The current time, in the elapsedRealtime time base
+     * @return the AppStandby bucket code the app currently resides in.  If the app is
+     *     unknown in the given user, STANDBY_BUCKET_NEVER is returned.
+     */
+    @StandbyBuckets public abstract int getAppStandbyBucket(String packageName, int userId,
+            long nowElapsed);
+
+    /**
      * Returns all of the uids for a given user where all packages associating with that uid
      * are in the app idle state -- there are no associated apps that are not idle.  This means
      * all of the returned uids can be safely considered app idle.
index 14cf855..713cd10 100644 (file)
@@ -164,6 +164,14 @@ public abstract class PackageManagerInternal {
             @PackageInfoFlags int flags, int filterCallingUid, int userId);
 
     /**
+     * Do a straight uid lookup for the given package/application in the given user.
+     * @see PackageManager#getPackageUidAsUser(String, int, int)
+     * @return The app's uid, or < 0 if the package was not found in that user
+     */
+    public abstract int getPackageUid(String packageName,
+            @PackageInfoFlags int flags, int userId);
+
+    /**
      * Retrieve all of the information we know about a particular package/application.
      * @param filterCallingUid The results will be filtered in the context of this UID instead
      * of the calling UID.
index 095526d..9bcf208 100644 (file)
@@ -26,6 +26,18 @@ import java.util.List;
  */
 public interface JobSchedulerInternal {
 
+    // Bookkeeping about app standby bucket scheduling
+
+    /**
+     * The current bucket heartbeat ordinal
+     */
+    long currentHeartbeat();
+
+    /**
+     * Heartbeat ordinal at which the given standby bucket's jobs next become runnable
+     */
+    long nextHeartbeatForBucket(int bucket);
+
     /**
      * Returns a list of pending jobs scheduled by the system service.
      */
index 4a3becb..4af86a0 100644 (file)
@@ -29,6 +29,9 @@ import android.app.job.JobParameters;
 import android.app.job.JobScheduler;
 import android.app.job.JobService;
 import android.app.job.JobWorkItem;
+import android.app.usage.AppStandby;
+import android.app.usage.UsageStatsManagerInternal;
+import android.app.usage.UsageStatsManagerInternal.AppIdleStateChangeListener;
 import android.content.BroadcastReceiver;
 import android.content.ComponentName;
 import android.content.ContentResolver;
@@ -37,6 +40,7 @@ import android.content.Intent;
 import android.content.IntentFilter;
 import android.content.pm.IPackageManager;
 import android.content.pm.PackageManager;
+import android.content.pm.PackageManagerInternal;
 import android.content.pm.PackageManager.NameNotFoundException;
 import android.content.pm.ServiceInfo;
 import android.database.ContentObserver;
@@ -46,7 +50,6 @@ import android.os.Binder;
 import android.os.Handler;
 import android.os.Looper;
 import android.os.Message;
-import android.os.PowerManager;
 import android.os.Process;
 import android.os.RemoteException;
 import android.os.ResultReceiver;
@@ -65,6 +68,7 @@ import android.util.TimeUtils;
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.app.IBatteryStats;
 import com.android.internal.app.procstats.ProcessStats;
+import com.android.internal.os.BackgroundThread;
 import com.android.internal.util.ArrayUtils;
 import com.android.internal.util.DumpUtils;
 import com.android.server.DeviceIdleController;
@@ -111,6 +115,7 @@ public final class JobSchedulerService extends com.android.server.SystemService
         implements StateChangedListener, JobCompletedListener {
     static final String TAG = "JobSchedulerService";
     public static final boolean DEBUG = false;
+    public static final boolean DEBUG_STANDBY = DEBUG || false;
 
     /** The maximum number of concurrent jobs we run at one time. */
     private static final int MAX_JOB_CONTEXTS_COUNT = 16;
@@ -130,6 +135,8 @@ public final class JobSchedulerService extends com.android.server.SystemService
     final Object mLock = new Object();
     /** Master list of jobs. */
     final JobStore mJobs;
+    /** Tracking the standby bucket state of each app */
+    final StandbyTracker mStandbyTracker;
     /** Tracking amount of time each package runs for. */
     final JobPackageTracker mJobPackageTracker = new JobPackageTracker();
 
@@ -163,8 +170,8 @@ public final class JobSchedulerService extends com.android.server.SystemService
     final JobHandler mHandler;
     final JobSchedulerStub mJobSchedulerStub;
 
+    PackageManagerInternal mLocalPM;
     IBatteryStats mBatteryStats;
-    PowerManager mPowerManager;
     DeviceIdleController.LocalService mLocalDeviceIdleController;
 
     /**
@@ -193,6 +200,17 @@ public final class JobSchedulerService extends com.android.server.SystemService
      */
     final SparseIntArray mBackingUpUids = new SparseIntArray();
 
+    /**
+     * Count standby heartbeats, and keep track of which beat each bucket's jobs will
+     * next become runnable.  Index into this array is by normalized bucket:
+     * { ACTIVE, WORKING, FREQUENT, RARE, NEVER }.  The ACTIVE and NEVER bucket
+     * milestones are not updated: ACTIVE apps get jobs whenever they ask for them,
+     * and NEVER apps don't get them at all.
+     */
+    final long[] mNextBucketHeartbeat = { 0, 0, 0, 0, Long.MAX_VALUE };
+    long mHeartbeat = 0;
+    long mLastHeartbeatTime = 0;
+
     // -- Pre-allocated temporaries only for use in assignJobsToContextsLocked --
 
     /**
@@ -237,6 +255,10 @@ public final class JobSchedulerService extends com.android.server.SystemService
         private static final String KEY_MAX_WORK_RESCHEDULE_COUNT = "max_work_reschedule_count";
         private static final String KEY_MIN_LINEAR_BACKOFF_TIME = "min_linear_backoff_time";
         private static final String KEY_MIN_EXP_BACKOFF_TIME = "min_exp_backoff_time";
+        private static final String KEY_STANDBY_HEARTBEAT_TIME = "standby_heartbeat_time";
+        private static final String KEY_STANDBY_WORKING_BEATS = "standby_working_beats";
+        private static final String KEY_STANDBY_FREQUENT_BEATS = "standby_frequent_beats";
+        private static final String KEY_STANDBY_RARE_BEATS = "standby_rare_beats";
 
         private static final int DEFAULT_MIN_IDLE_COUNT = 1;
         private static final int DEFAULT_MIN_CHARGING_COUNT = 1;
@@ -256,6 +278,10 @@ public final class JobSchedulerService extends com.android.server.SystemService
         private static final int DEFAULT_MAX_WORK_RESCHEDULE_COUNT = Integer.MAX_VALUE;
         private static final long DEFAULT_MIN_LINEAR_BACKOFF_TIME = JobInfo.MIN_BACKOFF_MILLIS;
         private static final long DEFAULT_MIN_EXP_BACKOFF_TIME = JobInfo.MIN_BACKOFF_MILLIS;
+        private static final long DEFAULT_STANDBY_HEARTBEAT_TIME = 11 * 60 * 1000L;
+        private static final int DEFAULT_STANDBY_WORKING_BEATS = 5;  // ~ 1 hour, with 11-min beats
+        private static final int DEFAULT_STANDBY_FREQUENT_BEATS = 31; // ~ 6 hours
+        private static final int DEFAULT_STANDBY_RARE_BEATS = 130; // ~ 24 hours
 
         /**
          * Minimum # of idle jobs that must be ready in order to force the JMS to schedule things
@@ -344,6 +370,27 @@ public final class JobSchedulerService extends com.android.server.SystemService
          * The minimum backoff time to allow for exponential backoff.
          */
         long MIN_EXP_BACKOFF_TIME = DEFAULT_MIN_EXP_BACKOFF_TIME;
+        /**
+         * How often we recalculate runnability based on apps' standby bucket assignment.
+         * This should be prime relative to common time interval lengths such as a quarter-
+         * hour or day, so that the heartbeat drifts relative to wall-clock milestones.
+         */
+        long STANDBY_HEARTBEAT_TIME = DEFAULT_STANDBY_HEARTBEAT_TIME;
+
+        /**
+         * Mapping: standby bucket -> number of heartbeats between each sweep of that
+         * bucket's jobs.
+         *
+         * Bucket assignments as recorded in the JobStatus objects are normalized to be
+         * indices into this array, rather than the raw constants used
+         * by AppIdleHistory.
+         */
+        final int[] STANDBY_BEATS = {
+                0,
+                DEFAULT_STANDBY_WORKING_BEATS,
+                DEFAULT_STANDBY_FREQUENT_BEATS,
+                DEFAULT_STANDBY_RARE_BEATS
+        };
 
         private ContentResolver mResolver;
         private final KeyValueListParser mParser = new KeyValueListParser(',');
@@ -423,6 +470,14 @@ public final class JobSchedulerService extends com.android.server.SystemService
                         DEFAULT_MIN_LINEAR_BACKOFF_TIME);
                 MIN_EXP_BACKOFF_TIME = mParser.getLong(KEY_MIN_EXP_BACKOFF_TIME,
                         DEFAULT_MIN_EXP_BACKOFF_TIME);
+                STANDBY_HEARTBEAT_TIME = mParser.getLong(KEY_STANDBY_HEARTBEAT_TIME,
+                        DEFAULT_STANDBY_HEARTBEAT_TIME);
+                STANDBY_BEATS[1] = mParser.getInt(KEY_STANDBY_WORKING_BEATS,
+                        DEFAULT_STANDBY_WORKING_BEATS);
+                STANDBY_BEATS[2] = mParser.getInt(KEY_STANDBY_FREQUENT_BEATS,
+                        DEFAULT_STANDBY_FREQUENT_BEATS);
+                STANDBY_BEATS[3] = mParser.getInt(KEY_STANDBY_RARE_BEATS,
+                        DEFAULT_STANDBY_RARE_BEATS);
             }
         }
 
@@ -482,6 +537,17 @@ public final class JobSchedulerService extends com.android.server.SystemService
 
             pw.print("    "); pw.print(KEY_MIN_EXP_BACKOFF_TIME); pw.print("=");
             pw.print(MIN_EXP_BACKOFF_TIME); pw.println();
+
+            pw.print("    "); pw.print(KEY_STANDBY_HEARTBEAT_TIME); pw.print("=");
+            pw.print(STANDBY_HEARTBEAT_TIME); pw.println();
+
+            pw.print("    standby_beats={");
+            pw.print(STANDBY_BEATS[0]);
+            for (int i = 1; i < STANDBY_BEATS.length; i++) {
+                pw.print(", ");
+                pw.print(STANDBY_BEATS[i]);
+            }
+            pw.println('}');
         }
     }
 
@@ -684,6 +750,7 @@ public final class JobSchedulerService extends com.android.server.SystemService
             }
         } catch (RemoteException e) {
         }
+
         synchronized (mLock) {
             final JobStatus toCancel = mJobs.getJobByUidAndJobId(uId, job.getId());
 
@@ -935,9 +1002,22 @@ public final class JobSchedulerService extends com.android.server.SystemService
      */
     public JobSchedulerService(Context context) {
         super(context);
+
+        mLocalPM = LocalServices.getService(PackageManagerInternal.class);
+
         mHandler = new JobHandler(context.getMainLooper());
         mConstants = new Constants(mHandler);
         mJobSchedulerStub = new JobSchedulerStub();
+
+        // Set up the app standby bucketing tracker
+        UsageStatsManagerInternal usageStats = LocalServices.getService(UsageStatsManagerInternal.class);
+        mStandbyTracker = new StandbyTracker(usageStats);
+        usageStats.addAppIdleStateChangeListener(mStandbyTracker);
+
+        // The job store needs to call back
+        publishLocalService(JobSchedulerInternal.class, new LocalService());
+
+        // Initialize the job store and set up any persisted jobs
         mJobs = JobStore.initAndGet(this);
 
         // Create the controllers.
@@ -1007,7 +1087,6 @@ public final class JobSchedulerService extends com.android.server.SystemService
 
     @Override
     public void onStart() {
-        publishLocalService(JobSchedulerInternal.class, new LocalService());
         publishBinderService(Context.JOB_SCHEDULER_SERVICE, mJobSchedulerStub);
     }
 
@@ -1027,7 +1106,6 @@ public final class JobSchedulerService extends com.android.server.SystemService
             final IntentFilter userFilter = new IntentFilter(Intent.ACTION_USER_REMOVED);
             getContext().registerReceiverAsUser(
                     mBroadcastReceiver, UserHandle.ALL, userFilter, null, null);
-            mPowerManager = (PowerManager)getContext().getSystemService(Context.POWER_SERVICE);
             try {
                 ActivityManager.getService().registerUidObserver(mUidObserver,
                         ActivityManager.UID_OBSERVER_PROCSTATE | ActivityManager.UID_OBSERVER_GONE
@@ -1207,7 +1285,8 @@ public final class JobSchedulerService extends com.android.server.SystemService
         }
         delayMillis =
                 Math.min(delayMillis, JobInfo.MAX_BACKOFF_DELAY_MILLIS);
-        JobStatus newJob = new JobStatus(failureToReschedule, elapsedNowMillis + delayMillis,
+        JobStatus newJob = new JobStatus(failureToReschedule, getCurrentHeartbeat(),
+                elapsedNowMillis + delayMillis,
                 JobStatus.NO_LATEST_RUNTIME, backoffAttempts,
                 failureToReschedule.getLastSuccessfulRunTime(), sSystemClock.millis());
         for (int ic=0; ic<mControllers.size(); ic++) {
@@ -1221,10 +1300,12 @@ public final class JobSchedulerService extends com.android.server.SystemService
      * Called after a periodic has executed so we can reschedule it. We take the last execution
      * time of the job to be the time of completion (i.e. the time at which this function is
      * called).
-     * This could be inaccurate b/c the job can run for as long as
+     * <p>This could be inaccurate b/c the job can run for as long as
      * {@link com.android.server.job.JobServiceContext#EXECUTING_TIMESLICE_MILLIS}, but will lead
      * to underscheduling at least, rather than if we had taken the last execution time to be the
      * start of the execution.
+     * <p>Unlike a reschedule prior to execution, in this case we advance the next-heartbeat
+     * tracking as though the job were newly-scheduled.
      * @return A new job representing the execution criteria for this instantiation of the
      * recurring job.
      */
@@ -1246,8 +1327,9 @@ public final class JobSchedulerService extends com.android.server.SystemService
             Slog.v(TAG, "Rescheduling executed periodic. New execution window [" +
                     newEarliestRunTimeElapsed/1000 + ", " + newLatestRuntimeElapsed/1000 + "]s");
         }
-        return new JobStatus(periodicToReschedule, newEarliestRunTimeElapsed,
-                newLatestRuntimeElapsed, 0 /* backoffAttempt */,
+        return new JobStatus(periodicToReschedule, getCurrentHeartbeat(),
+                newEarliestRunTimeElapsed, newLatestRuntimeElapsed,
+                0 /* backoffAttempt */,
                 sSystemClock.millis() /* lastSuccessfulRunTime */,
                 periodicToReschedule.getLastFailedRunTime());
     }
@@ -1394,7 +1476,9 @@ public final class JobSchedulerService extends com.android.server.SystemService
         noteJobsNonpending(mPendingJobs);
         mPendingJobs.clear();
         stopNonReadyActiveJobsLocked();
+        boolean updated = updateStandbyHeartbeatLocked();
         mJobs.forEachJob(mReadyQueueFunctor);
+        if (updated) updateNextStandbyHeartbeatsLocked();
         mReadyQueueFunctor.postProcess();
 
         if (DEBUG) {
@@ -1548,16 +1632,45 @@ public final class JobSchedulerService extends com.android.server.SystemService
         noteJobsNonpending(mPendingJobs);
         mPendingJobs.clear();
         stopNonReadyActiveJobsLocked();
+        boolean updated = updateStandbyHeartbeatLocked();
         mJobs.forEachJob(mMaybeQueueFunctor);
+        if (updated) updateNextStandbyHeartbeatsLocked();
         mMaybeQueueFunctor.postProcess();
     }
 
+    private boolean updateStandbyHeartbeatLocked() {
+        final long sinceLast = sElapsedRealtimeClock.millis() - mLastHeartbeatTime;
+        final long beatsElapsed = sinceLast / mConstants.STANDBY_HEARTBEAT_TIME;
+        if (beatsElapsed > 0) {
+            mHeartbeat += beatsElapsed;
+            mLastHeartbeatTime += beatsElapsed * mConstants.STANDBY_HEARTBEAT_TIME;
+            if (DEBUG_STANDBY) {
+                Slog.v(TAG, "Advancing standby heartbeat by " + beatsElapsed + " to " + mHeartbeat);
+            }
+            return true;
+        }
+        return false;
+    }
+
+    private void updateNextStandbyHeartbeatsLocked() {
+        // don't update ACTIVE or NEVER bucket milestones
+        for (int i = 1; i < mNextBucketHeartbeat.length - 1; i++) {
+            while (mHeartbeat >= mNextBucketHeartbeat[i]) {
+                mNextBucketHeartbeat[i] += mConstants.STANDBY_BEATS[i];
+            }
+            if (DEBUG_STANDBY) {
+                Slog.v(TAG, "   Bucket " + i + " next heartbeat " + mNextBucketHeartbeat[i]);
+            }
+        }
+    }
+
     /**
      * Criteria for moving a job into the pending queue:
      *      - It's ready.
      *      - It's not pending.
      *      - It's not already running on a JSC.
      *      - The user that requested the job is running.
+     *      - The job's standby bucket has come due to be runnable.
      *      - The component is enabled and runnable.
      */
     private boolean isReadyToBeExecutedLocked(JobStatus job) {
@@ -1606,6 +1719,22 @@ public final class JobSchedulerService extends com.android.server.SystemService
             return false;
         }
 
+        // 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);
+                }
+                job.setWhenStandbyDeferred(sElapsedRealtimeClock.millis());
+            }
+            return false;
+        }
+
+        // The expensive check last: validate that the defined package+service is
+        // still present & viable.
         final boolean componentPresent;
         try {
             componentPresent = (AppGlobals.getPackageManager().getServiceInfo(
@@ -1819,6 +1948,22 @@ public final class JobSchedulerService extends com.android.server.SystemService
     final class LocalService implements JobSchedulerInternal {
 
         /**
+         * The current bucket heartbeat ordinal
+         */
+        public long currentHeartbeat() {
+            return getCurrentHeartbeat();
+        }
+
+        /**
+         * Heartbeat ordinal at which the given standby bucket's jobs next become runnable
+         */
+        public long nextHeartbeatForBucket(int bucket) {
+            synchronized (mLock) {
+                return mNextBucketHeartbeat[bucket];
+            }
+        }
+
+        /**
          * Returns a list of all pending jobs. A running job is not considered pending. Periodic
          * jobs are always considered pending.
          */
@@ -1879,6 +2024,79 @@ public final class JobSchedulerService extends com.android.server.SystemService
     }
 
     /**
+     * Tracking of app assignments to standby buckets
+     */
+    final class StandbyTracker extends AppIdleStateChangeListener {
+        final UsageStatsManagerInternal mUsageStats;
+
+        StandbyTracker(UsageStatsManagerInternal usageStats) {
+            mUsageStats = usageStats;
+        }
+
+        // AppIdleStateChangeListener interface for live updates
+
+        @Override
+        public void onAppIdleStateChanged(final String packageName, final int userId,
+                boolean idle, int bucket) {
+            final int uid = mLocalPM.getPackageUid(packageName,
+                    PackageManager.MATCH_UNINSTALLED_PACKAGES, userId);
+            if (uid < 0) {
+                if (DEBUG_STANDBY) {
+                    Slog.i(TAG, "App idle state change for unknown app "
+                            + packageName + "/" + userId);
+                }
+                return;
+            }
+
+            final int bucketIndex = standbyBucketToBucketIndex(bucket);
+            // update job bookkeeping out of band
+            BackgroundThread.getHandler().post(() -> {
+                if (DEBUG_STANDBY) {
+                    Slog.i(TAG, "Moving uid " + uid + " to bucketIndex " + bucketIndex);
+                }
+                synchronized (mLock) {
+                    // TODO: update to be more efficient once we can slice by source UID
+                    mJobs.forEachJob((JobStatus job) -> {
+                        if (job.getSourceUid() == uid) {
+                            job.setStandbyBucket(bucketIndex);
+                        }
+                    });
+                    onControllerStateChanged();
+                }
+            });
+        }
+
+        @Override
+        public void onParoleStateChanged(boolean isParoleOn) {
+            // Unused
+        }
+    }
+
+    public static int standbyBucketToBucketIndex(int bucket) {
+        // Normalize AppStandby constants to indices into our bookkeeping
+        if (bucket == AppStandby.STANDBY_BUCKET_NEVER) return 4;
+        else if (bucket >= AppStandby.STANDBY_BUCKET_RARE) return 3;
+        else if (bucket >= AppStandby.STANDBY_BUCKET_FREQUENT) return 2;
+        else if (bucket >= AppStandby.STANDBY_BUCKET_WORKING_SET) return 1;
+        else return 0;
+    }
+
+    public static int standbyBucketForPackage(String packageName, int userId, long elapsedNow) {
+        UsageStatsManagerInternal usageStats = LocalServices.getService(
+                UsageStatsManagerInternal.class);
+        int bucket = usageStats != null
+                ? usageStats.getAppStandbyBucket(packageName, userId, elapsedNow)
+                : 0;
+
+        bucket = standbyBucketToBucketIndex(bucket);
+
+        if (DEBUG_STANDBY) {
+            Slog.v(TAG, packageName + "/" + userId + " standby bucket index: " + bucket);
+        }
+        return bucket;
+    }
+
+    /**
      * Binder stub trampoline implementation
      */
     final class JobSchedulerStub extends IJobScheduler.Stub {
@@ -1943,6 +2161,7 @@ public final class JobSchedulerService extends com.android.server.SystemService
             }
             final int pid = Binder.getCallingPid();
             final int uid = Binder.getCallingUid();
+            final int userId = UserHandle.getUserId(uid);
 
             enforceValidJobRequest(uid, job);
             if (job.isPersisted()) {
@@ -1959,7 +2178,8 @@ public final class JobSchedulerService extends com.android.server.SystemService
 
             long ident = Binder.clearCallingIdentity();
             try {
-                return JobSchedulerService.this.scheduleAsPackage(job, null, uid, null, -1, null);
+                return JobSchedulerService.this.scheduleAsPackage(job, null, uid, null, userId,
+                        null);
             } finally {
                 Binder.restoreCallingIdentity(ident);
             }
@@ -1971,8 +2191,8 @@ public final class JobSchedulerService extends com.android.server.SystemService
             if (DEBUG) {
                 Slog.d(TAG, "Enqueueing job: " + job.toString() + " work: " + work);
             }
-            final int pid = Binder.getCallingPid();
             final int uid = Binder.getCallingUid();
+            final int userId = UserHandle.getUserId(uid);
 
             enforceValidJobRequest(uid, job);
             if (job.isPersisted()) {
@@ -1989,7 +2209,8 @@ public final class JobSchedulerService extends com.android.server.SystemService
 
             long ident = Binder.clearCallingIdentity();
             try {
-                return JobSchedulerService.this.scheduleAsPackage(job, work, uid, null, -1, null);
+                return JobSchedulerService.this.scheduleAsPackage(job, work, uid, null, userId,
+                        null);
             } finally {
                 Binder.restoreCallingIdentity(ident);
             }
@@ -2001,7 +2222,7 @@ public final class JobSchedulerService extends com.android.server.SystemService
             final int callerUid = Binder.getCallingUid();
             if (DEBUG) {
                 Slog.d(TAG, "Caller uid " + callerUid + " scheduling job: " + job.toString()
-                        + " on behalf of " + packageName);
+                        + " on behalf of " + packageName + "/");
             }
 
             if (packageName == null) {
@@ -2235,6 +2456,12 @@ public final class JobSchedulerService extends com.android.server.SystemService
         }
     }
 
+    long getCurrentHeartbeat() {
+        synchronized (mLock) {
+            return mHeartbeat;
+        }
+    }
+
     int getJobState(PrintWriter pw, String pkgName, int userId, int jobId) {
         try {
             final int uid = AppGlobals.getPackageManager().getPackageUid(pkgName, 0,
index ac7297e..6a3fd04 100644 (file)
@@ -64,6 +64,8 @@ import com.android.server.job.controllers.JobStatus;
  */
 public final class JobServiceContext implements ServiceConnection {
     private static final boolean DEBUG = JobSchedulerService.DEBUG;
+    private static final boolean DEBUG_STANDBY = JobSchedulerService.DEBUG_STANDBY;
+
     private static final String TAG = "JobServiceContext";
     /** Amount of time a job is allowed to execute for before being considered timed-out. */
     public static final long EXECUTING_TIMESLICE_MILLIS = 10 * 60 * 1000;  // 10mins.
@@ -220,6 +222,17 @@ public final class JobServiceContext implements ServiceConnection {
                     isDeadlineExpired, triggeredUris, triggeredAuthorities, job.network);
             mExecutionStartTimeElapsed = sElapsedRealtimeClock.millis();
 
+            if (DEBUG_STANDBY) {
+                final long whenDeferred = job.getWhenStandbyDeferred();
+                if (whenDeferred > 0) {
+                    StringBuilder sb = new StringBuilder(128);
+                    sb.append("Starting job deferred for standby by ");
+                    TimeUtils.formatDuration(mExecutionStartTimeElapsed - whenDeferred, sb);
+                    sb.append(" : ");
+                    sb.append(job.toShortString());
+                    Slog.v(TAG, sb.toString());
+                }
+            }
             // Once we'e begun executing a job, we by definition no longer care whether
             // it was inflated from disk with not-yet-coherent delay/deadline bounds.
             job.clearPersistedUtcTimes();
index 28b60e3..219bc61 100644 (file)
@@ -43,6 +43,7 @@ import com.android.internal.util.ArrayUtils;
 import com.android.internal.util.BitUtils;
 import com.android.internal.util.FastXmlSerializer;
 import com.android.server.IoThread;
+import com.android.server.LocalServices;
 import com.android.server.job.JobSchedulerInternal.JobStorePersistStats;
 import com.android.server.job.controllers.JobStatus;
 
@@ -174,7 +175,8 @@ public final class JobStore {
             if (utcTimes != null) {
                 Pair<Long, Long> elapsedRuntimes =
                         convertRtcBoundsToElapsed(utcTimes, elapsedNow);
-                toAdd.add(new JobStatus(job, elapsedRuntimes.first, elapsedRuntimes.second,
+                toAdd.add(new JobStatus(job, job.getBaseHeartbeat(),
+                        elapsedRuntimes.first, elapsedRuntimes.second,
                         0, job.getLastSuccessfulRunTime(), job.getLastFailedRunTime()));
                 toRemove.add(job);
             }
@@ -846,8 +848,13 @@ public final class JobStore {
             }
 
             // And now we're done
+            JobSchedulerInternal service = LocalServices.getService(JobSchedulerInternal.class);
+            final int appBucket = JobSchedulerService.standbyBucketForPackage(sourcePackageName,
+                    sourceUserId, elapsedNow);
+            long currentHeartbeat = service != null ? service.currentHeartbeat() : 0;
             JobStatus js = new JobStatus(
-                    jobBuilder.build(), uid, sourcePackageName, sourceUserId, sourceTag,
+                    jobBuilder.build(), uid, sourcePackageName, sourceUserId,
+                    appBucket, currentHeartbeat, sourceTag,
                     elapsedRuntimes.first, elapsedRuntimes.second,
                     lastSuccessfulRunTime, lastFailedRunTime,
                     (rtcIsGood) ? null : rtcRuntimes);
index caa8522..a7ed2f5 100644 (file)
@@ -180,6 +180,7 @@ public final class AppIdleController extends StateController {
                 if (mAppIdleParoleOn) {
                     return;
                 }
+
                 PackageUpdateFunc update = new PackageUpdateFunc(userId, packageName, idle);
                 mJobSchedulerService.getJobStore().forEachJob(update);
                 if (update.mChanged) {
index a5906cb..e71b8ec 100644 (file)
@@ -34,7 +34,9 @@ import android.util.Pair;
 import android.util.Slog;
 import android.util.TimeUtils;
 
+import com.android.server.LocalServices;
 import com.android.server.job.GrantedUriPermissions;
+import com.android.server.job.JobSchedulerInternal;
 import com.android.server.job.JobSchedulerService;
 
 import java.io.PrintWriter;
@@ -120,6 +122,24 @@ public final class JobStatus {
     /** How many times this job has failed, used to compute back-off. */
     private final int numFailures;
 
+    /**
+     * Current standby heartbeat when this job was scheduled or last ran.  Used to
+     * pin the runnability check regardless of the job's app moving between buckets.
+     */
+    private final long baseHeartbeat;
+
+    /**
+     * Which app standby bucket this job's app is in.  Updated when the app is moved to a
+     * different bucket.
+     */
+    private int standbyBucket;
+
+    /**
+     * Debugging: timestamp if we ever defer this job based on standby bucketing, this
+     * is when we did so.
+     */
+    private long whenStandbyDeferred;
+
     // Constraints.
     final int requiredConstraints;
     int satisfiedConstraints = 0;
@@ -221,10 +241,13 @@ public final class JobStatus {
     }
 
     private JobStatus(JobInfo job, int callingUid, String sourcePackageName,
-            int sourceUserId, String tag, int numFailures, long earliestRunTimeElapsedMillis,
-            long latestRunTimeElapsedMillis, long lastSuccessfulRunTime, long lastFailedRunTime) {
+            int sourceUserId, int standbyBucket, long heartbeat, String tag, int numFailures,
+            long earliestRunTimeElapsedMillis, long latestRunTimeElapsedMillis,
+            long lastSuccessfulRunTime, long lastFailedRunTime) {
         this.job = job;
         this.callingUid = callingUid;
+        this.standbyBucket = standbyBucket;
+        this.baseHeartbeat = heartbeat;
 
         int tempSourceUid = -1;
         if (sourceUserId != -1 && sourcePackageName != null) {
@@ -283,6 +306,7 @@ public final class JobStatus {
     public JobStatus(JobStatus jobStatus) {
         this(jobStatus.getJob(), jobStatus.getUid(),
                 jobStatus.getSourcePackageName(), jobStatus.getSourceUserId(),
+                jobStatus.getStandbyBucket(), jobStatus.getBaseHeartbeat(),
                 jobStatus.getSourceTag(), jobStatus.getNumFailures(),
                 jobStatus.getEarliestRunTime(), jobStatus.getLatestRunTimeElapsed(),
                 jobStatus.getLastSuccessfulRunTime(), jobStatus.getLastFailedRunTime());
@@ -299,13 +323,17 @@ public final class JobStatus {
      * {@link android.app.job.JobInfo} time criteria because we can load a persisted periodic job
      * from the {@link com.android.server.job.JobStore} and still want to respect its
      * wallclock runtime rather than resetting it on every boot.
-     * We consider a freshly loaded job to no longer be in back-off.
+     * We consider a freshly loaded job to no longer be in back-off, and the associated
+     * standby bucket is whatever the OS thinks it should be at this moment.
      */
-    public JobStatus(JobInfo job, int callingUid, String sourcePackageName, int sourceUserId,
-            String sourceTag, long earliestRunTimeElapsedMillis, long latestRunTimeElapsedMillis,
+    public JobStatus(JobInfo job, int callingUid, String sourcePkgName, int sourceUserId,
+            int standbyBucket, long baseHeartbeat, String sourceTag,
+            long earliestRunTimeElapsedMillis, long latestRunTimeElapsedMillis,
             long lastSuccessfulRunTime, long lastFailedRunTime,
             Pair<Long, Long> persistedExecutionTimesUTC) {
-        this(job, callingUid, sourcePackageName, sourceUserId, sourceTag, 0,
+        this(job, callingUid, sourcePkgName, sourceUserId,
+                standbyBucket, baseHeartbeat,
+                sourceTag, 0,
                 earliestRunTimeElapsedMillis, latestRunTimeElapsedMillis,
                 lastSuccessfulRunTime, lastFailedRunTime);
 
@@ -322,11 +350,13 @@ public final class JobStatus {
     }
 
     /** Create a new job to be rescheduled with the provided parameters. */
-    public JobStatus(JobStatus rescheduling, long newEarliestRuntimeElapsedMillis,
+    public JobStatus(JobStatus rescheduling, long newBaseHeartbeat,
+            long newEarliestRuntimeElapsedMillis,
             long newLatestRuntimeElapsedMillis, int backoffAttempt,
             long lastSuccessfulRunTime, long lastFailedRunTime) {
         this(rescheduling.job, rescheduling.getUid(),
                 rescheduling.getSourcePackageName(), rescheduling.getSourceUserId(),
+                rescheduling.getStandbyBucket(), newBaseHeartbeat,
                 rescheduling.getSourceTag(), backoffAttempt, newEarliestRuntimeElapsedMillis,
                 newLatestRuntimeElapsedMillis,
                 lastSuccessfulRunTime, lastFailedRunTime);
@@ -335,11 +365,12 @@ public final class JobStatus {
     /**
      * Create a newly scheduled job.
      * @param callingUid Uid of the package that scheduled this job.
-     * @param sourcePackageName Package name on whose behalf this job is scheduled. Null indicates
+     * @param sourcePkg Package name on whose behalf this job is scheduled. Null indicates
      *                          the calling package is the source.
      * @param sourceUserId User id for whom this job is scheduled. -1 indicates this is same as the
+     *     caller.
      */
-    public static JobStatus createFromJobInfo(JobInfo job, int callingUid, String sourcePackageName,
+    public static JobStatus createFromJobInfo(JobInfo job, int callingUid, String sourcePkg,
             int sourceUserId, String tag) {
         final long elapsedNow = sElapsedRealtimeClock.millis();
         final long earliestRunTimeElapsedMillis, latestRunTimeElapsedMillis;
@@ -352,7 +383,14 @@ public final class JobStatus {
             latestRunTimeElapsedMillis = job.hasLateConstraint() ?
                     elapsedNow + job.getMaxExecutionDelayMillis() : NO_LATEST_RUNTIME;
         }
-        return new JobStatus(job, callingUid, sourcePackageName, sourceUserId, tag, 0,
+        String jobPackage = (sourcePkg != null) ? sourcePkg : job.getService().getPackageName();
+
+        int standbyBucket = JobSchedulerService.standbyBucketForPackage(jobPackage,
+                sourceUserId, elapsedNow);
+        JobSchedulerInternal js = LocalServices.getService(JobSchedulerInternal.class);
+        long currentHeartbeat = js != null ? js.currentHeartbeat() : 0;
+        return new JobStatus(job, callingUid, sourcePkg, sourceUserId,
+                standbyBucket, currentHeartbeat, tag, 0,
                 earliestRunTimeElapsedMillis, latestRunTimeElapsedMillis,
                 0 /* lastSuccessfulRunTime */, 0 /* lastFailedRunTime */);
     }
@@ -528,6 +566,29 @@ public final class JobStatus {
         return UserHandle.getUserId(callingUid);
     }
 
+    public int getStandbyBucket() {
+        return standbyBucket;
+    }
+
+    public long getBaseHeartbeat() {
+        return baseHeartbeat;
+    }
+
+    // Called only by the standby monitoring code
+    public void setStandbyBucket(int newBucket) {
+        standbyBucket = newBucket;
+    }
+
+    // Called only by the standby monitoring code
+    public long getWhenStandbyDeferred() {
+        return whenStandbyDeferred;
+    }
+
+    // Called only by the standby monitoring code
+    public void setWhenStandbyDeferred(long now) {
+        whenStandbyDeferred = now;
+    }
+
     public String getSourceTag() {
         return sourceTag;
     }
@@ -950,6 +1011,19 @@ public final class JobStatus {
         }
     }
 
+    // normalized bucket indices, not the AppStandby constants
+    private String bucketName(int bucket) {
+        switch (bucket) {
+            case 0: return "ACTIVE";
+            case 1: return "WORKING_SET";
+            case 2: return "FREQUENT";
+            case 3: return "RARE";
+            case 4: return "NEVER";
+            default:
+                return "Unknown: " + bucket;
+        }
+    }
+
     // Dumpsys infrastructure
     public void dump(PrintWriter pw, String prefix, boolean full, long elapsedRealtimeMillis) {
         pw.print(prefix); UserHandle.formatUid(pw, callingUid);
@@ -1098,6 +1172,8 @@ public final class JobStatus {
                 dumpJobWorkItem(pw, prefix, executingWork.get(i), i);
             }
         }
+        pw.print(prefix); pw.print("Standby bucket: ");
+        pw.println(bucketName(standbyBucket));
         pw.print(prefix); pw.print("Enqueue time: ");
         TimeUtils.formatDuration(enqueueTime, elapsedRealtimeMillis, pw);
         pw.println();
index b5bcf27..8a9b45b 100644 (file)
@@ -87,7 +87,6 @@ import static android.os.storage.StorageManager.FLAG_STORAGE_CE;
 import static android.os.storage.StorageManager.FLAG_STORAGE_DE;
 import static android.system.OsConstants.O_CREAT;
 import static android.system.OsConstants.O_RDWR;
-
 import static com.android.internal.app.IntentForwarderActivity.FORWARD_INTENT_TO_MANAGED_PROFILE;
 import static com.android.internal.app.IntentForwarderActivity.FORWARD_INTENT_TO_PARENT;
 import static com.android.internal.content.NativeLibraryHelper.LIB64_DIR_NAME;
@@ -167,6 +166,7 @@ import android.content.pm.PackageInstaller;
 import android.content.pm.PackageManager;
 import android.content.pm.PackageManagerInternal;
 import android.content.pm.PackageManager.LegacyPackageDeleteObserver;
+import android.content.pm.PackageManager.PackageInfoFlags;
 import android.content.pm.PackageParser;
 import android.content.pm.PackageParser.ActivityIntentInfo;
 import android.content.pm.PackageParser.Package;
@@ -22706,6 +22706,12 @@ Slog.v(TAG, ":: stepped forward, applying functor at tag " + parser.getName());
         }
 
         @Override
+        public int getPackageUid(String packageName, int flags, int userId) {
+            return PackageManagerService.this
+                    .getPackageUid(packageName, flags, userId);
+        }
+
+        @Override
         public ApplicationInfo getApplicationInfo(
                 String packageName, int flags, int filterCallingUid, int userId) {
             return PackageManagerService.this
index bf912dd..d8e3be9 100644 (file)
@@ -256,7 +256,7 @@ public class JobStoreTest {
                 invalidLateRuntimeElapsedMillis - TWO_HOURS;  // Early is (late - period).
         final Pair<Long, Long> persistedExecutionTimesUTC = new Pair<>(rtcNow, rtcNow + ONE_HOUR);
         final JobStatus js = new JobStatus(b.build(), SOME_UID, "somePackage",
-                0 /* sourceUserId */, "someTag",
+                0 /* sourceUserId */, 0, 0, "someTag",
                 invalidEarlyRuntimeElapsedMillis, invalidLateRuntimeElapsedMillis,
                 0 /* lastSuccessfulRunTime */, 0 /* lastFailedRunTime */,
                 persistedExecutionTimesUTC);
index 7ca17af..ee11241 100644 (file)
@@ -327,7 +327,8 @@ public class AppIdleHistory {
         return (elapsedRealtime - mElapsedSnapshot + mElapsedDuration);
     }
 
-    public void setIdle(String packageName, int userId, boolean idle, long elapsedRealtime) {
+    /* Returns the new standby bucket the app is assigned to */
+    public int setIdle(String packageName, int userId, boolean idle, long elapsedRealtime) {
         ArrayMap<String, AppUsageHistory> userHistory = getUserHistory(userId);
         AppUsageHistory appUsageHistory = getPackageHistory(userHistory, packageName,
                 elapsedRealtime, true);
@@ -339,6 +340,7 @@ public class AppIdleHistory {
             // This is to pretend that the app was just used, don't freeze the state anymore.
             appUsageHistory.bucketingReason = REASON_USAGE;
         }
+        return appUsageHistory.currentBucket;
     }
 
     public void clearUsage(String packageName, int userId) {
index cd0fce6..3c099c2 100644 (file)
@@ -154,7 +154,7 @@ public class AppStandbyController {
 
     private volatile boolean mPendingOneTimeCheckIdleStates;
 
-    private final Handler mHandler;
+    private final AppStandbyHandler mHandler;
     private final Context mContext;
 
     // TODO: Provide a mechanism to set an external bucketing service
@@ -412,6 +412,7 @@ public class AppStandbyController {
     private void maybeInformListeners(String packageName, int userId,
             long elapsedRealtime, int bucket) {
         synchronized (mAppIdleLock) {
+            // TODO: fold these into one call + lookup for efficiency if needed
             if (mAppIdleHistory.shouldInformListeners(packageName, userId,
                     elapsedRealtime, bucket)) {
                 mHandler.sendMessage(mHandler.obtainMessage(MSG_INFORM_LISTENERS,
@@ -533,15 +534,15 @@ public class AppStandbyController {
 
         final boolean previouslyIdle = isAppIdleFiltered(packageName, appId,
                 userId, elapsedRealtime);
+        final int standbyBucket;
         synchronized (mAppIdleLock) {
-            mAppIdleHistory.setIdle(packageName, userId, idle, elapsedRealtime);
+            standbyBucket = mAppIdleHistory.setIdle(packageName, userId, idle, elapsedRealtime);
         }
         final boolean stillIdle = isAppIdleFiltered(packageName, appId,
                 userId, elapsedRealtime);
         // Inform listeners if necessary
         if (previouslyIdle != stillIdle) {
-            mHandler.sendMessage(mHandler.obtainMessage(MSG_INFORM_LISTENERS, userId,
-                    /* idle = */ stillIdle ? 1 : 0, packageName));
+            maybeInformListeners(packageName, userId, elapsedRealtime, standbyBucket);
             if (!stillIdle) {
                 notifyBatteryStats(packageName, userId, idle);
             }
@@ -737,7 +738,7 @@ public class AppStandbyController {
                 .sendToTarget();
     }
 
-    @StandbyBuckets int getAppStandbyBucket(String packageName, int userId,
+    @StandbyBuckets public int getAppStandbyBucket(String packageName, int userId,
             long elapsedRealtime, boolean shouldObfuscateInstantApps) {
         if (shouldObfuscateInstantApps &&
                 mInjector.isPackageEphemeral(userId, packageName)) {
@@ -751,6 +752,8 @@ public class AppStandbyController {
             String reason, long elapsedRealtime) {
         mAppIdleHistory.setAppStandbyBucket(packageName, userId, elapsedRealtime, newBucket,
                 reason);
+        maybeInformListeners(packageName, userId, elapsedRealtime,
+                newBucket);
     }
 
     private boolean isActiveDeviceAdmin(String packageName, int userId) {
index 44e6a6c..65c1cef 100644 (file)
@@ -24,6 +24,7 @@ import android.app.usage.AppStandby;
 import android.app.usage.ConfigurationStats;
 import android.app.usage.IUsageStatsManager;
 import android.app.usage.UsageEvents;
+import android.app.usage.AppStandby.StandbyBuckets;
 import android.app.usage.UsageEvents.Event;
 import android.app.usage.UsageStats;
 import android.app.usage.UsageStatsManagerInternal;
@@ -867,6 +868,12 @@ public class UsageStatsService extends SystemService implements
         }
 
         @Override
+        @StandbyBuckets public int getAppStandbyBucket(String packageName, int userId,
+                long nowElapsed) {
+            return mAppStandby.getAppStandbyBucket(packageName, userId, nowElapsed, false);
+        }
+
+        @Override
         public int[] getIdleUidsForUser(int userId) {
             return mAppStandby.getIdleUidsForUser(userId);
         }