TimeController.java revision 7060b04f6d92351b67222e636ab378a0273bf3e7
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.PendingIntent; 21import android.content.BroadcastReceiver; 22import android.content.Context; 23import android.content.Intent; 24import android.content.IntentFilter; 25import android.os.SystemClock; 26import android.util.Slog; 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 private static final String ACTION_JOB_EXPIRED = 44 "android.content.jobscheduler.JOB_DEADLINE_EXPIRED"; 45 private static final String ACTION_JOB_DELAY_EXPIRED = 46 "android.content.jobscheduler.JOB_DELAY_EXPIRED"; 47 48 /** Set an alarm for the next job expiry. */ 49 private final PendingIntent mDeadlineExpiredAlarmIntent; 50 /** Set an alarm for the next job delay expiry. This*/ 51 private final PendingIntent mNextDelayExpiredAlarmIntent; 52 /** Constant time determining how near in the future we'll set an alarm for. */ 53 private static final long MIN_WAKEUP_INTERVAL_MILLIS = 15 * 1000; 54 55 private long mNextJobExpiredElapsedMillis; 56 private long mNextDelayExpiredElapsedMillis; 57 58 private AlarmManager mAlarmService = null; 59 /** List of tracked jobs, sorted asc. by deadline */ 60 private final List<JobStatus> mTrackedJobs = new LinkedList<JobStatus>(); 61 /** Singleton. */ 62 private static TimeController mSingleton; 63 64 public static synchronized TimeController get(JobSchedulerService jms) { 65 if (mSingleton == null) { 66 mSingleton = new TimeController(jms, jms.getContext()); 67 } 68 return mSingleton; 69 } 70 71 private TimeController(StateChangedListener stateChangedListener, Context context) { 72 super(stateChangedListener, context); 73 mDeadlineExpiredAlarmIntent = 74 PendingIntent.getBroadcast(mContext, 0 /* ignored */, 75 new Intent(ACTION_JOB_EXPIRED), 0); 76 mNextDelayExpiredAlarmIntent = 77 PendingIntent.getBroadcast(mContext, 0 /* ignored */, 78 new Intent(ACTION_JOB_DELAY_EXPIRED), 0); 79 mNextJobExpiredElapsedMillis = Long.MAX_VALUE; 80 mNextDelayExpiredElapsedMillis = Long.MAX_VALUE; 81 82 // Register BR for these intents. 83 IntentFilter intentFilter = new IntentFilter(ACTION_JOB_EXPIRED); 84 intentFilter.addAction(ACTION_JOB_DELAY_EXPIRED); 85 mContext.registerReceiver(mAlarmExpiredReceiver, intentFilter); 86 } 87 88 /** 89 * Check if the job has a timing constraint, and if so determine where to insert it in our 90 * list. 91 */ 92 @Override 93 public synchronized void maybeStartTrackingJob(JobStatus job) { 94 if (job.hasTimingDelayConstraint() || job.hasDeadlineConstraint()) { 95 maybeStopTrackingJob(job); 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 break; 102 } 103 } 104 it.add(job); 105 maybeUpdateAlarms( 106 job.hasTimingDelayConstraint() ? job.getEarliestRunTime() : Long.MAX_VALUE, 107 job.hasDeadlineConstraint() ? job.getLatestRunTimeElapsed() : Long.MAX_VALUE); 108 } 109 } 110 111 /** 112 * When we stop tracking a job, we only need to update our alarms if the job we're no longer 113 * tracking was the one our alarms were based off of. 114 * Really an == comparison should be enough, but why play with fate? We'll do <=. 115 */ 116 @Override 117 public synchronized void maybeStopTrackingJob(JobStatus job) { 118 if (mTrackedJobs.remove(job)) { 119 checkExpiredDelaysAndResetAlarm(); 120 checkExpiredDeadlinesAndResetAlarm(); 121 } 122 } 123 124 /** 125 * Determines whether this controller can stop tracking the given job. 126 * The controller is no longer interested in a job once its time constraint is satisfied, and 127 * the job's deadline is fulfilled - unlike other controllers a time constraint can't toggle 128 * back and forth. 129 */ 130 private boolean canStopTrackingJob(JobStatus job) { 131 return (!job.hasTimingDelayConstraint() || 132 job.timeDelayConstraintSatisfied.get()) && 133 (!job.hasDeadlineConstraint() || 134 job.deadlineConstraintSatisfied.get()); 135 } 136 137 private void ensureAlarmService() { 138 if (mAlarmService == null) { 139 mAlarmService = (AlarmManager) mContext.getSystemService(Context.ALARM_SERVICE); 140 } 141 } 142 143 /** 144 * Checks list of jobs for ones that have an expired deadline, sending them to the JobScheduler 145 * if so, removing them from this list, and updating the alarm for the next expiry time. 146 */ 147 private synchronized void checkExpiredDeadlinesAndResetAlarm() { 148 long nextExpiryTime = Long.MAX_VALUE; 149 final long nowElapsedMillis = SystemClock.elapsedRealtime(); 150 151 Iterator<JobStatus> it = mTrackedJobs.iterator(); 152 while (it.hasNext()) { 153 JobStatus job = it.next(); 154 if (!job.hasDeadlineConstraint()) { 155 continue; 156 } 157 final long jobDeadline = job.getLatestRunTimeElapsed(); 158 159 if (jobDeadline <= nowElapsedMillis) { 160 job.deadlineConstraintSatisfied.set(true); 161 mStateChangedListener.onRunJobNow(job); 162 it.remove(); 163 } else { // Sorted by expiry time, so take the next one and stop. 164 nextExpiryTime = jobDeadline; 165 break; 166 } 167 } 168 setDeadlineExpiredAlarm(nextExpiryTime); 169 } 170 171 /** 172 * Handles alarm that notifies us that a job's delay has expired. Iterates through the list of 173 * tracked jobs and marks them as ready as appropriate. 174 */ 175 private synchronized void checkExpiredDelaysAndResetAlarm() { 176 final long nowElapsedMillis = SystemClock.elapsedRealtime(); 177 long nextDelayTime = Long.MAX_VALUE; 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.timeDelayConstraintSatisfied.set(true); 188 if (canStopTrackingJob(job)) { 189 it.remove(); 190 } 191 if (job.isReady()) { 192 ready = true; 193 } 194 } else { // Keep going through list to get next delay time. 195 if (nextDelayTime > jobDelayTime) { 196 nextDelayTime = jobDelayTime; 197 } 198 } 199 } 200 if (ready) { 201 mStateChangedListener.onControllerStateChanged(); 202 } 203 setDelayExpiredAlarm(nextDelayTime); 204 } 205 206 private void maybeUpdateAlarms(long delayExpiredElapsed, long deadlineExpiredElapsed) { 207 if (delayExpiredElapsed < mNextDelayExpiredElapsedMillis) { 208 setDelayExpiredAlarm(delayExpiredElapsed); 209 } 210 if (deadlineExpiredElapsed < mNextJobExpiredElapsedMillis) { 211 setDeadlineExpiredAlarm(deadlineExpiredElapsed); 212 } 213 } 214 215 /** 216 * Set an alarm with the {@link android.app.AlarmManager} for the next time at which a job's 217 * delay will expire. 218 * This alarm <b>will not</b> wake up the phone. 219 */ 220 private void setDelayExpiredAlarm(long alarmTimeElapsedMillis) { 221 final long earliestWakeupTimeElapsed = 222 SystemClock.elapsedRealtime() + MIN_WAKEUP_INTERVAL_MILLIS; 223 if (alarmTimeElapsedMillis < earliestWakeupTimeElapsed) { 224 alarmTimeElapsedMillis = earliestWakeupTimeElapsed; 225 } 226 mNextDelayExpiredElapsedMillis = alarmTimeElapsedMillis; 227 updateAlarmWithPendingIntent(mNextDelayExpiredAlarmIntent, mNextDelayExpiredElapsedMillis); 228 } 229 230 /** 231 * Set an alarm with the {@link android.app.AlarmManager} for the next time at which a job's 232 * deadline will expire. 233 * This alarm <b>will</b> wake up the phone. 234 */ 235 private void setDeadlineExpiredAlarm(long alarmTimeElapsedMillis) { 236 final long earliestWakeupTimeElapsed = 237 SystemClock.elapsedRealtime() + MIN_WAKEUP_INTERVAL_MILLIS; 238 if (alarmTimeElapsedMillis < earliestWakeupTimeElapsed) { 239 alarmTimeElapsedMillis = earliestWakeupTimeElapsed; 240 } 241 mNextJobExpiredElapsedMillis = alarmTimeElapsedMillis; 242 updateAlarmWithPendingIntent(mDeadlineExpiredAlarmIntent, mNextJobExpiredElapsedMillis); 243 } 244 245 private void updateAlarmWithPendingIntent(PendingIntent pi, long alarmTimeElapsed) { 246 ensureAlarmService(); 247 if (alarmTimeElapsed == Long.MAX_VALUE) { 248 mAlarmService.cancel(pi); 249 } else { 250 if (DEBUG) { 251 Slog.d(TAG, "Setting " + pi.getIntent().getAction() + " for: " + alarmTimeElapsed); 252 } 253 mAlarmService.set(AlarmManager.ELAPSED_REALTIME, alarmTimeElapsed, pi); 254 } 255 } 256 257 private final BroadcastReceiver mAlarmExpiredReceiver = new BroadcastReceiver() { 258 @Override 259 public void onReceive(Context context, Intent intent) { 260 if (DEBUG) { 261 Slog.d(TAG, "Just received alarm: " + intent.getAction()); 262 } 263 // A job has just expired, so we run through the list of jobs that we have and 264 // notify our StateChangedListener. 265 if (ACTION_JOB_EXPIRED.equals(intent.getAction())) { 266 checkExpiredDeadlinesAndResetAlarm(); 267 } else if (ACTION_JOB_DELAY_EXPIRED.equals(intent.getAction())) { 268 checkExpiredDelaysAndResetAlarm(); 269 } 270 } 271 }; 272 273 @Override 274 public void dumpControllerState(PrintWriter pw) { 275 final long nowElapsed = SystemClock.elapsedRealtime(); 276 pw.println("Alarms (" + SystemClock.elapsedRealtime() + ")"); 277 pw.println( 278 "Next delay alarm in " + (mNextDelayExpiredElapsedMillis - nowElapsed)/1000 + "s"); 279 pw.println("Next deadline alarm in " + (mNextJobExpiredElapsedMillis - nowElapsed)/1000 280 + "s"); 281 pw.println("Tracking:"); 282 for (JobStatus ts : mTrackedJobs) { 283 pw.println(String.valueOf(ts.hashCode()).substring(0, 3) + ".." 284 + ": (" + (ts.hasTimingDelayConstraint() ? ts.getEarliestRunTime() : "N/A") 285 + ", " + (ts.hasDeadlineConstraint() ?ts.getLatestRunTimeElapsed() : "N/A") 286 + ")"); 287 } 288 } 289}