blob: b75036ce6331b4cca9153edb94ffb1dba7fc6b23 [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.task.controllers;
import android.app.AlarmManager;
import android.app.PendingIntent;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.SystemClock;
import android.util.Slog;
import com.android.server.task.StateChangedListener;
import com.android.server.task.TaskManagerService;
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 task, and determines whether a task's minimum
* delay has been satisfied.
*/
public class TimeController extends StateController {
private static final String TAG = "TaskManager.Time";
private static final String ACTION_TASK_EXPIRED =
"android.content.taskmanager.TASK_DEADLINE_EXPIRED";
private static final String ACTION_TASK_DELAY_EXPIRED =
"android.content.taskmanager.TASK_DELAY_EXPIRED";
/** Set an alarm for the next task expiry. */
private final PendingIntent mDeadlineExpiredAlarmIntent;
/** Set an alarm for the next task delay expiry. This*/
private final PendingIntent mNextDelayExpiredAlarmIntent;
/** Constant time determining how near in the future we'll set an alarm for. */
private static final long MIN_WAKEUP_INTERVAL_MILLIS = 15 * 1000;
private long mNextTaskExpiredElapsedMillis;
private long mNextDelayExpiredElapsedMillis;
private AlarmManager mAlarmService = null;
/** List of tracked tasks, sorted asc. by deadline */
private final List<TaskStatus> mTrackedTasks = new LinkedList<TaskStatus>();
/** Singleton. */
private static TimeController mSingleton;
public static synchronized TimeController get(TaskManagerService taskManager) {
if (mSingleton == null) {
mSingleton = new TimeController(taskManager, taskManager.getContext());
}
return mSingleton;
}
private TimeController(StateChangedListener stateChangedListener, Context context) {
super(stateChangedListener, context);
mDeadlineExpiredAlarmIntent =
PendingIntent.getBroadcast(mContext, 0 /* ignored */,
new Intent(ACTION_TASK_EXPIRED), 0);
mNextDelayExpiredAlarmIntent =
PendingIntent.getBroadcast(mContext, 0 /* ignored */,
new Intent(ACTION_TASK_DELAY_EXPIRED), 0);
mNextTaskExpiredElapsedMillis = Long.MAX_VALUE;
mNextDelayExpiredElapsedMillis = Long.MAX_VALUE;
// Register BR for these intents.
IntentFilter intentFilter = new IntentFilter(ACTION_TASK_EXPIRED);
intentFilter.addAction(ACTION_TASK_DELAY_EXPIRED);
mContext.registerReceiver(mAlarmExpiredReceiver, intentFilter);
}
/**
* Check if the task has a timing constraint, and if so determine where to insert it in our
* list.
*/
@Override
public synchronized void maybeStartTrackingTask(TaskStatus task) {
if (task.hasTimingDelayConstraint() || task.hasDeadlineConstraint()) {
maybeStopTrackingTask(task);
ListIterator<TaskStatus> it = mTrackedTasks.listIterator(mTrackedTasks.size());
while (it.hasPrevious()) {
TaskStatus ts = it.previous();
if (ts.getLatestRunTimeElapsed() < task.getLatestRunTimeElapsed()) {
// Insert
break;
}
}
it.add(task);
maybeUpdateAlarms(
task.hasTimingDelayConstraint() ? task.getEarliestRunTime() : Long.MAX_VALUE,
task.hasDeadlineConstraint() ? task.getLatestRunTimeElapsed() : Long.MAX_VALUE);
}
}
/**
* When we stop tracking a task, we only need to update our alarms if the task 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 synchronized void maybeStopTrackingTask(TaskStatus taskStatus) {
if (mTrackedTasks.remove(taskStatus)) {
checkExpiredDelaysAndResetAlarm();
checkExpiredDeadlinesAndResetAlarm();
}
}
/**
* Determines whether this controller can stop tracking the given task.
* The controller is no longer interested in a task once its time constraint is satisfied, and
* the task's deadline is fulfilled - unlike other controllers a time constraint can't toggle
* back and forth.
*/
private boolean canStopTrackingTask(TaskStatus taskStatus) {
return (!taskStatus.hasTimingDelayConstraint() ||
taskStatus.timeDelayConstraintSatisfied.get()) &&
(!taskStatus.hasDeadlineConstraint() ||
taskStatus.deadlineConstraintSatisfied.get());
}
private void ensureAlarmService() {
if (mAlarmService == null) {
mAlarmService = (AlarmManager) mContext.getSystemService(Context.ALARM_SERVICE);
}
}
/**
* Checks list of tasks for ones that have an expired deadline, sending them to the TaskManager
* if so, removing them from this list, and updating the alarm for the next expiry time.
*/
private synchronized void checkExpiredDeadlinesAndResetAlarm() {
long nextExpiryTime = Long.MAX_VALUE;
final long nowElapsedMillis = SystemClock.elapsedRealtime();
Iterator<TaskStatus> it = mTrackedTasks.iterator();
while (it.hasNext()) {
TaskStatus ts = it.next();
if (!ts.hasDeadlineConstraint()) {
continue;
}
final long taskDeadline = ts.getLatestRunTimeElapsed();
if (taskDeadline <= nowElapsedMillis) {
ts.deadlineConstraintSatisfied.set(true);
mStateChangedListener.onRunTaskNow(ts);
it.remove();
} else { // Sorted by expiry time, so take the next one and stop.
nextExpiryTime = taskDeadline;
break;
}
}
setDeadlineExpiredAlarm(nextExpiryTime);
}
/**
* Handles alarm that notifies us that a task's delay has expired. Iterates through the list of
* tracked tasks and marks them as ready as appropriate.
*/
private synchronized void checkExpiredDelaysAndResetAlarm() {
final long nowElapsedMillis = SystemClock.elapsedRealtime();
long nextDelayTime = Long.MAX_VALUE;
boolean ready = false;
Iterator<TaskStatus> it = mTrackedTasks.iterator();
while (it.hasNext()) {
final TaskStatus ts = it.next();
if (!ts.hasTimingDelayConstraint()) {
continue;
}
final long taskDelayTime = ts.getEarliestRunTime();
if (taskDelayTime <= nowElapsedMillis) {
ts.timeDelayConstraintSatisfied.set(true);
if (canStopTrackingTask(ts)) {
it.remove();
}
if (ts.isReady()) {
ready = true;
}
} else { // Keep going through list to get next delay time.
if (nextDelayTime > taskDelayTime) {
nextDelayTime = taskDelayTime;
}
}
}
if (ready) {
mStateChangedListener.onControllerStateChanged();
}
setDelayExpiredAlarm(nextDelayTime);
}
private void maybeUpdateAlarms(long delayExpiredElapsed, long deadlineExpiredElapsed) {
if (delayExpiredElapsed < mNextDelayExpiredElapsedMillis) {
setDelayExpiredAlarm(delayExpiredElapsed);
}
if (deadlineExpiredElapsed < mNextTaskExpiredElapsedMillis) {
setDeadlineExpiredAlarm(deadlineExpiredElapsed);
}
}
/**
* Set an alarm with the {@link android.app.AlarmManager} for the next time at which a task's
* delay will expire.
* This alarm <b>will not</b> wake up the phone.
*/
private void setDelayExpiredAlarm(long alarmTimeElapsedMillis) {
final long earliestWakeupTimeElapsed =
SystemClock.elapsedRealtime() + MIN_WAKEUP_INTERVAL_MILLIS;
if (alarmTimeElapsedMillis < earliestWakeupTimeElapsed) {
alarmTimeElapsedMillis = earliestWakeupTimeElapsed;
}
mNextDelayExpiredElapsedMillis = alarmTimeElapsedMillis;
updateAlarmWithPendingIntent(mNextDelayExpiredAlarmIntent, mNextDelayExpiredElapsedMillis);
}
/**
* Set an alarm with the {@link android.app.AlarmManager} for the next time at which a task's
* deadline will expire.
* This alarm <b>will</b> wake up the phone.
*/
private void setDeadlineExpiredAlarm(long alarmTimeElapsedMillis) {
final long earliestWakeupTimeElapsed =
SystemClock.elapsedRealtime() + MIN_WAKEUP_INTERVAL_MILLIS;
if (alarmTimeElapsedMillis < earliestWakeupTimeElapsed) {
alarmTimeElapsedMillis = earliestWakeupTimeElapsed;
}
mNextTaskExpiredElapsedMillis = alarmTimeElapsedMillis;
updateAlarmWithPendingIntent(mDeadlineExpiredAlarmIntent, mNextTaskExpiredElapsedMillis);
}
private void updateAlarmWithPendingIntent(PendingIntent pi, long alarmTimeElapsed) {
ensureAlarmService();
if (alarmTimeElapsed == Long.MAX_VALUE) {
mAlarmService.cancel(pi);
} else {
if (DEBUG) {
Slog.d(TAG, "Setting " + pi.getIntent().getAction() + " for: " + alarmTimeElapsed);
}
mAlarmService.set(AlarmManager.ELAPSED_REALTIME, alarmTimeElapsed, pi);
}
}
private final BroadcastReceiver mAlarmExpiredReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
if (DEBUG) {
Slog.d(TAG, "Just received alarm: " + intent.getAction());
}
// An task has just expired, so we run through the list of tasks that we have and
// notify our StateChangedListener.
if (ACTION_TASK_EXPIRED.equals(intent.getAction())) {
checkExpiredDeadlinesAndResetAlarm();
} else if (ACTION_TASK_DELAY_EXPIRED.equals(intent.getAction())) {
checkExpiredDelaysAndResetAlarm();
}
}
};
@Override
public void dumpControllerState(PrintWriter pw) {
final long nowElapsed = SystemClock.elapsedRealtime();
pw.println("Alarms (" + SystemClock.elapsedRealtime() + ")");
pw.println(
"Next delay alarm in " + (mNextDelayExpiredElapsedMillis - nowElapsed)/1000 + "s");
pw.println("Next deadline alarm in " + (mNextTaskExpiredElapsedMillis - nowElapsed)/1000
+ "s");
pw.println("Tracking:");
for (TaskStatus ts : mTrackedTasks) {
pw.println(String.valueOf(ts.hashCode()).substring(0, 3) + ".."
+ ": (" + (ts.hasTimingDelayConstraint() ? ts.getEarliestRunTime() : "N/A")
+ ", " + (ts.hasDeadlineConstraint() ?ts.getLatestRunTimeElapsed() : "N/A")
+ ")");
}
}
}