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 17package com.android.server.job.controllers; 18 19import android.app.AlarmManager; 20import android.app.AlarmManager.OnAlarmListener; 21import android.content.Context; 22import android.os.SystemClock; 23import android.os.UserHandle; 24import android.os.WorkSource; 25import android.util.Slog; 26import android.util.TimeUtils; 27 28import com.android.server.job.JobSchedulerService; 29import com.android.server.job.StateChangedListener; 30 31import java.io.PrintWriter; 32import java.util.Iterator; 33import java.util.LinkedList; 34import java.util.List; 35import 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 */ 41public 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<JobStatus>(); 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 boolean isInsert = false; 82 ListIterator<JobStatus> it = mTrackedJobs.listIterator(mTrackedJobs.size()); 83 while (it.hasPrevious()) { 84 JobStatus ts = it.previous(); 85 if (ts.getLatestRunTimeElapsed() < job.getLatestRunTimeElapsed()) { 86 // Insert 87 isInsert = true; 88 break; 89 } 90 } 91 if (isInsert) { 92 it.next(); 93 } 94 it.add(job); 95 maybeUpdateAlarmsLocked( 96 job.hasTimingDelayConstraint() ? job.getEarliestRunTime() : Long.MAX_VALUE, 97 job.hasDeadlineConstraint() ? job.getLatestRunTimeElapsed() : Long.MAX_VALUE, 98 job.getSourceUid()); 99 } 100 } 101 102 /** 103 * When we stop tracking a job, we only need to update our alarms if the job we're no longer 104 * tracking was the one our alarms were based off of. 105 * Really an == comparison should be enough, but why play with fate? We'll do <=. 106 */ 107 @Override 108 public void maybeStopTrackingJobLocked(JobStatus job, JobStatus incomingJob, boolean forUpdate) { 109 if (mTrackedJobs.remove(job)) { 110 checkExpiredDelaysAndResetAlarm(); 111 checkExpiredDeadlinesAndResetAlarm(); 112 } 113 } 114 115 /** 116 * Determines whether this controller can stop tracking the given job. 117 * The controller is no longer interested in a job once its time constraint is satisfied, and 118 * the job's deadline is fulfilled - unlike other controllers a time constraint can't toggle 119 * back and forth. 120 */ 121 private boolean canStopTrackingJobLocked(JobStatus job) { 122 return (!job.hasTimingDelayConstraint() || 123 (job.satisfiedConstraints&JobStatus.CONSTRAINT_TIMING_DELAY) != 0) && 124 (!job.hasDeadlineConstraint() || 125 (job.satisfiedConstraints&JobStatus.CONSTRAINT_DEADLINE) != 0); 126 } 127 128 private void ensureAlarmServiceLocked() { 129 if (mAlarmService == null) { 130 mAlarmService = (AlarmManager) mContext.getSystemService(Context.ALARM_SERVICE); 131 } 132 } 133 134 /** 135 * Checks list of jobs for ones that have an expired deadline, sending them to the JobScheduler 136 * if so, removing them from this list, and updating the alarm for the next expiry time. 137 */ 138 private void checkExpiredDeadlinesAndResetAlarm() { 139 synchronized (mLock) { 140 long nextExpiryTime = Long.MAX_VALUE; 141 int nextExpiryUid = 0; 142 final long nowElapsedMillis = SystemClock.elapsedRealtime(); 143 144 Iterator<JobStatus> it = mTrackedJobs.iterator(); 145 while (it.hasNext()) { 146 JobStatus job = it.next(); 147 if (!job.hasDeadlineConstraint()) { 148 continue; 149 } 150 final long jobDeadline = job.getLatestRunTimeElapsed(); 151 152 if (jobDeadline <= nowElapsedMillis) { 153 if (job.hasTimingDelayConstraint()) { 154 job.setTimingDelayConstraintSatisfied(true); 155 } 156 job.setDeadlineConstraintSatisfied(true); 157 mStateChangedListener.onRunJobNow(job); 158 it.remove(); 159 } else { // Sorted by expiry time, so take the next one and stop. 160 nextExpiryTime = jobDeadline; 161 nextExpiryUid = job.getSourceUid(); 162 break; 163 } 164 } 165 setDeadlineExpiredAlarmLocked(nextExpiryTime, nextExpiryUid); 166 } 167 } 168 169 /** 170 * Handles alarm that notifies us that a job's delay has expired. Iterates through the list of 171 * tracked jobs and marks them as ready as appropriate. 172 */ 173 private void checkExpiredDelaysAndResetAlarm() { 174 synchronized (mLock) { 175 final long nowElapsedMillis = SystemClock.elapsedRealtime(); 176 long nextDelayTime = Long.MAX_VALUE; 177 int nextDelayUid = 0; 178 boolean ready = false; 179 Iterator<JobStatus> it = mTrackedJobs.iterator(); 180 while (it.hasNext()) { 181 final JobStatus job = it.next(); 182 if (!job.hasTimingDelayConstraint()) { 183 continue; 184 } 185 final long jobDelayTime = job.getEarliestRunTime(); 186 if (jobDelayTime <= nowElapsedMillis) { 187 job.setTimingDelayConstraintSatisfied(true); 188 if (canStopTrackingJobLocked(job)) { 189 it.remove(); 190 } 191 if (job.isReady()) { 192 ready = true; 193 } 194 } else if (!job.isConstraintSatisfied(JobStatus.CONSTRAINT_TIMING_DELAY)) { 195 // If this job still doesn't have its delay constraint satisfied, 196 // then see if it is the next upcoming delay time for the alarm. 197 if (nextDelayTime > jobDelayTime) { 198 nextDelayTime = jobDelayTime; 199 nextDelayUid = job.getSourceUid(); 200 } 201 } 202 } 203 if (ready) { 204 mStateChangedListener.onControllerStateChanged(); 205 } 206 setDelayExpiredAlarmLocked(nextDelayTime, nextDelayUid); 207 } 208 } 209 210 private void maybeUpdateAlarmsLocked(long delayExpiredElapsed, long deadlineExpiredElapsed, 211 int uid) { 212 if (delayExpiredElapsed < mNextDelayExpiredElapsedMillis) { 213 setDelayExpiredAlarmLocked(delayExpiredElapsed, uid); 214 } 215 if (deadlineExpiredElapsed < mNextJobExpiredElapsedMillis) { 216 setDeadlineExpiredAlarmLocked(deadlineExpiredElapsed, uid); 217 } 218 } 219 220 /** 221 * Set an alarm with the {@link android.app.AlarmManager} for the next time at which a job's 222 * delay will expire. 223 * This alarm <b>will</b> wake up the phone. 224 */ 225 private void setDelayExpiredAlarmLocked(long alarmTimeElapsedMillis, int uid) { 226 alarmTimeElapsedMillis = maybeAdjustAlarmTime(alarmTimeElapsedMillis); 227 mNextDelayExpiredElapsedMillis = alarmTimeElapsedMillis; 228 updateAlarmWithListenerLocked(DELAY_TAG, mNextDelayExpiredListener, 229 mNextDelayExpiredElapsedMillis, uid); 230 } 231 232 /** 233 * Set an alarm with the {@link android.app.AlarmManager} for the next time at which a job's 234 * deadline will expire. 235 * This alarm <b>will</b> wake up the phone. 236 */ 237 private void setDeadlineExpiredAlarmLocked(long alarmTimeElapsedMillis, int uid) { 238 alarmTimeElapsedMillis = maybeAdjustAlarmTime(alarmTimeElapsedMillis); 239 mNextJobExpiredElapsedMillis = alarmTimeElapsedMillis; 240 updateAlarmWithListenerLocked(DEADLINE_TAG, mDeadlineExpiredListener, 241 mNextJobExpiredElapsedMillis, uid); 242 } 243 244 private long maybeAdjustAlarmTime(long proposedAlarmTimeElapsedMillis) { 245 final long earliestWakeupTimeElapsed = SystemClock.elapsedRealtime(); 246 if (proposedAlarmTimeElapsedMillis < earliestWakeupTimeElapsed) { 247 return earliestWakeupTimeElapsed; 248 } 249 return proposedAlarmTimeElapsedMillis; 250 } 251 252 private void updateAlarmWithListenerLocked(String tag, OnAlarmListener listener, 253 long alarmTimeElapsed, int uid) { 254 ensureAlarmServiceLocked(); 255 if (alarmTimeElapsed == Long.MAX_VALUE) { 256 mAlarmService.cancel(listener); 257 } else { 258 if (DEBUG) { 259 Slog.d(TAG, "Setting " + tag + " for: " + alarmTimeElapsed); 260 } 261 mAlarmService.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, alarmTimeElapsed, 262 AlarmManager.WINDOW_HEURISTIC, 0, tag, listener, null, new WorkSource(uid)); 263 } 264 } 265 266 // Job/delay expiration alarm handling 267 268 private final OnAlarmListener mDeadlineExpiredListener = new OnAlarmListener() { 269 @Override 270 public void onAlarm() { 271 if (DEBUG) { 272 Slog.d(TAG, "Deadline-expired alarm fired"); 273 } 274 checkExpiredDeadlinesAndResetAlarm(); 275 } 276 }; 277 278 private final OnAlarmListener mNextDelayExpiredListener = new OnAlarmListener() { 279 @Override 280 public void onAlarm() { 281 if (DEBUG) { 282 Slog.d(TAG, "Delay-expired alarm fired"); 283 } 284 checkExpiredDelaysAndResetAlarm(); 285 } 286 }; 287 288 @Override 289 public void dumpControllerStateLocked(PrintWriter pw, int filterUid) { 290 final long nowElapsed = SystemClock.elapsedRealtime(); 291 pw.print("Alarms: now="); 292 pw.print(SystemClock.elapsedRealtime()); 293 pw.println(); 294 pw.print("Next delay alarm in "); 295 TimeUtils.formatDuration(mNextDelayExpiredElapsedMillis, nowElapsed, pw); 296 pw.println(); 297 pw.print("Next deadline alarm in "); 298 TimeUtils.formatDuration(mNextJobExpiredElapsedMillis, nowElapsed, pw); 299 pw.println(); 300 pw.print("Tracking "); 301 pw.print(mTrackedJobs.size()); 302 pw.println(":"); 303 for (JobStatus ts : mTrackedJobs) { 304 if (!ts.shouldDump(filterUid)) { 305 continue; 306 } 307 pw.print(" #"); 308 ts.printUniqueId(pw); 309 pw.print(" from "); 310 UserHandle.formatUid(pw, ts.getSourceUid()); 311 pw.print(": Delay="); 312 if (ts.hasTimingDelayConstraint()) { 313 TimeUtils.formatDuration(ts.getEarliestRunTime(), nowElapsed, pw); 314 } else { 315 pw.print("N/A"); 316 } 317 pw.print(", Deadline="); 318 if (ts.hasDeadlineConstraint()) { 319 TimeUtils.formatDuration(ts.getLatestRunTimeElapsed(), nowElapsed, pw); 320 } else { 321 pw.print("N/A"); 322 } 323 pw.println(); 324 } 325 } 326}