1 /* 2 * Copyright (C) 2014 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License 15 */ 16 17 package com.android.server.job.controllers; 18 19 import android.app.AlarmManager; 20 import android.app.AlarmManager.OnAlarmListener; 21 import android.content.Context; 22 import android.os.SystemClock; 23 import android.os.UserHandle; 24 import android.os.WorkSource; 25 import android.util.Slog; 26 import android.util.TimeUtils; 27 28 import com.android.server.job.JobSchedulerService; 29 import com.android.server.job.StateChangedListener; 30 31 import java.io.PrintWriter; 32 import java.util.Iterator; 33 import java.util.LinkedList; 34 import java.util.List; 35 import java.util.ListIterator; 36 37 /** 38 * This class sets an alarm for the next expiring job, and determines whether a job's minimum 39 * delay has been satisfied. 40 */ 41 public final class TimeController extends StateController { 42 private static final String TAG = "JobScheduler.Time"; 43 44 /** Deadline alarm tag for logging purposes */ 45 private final String DEADLINE_TAG = "*job.deadline*"; 46 /** Delay alarm tag for logging purposes */ 47 private final String DELAY_TAG = "*job.delay*"; 48 49 private long mNextJobExpiredElapsedMillis; 50 private long mNextDelayExpiredElapsedMillis; 51 52 private AlarmManager mAlarmService = null; 53 /** List of tracked jobs, sorted asc. by deadline */ 54 private final List<JobStatus> mTrackedJobs = new LinkedList<>(); 55 /** Singleton. */ 56 private static TimeController mSingleton; 57 58 public static synchronized TimeController get(JobSchedulerService jms) { 59 if (mSingleton == null) { 60 mSingleton = new TimeController(jms, jms.getContext(), jms.getLock()); 61 } 62 return mSingleton; 63 } 64 65 private TimeController(StateChangedListener stateChangedListener, Context context, 66 Object lock) { 67 super(stateChangedListener, context, lock); 68 69 mNextJobExpiredElapsedMillis = Long.MAX_VALUE; 70 mNextDelayExpiredElapsedMillis = Long.MAX_VALUE; 71 } 72 73 /** 74 * Check if the job has a timing constraint, and if so determine where to insert it in our 75 * list. 76 */ 77 @Override 78 public void maybeStartTrackingJobLocked(JobStatus job, JobStatus lastJob) { 79 if (job.hasTimingDelayConstraint() || job.hasDeadlineConstraint()) { 80 maybeStopTrackingJobLocked(job, null, false); 81 82 // First: check the constraints now, because if they are already satisfied 83 // then there is no need to track it. This gives us a fast path for a common 84 // pattern of having a job with a 0 deadline constraint ("run immediately"). 85 // Unlike most controllers, once one of our constraints has been satisfied, it 86 // will never be unsatisfied (our time base can not go backwards). 87 final long nowElapsedMillis = SystemClock.elapsedRealtime(); 88 if (job.hasDeadlineConstraint() && evaluateDeadlineConstraint(job, nowElapsedMillis)) { 89 return; 90 } else if (job.hasTimingDelayConstraint() && evaluateTimingDelayConstraint(job, 91 nowElapsedMillis)) { 92 return; 93 } 94 95 boolean isInsert = false; 96 ListIterator<JobStatus> it = mTrackedJobs.listIterator(mTrackedJobs.size()); 97 while (it.hasPrevious()) { 98 JobStatus ts = it.previous(); 99 if (ts.getLatestRunTimeElapsed() < job.getLatestRunTimeElapsed()) { 100 // Insert 101 isInsert = true; 102 break; 103 } 104 } 105 if (isInsert) { 106 it.next(); 107 } 108 it.add(job); 109 job.setTrackingController(JobStatus.TRACKING_TIME); 110 maybeUpdateAlarmsLocked( 111 job.hasTimingDelayConstraint() ? job.getEarliestRunTime() : Long.MAX_VALUE, 112 job.hasDeadlineConstraint() ? job.getLatestRunTimeElapsed() : Long.MAX_VALUE, 113 job.getSourceUid()); 114 } 115 } 116 117 /** 118 * When we stop tracking a job, we only need to update our alarms if the job we're no longer 119 * tracking was the one our alarms were based off of. 120 */ 121 @Override 122 public void maybeStopTrackingJobLocked(JobStatus job, JobStatus incomingJob, 123 boolean forUpdate) { 124 if (job.clearTrackingController(JobStatus.TRACKING_TIME)) { 125 if (mTrackedJobs.remove(job)) { 126 checkExpiredDelaysAndResetAlarm(); 127 checkExpiredDeadlinesAndResetAlarm(); 128 } 129 } 130 } 131 132 /** 133 * Determines whether this controller can stop tracking the given job. 134 * The controller is no longer interested in a job once its time constraint is satisfied, and 135 * the job's deadline is fulfilled - unlike other controllers a time constraint can't toggle 136 * back and forth. 137 */ 138 private boolean canStopTrackingJobLocked(JobStatus job) { 139 return (!job.hasTimingDelayConstraint() || 140 (job.satisfiedConstraints&JobStatus.CONSTRAINT_TIMING_DELAY) != 0) && 141 (!job.hasDeadlineConstraint() || 142 (job.satisfiedConstraints&JobStatus.CONSTRAINT_DEADLINE) != 0); 143 } 144 145 private void ensureAlarmServiceLocked() { 146 if (mAlarmService == null) { 147 mAlarmService = (AlarmManager) mContext.getSystemService(Context.ALARM_SERVICE); 148 } 149 } 150 151 /** 152 * Checks list of jobs for ones that have an expired deadline, sending them to the JobScheduler 153 * if so, removing them from this list, and updating the alarm for the next expiry time. 154 */ 155 private void checkExpiredDeadlinesAndResetAlarm() { 156 synchronized (mLock) { 157 long nextExpiryTime = Long.MAX_VALUE; 158 int nextExpiryUid = 0; 159 final long nowElapsedMillis = SystemClock.elapsedRealtime(); 160 161 Iterator<JobStatus> it = mTrackedJobs.iterator(); 162 while (it.hasNext()) { 163 JobStatus job = it.next(); 164 if (!job.hasDeadlineConstraint()) { 165 continue; 166 } 167 168 if (evaluateDeadlineConstraint(job, nowElapsedMillis)) { 169 mStateChangedListener.onRunJobNow(job); 170 it.remove(); 171 } else { // Sorted by expiry time, so take the next one and stop. 172 nextExpiryTime = job.getLatestRunTimeElapsed(); 173 nextExpiryUid = job.getSourceUid(); 174 break; 175 } 176 } 177 setDeadlineExpiredAlarmLocked(nextExpiryTime, nextExpiryUid); 178 } 179 } 180 181 private boolean evaluateDeadlineConstraint(JobStatus job, long nowElapsedMillis) { 182 final long jobDeadline = job.getLatestRunTimeElapsed(); 183 184 if (jobDeadline <= nowElapsedMillis) { 185 if (job.hasTimingDelayConstraint()) { 186 job.setTimingDelayConstraintSatisfied(true); 187 } 188 job.setDeadlineConstraintSatisfied(true); 189 return true; 190 } 191 return false; 192 } 193 194 /** 195 * Handles alarm that notifies us that a job's delay has expired. Iterates through the list of 196 * tracked jobs and marks them as ready as appropriate. 197 */ 198 private void checkExpiredDelaysAndResetAlarm() { 199 synchronized (mLock) { 200 final long nowElapsedMillis = SystemClock.elapsedRealtime(); 201 long nextDelayTime = Long.MAX_VALUE; 202 int nextDelayUid = 0; 203 boolean ready = false; 204 Iterator<JobStatus> it = mTrackedJobs.iterator(); 205 while (it.hasNext()) { 206 final JobStatus job = it.next(); 207 if (!job.hasTimingDelayConstraint()) { 208 continue; 209 } 210 if (evaluateTimingDelayConstraint(job, nowElapsedMillis)) { 211 if (canStopTrackingJobLocked(job)) { 212 it.remove(); 213 } 214 if (job.isReady()) { 215 ready = true; 216 } 217 } else if (!job.isConstraintSatisfied(JobStatus.CONSTRAINT_TIMING_DELAY)) { 218 // If this job still doesn't have its delay constraint satisfied, 219 // then see if it is the next upcoming delay time for the alarm. 220 final long jobDelayTime = job.getEarliestRunTime(); 221 if (nextDelayTime > jobDelayTime) { 222 nextDelayTime = jobDelayTime; 223 nextDelayUid = job.getSourceUid(); 224 } 225 } 226 } 227 if (ready) { 228 mStateChangedListener.onControllerStateChanged(); 229 } 230 setDelayExpiredAlarmLocked(nextDelayTime, nextDelayUid); 231 } 232 } 233 234 private boolean evaluateTimingDelayConstraint(JobStatus job, long nowElapsedMillis) { 235 final long jobDelayTime = job.getEarliestRunTime(); 236 if (jobDelayTime <= nowElapsedMillis) { 237 job.setTimingDelayConstraintSatisfied(true); 238 return true; 239 } 240 return false; 241 } 242 243 private void maybeUpdateAlarmsLocked(long delayExpiredElapsed, long deadlineExpiredElapsed, 244 int uid) { 245 if (delayExpiredElapsed < mNextDelayExpiredElapsedMillis) { 246 setDelayExpiredAlarmLocked(delayExpiredElapsed, uid); 247 } 248 if (deadlineExpiredElapsed < mNextJobExpiredElapsedMillis) { 249 setDeadlineExpiredAlarmLocked(deadlineExpiredElapsed, uid); 250 } 251 } 252 253 /** 254 * Set an alarm with the {@link android.app.AlarmManager} for the next time at which a job's 255 * delay will expire. 256 * This alarm <b>will</b> wake up the phone. 257 */ 258 private void setDelayExpiredAlarmLocked(long alarmTimeElapsedMillis, int uid) { 259 alarmTimeElapsedMillis = maybeAdjustAlarmTime(alarmTimeElapsedMillis); 260 mNextDelayExpiredElapsedMillis = alarmTimeElapsedMillis; 261 updateAlarmWithListenerLocked(DELAY_TAG, mNextDelayExpiredListener, 262 mNextDelayExpiredElapsedMillis, uid); 263 } 264 265 /** 266 * Set an alarm with the {@link android.app.AlarmManager} for the next time at which a job's 267 * deadline will expire. 268 * This alarm <b>will</b> wake up the phone. 269 */ 270 private void setDeadlineExpiredAlarmLocked(long alarmTimeElapsedMillis, int uid) { 271 alarmTimeElapsedMillis = maybeAdjustAlarmTime(alarmTimeElapsedMillis); 272 mNextJobExpiredElapsedMillis = alarmTimeElapsedMillis; 273 updateAlarmWithListenerLocked(DEADLINE_TAG, mDeadlineExpiredListener, 274 mNextJobExpiredElapsedMillis, uid); 275 } 276 277 private long maybeAdjustAlarmTime(long proposedAlarmTimeElapsedMillis) { 278 final long earliestWakeupTimeElapsed = SystemClock.elapsedRealtime(); 279 if (proposedAlarmTimeElapsedMillis < earliestWakeupTimeElapsed) { 280 return earliestWakeupTimeElapsed; 281 } 282 return proposedAlarmTimeElapsedMillis; 283 } 284 285 private void updateAlarmWithListenerLocked(String tag, OnAlarmListener listener, 286 long alarmTimeElapsed, int uid) { 287 ensureAlarmServiceLocked(); 288 if (alarmTimeElapsed == Long.MAX_VALUE) { 289 mAlarmService.cancel(listener); 290 } else { 291 if (DEBUG) { 292 Slog.d(TAG, "Setting " + tag + " for: " + alarmTimeElapsed); 293 } 294 mAlarmService.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, alarmTimeElapsed, 295 AlarmManager.WINDOW_HEURISTIC, 0, tag, listener, null, new WorkSource(uid)); 296 } 297 } 298 299 // Job/delay expiration alarm handling 300 301 private final OnAlarmListener mDeadlineExpiredListener = new OnAlarmListener() { 302 @Override 303 public void onAlarm() { 304 if (DEBUG) { 305 Slog.d(TAG, "Deadline-expired alarm fired"); 306 } 307 checkExpiredDeadlinesAndResetAlarm(); 308 } 309 }; 310 311 private final OnAlarmListener mNextDelayExpiredListener = new OnAlarmListener() { 312 @Override 313 public void onAlarm() { 314 if (DEBUG) { 315 Slog.d(TAG, "Delay-expired alarm fired"); 316 } 317 checkExpiredDelaysAndResetAlarm(); 318 } 319 }; 320 321 @Override 322 public void dumpControllerStateLocked(PrintWriter pw, int filterUid) { 323 final long nowElapsed = SystemClock.elapsedRealtime(); 324 pw.print("Alarms: now="); 325 pw.print(SystemClock.elapsedRealtime()); 326 pw.println(); 327 pw.print("Next delay alarm in "); 328 TimeUtils.formatDuration(mNextDelayExpiredElapsedMillis, nowElapsed, pw); 329 pw.println(); 330 pw.print("Next deadline alarm in "); 331 TimeUtils.formatDuration(mNextJobExpiredElapsedMillis, nowElapsed, pw); 332 pw.println(); 333 pw.print("Tracking "); 334 pw.print(mTrackedJobs.size()); 335 pw.println(":"); 336 for (JobStatus ts : mTrackedJobs) { 337 if (!ts.shouldDump(filterUid)) { 338 continue; 339 } 340 pw.print(" #"); 341 ts.printUniqueId(pw); 342 pw.print(" from "); 343 UserHandle.formatUid(pw, ts.getSourceUid()); 344 pw.print(": Delay="); 345 if (ts.hasTimingDelayConstraint()) { 346 TimeUtils.formatDuration(ts.getEarliestRunTime(), nowElapsed, pw); 347 } else { 348 pw.print("N/A"); 349 } 350 pw.print(", Deadline="); 351 if (ts.hasDeadlineConstraint()) { 352 TimeUtils.formatDuration(ts.getLatestRunTimeElapsed(), nowElapsed, pw); 353 } else { 354 pw.print("N/A"); 355 } 356 pw.println(); 357 } 358 } 359 }