Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit a732f014 authored by Christopher Tate's avatar Christopher Tate Committed by Amith Yamasani
Browse files

The job scheduler now backs off jobs based on standby bucketing

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
parent 560d5f85
Loading
Loading
Loading
Loading
+27 −27
Original line number Diff line number Diff line
@@ -71,6 +71,33 @@ public abstract class JobService extends Service {
        return mEngine.getBinder();
    }

    /**
     * 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
@@ -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);
    }
}
+14 −0
Original line number Diff line number Diff line
@@ -16,6 +16,7 @@

package android.app.usage;

import android.app.usage.AppStandby.StandbyBuckets;
import android.content.ComponentName;
import android.content.res.Configuration;

@@ -90,6 +91,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
+8 −0
Original line number Diff line number Diff line
@@ -163,6 +163,14 @@ public abstract class PackageManagerInternal {
    public abstract PackageInfo getPackageInfo(String packageName,
            @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
+12 −0
Original line number Diff line number Diff line
@@ -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.
     */
+239 −12
Original line number Diff line number Diff line
@@ -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(
@@ -1818,6 +1947,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.
@@ -1878,6 +2023,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
     */
@@ -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,
Loading