blob: 0b3b00fa84aa9cc1af90a4ddfcca0e284a792d0c [file] [log] [blame]
/*
* Copyright (C) 2014 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License
*/
package com.android.server.job.controllers;
import android.app.AlarmManager;
import android.app.AlarmManager.OnAlarmListener;
import android.content.Context;
import android.os.SystemClock;
import android.os.UserHandle;
import android.os.WorkSource;
import android.util.Slog;
import android.util.TimeUtils;
import com.android.server.job.JobSchedulerService;
import com.android.server.job.StateChangedListener;
import java.io.PrintWriter;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.ListIterator;
/**
* This class sets an alarm for the next expiring job, and determines whether a job's minimum
* delay has been satisfied.
*/
public class TimeController extends StateController {
private static final String TAG = "JobScheduler.Time";
/** Deadline alarm tag for logging purposes */
private final String DEADLINE_TAG = "*job.deadline*";
/** Delay alarm tag for logging purposes */
private final String DELAY_TAG = "*job.delay*";
private long mNextJobExpiredElapsedMillis;
private long mNextDelayExpiredElapsedMillis;
private AlarmManager mAlarmService = null;
/** List of tracked jobs, sorted asc. by deadline */
private final List<JobStatus> mTrackedJobs = new LinkedList<JobStatus>();
/** Singleton. */
private static TimeController mSingleton;
public static synchronized TimeController get(JobSchedulerService jms) {
if (mSingleton == null) {
mSingleton = new TimeController(jms, jms.getContext(), jms.getLock());
}
return mSingleton;
}
private TimeController(StateChangedListener stateChangedListener, Context context,
Object lock) {
super(stateChangedListener, context, lock);
mNextJobExpiredElapsedMillis = Long.MAX_VALUE;
mNextDelayExpiredElapsedMillis = Long.MAX_VALUE;
}
/**
* Check if the job has a timing constraint, and if so determine where to insert it in our
* list.
*/
@Override
public void maybeStartTrackingJobLocked(JobStatus job, JobStatus lastJob) {
if (job.hasTimingDelayConstraint() || job.hasDeadlineConstraint()) {
maybeStopTrackingJobLocked(job, null, false);
boolean isInsert = false;
ListIterator<JobStatus> it = mTrackedJobs.listIterator(mTrackedJobs.size());
while (it.hasPrevious()) {
JobStatus ts = it.previous();
if (ts.getLatestRunTimeElapsed() < job.getLatestRunTimeElapsed()) {
// Insert
isInsert = true;
break;
}
}
if (isInsert) {
it.next();
}
it.add(job);
maybeUpdateAlarmsLocked(
job.hasTimingDelayConstraint() ? job.getEarliestRunTime() : Long.MAX_VALUE,
job.hasDeadlineConstraint() ? job.getLatestRunTimeElapsed() : Long.MAX_VALUE,
job.getSourceUid());
}
}
/**
* When we stop tracking a job, we only need to update our alarms if the job we're no longer
* tracking was the one our alarms were based off of.
* Really an == comparison should be enough, but why play with fate? We'll do <=.
*/
@Override
public void maybeStopTrackingJobLocked(JobStatus job, JobStatus incomingJob, boolean forUpdate) {
if (mTrackedJobs.remove(job)) {
checkExpiredDelaysAndResetAlarm();
checkExpiredDeadlinesAndResetAlarm();
}
}
/**
* Determines whether this controller can stop tracking the given job.
* The controller is no longer interested in a job once its time constraint is satisfied, and
* the job's deadline is fulfilled - unlike other controllers a time constraint can't toggle
* back and forth.
*/
private boolean canStopTrackingJobLocked(JobStatus job) {
return (!job.hasTimingDelayConstraint() ||
(job.satisfiedConstraints&JobStatus.CONSTRAINT_TIMING_DELAY) != 0) &&
(!job.hasDeadlineConstraint() ||
(job.satisfiedConstraints&JobStatus.CONSTRAINT_DEADLINE) != 0);
}
private void ensureAlarmServiceLocked() {
if (mAlarmService == null) {
mAlarmService = (AlarmManager) mContext.getSystemService(Context.ALARM_SERVICE);
}
}
/**
* Checks list of jobs for ones that have an expired deadline, sending them to the JobScheduler
* if so, removing them from this list, and updating the alarm for the next expiry time.
*/
private void checkExpiredDeadlinesAndResetAlarm() {
synchronized (mLock) {
long nextExpiryTime = Long.MAX_VALUE;
int nextExpiryUid = 0;
final long nowElapsedMillis = SystemClock.elapsedRealtime();
Iterator<JobStatus> it = mTrackedJobs.iterator();
while (it.hasNext()) {
JobStatus job = it.next();
if (!job.hasDeadlineConstraint()) {
continue;
}
final long jobDeadline = job.getLatestRunTimeElapsed();
if (jobDeadline <= nowElapsedMillis) {
if (job.hasTimingDelayConstraint()) {
job.setTimingDelayConstraintSatisfied(true);
}
job.setDeadlineConstraintSatisfied(true);
mStateChangedListener.onRunJobNow(job);
it.remove();
} else { // Sorted by expiry time, so take the next one and stop.
nextExpiryTime = jobDeadline;
nextExpiryUid = job.getSourceUid();
break;
}
}
setDeadlineExpiredAlarmLocked(nextExpiryTime, nextExpiryUid);
}
}
/**
* Handles alarm that notifies us that a job's delay has expired. Iterates through the list of
* tracked jobs and marks them as ready as appropriate.
*/
private void checkExpiredDelaysAndResetAlarm() {
synchronized (mLock) {
final long nowElapsedMillis = SystemClock.elapsedRealtime();
long nextDelayTime = Long.MAX_VALUE;
int nextDelayUid = 0;
boolean ready = false;
Iterator<JobStatus> it = mTrackedJobs.iterator();
while (it.hasNext()) {
final JobStatus job = it.next();
if (!job.hasTimingDelayConstraint()) {
continue;
}
final long jobDelayTime = job.getEarliestRunTime();
if (jobDelayTime <= nowElapsedMillis) {
job.setTimingDelayConstraintSatisfied(true);
if (canStopTrackingJobLocked(job)) {
it.remove();
}
if (job.isReady()) {
ready = true;
}
} else if (!job.isConstraintSatisfied(JobStatus.CONSTRAINT_TIMING_DELAY)) {
// If this job still doesn't have its delay constraint satisfied,
// then see if it is the next upcoming delay time for the alarm.
if (nextDelayTime > jobDelayTime) {
nextDelayTime = jobDelayTime;
nextDelayUid = job.getSourceUid();
}
}
}
if (ready) {
mStateChangedListener.onControllerStateChanged();
}
setDelayExpiredAlarmLocked(nextDelayTime, nextDelayUid);
}
}
private void maybeUpdateAlarmsLocked(long delayExpiredElapsed, long deadlineExpiredElapsed,
int uid) {
if (delayExpiredElapsed < mNextDelayExpiredElapsedMillis) {
setDelayExpiredAlarmLocked(delayExpiredElapsed, uid);
}
if (deadlineExpiredElapsed < mNextJobExpiredElapsedMillis) {
setDeadlineExpiredAlarmLocked(deadlineExpiredElapsed, uid);
}
}
/**
* Set an alarm with the {@link android.app.AlarmManager} for the next time at which a job's
* delay will expire.
* This alarm <b>will</b> wake up the phone.
*/
private void setDelayExpiredAlarmLocked(long alarmTimeElapsedMillis, int uid) {
alarmTimeElapsedMillis = maybeAdjustAlarmTime(alarmTimeElapsedMillis);
mNextDelayExpiredElapsedMillis = alarmTimeElapsedMillis;
updateAlarmWithListenerLocked(DELAY_TAG, mNextDelayExpiredListener,
mNextDelayExpiredElapsedMillis, uid);
}
/**
* Set an alarm with the {@link android.app.AlarmManager} for the next time at which a job's
* deadline will expire.
* This alarm <b>will</b> wake up the phone.
*/
private void setDeadlineExpiredAlarmLocked(long alarmTimeElapsedMillis, int uid) {
alarmTimeElapsedMillis = maybeAdjustAlarmTime(alarmTimeElapsedMillis);
mNextJobExpiredElapsedMillis = alarmTimeElapsedMillis;
updateAlarmWithListenerLocked(DEADLINE_TAG, mDeadlineExpiredListener,
mNextJobExpiredElapsedMillis, uid);
}
private long maybeAdjustAlarmTime(long proposedAlarmTimeElapsedMillis) {
final long earliestWakeupTimeElapsed = SystemClock.elapsedRealtime();
if (proposedAlarmTimeElapsedMillis < earliestWakeupTimeElapsed) {
return earliestWakeupTimeElapsed;
}
return proposedAlarmTimeElapsedMillis;
}
private void updateAlarmWithListenerLocked(String tag, OnAlarmListener listener,
long alarmTimeElapsed, int uid) {
ensureAlarmServiceLocked();
if (alarmTimeElapsed == Long.MAX_VALUE) {
mAlarmService.cancel(listener);
} else {
if (DEBUG) {
Slog.d(TAG, "Setting " + tag + " for: " + alarmTimeElapsed);
}
mAlarmService.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, alarmTimeElapsed,
AlarmManager.WINDOW_HEURISTIC, 0, tag, listener, null, new WorkSource(uid));
}
}
// Job/delay expiration alarm handling
private final OnAlarmListener mDeadlineExpiredListener = new OnAlarmListener() {
@Override
public void onAlarm() {
if (DEBUG) {
Slog.d(TAG, "Deadline-expired alarm fired");
}
checkExpiredDeadlinesAndResetAlarm();
}
};
private final OnAlarmListener mNextDelayExpiredListener = new OnAlarmListener() {
@Override
public void onAlarm() {
if (DEBUG) {
Slog.d(TAG, "Delay-expired alarm fired");
}
checkExpiredDelaysAndResetAlarm();
}
};
@Override
public void dumpControllerStateLocked(PrintWriter pw, int filterUid) {
final long nowElapsed = SystemClock.elapsedRealtime();
pw.print("Alarms: now=");
pw.print(SystemClock.elapsedRealtime());
pw.println();
pw.print("Next delay alarm in ");
TimeUtils.formatDuration(mNextDelayExpiredElapsedMillis, nowElapsed, pw);
pw.println();
pw.print("Next deadline alarm in ");
TimeUtils.formatDuration(mNextJobExpiredElapsedMillis, nowElapsed, pw);
pw.println();
pw.print("Tracking ");
pw.print(mTrackedJobs.size());
pw.println(":");
for (JobStatus ts : mTrackedJobs) {
if (!ts.shouldDump(filterUid)) {
continue;
}
pw.print(" #");
ts.printUniqueId(pw);
pw.print(" from ");
UserHandle.formatUid(pw, ts.getSourceUid());
pw.print(": Delay=");
if (ts.hasTimingDelayConstraint()) {
TimeUtils.formatDuration(ts.getEarliestRunTime(), nowElapsed, pw);
} else {
pw.print("N/A");
}
pw.print(", Deadline=");
if (ts.hasDeadlineConstraint()) {
TimeUtils.formatDuration(ts.getLatestRunTimeElapsed(), nowElapsed, pw);
} else {
pw.print("N/A");
}
pw.println();
}
}
}