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}