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}