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.PendingIntent; 21 import android.content.BroadcastReceiver; 22 import android.content.Context; 23 import android.content.Intent; 24 import android.content.IntentFilter; 25 import android.os.SystemClock; 26 import android.util.Slog; 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 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 boolean isInsert = false; 95 ListIterator<JobStatus> it = mTrackedJobs.listIterator(mTrackedJobs.size()); 96 while (it.hasPrevious()) { 97 JobStatus ts = it.previous(); 98 if (ts.getLatestRunTimeElapsed() < job.getLatestRunTimeElapsed()) { 99 // Insert 100 isInsert = true; 101 break; 102 } 103 } 104 if(isInsert) 105 { 106 it.next(); 107 } 108 it.add(job); 109 maybeUpdateAlarms( 110 job.hasTimingDelayConstraint() ? job.getEarliestRunTime() : Long.MAX_VALUE, 111 job.hasDeadlineConstraint() ? job.getLatestRunTimeElapsed() : Long.MAX_VALUE); 112 } 113 } 114 115 /** 116 * When we stop tracking a job, we only need to update our alarms if the job we're no longer 117 * tracking was the one our alarms were based off of. 118 * Really an == comparison should be enough, but why play with fate? We'll do <=. 119 */ 120 @Override 121 public synchronized void maybeStopTrackingJob(JobStatus job) { 122 if (mTrackedJobs.remove(job)) { 123 checkExpiredDelaysAndResetAlarm(); 124 checkExpiredDeadlinesAndResetAlarm(); 125 } 126 } 127 128 /** 129 * Determines whether this controller can stop tracking the given job. 130 * The controller is no longer interested in a job once its time constraint is satisfied, and 131 * the job's deadline is fulfilled - unlike other controllers a time constraint can't toggle 132 * back and forth. 133 */ 134 private boolean canStopTrackingJob(JobStatus job) { 135 return (!job.hasTimingDelayConstraint() || 136 job.timeDelayConstraintSatisfied.get()) && 137 (!job.hasDeadlineConstraint() || 138 job.deadlineConstraintSatisfied.get()); 139 } 140 141 private void ensureAlarmService() { 142 if (mAlarmService == null) { 143 mAlarmService = (AlarmManager) mContext.getSystemService(Context.ALARM_SERVICE); 144 } 145 } 146 147 /** 148 * Checks list of jobs for ones that have an expired deadline, sending them to the JobScheduler 149 * if so, removing them from this list, and updating the alarm for the next expiry time. 150 */ 151 private synchronized void checkExpiredDeadlinesAndResetAlarm() { 152 long nextExpiryTime = Long.MAX_VALUE; 153 final long nowElapsedMillis = SystemClock.elapsedRealtime(); 154 155 Iterator<JobStatus> it = mTrackedJobs.iterator(); 156 while (it.hasNext()) { 157 JobStatus job = it.next(); 158 if (!job.hasDeadlineConstraint()) { 159 continue; 160 } 161 final long jobDeadline = job.getLatestRunTimeElapsed(); 162 163 if (jobDeadline <= nowElapsedMillis) { 164 job.deadlineConstraintSatisfied.set(true); 165 mStateChangedListener.onRunJobNow(job); 166 it.remove(); 167 } else { // Sorted by expiry time, so take the next one and stop. 168 nextExpiryTime = jobDeadline; 169 break; 170 } 171 } 172 setDeadlineExpiredAlarm(nextExpiryTime); 173 } 174 175 /** 176 * Handles alarm that notifies us that a job's delay has expired. Iterates through the list of 177 * tracked jobs and marks them as ready as appropriate. 178 */ 179 private synchronized void checkExpiredDelaysAndResetAlarm() { 180 final long nowElapsedMillis = SystemClock.elapsedRealtime(); 181 long nextDelayTime = Long.MAX_VALUE; 182 boolean ready = false; 183 Iterator<JobStatus> it = mTrackedJobs.iterator(); 184 while (it.hasNext()) { 185 final JobStatus job = it.next(); 186 if (!job.hasTimingDelayConstraint()) { 187 continue; 188 } 189 final long jobDelayTime = job.getEarliestRunTime(); 190 if (jobDelayTime <= nowElapsedMillis) { 191 job.timeDelayConstraintSatisfied.set(true); 192 if (canStopTrackingJob(job)) { 193 it.remove(); 194 } 195 if (job.isReady()) { 196 ready = true; 197 } 198 } else { // Keep going through list to get next delay time. 199 if (nextDelayTime > jobDelayTime) { 200 nextDelayTime = jobDelayTime; 201 } 202 } 203 } 204 if (ready) { 205 mStateChangedListener.onControllerStateChanged(); 206 } 207 setDelayExpiredAlarm(nextDelayTime); 208 } 209 210 private void maybeUpdateAlarms(long delayExpiredElapsed, long deadlineExpiredElapsed) { 211 if (delayExpiredElapsed < mNextDelayExpiredElapsedMillis) { 212 setDelayExpiredAlarm(delayExpiredElapsed); 213 } 214 if (deadlineExpiredElapsed < mNextJobExpiredElapsedMillis) { 215 setDeadlineExpiredAlarm(deadlineExpiredElapsed); 216 } 217 } 218 219 /** 220 * Set an alarm with the {@link android.app.AlarmManager} for the next time at which a job's 221 * delay will expire. 222 * This alarm <b>will not</b> wake up the phone. 223 */ 224 private void setDelayExpiredAlarm(long alarmTimeElapsedMillis) { 225 alarmTimeElapsedMillis = maybeAdjustAlarmTime(alarmTimeElapsedMillis); 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 alarmTimeElapsedMillis = maybeAdjustAlarmTime(alarmTimeElapsedMillis); 237 mNextJobExpiredElapsedMillis = alarmTimeElapsedMillis; 238 updateAlarmWithPendingIntent(mDeadlineExpiredAlarmIntent, mNextJobExpiredElapsedMillis); 239 } 240 241 private long maybeAdjustAlarmTime(long proposedAlarmTimeElapsedMillis) { 242 final long earliestWakeupTimeElapsed = SystemClock.elapsedRealtime(); 243 if (proposedAlarmTimeElapsedMillis < earliestWakeupTimeElapsed) { 244 return earliestWakeupTimeElapsed; 245 } 246 return proposedAlarmTimeElapsedMillis; 247 } 248 249 private void updateAlarmWithPendingIntent(PendingIntent pi, long alarmTimeElapsed) { 250 ensureAlarmService(); 251 if (alarmTimeElapsed == Long.MAX_VALUE) { 252 mAlarmService.cancel(pi); 253 } else { 254 if (DEBUG) { 255 Slog.d(TAG, "Setting " + pi.getIntent().getAction() + " for: " + alarmTimeElapsed); 256 } 257 mAlarmService.set(AlarmManager.ELAPSED_REALTIME, alarmTimeElapsed, pi); 258 } 259 } 260 261 private final BroadcastReceiver mAlarmExpiredReceiver = new BroadcastReceiver() { 262 @Override 263 public void onReceive(Context context, Intent intent) { 264 if (DEBUG) { 265 Slog.d(TAG, "Just received alarm: " + intent.getAction()); 266 } 267 // A job has just expired, so we run through the list of jobs that we have and 268 // notify our StateChangedListener. 269 if (ACTION_JOB_EXPIRED.equals(intent.getAction())) { 270 checkExpiredDeadlinesAndResetAlarm(); 271 } else if (ACTION_JOB_DELAY_EXPIRED.equals(intent.getAction())) { 272 checkExpiredDelaysAndResetAlarm(); 273 } 274 } 275 }; 276 277 @Override 278 public void dumpControllerState(PrintWriter pw) { 279 final long nowElapsed = SystemClock.elapsedRealtime(); 280 pw.println("Alarms (" + SystemClock.elapsedRealtime() + ")"); 281 pw.println( 282 "Next delay alarm in " + (mNextDelayExpiredElapsedMillis - nowElapsed)/1000 + "s"); 283 pw.println("Next deadline alarm in " + (mNextJobExpiredElapsedMillis - nowElapsed)/1000 284 + "s"); 285 pw.println("Tracking:"); 286 for (JobStatus ts : mTrackedJobs) { 287 pw.println(String.valueOf(ts.hashCode()).substring(0, 3) + ".." 288 + ": (" + (ts.hasTimingDelayConstraint() ? ts.getEarliestRunTime() : "N/A") 289 + ", " + (ts.hasDeadlineConstraint() ?ts.getLatestRunTimeElapsed() : "N/A") 290 + ")"); 291 } 292 } 293 }