TimeController.java revision e9a988caca733d2f292991a52a0047685a69812f
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.AlarmManager.OnAlarmListener;
21import android.content.Context;
22import android.os.SystemClock;
23import android.os.UserHandle;
24import android.os.WorkSource;
25import android.util.Slog;
26import android.util.TimeUtils;
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
44    /** Deadline alarm tag for logging purposes */
45    private final String DEADLINE_TAG = "*job.deadline*";
46    /** Delay alarm tag for logging purposes */
47    private final String DELAY_TAG = "*job.delay*";
48
49    private long mNextJobExpiredElapsedMillis;
50    private long mNextDelayExpiredElapsedMillis;
51
52    private AlarmManager mAlarmService = null;
53    /** List of tracked jobs, sorted asc. by deadline */
54    private final List<JobStatus> mTrackedJobs = new LinkedList<JobStatus>();
55    /** Singleton. */
56    private static TimeController mSingleton;
57
58    public static synchronized TimeController get(JobSchedulerService jms) {
59        if (mSingleton == null) {
60            mSingleton = new TimeController(jms, jms.getContext(), jms.getLock());
61        }
62        return mSingleton;
63    }
64
65    private TimeController(StateChangedListener stateChangedListener, Context context,
66                Object lock) {
67        super(stateChangedListener, context, lock);
68
69        mNextJobExpiredElapsedMillis = Long.MAX_VALUE;
70        mNextDelayExpiredElapsedMillis = Long.MAX_VALUE;
71    }
72
73    /**
74     * Check if the job has a timing constraint, and if so determine where to insert it in our
75     * list.
76     */
77    @Override
78    public void maybeStartTrackingJobLocked(JobStatus job, JobStatus lastJob) {
79        if (job.hasTimingDelayConstraint() || job.hasDeadlineConstraint()) {
80            maybeStopTrackingJobLocked(job, null, false);
81            boolean isInsert = false;
82            ListIterator<JobStatus> it = mTrackedJobs.listIterator(mTrackedJobs.size());
83            while (it.hasPrevious()) {
84                JobStatus ts = it.previous();
85                if (ts.getLatestRunTimeElapsed() < job.getLatestRunTimeElapsed()) {
86                    // Insert
87                    isInsert = true;
88                    break;
89                }
90            }
91            if (isInsert) {
92                it.next();
93            }
94            it.add(job);
95            maybeUpdateAlarmsLocked(
96                    job.hasTimingDelayConstraint() ? job.getEarliestRunTime() : Long.MAX_VALUE,
97                    job.hasDeadlineConstraint() ? job.getLatestRunTimeElapsed() : Long.MAX_VALUE,
98                    job.getSourceUid());
99        }
100    }
101
102    /**
103     * When we stop tracking a job, we only need to update our alarms if the job we're no longer
104     * tracking was the one our alarms were based off of.
105     * Really an == comparison should be enough, but why play with fate? We'll do <=.
106     */
107    @Override
108    public void maybeStopTrackingJobLocked(JobStatus job, JobStatus incomingJob, boolean forUpdate) {
109        if (mTrackedJobs.remove(job)) {
110            checkExpiredDelaysAndResetAlarm();
111            checkExpiredDeadlinesAndResetAlarm();
112        }
113    }
114
115    /**
116     * Determines whether this controller can stop tracking the given job.
117     * The controller is no longer interested in a job once its time constraint is satisfied, and
118     * the job's deadline is fulfilled - unlike other controllers a time constraint can't toggle
119     * back and forth.
120     */
121    private boolean canStopTrackingJobLocked(JobStatus job) {
122        return (!job.hasTimingDelayConstraint() ||
123                (job.satisfiedConstraints&JobStatus.CONSTRAINT_TIMING_DELAY) != 0) &&
124                (!job.hasDeadlineConstraint() ||
125                        (job.satisfiedConstraints&JobStatus.CONSTRAINT_DEADLINE) != 0);
126    }
127
128    private void ensureAlarmServiceLocked() {
129        if (mAlarmService == null) {
130            mAlarmService = (AlarmManager) mContext.getSystemService(Context.ALARM_SERVICE);
131        }
132    }
133
134    /**
135     * Checks list of jobs for ones that have an expired deadline, sending them to the JobScheduler
136     * if so, removing them from this list, and updating the alarm for the next expiry time.
137     */
138    private void checkExpiredDeadlinesAndResetAlarm() {
139        synchronized (mLock) {
140            long nextExpiryTime = Long.MAX_VALUE;
141            int nextExpiryUid = 0;
142            final long nowElapsedMillis = SystemClock.elapsedRealtime();
143
144            Iterator<JobStatus> it = mTrackedJobs.iterator();
145            while (it.hasNext()) {
146                JobStatus job = it.next();
147                if (!job.hasDeadlineConstraint()) {
148                    continue;
149                }
150                final long jobDeadline = job.getLatestRunTimeElapsed();
151
152                if (jobDeadline <= nowElapsedMillis) {
153                    if (job.hasTimingDelayConstraint()) {
154                        job.setTimingDelayConstraintSatisfied(true);
155                    }
156                    job.setDeadlineConstraintSatisfied(true);
157                    mStateChangedListener.onRunJobNow(job);
158                    it.remove();
159                } else {  // Sorted by expiry time, so take the next one and stop.
160                    nextExpiryTime = jobDeadline;
161                    nextExpiryUid = job.getSourceUid();
162                    break;
163                }
164            }
165            setDeadlineExpiredAlarmLocked(nextExpiryTime, nextExpiryUid);
166        }
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 void checkExpiredDelaysAndResetAlarm() {
174        synchronized (mLock) {
175            final long nowElapsedMillis = SystemClock.elapsedRealtime();
176            long nextDelayTime = Long.MAX_VALUE;
177            int nextDelayUid = 0;
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.setTimingDelayConstraintSatisfied(true);
188                    if (canStopTrackingJobLocked(job)) {
189                        it.remove();
190                    }
191                    if (job.isReady()) {
192                        ready = true;
193                    }
194                } else if (!job.isConstraintSatisfied(JobStatus.CONSTRAINT_TIMING_DELAY)) {
195                    // If this job still doesn't have its delay constraint satisfied,
196                    // then see if it is the next upcoming delay time for the alarm.
197                    if (nextDelayTime > jobDelayTime) {
198                        nextDelayTime = jobDelayTime;
199                        nextDelayUid = job.getSourceUid();
200                    }
201                }
202            }
203            if (ready) {
204                mStateChangedListener.onControllerStateChanged();
205            }
206            setDelayExpiredAlarmLocked(nextDelayTime, nextDelayUid);
207        }
208    }
209
210    private void maybeUpdateAlarmsLocked(long delayExpiredElapsed, long deadlineExpiredElapsed,
211            int uid) {
212        if (delayExpiredElapsed < mNextDelayExpiredElapsedMillis) {
213            setDelayExpiredAlarmLocked(delayExpiredElapsed, uid);
214        }
215        if (deadlineExpiredElapsed < mNextJobExpiredElapsedMillis) {
216            setDeadlineExpiredAlarmLocked(deadlineExpiredElapsed, uid);
217        }
218    }
219
220    /**
221     * Set an alarm with the {@link android.app.AlarmManager} for the next time at which a job's
222     * delay will expire.
223     * This alarm <b>will</b> wake up the phone.
224     */
225    private void setDelayExpiredAlarmLocked(long alarmTimeElapsedMillis, int uid) {
226        alarmTimeElapsedMillis = maybeAdjustAlarmTime(alarmTimeElapsedMillis);
227        mNextDelayExpiredElapsedMillis = alarmTimeElapsedMillis;
228        updateAlarmWithListenerLocked(DELAY_TAG, mNextDelayExpiredListener,
229                mNextDelayExpiredElapsedMillis, uid);
230    }
231
232    /**
233     * Set an alarm with the {@link android.app.AlarmManager} for the next time at which a job's
234     * deadline will expire.
235     * This alarm <b>will</b> wake up the phone.
236     */
237    private void setDeadlineExpiredAlarmLocked(long alarmTimeElapsedMillis, int uid) {
238        alarmTimeElapsedMillis = maybeAdjustAlarmTime(alarmTimeElapsedMillis);
239        mNextJobExpiredElapsedMillis = alarmTimeElapsedMillis;
240        updateAlarmWithListenerLocked(DEADLINE_TAG, mDeadlineExpiredListener,
241                mNextJobExpiredElapsedMillis, uid);
242    }
243
244    private long maybeAdjustAlarmTime(long proposedAlarmTimeElapsedMillis) {
245        final long earliestWakeupTimeElapsed = SystemClock.elapsedRealtime();
246        if (proposedAlarmTimeElapsedMillis < earliestWakeupTimeElapsed) {
247            return earliestWakeupTimeElapsed;
248        }
249        return proposedAlarmTimeElapsedMillis;
250    }
251
252    private void updateAlarmWithListenerLocked(String tag, OnAlarmListener listener,
253            long alarmTimeElapsed, int uid) {
254        ensureAlarmServiceLocked();
255        if (alarmTimeElapsed == Long.MAX_VALUE) {
256            mAlarmService.cancel(listener);
257        } else {
258            if (DEBUG) {
259                Slog.d(TAG, "Setting " + tag + " for: " + alarmTimeElapsed);
260            }
261            mAlarmService.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, alarmTimeElapsed,
262                    AlarmManager.WINDOW_HEURISTIC, 0, tag, listener, null, new WorkSource(uid));
263        }
264    }
265
266    // Job/delay expiration alarm handling
267
268    private final OnAlarmListener mDeadlineExpiredListener = new OnAlarmListener() {
269        @Override
270        public void onAlarm() {
271            if (DEBUG) {
272                Slog.d(TAG, "Deadline-expired alarm fired");
273            }
274            checkExpiredDeadlinesAndResetAlarm();
275        }
276    };
277
278    private final OnAlarmListener mNextDelayExpiredListener = new OnAlarmListener() {
279        @Override
280        public void onAlarm() {
281            if (DEBUG) {
282                Slog.d(TAG, "Delay-expired alarm fired");
283            }
284            checkExpiredDelaysAndResetAlarm();
285        }
286    };
287
288    @Override
289    public void dumpControllerStateLocked(PrintWriter pw, int filterUid) {
290        final long nowElapsed = SystemClock.elapsedRealtime();
291        pw.print("Alarms: now=");
292        pw.print(SystemClock.elapsedRealtime());
293        pw.println();
294        pw.print("Next delay alarm in ");
295        TimeUtils.formatDuration(mNextDelayExpiredElapsedMillis, nowElapsed, pw);
296        pw.println();
297        pw.print("Next deadline alarm in ");
298        TimeUtils.formatDuration(mNextJobExpiredElapsedMillis, nowElapsed, pw);
299        pw.println();
300        pw.print("Tracking ");
301        pw.print(mTrackedJobs.size());
302        pw.println(":");
303        for (JobStatus ts : mTrackedJobs) {
304            if (!ts.shouldDump(filterUid)) {
305                continue;
306            }
307            pw.print("  #");
308            ts.printUniqueId(pw);
309            pw.print(" from ");
310            UserHandle.formatUid(pw, ts.getSourceUid());
311            pw.print(": Delay=");
312            if (ts.hasTimingDelayConstraint()) {
313                TimeUtils.formatDuration(ts.getEarliestRunTime(), nowElapsed, pw);
314            } else {
315                pw.print("N/A");
316            }
317            pw.print(", Deadline=");
318            if (ts.hasDeadlineConstraint()) {
319                TimeUtils.formatDuration(ts.getLatestRunTimeElapsed(), nowElapsed, pw);
320            } else {
321                pw.print("N/A");
322            }
323            pw.println();
324        }
325    }
326}