2 * Copyright (C) 2014 The Android Open Source Project
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
8 * http://www.apache.org/licenses/LICENSE-2.0
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
17 package com.android.server.job.controllers;
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;
28 import com.android.server.job.JobSchedulerService;
29 import com.android.server.job.StateChangedListener;
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;
38 * This class sets an alarm for the next expiring job, and determines whether a job's minimum
39 * delay has been satisfied.
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";
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;
53 private long mNextJobExpiredElapsedMillis;
54 private long mNextDelayExpiredElapsedMillis;
56 private AlarmManager mAlarmService = null;
57 /** List of tracked jobs, sorted asc. by deadline */
58 private final List<JobStatus> mTrackedJobs = new LinkedList<JobStatus>();
60 private static TimeController mSingleton;
62 public static synchronized TimeController get(JobSchedulerService jms) {
63 if (mSingleton == null) {
64 mSingleton = new TimeController(jms, jms.getContext());
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;
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);
87 * Check if the job has a timing constraint, and if so determine where to insert it in our
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()) {
110 job.hasTimingDelayConstraint() ? job.getEarliestRunTime() : Long.MAX_VALUE,
111 job.hasDeadlineConstraint() ? job.getLatestRunTimeElapsed() : Long.MAX_VALUE);
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 <=.
121 public synchronized void maybeStopTrackingJob(JobStatus job) {
122 if (mTrackedJobs.remove(job)) {
123 checkExpiredDelaysAndResetAlarm();
124 checkExpiredDeadlinesAndResetAlarm();
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
134 private boolean canStopTrackingJob(JobStatus job) {
135 return (!job.hasTimingDelayConstraint() ||
136 job.timeDelayConstraintSatisfied.get()) &&
137 (!job.hasDeadlineConstraint() ||
138 job.deadlineConstraintSatisfied.get());
141 private void ensureAlarmService() {
142 if (mAlarmService == null) {
143 mAlarmService = (AlarmManager) mContext.getSystemService(Context.ALARM_SERVICE);
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.
151 private synchronized void checkExpiredDeadlinesAndResetAlarm() {
152 long nextExpiryTime = Long.MAX_VALUE;
153 final long nowElapsedMillis = SystemClock.elapsedRealtime();
155 Iterator<JobStatus> it = mTrackedJobs.iterator();
156 while (it.hasNext()) {
157 JobStatus job = it.next();
158 if (!job.hasDeadlineConstraint()) {
161 final long jobDeadline = job.getLatestRunTimeElapsed();
163 if (jobDeadline <= nowElapsedMillis) {
164 job.deadlineConstraintSatisfied.set(true);
165 mStateChangedListener.onRunJobNow(job);
167 } else { // Sorted by expiry time, so take the next one and stop.
168 nextExpiryTime = jobDeadline;
172 setDeadlineExpiredAlarm(nextExpiryTime);
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.
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()) {
189 final long jobDelayTime = job.getEarliestRunTime();
190 if (jobDelayTime <= nowElapsedMillis) {
191 job.timeDelayConstraintSatisfied.set(true);
192 if (canStopTrackingJob(job)) {
198 } else { // Keep going through list to get next delay time.
199 if (nextDelayTime > jobDelayTime) {
200 nextDelayTime = jobDelayTime;
205 mStateChangedListener.onControllerStateChanged();
207 setDelayExpiredAlarm(nextDelayTime);
210 private void maybeUpdateAlarms(long delayExpiredElapsed, long deadlineExpiredElapsed) {
211 if (delayExpiredElapsed < mNextDelayExpiredElapsedMillis) {
212 setDelayExpiredAlarm(delayExpiredElapsed);
214 if (deadlineExpiredElapsed < mNextJobExpiredElapsedMillis) {
215 setDeadlineExpiredAlarm(deadlineExpiredElapsed);
220 * Set an alarm with the {@link android.app.AlarmManager} for the next time at which a job's
222 * This alarm <b>will not</b> wake up the phone.
224 private void setDelayExpiredAlarm(long alarmTimeElapsedMillis) {
225 alarmTimeElapsedMillis = maybeAdjustAlarmTime(alarmTimeElapsedMillis);
226 mNextDelayExpiredElapsedMillis = alarmTimeElapsedMillis;
227 updateAlarmWithPendingIntent(mNextDelayExpiredAlarmIntent, mNextDelayExpiredElapsedMillis);
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.
235 private void setDeadlineExpiredAlarm(long alarmTimeElapsedMillis) {
236 alarmTimeElapsedMillis = maybeAdjustAlarmTime(alarmTimeElapsedMillis);
237 mNextJobExpiredElapsedMillis = alarmTimeElapsedMillis;
238 updateAlarmWithPendingIntent(mDeadlineExpiredAlarmIntent, mNextJobExpiredElapsedMillis);
241 private long maybeAdjustAlarmTime(long proposedAlarmTimeElapsedMillis) {
242 final long earliestWakeupTimeElapsed = SystemClock.elapsedRealtime();
243 if (proposedAlarmTimeElapsedMillis < earliestWakeupTimeElapsed) {
244 return earliestWakeupTimeElapsed;
246 return proposedAlarmTimeElapsedMillis;
249 private void updateAlarmWithPendingIntent(PendingIntent pi, long alarmTimeElapsed) {
250 ensureAlarmService();
251 if (alarmTimeElapsed == Long.MAX_VALUE) {
252 mAlarmService.cancel(pi);
255 Slog.d(TAG, "Setting " + pi.getIntent().getAction() + " for: " + alarmTimeElapsed);
257 mAlarmService.set(AlarmManager.ELAPSED_REALTIME, alarmTimeElapsed, pi);
261 private final BroadcastReceiver mAlarmExpiredReceiver = new BroadcastReceiver() {
263 public void onReceive(Context context, Intent intent) {
265 Slog.d(TAG, "Just received alarm: " + intent.getAction());
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();
278 public void dumpControllerState(PrintWriter pw) {
279 final long nowElapsed = SystemClock.elapsedRealtime();
280 pw.println("Alarms (" + SystemClock.elapsedRealtime() + ")");
282 "Next delay alarm in " + (mNextDelayExpiredElapsedMillis - nowElapsed)/1000 + "s");
283 pw.println("Next deadline alarm in " + (mNextJobExpiredElapsedMillis - nowElapsed)/1000
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")