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 static com.android.server.job.JobSchedulerService.sElapsedRealtimeClock; 20 21 import android.annotation.Nullable; 22 import android.app.AlarmManager; 23 import android.app.AlarmManager.OnAlarmListener; 24 import android.content.Context; 25 import android.os.Process; 26 import android.os.UserHandle; 27 import android.os.WorkSource; 28 import android.util.Log; 29 import android.util.Slog; 30 import android.util.TimeUtils; 31 import android.util.proto.ProtoOutputStream; 32 33 import com.android.internal.util.IndentingPrintWriter; 34 import com.android.server.job.JobSchedulerService; 35 import com.android.server.job.StateControllerProto; 36 37 import java.util.Iterator; 38 import java.util.LinkedList; 39 import java.util.List; 40 import java.util.ListIterator; 41 import java.util.function.Predicate; 42 43 /** 44 * This class sets an alarm for the next expiring job, and determines whether a job's minimum 45 * delay has been satisfied. 46 */ 47 public final class TimeController extends StateController { 48 private static final String TAG = "JobScheduler.Time"; 49 private static final boolean DEBUG = JobSchedulerService.DEBUG 50 || Log.isLoggable(TAG, Log.DEBUG); 51 52 /** Deadline alarm tag for logging purposes */ 53 private final String DEADLINE_TAG = "*job.deadline*"; 54 /** Delay alarm tag for logging purposes */ 55 private final String DELAY_TAG = "*job.delay*"; 56 57 private long mNextJobExpiredElapsedMillis; 58 private long mNextDelayExpiredElapsedMillis; 59 60 private final boolean mChainedAttributionEnabled; 61 62 private AlarmManager mAlarmService = null; 63 /** List of tracked jobs, sorted asc. by deadline */ 64 private final List<JobStatus> mTrackedJobs = new LinkedList<>(); 65 66 public TimeController(JobSchedulerService service) { 67 super(service); 68 69 mNextJobExpiredElapsedMillis = Long.MAX_VALUE; 70 mNextDelayExpiredElapsedMillis = Long.MAX_VALUE; 71 mChainedAttributionEnabled = WorkSource.isChainedBatteryAttributionEnabled(mContext); 72 } 73 74 /** 75 * Check if the job has a timing constraint, and if so determine where to insert it in our 76 * list. 77 */ 78 @Override 79 public void maybeStartTrackingJobLocked(JobStatus job, JobStatus lastJob) { 80 if (job.hasTimingDelayConstraint() || job.hasDeadlineConstraint()) { 81 maybeStopTrackingJobLocked(job, null, false); 82 83 // First: check the constraints now, because if they are already satisfied 84 // then there is no need to track it. This gives us a fast path for a common 85 // pattern of having a job with a 0 deadline constraint ("run immediately"). 86 // Unlike most controllers, once one of our constraints has been satisfied, it 87 // will never be unsatisfied (our time base can not go backwards). 88 final long nowElapsedMillis = sElapsedRealtimeClock.millis(); 89 if (job.hasDeadlineConstraint() && evaluateDeadlineConstraint(job, nowElapsedMillis)) { 90 return; 91 } else if (job.hasTimingDelayConstraint() && evaluateTimingDelayConstraint(job, 92 nowElapsedMillis)) { 93 if (!job.hasDeadlineConstraint()) { 94 // If it doesn't have a deadline, we'll never have to touch it again. 95 return; 96 } 97 } 98 99 boolean isInsert = false; 100 ListIterator<JobStatus> it = mTrackedJobs.listIterator(mTrackedJobs.size()); 101 while (it.hasPrevious()) { 102 JobStatus ts = it.previous(); 103 if (ts.getLatestRunTimeElapsed() < job.getLatestRunTimeElapsed()) { 104 // Insert 105 isInsert = true; 106 break; 107 } 108 } 109 if (isInsert) { 110 it.next(); 111 } 112 it.add(job); 113 job.setTrackingController(JobStatus.TRACKING_TIME); 114 maybeUpdateAlarmsLocked( 115 job.hasTimingDelayConstraint() ? job.getEarliestRunTime() : Long.MAX_VALUE, 116 job.hasDeadlineConstraint() ? job.getLatestRunTimeElapsed() : Long.MAX_VALUE, 117 deriveWorkSource(job.getSourceUid(), job.getSourcePackageName())); 118 } 119 } 120 121 /** 122 * When we stop tracking a job, we only need to update our alarms if the job we're no longer 123 * tracking was the one our alarms were based off of. 124 */ 125 @Override 126 public void maybeStopTrackingJobLocked(JobStatus job, JobStatus incomingJob, 127 boolean forUpdate) { 128 if (job.clearTrackingController(JobStatus.TRACKING_TIME)) { 129 if (mTrackedJobs.remove(job)) { 130 checkExpiredDelaysAndResetAlarm(); 131 checkExpiredDeadlinesAndResetAlarm(); 132 } 133 } 134 } 135 136 /** 137 * Determines whether this controller can stop tracking the given job. 138 * The controller is no longer interested in a job once its time constraint is satisfied, and 139 * the job's deadline is fulfilled - unlike other controllers a time constraint can't toggle 140 * back and forth. 141 */ 142 private boolean canStopTrackingJobLocked(JobStatus job) { 143 return (!job.hasTimingDelayConstraint() || 144 (job.satisfiedConstraints&JobStatus.CONSTRAINT_TIMING_DELAY) != 0) && 145 (!job.hasDeadlineConstraint() || 146 (job.satisfiedConstraints&JobStatus.CONSTRAINT_DEADLINE) != 0); 147 } 148 149 private void ensureAlarmServiceLocked() { 150 if (mAlarmService == null) { 151 mAlarmService = (AlarmManager) mContext.getSystemService(Context.ALARM_SERVICE); 152 } 153 } 154 155 /** 156 * Checks list of jobs for ones that have an expired deadline, sending them to the JobScheduler 157 * if so, removing them from this list, and updating the alarm for the next expiry time. 158 */ 159 private void checkExpiredDeadlinesAndResetAlarm() { 160 synchronized (mLock) { 161 long nextExpiryTime = Long.MAX_VALUE; 162 int nextExpiryUid = 0; 163 String nextExpiryPackageName = null; 164 final long nowElapsedMillis = sElapsedRealtimeClock.millis(); 165 166 Iterator<JobStatus> it = mTrackedJobs.iterator(); 167 while (it.hasNext()) { 168 JobStatus job = it.next(); 169 if (!job.hasDeadlineConstraint()) { 170 continue; 171 } 172 173 if (evaluateDeadlineConstraint(job, nowElapsedMillis)) { 174 mStateChangedListener.onRunJobNow(job); 175 it.remove(); 176 } else { // Sorted by expiry time, so take the next one and stop. 177 nextExpiryTime = job.getLatestRunTimeElapsed(); 178 nextExpiryUid = job.getSourceUid(); 179 nextExpiryPackageName = job.getSourcePackageName(); 180 break; 181 } 182 } 183 setDeadlineExpiredAlarmLocked(nextExpiryTime, 184 deriveWorkSource(nextExpiryUid, nextExpiryPackageName)); 185 } 186 } 187 188 private boolean evaluateDeadlineConstraint(JobStatus job, long nowElapsedMillis) { 189 final long jobDeadline = job.getLatestRunTimeElapsed(); 190 191 if (jobDeadline <= nowElapsedMillis) { 192 if (job.hasTimingDelayConstraint()) { 193 job.setTimingDelayConstraintSatisfied(true); 194 } 195 job.setDeadlineConstraintSatisfied(true); 196 return true; 197 } 198 return false; 199 } 200 201 /** 202 * Handles alarm that notifies us that a job's delay has expired. Iterates through the list of 203 * tracked jobs and marks them as ready as appropriate. 204 */ 205 private void checkExpiredDelaysAndResetAlarm() { 206 synchronized (mLock) { 207 final long nowElapsedMillis = sElapsedRealtimeClock.millis(); 208 long nextDelayTime = Long.MAX_VALUE; 209 int nextDelayUid = 0; 210 String nextDelayPackageName = null; 211 boolean ready = false; 212 Iterator<JobStatus> it = mTrackedJobs.iterator(); 213 while (it.hasNext()) { 214 final JobStatus job = it.next(); 215 if (!job.hasTimingDelayConstraint()) { 216 continue; 217 } 218 if (evaluateTimingDelayConstraint(job, nowElapsedMillis)) { 219 if (canStopTrackingJobLocked(job)) { 220 it.remove(); 221 } 222 if (job.isReady()) { 223 ready = true; 224 } 225 } else if (!job.isConstraintSatisfied(JobStatus.CONSTRAINT_TIMING_DELAY)) { 226 // If this job still doesn't have its delay constraint satisfied, 227 // then see if it is the next upcoming delay time for the alarm. 228 final long jobDelayTime = job.getEarliestRunTime(); 229 if (nextDelayTime > jobDelayTime) { 230 nextDelayTime = jobDelayTime; 231 nextDelayUid = job.getSourceUid(); 232 nextDelayPackageName = job.getSourcePackageName(); 233 } 234 } 235 } 236 if (ready) { 237 mStateChangedListener.onControllerStateChanged(); 238 } 239 setDelayExpiredAlarmLocked(nextDelayTime, 240 deriveWorkSource(nextDelayUid, nextDelayPackageName)); 241 } 242 } 243 244 private WorkSource deriveWorkSource(int uid, @Nullable String packageName) { 245 if (mChainedAttributionEnabled) { 246 WorkSource ws = new WorkSource(); 247 ws.createWorkChain() 248 .addNode(uid, packageName) 249 .addNode(Process.SYSTEM_UID, "JobScheduler"); 250 return ws; 251 } else { 252 return packageName == null ? new WorkSource(uid) : new WorkSource(uid, packageName); 253 } 254 } 255 256 private boolean evaluateTimingDelayConstraint(JobStatus job, long nowElapsedMillis) { 257 final long jobDelayTime = job.getEarliestRunTime(); 258 if (jobDelayTime <= nowElapsedMillis) { 259 job.setTimingDelayConstraintSatisfied(true); 260 return true; 261 } 262 return false; 263 } 264 265 private void maybeUpdateAlarmsLocked(long delayExpiredElapsed, long deadlineExpiredElapsed, 266 WorkSource ws) { 267 if (delayExpiredElapsed < mNextDelayExpiredElapsedMillis) { 268 setDelayExpiredAlarmLocked(delayExpiredElapsed, ws); 269 } 270 if (deadlineExpiredElapsed < mNextJobExpiredElapsedMillis) { 271 setDeadlineExpiredAlarmLocked(deadlineExpiredElapsed, ws); 272 } 273 } 274 275 /** 276 * Set an alarm with the {@link android.app.AlarmManager} for the next time at which a job's 277 * delay will expire. 278 * This alarm <b>will</b> wake up the phone. 279 */ 280 private void setDelayExpiredAlarmLocked(long alarmTimeElapsedMillis, WorkSource ws) { 281 alarmTimeElapsedMillis = maybeAdjustAlarmTime(alarmTimeElapsedMillis); 282 mNextDelayExpiredElapsedMillis = alarmTimeElapsedMillis; 283 updateAlarmWithListenerLocked(DELAY_TAG, mNextDelayExpiredListener, 284 mNextDelayExpiredElapsedMillis, ws); 285 } 286 287 /** 288 * Set an alarm with the {@link android.app.AlarmManager} for the next time at which a job's 289 * deadline will expire. 290 * This alarm <b>will</b> wake up the phone. 291 */ 292 private void setDeadlineExpiredAlarmLocked(long alarmTimeElapsedMillis, WorkSource ws) { 293 alarmTimeElapsedMillis = maybeAdjustAlarmTime(alarmTimeElapsedMillis); 294 mNextJobExpiredElapsedMillis = alarmTimeElapsedMillis; 295 updateAlarmWithListenerLocked(DEADLINE_TAG, mDeadlineExpiredListener, 296 mNextJobExpiredElapsedMillis, ws); 297 } 298 299 private long maybeAdjustAlarmTime(long proposedAlarmTimeElapsedMillis) { 300 final long earliestWakeupTimeElapsed = sElapsedRealtimeClock.millis(); 301 if (proposedAlarmTimeElapsedMillis < earliestWakeupTimeElapsed) { 302 return earliestWakeupTimeElapsed; 303 } 304 return proposedAlarmTimeElapsedMillis; 305 } 306 307 private void updateAlarmWithListenerLocked(String tag, OnAlarmListener listener, 308 long alarmTimeElapsed, WorkSource ws) { 309 ensureAlarmServiceLocked(); 310 if (alarmTimeElapsed == Long.MAX_VALUE) { 311 mAlarmService.cancel(listener); 312 } else { 313 if (DEBUG) { 314 Slog.d(TAG, "Setting " + tag + " for: " + alarmTimeElapsed); 315 } 316 mAlarmService.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, alarmTimeElapsed, 317 AlarmManager.WINDOW_HEURISTIC, 0, tag, listener, null, ws); 318 } 319 } 320 321 // Job/delay expiration alarm handling 322 323 private final OnAlarmListener mDeadlineExpiredListener = new OnAlarmListener() { 324 @Override 325 public void onAlarm() { 326 if (DEBUG) { 327 Slog.d(TAG, "Deadline-expired alarm fired"); 328 } 329 checkExpiredDeadlinesAndResetAlarm(); 330 } 331 }; 332 333 private final OnAlarmListener mNextDelayExpiredListener = new OnAlarmListener() { 334 @Override 335 public void onAlarm() { 336 if (DEBUG) { 337 Slog.d(TAG, "Delay-expired alarm fired"); 338 } 339 checkExpiredDelaysAndResetAlarm(); 340 } 341 }; 342 343 @Override 344 public void dumpControllerStateLocked(IndentingPrintWriter pw, 345 Predicate<JobStatus> predicate) { 346 final long nowElapsed = sElapsedRealtimeClock.millis(); 347 pw.println("Elapsed clock: " + nowElapsed); 348 349 pw.print("Next delay alarm in "); 350 TimeUtils.formatDuration(mNextDelayExpiredElapsedMillis, nowElapsed, pw); 351 pw.println(); 352 pw.print("Next deadline alarm in "); 353 TimeUtils.formatDuration(mNextJobExpiredElapsedMillis, nowElapsed, pw); 354 pw.println(); 355 pw.println(); 356 357 for (JobStatus ts : mTrackedJobs) { 358 if (!predicate.test(ts)) { 359 continue; 360 } 361 pw.print("#"); 362 ts.printUniqueId(pw); 363 pw.print(" from "); 364 UserHandle.formatUid(pw, ts.getSourceUid()); 365 pw.print(": Delay="); 366 if (ts.hasTimingDelayConstraint()) { 367 TimeUtils.formatDuration(ts.getEarliestRunTime(), nowElapsed, pw); 368 } else { 369 pw.print("N/A"); 370 } 371 pw.print(", Deadline="); 372 if (ts.hasDeadlineConstraint()) { 373 TimeUtils.formatDuration(ts.getLatestRunTimeElapsed(), nowElapsed, pw); 374 } else { 375 pw.print("N/A"); 376 } 377 pw.println(); 378 } 379 } 380 381 @Override 382 public void dumpControllerStateLocked(ProtoOutputStream proto, long fieldId, 383 Predicate<JobStatus> predicate) { 384 final long token = proto.start(fieldId); 385 final long mToken = proto.start(StateControllerProto.TIME); 386 387 final long nowElapsed = sElapsedRealtimeClock.millis(); 388 proto.write(StateControllerProto.TimeController.NOW_ELAPSED_REALTIME, nowElapsed); 389 proto.write(StateControllerProto.TimeController.TIME_UNTIL_NEXT_DELAY_ALARM_MS, 390 mNextDelayExpiredElapsedMillis - nowElapsed); 391 proto.write(StateControllerProto.TimeController.TIME_UNTIL_NEXT_DEADLINE_ALARM_MS, 392 mNextJobExpiredElapsedMillis - nowElapsed); 393 394 for (JobStatus ts : mTrackedJobs) { 395 if (!predicate.test(ts)) { 396 continue; 397 } 398 final long tsToken = proto.start(StateControllerProto.TimeController.TRACKED_JOBS); 399 ts.writeToShortProto(proto, StateControllerProto.TimeController.TrackedJob.INFO); 400 401 proto.write(StateControllerProto.TimeController.TrackedJob.HAS_TIMING_DELAY_CONSTRAINT, 402 ts.hasTimingDelayConstraint()); 403 proto.write(StateControllerProto.TimeController.TrackedJob.DELAY_TIME_REMAINING_MS, 404 ts.getEarliestRunTime() - nowElapsed); 405 406 proto.write(StateControllerProto.TimeController.TrackedJob.HAS_DEADLINE_CONSTRAINT, 407 ts.hasDeadlineConstraint()); 408 proto.write(StateControllerProto.TimeController.TrackedJob.TIME_REMAINING_UNTIL_DEADLINE_MS, 409 ts.getLatestRunTimeElapsed() - nowElapsed); 410 411 proto.end(tsToken); 412 } 413 414 proto.end(mToken); 415 proto.end(token); 416 } 417 } 418