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