}
/**
+ * 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.
* 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);
- }
}
package android.app.usage;
+import android.app.usage.AppStandby.StandbyBuckets;
import android.content.ComponentName;
import android.content.res.Configuration;
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.
@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.
*/
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.
*/
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;
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;
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;
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;
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;
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();
final JobHandler mHandler;
final JobSchedulerStub mJobSchedulerStub;
+ PackageManagerInternal mLocalPM;
IBatteryStats mBatteryStats;
- PowerManager mPowerManager;
DeviceIdleController.LocalService mLocalDeviceIdleController;
/**
*/
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 --
/**
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;
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
* 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(',');
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);
}
}
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('}');
}
}
}
} catch (RemoteException e) {
}
+
synchronized (mLock) {
final JobStatus toCancel = mJobs.getJobByUidAndJobId(uId, job.getId());
*/
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.
@Override
public void onStart() {
- publishLocalService(JobSchedulerInternal.class, new LocalService());
publishBinderService(Context.JOB_SCHEDULER_SERVICE, mJobSchedulerStub);
}
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
}
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++) {
* 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.
*/
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());
}
noteJobsNonpending(mPendingJobs);
mPendingJobs.clear();
stopNonReadyActiveJobsLocked();
+ boolean updated = updateStandbyHeartbeatLocked();
mJobs.forEachJob(mReadyQueueFunctor);
+ if (updated) updateNextStandbyHeartbeatsLocked();
mReadyQueueFunctor.postProcess();
if (DEBUG) {
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) {
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(
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.
*/
}
/**
+ * 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 {
}
final int pid = Binder.getCallingPid();
final int uid = Binder.getCallingUid();
+ final int userId = UserHandle.getUserId(uid);
enforceValidJobRequest(uid, job);
if (job.isPersisted()) {
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);
}
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()) {
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);
}
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) {
}
}
+ 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,
*/
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.
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();
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;
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);
}
}
// 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);
if (mAppIdleParoleOn) {
return;
}
+
PackageUpdateFunc update = new PackageUpdateFunc(userId, packageName, idle);
mJobSchedulerService.getJobStore().forEachJob(update);
if (update.mChanged) {
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;
/** 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;
}
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) {
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());
* {@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);
}
/** 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);
/**
* 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;
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 */);
}
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;
}
}
}
+ // 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);
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();
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;
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;
}
@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
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);
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);
// 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) {
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
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,
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);
}
.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)) {
String reason, long elapsedRealtime) {
mAppIdleHistory.setAppStandbyBucket(packageName, userId, elapsedRealtime, newBucket,
reason);
+ maybeInformListeners(packageName, userId, elapsedRealtime,
+ newBucket);
}
private boolean isActiveDeviceAdmin(String packageName, int userId) {
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;
}
@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);
}