From f9bac16d61db0fceb15484587ecf876c2b802c37 Mon Sep 17 00:00:00 2001 From: Dianne Hackborn Date: Thu, 20 Apr 2017 17:17:48 -0700 Subject: [PATCH] Fix issue #32180780: Sync adapters inappropriately being run... ...during full-data backup/restore The activity manager now tells job scheduler service about the current backup that is running (only if it is a full backup), it there is a new condition where we won't consider jobs associated with the current backup to be ready to run. Also... just a little optimization here. :) The focus is on scheduling jobs with a 0 deadline, meaning they should run right away. Now the timing controller does a quick check for a new job to see if its constraints are already satisifed, and doesn't do anything further if that is the case (doesn't add to the list, doesn't re-evaluate alarms, etc). And in the path to scheduling a job, we do a check to see if the new job is already ready and if so then just directly add it to the pending list and schedule it. Doing this required removing what I think is the last bit of code relying on handler serializing for thread safety, so now everything in the job scheduler is protected by our global lock and we can do whatever we want with the lock held and be assured the state remains consistent. Also did some small optimizations to many of the other controllers, mostly switching from an ArrayList to an ArraySet for their tracked jobs, since one of the things we do frequently is add/remove jobs. Finally, added some nullability annotations to the JobScheduler APIs. Test: bit CtsJobSchedulerTestCases:* Change-Id: I533fad94ba59468a52fe3d077a0ceab3427f0012 --- core/java/android/app/job/JobInfo.java | 19 +- core/java/android/app/job/JobParameters.java | 16 +- core/java/android/app/job/JobScheduler.java | 7 +- .../server/am/ActivityManagerService.java | 27 + .../server/job/JobSchedulerInternal.java | 7 + .../server/job/JobSchedulerService.java | 650 ++++++++++-------- .../job/controllers/AppIdleController.java | 3 +- .../job/controllers/BatteryController.java | 13 +- .../controllers/ConnectivityController.java | 17 +- .../ContentObserverController.java | 17 +- .../controllers/DeviceIdleJobsController.java | 7 +- .../job/controllers/IdleController.java | 18 +- .../server/job/controllers/JobStatus.java | 54 ++ .../job/controllers/StorageController.java | 16 +- .../job/controllers/TimeController.java | 65 +- 15 files changed, 562 insertions(+), 374 deletions(-) diff --git a/core/java/android/app/job/JobInfo.java b/core/java/android/app/job/JobInfo.java index 23baa17d947f8..fa07fbdbcc53d 100644 --- a/core/java/android/app/job/JobInfo.java +++ b/core/java/android/app/job/JobInfo.java @@ -244,7 +244,7 @@ public class JobInfo implements Parcelable { /** * Bundle of extras which are returned to your application at execution time. */ - public PersistableBundle getExtras() { + public @NonNull PersistableBundle getExtras() { return extras; } @@ -252,7 +252,7 @@ public class JobInfo implements Parcelable { * Bundle of transient extras which are returned to your application at execution time, * but not persisted by the system. */ - public Bundle getTransientExtras() { + public @NonNull Bundle getTransientExtras() { return transientExtras; } @@ -260,7 +260,7 @@ public class JobInfo implements Parcelable { * ClipData of information that is returned to your application at execution time, * but not persisted by the system. */ - public ClipData getClipData() { + public @Nullable ClipData getClipData() { return clipData; } @@ -274,7 +274,7 @@ public class JobInfo implements Parcelable { /** * Name of the service endpoint that will be called back into by the JobScheduler. */ - public ComponentName getService() { + public @NonNull ComponentName getService() { return service; } @@ -327,8 +327,7 @@ public class JobInfo implements Parcelable { * Which content: URIs must change for the job to be scheduled. Returns null * if there are none required. */ - @Nullable - public TriggerContentUri[] getTriggerContentUris() { + public @Nullable TriggerContentUri[] getTriggerContentUris() { return triggerContentUris; } @@ -811,7 +810,7 @@ public class JobInfo implements Parcelable { * @param jobService The endpoint that you implement that will receive the callback from the * JobScheduler. */ - public Builder(int jobId, ComponentName jobService) { + public Builder(int jobId, @NonNull ComponentName jobService) { mJobService = jobService; mJobId = jobId; } @@ -832,7 +831,7 @@ public class JobInfo implements Parcelable { * Set optional extras. This is persisted, so we only allow primitive types. * @param extras Bundle containing extras you want the scheduler to hold on to for you. */ - public Builder setExtras(PersistableBundle extras) { + public Builder setExtras(@NonNull PersistableBundle extras) { mExtras = extras; return this; } @@ -842,7 +841,7 @@ public class JobInfo implements Parcelable { * persisted with {@link #setPersisted(boolean)}; mixing the two is not allowed. * @param extras Bundle containing extras you want the scheduler to hold on to for you. */ - public Builder setTransientExtras(Bundle extras) { + public Builder setTransientExtras(@NonNull Bundle extras) { mTransientExtras = extras; return this; } @@ -869,7 +868,7 @@ public class JobInfo implements Parcelable { * {@link android.content.Intent#FLAG_GRANT_WRITE_URI_PERMISSION}, and * {@link android.content.Intent#FLAG_GRANT_PREFIX_URI_PERMISSION}. */ - public Builder setClipData(ClipData clip, int grantFlags) { + public Builder setClipData(@Nullable ClipData clip, int grantFlags) { mClipData = clip; mClipGrantFlags = grantFlags; return this; diff --git a/core/java/android/app/job/JobParameters.java b/core/java/android/app/job/JobParameters.java index 673d1b886b251..0985f5f91373a 100644 --- a/core/java/android/app/job/JobParameters.java +++ b/core/java/android/app/job/JobParameters.java @@ -16,6 +16,8 @@ package android.app.job; +import android.annotation.NonNull; +import android.annotation.Nullable; import android.app.job.IJobCallback; import android.content.ClipData; import android.net.Uri; @@ -91,7 +93,7 @@ public class JobParameters implements Parcelable { * {@link android.app.job.JobInfo.Builder#setExtras(android.os.PersistableBundle)}. This will * never be null. If you did not set any extras this will be an empty bundle. */ - public PersistableBundle getExtras() { + public @NonNull PersistableBundle getExtras() { return extras; } @@ -100,7 +102,7 @@ public class JobParameters implements Parcelable { * {@link android.app.job.JobInfo.Builder#setTransientExtras(android.os.Bundle)}. This will * never be null. If you did not set any extras this will be an empty bundle. */ - public Bundle getTransientExtras() { + public @NonNull Bundle getTransientExtras() { return transientExtras; } @@ -109,7 +111,7 @@ public class JobParameters implements Parcelable { * {@link android.app.job.JobInfo.Builder#setClipData(ClipData, int)}. Will be null * if it was not set. */ - public ClipData getClipData() { + public @Nullable ClipData getClipData() { return clipData; } @@ -140,7 +142,7 @@ public class JobParameters implements Parcelable { * always use {@link #getTriggeredContentAuthorities()} to determine whether the job was * triggered due to any content changes and the authorities they are associated with. */ - public Uri[] getTriggeredContentUris() { + public @Nullable Uri[] getTriggeredContentUris() { return mTriggeredContentUris; } @@ -152,7 +154,7 @@ public class JobParameters implements Parcelable { * to retrieve the details of which URIs changed (as long as that has not exceeded the maximum * number it can reported). */ - public String[] getTriggeredContentAuthorities() { + public @Nullable String[] getTriggeredContentAuthorities() { return mTriggeredContentAuthorities; } @@ -183,7 +185,7 @@ public class JobParameters implements Parcelable { * (This means that for correct operation, you must always call dequeueWork() after you have * completed other work, to check either for more work or allow the system to stop the job.) */ - public JobWorkItem dequeueWork() { + public @Nullable JobWorkItem dequeueWork() { try { return getCallback().dequeueWork(getJobId()); } catch (RemoteException e) { @@ -207,7 +209,7 @@ public class JobParameters implements Parcelable { * @param work The work you have completed processing, as previously returned by * {@link #dequeueWork()} */ - public void completeWork(JobWorkItem work) { + public void completeWork(@NonNull JobWorkItem work) { try { if (!getCallback().completeWork(getJobId(), work.getWorkId())) { throw new IllegalArgumentException("Given work is not active: " + work); diff --git a/core/java/android/app/job/JobScheduler.java b/core/java/android/app/job/JobScheduler.java index e0afe039478e9..4d6e3a22c83c8 100644 --- a/core/java/android/app/job/JobScheduler.java +++ b/core/java/android/app/job/JobScheduler.java @@ -72,7 +72,7 @@ public abstract class JobScheduler { * you can schedule. * @return An int representing ({@link #RESULT_SUCCESS} or {@link #RESULT_FAILURE}). */ - public abstract int schedule(JobInfo job); + public abstract int schedule(@NonNull JobInfo job); /** * Similar to {@link #schedule}, but allows you to enqueue work for an existing job. If a job @@ -108,7 +108,7 @@ public abstract class JobScheduler { * @param work New work to enqueue. This will be available later when the job starts running. * @return An int representing ({@link #RESULT_SUCCESS} or {@link #RESULT_FAILURE}). */ - public abstract int enqueue(JobInfo job, JobWorkItem work); + public abstract int enqueue(@NonNull JobInfo job, @NonNull JobWorkItem work); /** * @@ -121,7 +121,8 @@ public abstract class JobScheduler { * @hide */ @SystemApi - public abstract int scheduleAsPackage(JobInfo job, String packageName, int userId, String tag); + public abstract int scheduleAsPackage(@NonNull JobInfo job, @NonNull String packageName, + int userId, String tag); /** * Cancel a job that is pending in the JobScheduler. diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java index fa936c209099c..25c1c5626c962 100644 --- a/services/core/java/com/android/server/am/ActivityManagerService.java +++ b/services/core/java/com/android/server/am/ActivityManagerService.java @@ -358,6 +358,7 @@ import android.view.LayoutInflater; import android.view.View; import android.view.WindowManager; +import com.android.server.job.JobSchedulerInternal; import com.google.android.collect.Lists; import com.google.android.collect.Maps; @@ -18158,6 +18159,9 @@ public class ActivityManagerService extends IActivityManager.Stub return false; } + int oldBackupUid; + int newBackupUid; + synchronized(this) { // !!! TODO: currently no check here that we're already bound BatteryStatsImpl.Uid.Pkg.Serv ss = null; @@ -18198,6 +18202,8 @@ public class ActivityManagerService extends IActivityManager.Stub proc.inFullBackup = true; } r.app = proc; + oldBackupUid = mBackupTarget != null ? mBackupTarget.appInfo.uid : -1; + newBackupUid = proc.inFullBackup ? r.appInfo.uid : -1; mBackupTarget = r; mBackupAppName = app.packageName; @@ -18223,6 +18229,14 @@ public class ActivityManagerService extends IActivityManager.Stub // know that it's scheduled for a backup-agent operation. } + JobSchedulerInternal js = LocalServices.getService(JobSchedulerInternal.class); + if (oldBackupUid != -1) { + js.removeBackingUpUid(oldBackupUid); + } + if (newBackupUid != -1) { + js.addBackingUpUid(newBackupUid); + } + return true; } @@ -18235,6 +18249,9 @@ public class ActivityManagerService extends IActivityManager.Stub mBackupTarget = null; mBackupAppName = null; } + + JobSchedulerInternal js = LocalServices.getService(JobSchedulerInternal.class); + js.clearAllBackingUpUids(); } // A backup agent has just come up @@ -18272,6 +18289,8 @@ public class ActivityManagerService extends IActivityManager.Stub return; } + int oldBackupUid; + synchronized(this) { try { if (mBackupAppName == null) { @@ -18289,6 +18308,8 @@ public class ActivityManagerService extends IActivityManager.Stub updateOomAdjLocked(proc); proc.inFullBackup = false; + oldBackupUid = mBackupTarget != null ? mBackupTarget.appInfo.uid : -1; + // If the app crashed during backup, 'thread' will be null here if (proc.thread != null) { try { @@ -18304,7 +18325,13 @@ public class ActivityManagerService extends IActivityManager.Stub mBackupAppName = null; } } + + if (oldBackupUid != -1) { + JobSchedulerInternal js = LocalServices.getService(JobSchedulerInternal.class); + js.removeBackingUpUid(oldBackupUid); + } } + // ========================================================= // BROADCASTS // ========================================================= diff --git a/services/core/java/com/android/server/job/JobSchedulerInternal.java b/services/core/java/com/android/server/job/JobSchedulerInternal.java index 75170ec9acad7..bc6bd501176c0 100644 --- a/services/core/java/com/android/server/job/JobSchedulerInternal.java +++ b/services/core/java/com/android/server/job/JobSchedulerInternal.java @@ -30,4 +30,11 @@ public interface JobSchedulerInternal { * Returns a list of pending jobs scheduled by the system service. */ List getSystemScheduledPendingJobs(); + + /** + * These are for activity manager to communicate to use what is currently performing backups. + */ + void addBackingUpUid(int uid); + void removeBackingUpUid(int uid); + void clearAllBackingUpUids(); } diff --git a/services/core/java/com/android/server/job/JobSchedulerService.java b/services/core/java/com/android/server/job/JobSchedulerService.java index c8bfa345af107..3db2f31f0427b 100644 --- a/services/core/java/com/android/server/job/JobSchedulerService.java +++ b/services/core/java/com/android/server/job/JobSchedulerService.java @@ -174,6 +174,11 @@ public final class JobSchedulerService extends com.android.server.SystemService */ final SparseIntArray mUidPriorityOverride = new SparseIntArray(); + /** + * Which uids are currently performing backups, so we shouldn't allow their jobs to run. + */ + final SparseIntArray mBackingUpUids = new SparseIntArray(); + // -- Pre-allocated temporaries only for use in assignJobsToContextsLocked -- /** @@ -621,14 +626,30 @@ public final class JobSchedulerService extends com.android.server.SystemService jobStatus.prepareLocked(ActivityManager.getService()); if (toCancel != null) { - cancelJobImpl(toCancel, jobStatus); + cancelJobImplLocked(toCancel, jobStatus); } if (work != null) { // If work has been supplied, enqueue it into the new job. jobStatus.enqueueWorkLocked(ActivityManager.getService(), work); } startTrackingJobLocked(jobStatus, toCancel); - mHandler.obtainMessage(MSG_CHECK_JOB).sendToTarget(); + + // If the job is immediately ready to run, then we can just immediately + // put it in the pending list and try to schedule it. This is especially + // important for jobs with a 0 deadline constraint, since they will happen a fair + // amount, we want to handle them as quickly as possible, and semantically we want to + // make sure we have started holding the wake lock for the job before returning to + // the caller. + // If the job is not yet ready to run, there is nothing more to do -- we are + // now just waiting for one of its controllers to change state and schedule + // the job appropriately. + if (isReadyToBeExecutedLocked(jobStatus)) { + // This is a new job, we can just immediately put it on the pending + // list and try to run it. + mJobPackageTracker.notePending(jobStatus); + mPendingJobs.add(jobStatus); + maybeRunPendingJobsLocked(); + } } return JobScheduler.RESULT_SUCCESS; } @@ -659,25 +680,23 @@ public final class JobSchedulerService extends com.android.server.SystemService } void cancelJobsForUser(int userHandle) { - List jobsForUser; synchronized (mLock) { - jobsForUser = mJobs.getJobsByUser(userHandle); - } - for (int i=0; i jobsForUser = mJobs.getJobsByUser(userHandle); + for (int i=0; i jobsForUid; synchronized (mLock) { - jobsForUid = mJobs.getJobsByUid(uid); - } - for (int i = jobsForUid.size() - 1; i >= 0; i--) { - final JobStatus job = jobsForUid.get(i); - if (job.getSourcePackageName().equals(pkgName)) { - cancelJobImpl(job, null); + final List jobsForUid = mJobs.getJobsByUid(uid); + for (int i = jobsForUid.size() - 1; i >= 0; i--) { + final JobStatus job = jobsForUid.get(i); + if (job.getSourcePackageName().equals(pkgName)) { + cancelJobImplLocked(job, null); + } } } } @@ -690,13 +709,12 @@ public final class JobSchedulerService extends com.android.server.SystemService * */ public void cancelJobsForUid(int uid) { - List jobsForUid; synchronized (mLock) { - jobsForUid = mJobs.getJobsByUid(uid); - } - for (int i=0; i jobsForUid = mJobs.getJobsByUid(uid); + for (int i=0; i newReadyJobs; + /** + * Run through list of jobs and execute all possible - at least one is expired so we do + * as many as we can. + */ + private void queueReadyJobsForExecutionLocked() { + if (DEBUG) { + Slog.d(TAG, "queuing all ready jobs for execution:"); + } + noteJobsNonpending(mPendingJobs); + mPendingJobs.clear(); + mJobs.forEachJob(mReadyQueueFunctor); + mReadyQueueFunctor.postProcess(); - @Override - public void process(JobStatus job) { - if (isReadyToBeExecutedLocked(job)) { - if (DEBUG) { - Slog.d(TAG, " queued " + job.toShortString()); - } - if (newReadyJobs == null) { - newReadyJobs = new ArrayList(); - } - newReadyJobs.add(job); - } else if (areJobConstraintsNotSatisfiedLocked(job)) { - stopJobOnServiceContextLocked(job, - JobParameters.REASON_CONSTRAINTS_NOT_SATISFIED); - } - } - - public void postProcess() { - if (newReadyJobs != null) { - noteJobsPending(newReadyJobs); - mPendingJobs.addAll(newReadyJobs); - } - newReadyJobs = null; + if (DEBUG) { + final int queuedJobs = mPendingJobs.size(); + if (queuedJobs == 0) { + Slog.d(TAG, "No jobs pending."); + } else { + Slog.d(TAG, queuedJobs + " jobs queued."); } } - private final ReadyJobQueueFunctor mReadyQueueFunctor = new ReadyJobQueueFunctor(); + } - /** - * The state of at least one job has changed. Here is where we could enforce various - * policies on when we want to execute jobs. - * Right now the policy is such: - * If >1 of the ready jobs is idle mode we send all of them off - * if more than 2 network connectivity jobs are ready we send them all off. - * If more than 4 jobs total are ready we send them all off. - * TODO: It would be nice to consolidate these sort of high-level policies somewhere. - */ - class MaybeReadyJobQueueFunctor implements JobStatusFunctor { - int chargingCount; - int batteryNotLowCount; - int storageNotLowCount; - int idleCount; - int backoffCount; - int connectivityCount; - int contentCount; - List runnableJobs; + final class ReadyJobQueueFunctor implements JobStatusFunctor { + ArrayList newReadyJobs; - public MaybeReadyJobQueueFunctor() { - reset(); - } - - // Functor method invoked for each job via JobStore.forEachJob() - @Override - public void process(JobStatus job) { - if (isReadyToBeExecutedLocked(job)) { - try { - if (ActivityManager.getService().isAppStartModeDisabled(job.getUid(), - job.getJob().getService().getPackageName())) { - Slog.w(TAG, "Aborting job " + job.getUid() + ":" - + job.getJob().toString() + " -- package not allowed to start"); - mHandler.obtainMessage(MSG_STOP_JOB, job).sendToTarget(); - return; - } - } catch (RemoteException e) { - } - if (job.getNumFailures() > 0) { - backoffCount++; - } - if (job.hasIdleConstraint()) { - idleCount++; - } - if (job.hasConnectivityConstraint()) { - connectivityCount++; - } - if (job.hasChargingConstraint()) { - chargingCount++; - } - if (job.hasBatteryNotLowConstraint()) { - batteryNotLowCount++; - } - if (job.hasStorageNotLowConstraint()) { - storageNotLowCount++; - } - if (job.hasContentTriggerConstraint()) { - contentCount++; - } - if (runnableJobs == null) { - runnableJobs = new ArrayList<>(); - } - runnableJobs.add(job); - } else if (areJobConstraintsNotSatisfiedLocked(job)) { - stopJobOnServiceContextLocked(job, - JobParameters.REASON_CONSTRAINTS_NOT_SATISFIED); - } - } - - public void postProcess() { - if (backoffCount > 0 || - idleCount >= mConstants.MIN_IDLE_COUNT || - connectivityCount >= mConstants.MIN_CONNECTIVITY_COUNT || - chargingCount >= mConstants.MIN_CHARGING_COUNT || - batteryNotLowCount >= mConstants.MIN_BATTERY_NOT_LOW_COUNT || - storageNotLowCount >= mConstants.MIN_STORAGE_NOT_LOW_COUNT || - contentCount >= mConstants.MIN_CONTENT_COUNT || - (runnableJobs != null - && runnableJobs.size() >= mConstants.MIN_READY_JOBS_COUNT)) { - if (DEBUG) { - Slog.d(TAG, "maybeQueueReadyJobsForExecutionLockedH: Running jobs."); - } - noteJobsPending(runnableJobs); - mPendingJobs.addAll(runnableJobs); - } else { - if (DEBUG) { - Slog.d(TAG, "maybeQueueReadyJobsForExecutionLockedH: Not running anything."); - } - } - - // Be ready for next time - reset(); - } - - private void reset() { - chargingCount = 0; - idleCount = 0; - backoffCount = 0; - connectivityCount = 0; - batteryNotLowCount = 0; - storageNotLowCount = 0; - contentCount = 0; - runnableJobs = null; - } - } - private final MaybeReadyJobQueueFunctor mMaybeQueueFunctor = new MaybeReadyJobQueueFunctor(); - - private void maybeQueueReadyJobsForExecutionLockedH() { - if (DEBUG) Slog.d(TAG, "Maybe queuing ready jobs..."); - - noteJobsNonpending(mPendingJobs); - mPendingJobs.clear(); - mJobs.forEachJob(mMaybeQueueFunctor); - mMaybeQueueFunctor.postProcess(); - } - - /** - * 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 component is enabled and runnable. - */ - private boolean isReadyToBeExecutedLocked(JobStatus job) { - final boolean jobExists = mJobs.containsJob(job); - final boolean jobReady = job.isReady(); - final boolean jobPending = mPendingJobs.contains(job); - final boolean jobActive = isCurrentlyActiveLocked(job); - - final int userId = job.getUserId(); - final boolean userStarted = ArrayUtils.contains(mStartedUsers, userId); - - if (DEBUG) { - Slog.v(TAG, "isReadyToBeExecutedLocked: " + job.toShortString() - + " exists=" + jobExists - + " ready=" + jobReady + " pending=" + jobPending - + " active=" + jobActive + " userStarted=" + userStarted); - } - - // Short circuit: don't do the expensive PM check unless we really think - // we might need to run this job now. - if (!jobExists || !userStarted || !jobReady || jobPending || jobActive) { - return false; - } - - final boolean componentPresent; - try { - componentPresent = (AppGlobals.getPackageManager().getServiceInfo( - job.getServiceComponent(), PackageManager.MATCH_DEBUG_TRIAGED_MISSING, - userId) != null); - } catch (RemoteException e) { - throw e.rethrowAsRuntimeException(); - } - - if (DEBUG) { - Slog.v(TAG, "isReadyToBeExecutedLocked: " + job.toShortString() - + " componentPresent=" + componentPresent); - } - - // Everything else checked out so far, so this is the final yes/no check - return componentPresent; - } - - /** - * Criteria for cancelling an active job: - * - It's not ready - * - It's running on a JSC. - */ - private boolean areJobConstraintsNotSatisfiedLocked(JobStatus job) { - return !job.isReady() && isCurrentlyActiveLocked(job); - } - - /** - * Reconcile jobs in the pending queue against available execution contexts. - * A controller can force a job into the pending queue even if it's already running, but - * here is where we decide whether to actually execute it. - */ - private void maybeRunPendingJobsH() { - synchronized (mLock) { + @Override + public void process(JobStatus job) { + if (isReadyToBeExecutedLocked(job)) { if (DEBUG) { - Slog.d(TAG, "pending queue: " + mPendingJobs.size() + " jobs."); + Slog.d(TAG, " queued " + job.toShortString()); } - assignJobsToContextsLocked(); - reportActiveLocked(); + if (newReadyJobs == null) { + newReadyJobs = new ArrayList(); + } + newReadyJobs.add(job); + } else if (areJobConstraintsNotSatisfiedLocked(job)) { + stopJobOnServiceContextLocked(job, + JobParameters.REASON_CONSTRAINTS_NOT_SATISFIED); } } + + public void postProcess() { + if (newReadyJobs != null) { + noteJobsPending(newReadyJobs); + mPendingJobs.addAll(newReadyJobs); + } + newReadyJobs = null; + } + } + private final ReadyJobQueueFunctor mReadyQueueFunctor = new ReadyJobQueueFunctor(); + + /** + * The state of at least one job has changed. Here is where we could enforce various + * policies on when we want to execute jobs. + * Right now the policy is such: + * If >1 of the ready jobs is idle mode we send all of them off + * if more than 2 network connectivity jobs are ready we send them all off. + * If more than 4 jobs total are ready we send them all off. + * TODO: It would be nice to consolidate these sort of high-level policies somewhere. + */ + final class MaybeReadyJobQueueFunctor implements JobStatusFunctor { + int chargingCount; + int batteryNotLowCount; + int storageNotLowCount; + int idleCount; + int backoffCount; + int connectivityCount; + int contentCount; + List runnableJobs; + + public MaybeReadyJobQueueFunctor() { + reset(); + } + + // Functor method invoked for each job via JobStore.forEachJob() + @Override + public void process(JobStatus job) { + if (isReadyToBeExecutedLocked(job)) { + try { + if (ActivityManager.getService().isAppStartModeDisabled(job.getUid(), + job.getJob().getService().getPackageName())) { + Slog.w(TAG, "Aborting job " + job.getUid() + ":" + + job.getJob().toString() + " -- package not allowed to start"); + mHandler.obtainMessage(MSG_STOP_JOB, job).sendToTarget(); + return; + } + } catch (RemoteException e) { + } + if (job.getNumFailures() > 0) { + backoffCount++; + } + if (job.hasIdleConstraint()) { + idleCount++; + } + if (job.hasConnectivityConstraint()) { + connectivityCount++; + } + if (job.hasChargingConstraint()) { + chargingCount++; + } + if (job.hasBatteryNotLowConstraint()) { + batteryNotLowCount++; + } + if (job.hasStorageNotLowConstraint()) { + storageNotLowCount++; + } + if (job.hasContentTriggerConstraint()) { + contentCount++; + } + if (runnableJobs == null) { + runnableJobs = new ArrayList<>(); + } + runnableJobs.add(job); + } else if (areJobConstraintsNotSatisfiedLocked(job)) { + stopJobOnServiceContextLocked(job, + JobParameters.REASON_CONSTRAINTS_NOT_SATISFIED); + } + } + + public void postProcess() { + if (backoffCount > 0 || + idleCount >= mConstants.MIN_IDLE_COUNT || + connectivityCount >= mConstants.MIN_CONNECTIVITY_COUNT || + chargingCount >= mConstants.MIN_CHARGING_COUNT || + batteryNotLowCount >= mConstants.MIN_BATTERY_NOT_LOW_COUNT || + storageNotLowCount >= mConstants.MIN_STORAGE_NOT_LOW_COUNT || + contentCount >= mConstants.MIN_CONTENT_COUNT || + (runnableJobs != null + && runnableJobs.size() >= mConstants.MIN_READY_JOBS_COUNT)) { + if (DEBUG) { + Slog.d(TAG, "maybeQueueReadyJobsForExecutionLocked: Running jobs."); + } + noteJobsPending(runnableJobs); + mPendingJobs.addAll(runnableJobs); + } else { + if (DEBUG) { + Slog.d(TAG, "maybeQueueReadyJobsForExecutionLocked: Not running anything."); + } + } + + // Be ready for next time + reset(); + } + + private void reset() { + chargingCount = 0; + idleCount = 0; + backoffCount = 0; + connectivityCount = 0; + batteryNotLowCount = 0; + storageNotLowCount = 0; + contentCount = 0; + runnableJobs = null; + } + } + private final MaybeReadyJobQueueFunctor mMaybeQueueFunctor = new MaybeReadyJobQueueFunctor(); + + private void maybeQueueReadyJobsForExecutionLocked() { + if (DEBUG) Slog.d(TAG, "Maybe queuing ready jobs..."); + + noteJobsNonpending(mPendingJobs); + mPendingJobs.clear(); + mJobs.forEachJob(mMaybeQueueFunctor); + mMaybeQueueFunctor.postProcess(); + } + + /** + * 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 component is enabled and runnable. + */ + private boolean isReadyToBeExecutedLocked(JobStatus job) { + final boolean jobExists = mJobs.containsJob(job); + final boolean jobReady = job.isReady(); + final boolean jobPending = mPendingJobs.contains(job); + final boolean jobActive = isCurrentlyActiveLocked(job); + final boolean jobBackingUp = mBackingUpUids.indexOfKey(job.getSourceUid()) >= 0; + + final int userId = job.getUserId(); + final boolean userStarted = ArrayUtils.contains(mStartedUsers, userId); + + if (DEBUG) { + Slog.v(TAG, "isReadyToBeExecutedLocked: " + job.toShortString() + + " exists=" + jobExists + + " ready=" + jobReady + " pending=" + jobPending + + " active=" + jobActive + " backingup=" + jobBackingUp + + " userStarted=" + userStarted); + } + + // Short circuit: don't do the expensive PM check unless we really think + // we might need to run this job now. + if (!jobExists || !userStarted || !jobReady || jobPending || jobActive || jobBackingUp) { + return false; + } + + final boolean componentPresent; + try { + componentPresent = (AppGlobals.getPackageManager().getServiceInfo( + job.getServiceComponent(), PackageManager.MATCH_DEBUG_TRIAGED_MISSING, + userId) != null); + } catch (RemoteException e) { + throw e.rethrowAsRuntimeException(); + } + + if (DEBUG) { + Slog.v(TAG, "isReadyToBeExecutedLocked: " + job.toShortString() + + " componentPresent=" + componentPresent); + } + + // Everything else checked out so far, so this is the final yes/no check + return componentPresent; + } + + /** + * Criteria for cancelling an active job: + * - It's not ready + * - It's running on a JSC. + */ + private boolean areJobConstraintsNotSatisfiedLocked(JobStatus job) { + return !job.isReady() && isCurrentlyActiveLocked(job); + } + + /** + * Reconcile jobs in the pending queue against available execution contexts. + * A controller can force a job into the pending queue even if it's already running, but + * here is where we decide whether to actually execute it. + */ + private void maybeRunPendingJobsLocked() { + if (DEBUG) { + Slog.d(TAG, "pending queue: " + mPendingJobs.size() + " jobs."); + } + assignJobsToContextsLocked(); + reportActiveLocked(); } private int adjustJobPriority(int curPriority, JobStatus job) { @@ -1619,6 +1630,38 @@ public final class JobSchedulerService extends com.android.server.SystemService return pendingJobs; } } + + @Override + public void addBackingUpUid(int uid) { + synchronized (mLock) { + // No need to actually do anything here, since for a full backup the + // activity manager will kill the process which will kill the job (and + // cause it to restart, but now it can't run). + mBackingUpUids.put(uid, uid); + } + } + + @Override + public void removeBackingUpUid(int uid) { + synchronized (mLock) { + mBackingUpUids.delete(uid); + // If there are any jobs for this uid, we need to rebuild the pending list + // in case they are now ready to run. + if (mJobs.countJobsForUid(uid) > 0) { + mHandler.obtainMessage(MSG_CHECK_JOB).sendToTarget(); + } + } + } + + @Override + public void clearAllBackingUpUids() { + synchronized (mLock) { + if (mBackingUpUids.size() > 0) { + mBackingUpUids.clear(); + mHandler.obtainMessage(MSG_CHECK_JOB).sendToTarget(); + } + } + } } /** @@ -1868,7 +1911,8 @@ public final class JobSchedulerService extends com.android.server.SystemService return JobSchedulerShellCommand.CMD_ERR_CONSTRAINTS; } - mHandler.obtainMessage(MSG_CHECK_JOB_GREEDY).sendToTarget(); + queueReadyJobsForExecutionLocked(); + maybeRunPendingJobsLocked(); } } catch (RemoteException e) { // can't happen @@ -2015,7 +2059,7 @@ public final class JobSchedulerService extends com.android.server.SystemService job.dump(pw, " ", true); pw.print(" Ready: "); - pw.print(mHandler.isReadyToBeExecutedLocked(job)); + pw.print(isReadyToBeExecutedLocked(job)); pw.print(" (job="); pw.print(job.isReady()); pw.print(" user="); @@ -2024,6 +2068,8 @@ public final class JobSchedulerService extends com.android.server.SystemService pw.print(!mPendingJobs.contains(job)); pw.print(" !active="); pw.print(!isCurrentlyActiveLocked(job)); + pw.print(" !backingup="); + pw.print(!(mBackingUpUids.indexOfKey(job.getSourceUid()) >= 0)); pw.print(" comp="); boolean componentPresent = false; try { @@ -2052,6 +2098,24 @@ public final class JobSchedulerService extends com.android.server.SystemService pw.print(": "); pw.println(mUidPriorityOverride.valueAt(i)); } } + if (mBackingUpUids.size() > 0) { + pw.println(); + pw.println("Backing up uids:"); + boolean first = true; + for (int i = 0; i < mBackingUpUids.size(); i++) { + int uid = mBackingUpUids.keyAt(i); + if (filterUidFinal == -1 || filterUidFinal == UserHandle.getAppId(uid)) { + if (first) { + pw.print(" "); + first = false; + } else { + pw.print(", "); + } + pw.print(UserHandle.formatUid(uid)); + } + } + pw.println(); + } pw.println(); mJobPackageTracker.dump(pw, "", filterUidFinal); pw.println(); diff --git a/services/core/java/com/android/server/job/controllers/AppIdleController.java b/services/core/java/com/android/server/job/controllers/AppIdleController.java index 2dbecbd4f5156..68dd00ff00c13 100644 --- a/services/core/java/com/android/server/job/controllers/AppIdleController.java +++ b/services/core/java/com/android/server/job/controllers/AppIdleController.java @@ -123,7 +123,8 @@ public class AppIdleController extends StateController { } @Override - public void maybeStopTrackingJobLocked(JobStatus jobStatus, JobStatus incomingJob, boolean forUpdate) { + public void maybeStopTrackingJobLocked(JobStatus jobStatus, JobStatus incomingJob, + boolean forUpdate) { } @Override diff --git a/services/core/java/com/android/server/job/controllers/BatteryController.java b/services/core/java/com/android/server/job/controllers/BatteryController.java index 91a962dc1ce26..b1f8f6b45ec33 100644 --- a/services/core/java/com/android/server/job/controllers/BatteryController.java +++ b/services/core/java/com/android/server/job/controllers/BatteryController.java @@ -24,6 +24,7 @@ import android.os.BatteryManager; import android.os.BatteryManagerInternal; import android.os.SystemClock; import android.os.UserHandle; +import android.util.ArraySet; import android.util.Slog; import com.android.internal.annotations.VisibleForTesting; @@ -32,9 +33,6 @@ import com.android.server.job.JobSchedulerService; import com.android.server.job.StateChangedListener; import java.io.PrintWriter; -import java.util.ArrayList; -import java.util.Iterator; -import java.util.List; /** * Simple controller that tracks whether the phone is charging or not. The phone is considered to @@ -47,7 +45,7 @@ public class BatteryController extends StateController { private static final Object sCreationLock = new Object(); private static volatile BatteryController sController; - private List mTrackedTasks = new ArrayList(); + private final ArraySet mTrackedTasks = new ArraySet<>(); private ChargingTracker mChargeTracker; public static BatteryController get(JobSchedulerService taskManagerService) { @@ -82,6 +80,7 @@ public class BatteryController extends StateController { public void maybeStartTrackingJobLocked(JobStatus taskStatus, JobStatus lastJob) { if (taskStatus.hasPowerConstraint()) { mTrackedTasks.add(taskStatus); + taskStatus.setTrackingController(JobStatus.TRACKING_BATTERY); taskStatus.setChargingConstraintSatisfied(mChargeTracker.isOnStablePower()); taskStatus.setBatteryNotLowConstraintSatisfied(mChargeTracker.isBatteryNotLow()); } @@ -89,7 +88,7 @@ public class BatteryController extends StateController { @Override public void maybeStopTrackingJobLocked(JobStatus taskStatus, JobStatus incomingJob, boolean forUpdate) { - if (taskStatus.hasPowerConstraint()) { + if (taskStatus.clearTrackingController(JobStatus.TRACKING_BATTERY)) { mTrackedTasks.remove(taskStatus); } } @@ -103,7 +102,7 @@ public class BatteryController extends StateController { boolean reportChange = false; synchronized (mLock) { for (int i = mTrackedTasks.size() - 1; i >= 0; i--) { - final JobStatus ts = mTrackedTasks.get(i); + final JobStatus ts = mTrackedTasks.valueAt(i); boolean previous = ts.setChargingConstraintSatisfied(stablePower); if (previous != stablePower) { reportChange = true; @@ -251,7 +250,7 @@ public class BatteryController extends StateController { pw.print(mTrackedTasks.size()); pw.println(":"); for (int i = 0; i < mTrackedTasks.size(); i++) { - final JobStatus js = mTrackedTasks.get(i); + final JobStatus js = mTrackedTasks.valueAt(i); if (!js.shouldDump(filterUid)) { continue; } diff --git a/services/core/java/com/android/server/job/controllers/ConnectivityController.java b/services/core/java/com/android/server/job/controllers/ConnectivityController.java index 5ebcc93cda09e..f4268185aa409 100644 --- a/services/core/java/com/android/server/job/controllers/ConnectivityController.java +++ b/services/core/java/com/android/server/job/controllers/ConnectivityController.java @@ -27,6 +27,7 @@ import android.net.NetworkInfo; import android.net.NetworkPolicyManager; import android.os.Process; import android.os.UserHandle; +import android.util.ArraySet; import android.util.Slog; import com.android.internal.annotations.GuardedBy; @@ -34,7 +35,6 @@ import com.android.server.job.JobSchedulerService; import com.android.server.job.StateChangedListener; import java.io.PrintWriter; -import java.util.ArrayList; /** * Handles changes in connectivity. @@ -54,7 +54,7 @@ public class ConnectivityController extends StateController implements private boolean mValidated; @GuardedBy("mLock") - private final ArrayList mTrackedJobs = new ArrayList(); + private final ArraySet mTrackedJobs = new ArraySet<>(); /** Singleton. */ private static ConnectivityController mSingleton; @@ -87,13 +87,14 @@ public class ConnectivityController extends StateController implements if (jobStatus.hasConnectivityConstraint()) { updateConstraintsSatisfied(jobStatus, null); mTrackedJobs.add(jobStatus); + jobStatus.setTrackingController(JobStatus.TRACKING_CONNECTIVITY); } } @Override public void maybeStopTrackingJobLocked(JobStatus jobStatus, JobStatus incomingJob, boolean forUpdate) { - if (jobStatus.hasConnectivityConstraint()) { + if (jobStatus.clearTrackingController(JobStatus.TRACKING_CONNECTIVITY)) { mTrackedJobs.remove(jobStatus); } } @@ -150,8 +151,8 @@ public class ConnectivityController extends StateController implements private void updateTrackedJobs(int uid, NetworkCapabilities capabilities) { synchronized (mLock) { boolean changed = false; - for (int i = 0; i < mTrackedJobs.size(); i++) { - final JobStatus js = mTrackedJobs.get(i); + for (int i = mTrackedJobs.size()-1; i >= 0; i--) { + final JobStatus js = mTrackedJobs.valueAt(i); if (uid == -1 || uid == js.getSourceUid()) { changed |= updateConstraintsSatisfied(js, capabilities); } @@ -168,8 +169,8 @@ public class ConnectivityController extends StateController implements @Override public void onNetworkActive() { synchronized (mLock) { - for (int i = 0; i < mTrackedJobs.size(); i++) { - final JobStatus js = mTrackedJobs.get(i); + for (int i = mTrackedJobs.size()-1; i >= 0; i--) { + final JobStatus js = mTrackedJobs.valueAt(i); if (js.isReady()) { if (DEBUG) { Slog.d(TAG, "Running " + js + " due to network activity."); @@ -239,7 +240,7 @@ public class ConnectivityController extends StateController implements pw.print(mTrackedJobs.size()); pw.println(":"); for (int i = 0; i < mTrackedJobs.size(); i++) { - final JobStatus js = mTrackedJobs.get(i); + final JobStatus js = mTrackedJobs.valueAt(i); if (js.shouldDump(filterUid)) { pw.print(" #"); js.printUniqueId(pw); diff --git a/services/core/java/com/android/server/job/controllers/ContentObserverController.java b/services/core/java/com/android/server/job/controllers/ContentObserverController.java index 29f0e2c3b9576..cfafc38428f38 100644 --- a/services/core/java/com/android/server/job/controllers/ContentObserverController.java +++ b/services/core/java/com/android/server/job/controllers/ContentObserverController.java @@ -35,9 +35,6 @@ import com.android.server.job.StateChangedListener; import java.io.PrintWriter; import java.util.ArrayList; -import java.util.Iterator; -import java.util.List; -import java.util.Objects; /** * Controller for monitoring changes to content URIs through a ContentObserver. @@ -61,11 +58,11 @@ public class ContentObserverController extends StateController { private static final Object sCreationLock = new Object(); private static volatile ContentObserverController sController; - final private List mTrackedTasks = new ArrayList(); + final private ArraySet mTrackedTasks = new ArraySet<>(); /** * Per-userid {@link JobInfo.TriggerContentUri} keyed ContentObserver cache. */ - SparseArray> mObservers = + final SparseArray> mObservers = new SparseArray<>(); final Handler mHandler; @@ -101,6 +98,7 @@ public class ContentObserverController extends StateController { Slog.i(TAG, "Tracking content-trigger job " + taskStatus); } mTrackedTasks.add(taskStatus); + taskStatus.setTrackingController(JobStatus.TRACKING_CONTENT); boolean havePendingUris = false; // If there is a previous job associated with the new job, propagate over // any pending content URI trigger reports. @@ -156,7 +154,8 @@ public class ContentObserverController extends StateController { @Override public void maybeStopTrackingJobLocked(JobStatus taskStatus, JobStatus incomingJob, boolean forUpdate) { - if (taskStatus.hasContentTriggerConstraint()) { + if (taskStatus.clearTrackingController(JobStatus.TRACKING_CONTENT)) { + mTrackedTasks.remove(taskStatus); if (taskStatus.contentObserverJobInstance != null) { taskStatus.contentObserverJobInstance.unscheduleLocked(); if (incomingJob != null) { @@ -190,7 +189,6 @@ public class ContentObserverController extends StateController { if (DEBUG) { Slog.i(TAG, "No longer tracking job " + taskStatus); } - mTrackedTasks.remove(taskStatus); } } @@ -374,9 +372,8 @@ public class ContentObserverController extends StateController { @Override public void dumpControllerStateLocked(PrintWriter pw, int filterUid) { pw.println("Content:"); - Iterator it = mTrackedTasks.iterator(); - while (it.hasNext()) { - JobStatus js = it.next(); + for (int i = 0; i < mTrackedTasks.size(); i++) { + JobStatus js = mTrackedTasks.valueAt(i); if (!js.shouldDump(filterUid)) { continue; } diff --git a/services/core/java/com/android/server/job/controllers/DeviceIdleJobsController.java b/services/core/java/com/android/server/job/controllers/DeviceIdleJobsController.java index f7706d7e048a4..5ccf812882553 100644 --- a/services/core/java/com/android/server/job/controllers/DeviceIdleJobsController.java +++ b/services/core/java/com/android/server/job/controllers/DeviceIdleJobsController.java @@ -164,13 +164,12 @@ public class DeviceIdleJobsController extends StateController { @Override public void maybeStartTrackingJobLocked(JobStatus jobStatus, JobStatus lastJob) { - synchronized (mLock) { - updateTaskStateLocked(jobStatus); - } + updateTaskStateLocked(jobStatus); } @Override - public void maybeStopTrackingJobLocked(JobStatus jobStatus, JobStatus incomingJob, boolean forUpdate) { + public void maybeStopTrackingJobLocked(JobStatus jobStatus, JobStatus incomingJob, + boolean forUpdate) { } @Override diff --git a/services/core/java/com/android/server/job/controllers/IdleController.java b/services/core/java/com/android/server/job/controllers/IdleController.java index 0e04d2441bf8c..7e922930e6e86 100644 --- a/services/core/java/com/android/server/job/controllers/IdleController.java +++ b/services/core/java/com/android/server/job/controllers/IdleController.java @@ -17,7 +17,6 @@ package com.android.server.job.controllers; import java.io.PrintWriter; -import java.util.ArrayList; import android.app.AlarmManager; import android.app.PendingIntent; @@ -27,6 +26,7 @@ import android.content.Intent; import android.content.IntentFilter; import android.os.SystemClock; import android.os.UserHandle; +import android.util.ArraySet; import android.util.Slog; import com.android.server.am.ActivityManagerService; @@ -40,7 +40,7 @@ public class IdleController extends StateController { // screen off or dreaming for at least this long private long mInactivityIdleThreshold; private long mIdleWindowSlop; - final ArrayList mTrackedTasks = new ArrayList(); + final ArraySet mTrackedTasks = new ArraySet<>(); IdlenessTracker mIdleTracker; // Singleton factory @@ -69,13 +69,17 @@ public class IdleController extends StateController { public void maybeStartTrackingJobLocked(JobStatus taskStatus, JobStatus lastJob) { if (taskStatus.hasIdleConstraint()) { mTrackedTasks.add(taskStatus); + taskStatus.setTrackingController(JobStatus.TRACKING_IDLE); taskStatus.setIdleConstraintSatisfied(mIdleTracker.isIdle()); } } @Override - public void maybeStopTrackingJobLocked(JobStatus taskStatus, JobStatus incomingJob, boolean forUpdate) { - mTrackedTasks.remove(taskStatus); + public void maybeStopTrackingJobLocked(JobStatus taskStatus, JobStatus incomingJob, + boolean forUpdate) { + if (taskStatus.clearTrackingController(JobStatus.TRACKING_IDLE)) { + mTrackedTasks.remove(taskStatus); + } } /** @@ -83,8 +87,8 @@ public class IdleController extends StateController { */ void reportNewIdleState(boolean isIdle) { synchronized (mLock) { - for (JobStatus task : mTrackedTasks) { - task.setIdleConstraintSatisfied(isIdle); + for (int i = mTrackedTasks.size()-1; i >= 0; i--) { + mTrackedTasks.valueAt(i).setIdleConstraintSatisfied(isIdle); } } mStateChangedListener.onControllerStateChanged(); @@ -200,7 +204,7 @@ public class IdleController extends StateController { pw.print(mTrackedTasks.size()); pw.println(":"); for (int i = 0; i < mTrackedTasks.size(); i++) { - final JobStatus js = mTrackedTasks.get(i); + final JobStatus js = mTrackedTasks.valueAt(i); if (!js.shouldDump(filterUid)) { continue; } diff --git a/services/core/java/com/android/server/job/controllers/JobStatus.java b/services/core/java/com/android/server/job/controllers/JobStatus.java index 1ab66b9abb7fb..4d5aba3c1daba 100644 --- a/services/core/java/com/android/server/job/controllers/JobStatus.java +++ b/services/core/java/com/android/server/job/controllers/JobStatus.java @@ -128,6 +128,38 @@ public final class JobStatus { // Set to true if doze constraint was satisfied due to app being whitelisted. public boolean dozeWhitelisted; + /** + * Flag for {@link #trackingControllers}: the battery controller is currently tracking this job. + */ + public static final int TRACKING_BATTERY = 1<<0; + /** + * Flag for {@link #trackingControllers}: the network connectivity controller is currently + * tracking this job. + */ + public static final int TRACKING_CONNECTIVITY = 1<<1; + /** + * Flag for {@link #trackingControllers}: the content observer controller is currently + * tracking this job. + */ + public static final int TRACKING_CONTENT = 1<<2; + /** + * Flag for {@link #trackingControllers}: the idle controller is currently tracking this job. + */ + public static final int TRACKING_IDLE = 1<<3; + /** + * Flag for {@link #trackingControllers}: the storage controller is currently tracking this job. + */ + public static final int TRACKING_STORAGE = 1<<4; + /** + * Flag for {@link #trackingControllers}: the time controller is currently tracking this job. + */ + public static final int TRACKING_TIME = 1<<5; + + /** + * Bit mask of controllers that are currently tracking the job. + */ + private int trackingControllers; + // These are filled in by controllers when preparing for execution. public ArraySet changedUris; public ArraySet changedAuthorities; @@ -609,6 +641,18 @@ public final class JobStatus { return (satisfiedConstraints&constraint) != 0; } + boolean clearTrackingController(int which) { + if ((trackingControllers&which) != 0) { + trackingControllers &= ~which; + return true; + } + return false; + } + + void setTrackingController(int which) { + trackingControllers |= which; + } + public boolean shouldDump(int filterUid) { return filterUid == -1 || UserHandle.getAppId(getUid()) == filterUid || UserHandle.getAppId(getSourceUid()) == filterUid; @@ -925,6 +969,16 @@ public final class JobStatus { pw.print(prefix); pw.println("Doze whitelisted: true"); } } + if (trackingControllers != 0) { + pw.print(prefix); pw.print("Tracking:"); + if ((trackingControllers&TRACKING_BATTERY) != 0) pw.print(" BATTERY"); + if ((trackingControllers&TRACKING_CONNECTIVITY) != 0) pw.print(" CONNECTIVITY"); + if ((trackingControllers&TRACKING_CONTENT) != 0) pw.print(" CONTENT"); + if ((trackingControllers&TRACKING_IDLE) != 0) pw.print(" IDLE"); + if ((trackingControllers&TRACKING_STORAGE) != 0) pw.print(" STORAGE"); + if ((trackingControllers&TRACKING_TIME) != 0) pw.print(" TIME"); + pw.println(); + } if (changedAuthorities != null) { pw.print(prefix); pw.println("Changed authorities:"); for (int i=0; i mTrackedTasks = new ArrayList(); + private final ArraySet mTrackedTasks = new ArraySet(); private StorageTracker mStorageTracker; public static StorageController get(JobSchedulerService taskManagerService) { @@ -78,13 +76,15 @@ public class StorageController extends StateController { public void maybeStartTrackingJobLocked(JobStatus taskStatus, JobStatus lastJob) { if (taskStatus.hasStorageNotLowConstraint()) { mTrackedTasks.add(taskStatus); + taskStatus.setTrackingController(JobStatus.TRACKING_STORAGE); taskStatus.setStorageNotLowConstraintSatisfied(mStorageTracker.isStorageNotLow()); } } @Override - public void maybeStopTrackingJobLocked(JobStatus taskStatus, JobStatus incomingJob, boolean forUpdate) { - if (taskStatus.hasPowerConstraint()) { + public void maybeStopTrackingJobLocked(JobStatus taskStatus, JobStatus incomingJob, + boolean forUpdate) { + if (taskStatus.clearTrackingController(JobStatus.TRACKING_STORAGE)) { mTrackedTasks.remove(taskStatus); } } @@ -94,7 +94,7 @@ public class StorageController extends StateController { boolean reportChange = false; synchronized (mLock) { for (int i = mTrackedTasks.size() - 1; i >= 0; i--) { - final JobStatus ts = mTrackedTasks.get(i); + final JobStatus ts = mTrackedTasks.valueAt(i); boolean previous = ts.setStorageNotLowConstraintSatisfied(storageNotLow); if (previous != storageNotLow) { reportChange = true; @@ -178,7 +178,7 @@ public class StorageController extends StateController { pw.print(mTrackedTasks.size()); pw.println(":"); for (int i = 0; i < mTrackedTasks.size(); i++) { - final JobStatus js = mTrackedTasks.get(i); + final JobStatus js = mTrackedTasks.valueAt(i); if (!js.shouldDump(filterUid)) { continue; } diff --git a/services/core/java/com/android/server/job/controllers/TimeController.java b/services/core/java/com/android/server/job/controllers/TimeController.java index 0b3b00fa84aa9..01c841e2083cc 100644 --- a/services/core/java/com/android/server/job/controllers/TimeController.java +++ b/services/core/java/com/android/server/job/controllers/TimeController.java @@ -51,7 +51,7 @@ public class TimeController extends StateController { private AlarmManager mAlarmService = null; /** List of tracked jobs, sorted asc. by deadline */ - private final List mTrackedJobs = new LinkedList(); + private final List mTrackedJobs = new LinkedList<>(); /** Singleton. */ private static TimeController mSingleton; @@ -78,6 +78,20 @@ public class TimeController extends StateController { public void maybeStartTrackingJobLocked(JobStatus job, JobStatus lastJob) { if (job.hasTimingDelayConstraint() || job.hasDeadlineConstraint()) { maybeStopTrackingJobLocked(job, null, false); + + // First: check the constraints now, because if they are already satisfied + // then there is no need to track it. This gives us a fast path for a common + // pattern of having a job with a 0 deadline constraint ("run immediately"). + // Unlike most controllers, once one of our constraints has been satisfied, it + // will never be unsatisfied (our time base can not go backwards). + final long nowElapsedMillis = SystemClock.elapsedRealtime(); + if (job.hasDeadlineConstraint() && evaluateDeadlineConstraint(job, nowElapsedMillis)) { + return; + } else if (job.hasTimingDelayConstraint() && evaluateTimingDelayConstraint(job, + nowElapsedMillis)) { + return; + } + boolean isInsert = false; ListIterator it = mTrackedJobs.listIterator(mTrackedJobs.size()); while (it.hasPrevious()) { @@ -92,6 +106,7 @@ public class TimeController extends StateController { it.next(); } it.add(job); + job.setTrackingController(JobStatus.TRACKING_TIME); maybeUpdateAlarmsLocked( job.hasTimingDelayConstraint() ? job.getEarliestRunTime() : Long.MAX_VALUE, job.hasDeadlineConstraint() ? job.getLatestRunTimeElapsed() : Long.MAX_VALUE, @@ -102,13 +117,15 @@ public class TimeController extends StateController { /** * When we stop tracking a job, we only need to update our alarms if the job we're no longer * tracking was the one our alarms were based off of. - * Really an == comparison should be enough, but why play with fate? We'll do <=. */ @Override - public void maybeStopTrackingJobLocked(JobStatus job, JobStatus incomingJob, boolean forUpdate) { - if (mTrackedJobs.remove(job)) { - checkExpiredDelaysAndResetAlarm(); - checkExpiredDeadlinesAndResetAlarm(); + public void maybeStopTrackingJobLocked(JobStatus job, JobStatus incomingJob, + boolean forUpdate) { + if (job.clearTrackingController(JobStatus.TRACKING_TIME)) { + if (mTrackedJobs.remove(job)) { + checkExpiredDelaysAndResetAlarm(); + checkExpiredDeadlinesAndResetAlarm(); + } } } @@ -147,17 +164,12 @@ public class TimeController extends StateController { if (!job.hasDeadlineConstraint()) { continue; } - final long jobDeadline = job.getLatestRunTimeElapsed(); - if (jobDeadline <= nowElapsedMillis) { - if (job.hasTimingDelayConstraint()) { - job.setTimingDelayConstraintSatisfied(true); - } - job.setDeadlineConstraintSatisfied(true); + if (evaluateDeadlineConstraint(job, nowElapsedMillis)) { mStateChangedListener.onRunJobNow(job); it.remove(); } else { // Sorted by expiry time, so take the next one and stop. - nextExpiryTime = jobDeadline; + nextExpiryTime = job.getLatestRunTimeElapsed(); nextExpiryUid = job.getSourceUid(); break; } @@ -166,6 +178,19 @@ public class TimeController extends StateController { } } + private boolean evaluateDeadlineConstraint(JobStatus job, long nowElapsedMillis) { + final long jobDeadline = job.getLatestRunTimeElapsed(); + + if (jobDeadline <= nowElapsedMillis) { + if (job.hasTimingDelayConstraint()) { + job.setTimingDelayConstraintSatisfied(true); + } + job.setDeadlineConstraintSatisfied(true); + return true; + } + return false; + } + /** * Handles alarm that notifies us that a job's delay has expired. Iterates through the list of * tracked jobs and marks them as ready as appropriate. @@ -182,9 +207,7 @@ public class TimeController extends StateController { if (!job.hasTimingDelayConstraint()) { continue; } - final long jobDelayTime = job.getEarliestRunTime(); - if (jobDelayTime <= nowElapsedMillis) { - job.setTimingDelayConstraintSatisfied(true); + if (evaluateTimingDelayConstraint(job, nowElapsedMillis)) { if (canStopTrackingJobLocked(job)) { it.remove(); } @@ -194,6 +217,7 @@ public class TimeController extends StateController { } else if (!job.isConstraintSatisfied(JobStatus.CONSTRAINT_TIMING_DELAY)) { // If this job still doesn't have its delay constraint satisfied, // then see if it is the next upcoming delay time for the alarm. + final long jobDelayTime = job.getEarliestRunTime(); if (nextDelayTime > jobDelayTime) { nextDelayTime = jobDelayTime; nextDelayUid = job.getSourceUid(); @@ -207,6 +231,15 @@ public class TimeController extends StateController { } } + private boolean evaluateTimingDelayConstraint(JobStatus job, long nowElapsedMillis) { + final long jobDelayTime = job.getEarliestRunTime(); + if (jobDelayTime <= nowElapsedMillis) { + job.setTimingDelayConstraintSatisfied(true); + return true; + } + return false; + } + private void maybeUpdateAlarmsLocked(long delayExpiredElapsed, long deadlineExpiredElapsed, int uid) { if (delayExpiredElapsed < mNextDelayExpiredElapsedMillis) {