Add persistence of tasks for TaskManager & BatteryController
Tasks are persisted only if the client has the RECEIVE_BOOT_COMPLETED permission. This applies to both periodic and one-off tasks. Write out task as xml, using PersistableBundle for the extras data. Todo: Add persistable bundle to xml when ag/468207 is merged. Also added BatteryController logic. Change-Id: I23eeeb8b3bc6ba155e7fa4ec25857a68ee8b1567
This commit is contained in:
@@ -5362,7 +5362,7 @@ package android.app.task {
|
||||
public class Task implements android.os.Parcelable {
|
||||
method public int describeContents();
|
||||
method public int getBackoffPolicy();
|
||||
method public android.os.Bundle getExtras();
|
||||
method public android.os.PersistableBundle getExtras();
|
||||
method public int getId();
|
||||
method public long getInitialBackoffMillis();
|
||||
method public long getIntervalMillis();
|
||||
@@ -5386,7 +5386,7 @@ package android.app.task {
|
||||
ctor public Task.Builder(int, android.content.ComponentName);
|
||||
method public android.app.task.Task build();
|
||||
method public android.app.task.Task.Builder setBackoffCriteria(long, int);
|
||||
method public android.app.task.Task.Builder setExtras(android.os.Bundle);
|
||||
method public android.app.task.Task.Builder setExtras(android.os.PersistableBundle);
|
||||
method public android.app.task.Task.Builder setMinimumLatency(long);
|
||||
method public android.app.task.Task.Builder setOverrideDeadline(long);
|
||||
method public android.app.task.Task.Builder setPeriodic(long);
|
||||
@@ -5413,7 +5413,7 @@ package android.app.task {
|
||||
|
||||
public class TaskParams implements android.os.Parcelable {
|
||||
method public int describeContents();
|
||||
method public android.os.Bundle getExtras();
|
||||
method public android.os.PersistableBundle getExtras();
|
||||
method public int getTaskId();
|
||||
method public void writeToParcel(android.os.Parcel, int);
|
||||
field public static final android.os.Parcelable.Creator CREATOR;
|
||||
|
||||
@@ -20,6 +20,7 @@ import android.content.ComponentName;
|
||||
import android.os.Bundle;
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
import android.os.PersistableBundle;
|
||||
|
||||
/**
|
||||
* Container of data passed to the {@link android.app.task.TaskManager} fully encapsulating the
|
||||
@@ -36,6 +37,18 @@ public class Task implements Parcelable {
|
||||
public final int UNMETERED = 2;
|
||||
}
|
||||
|
||||
/**
|
||||
* Amount of backoff a task has initially by default, in milliseconds.
|
||||
* @hide.
|
||||
*/
|
||||
public static final long DEFAULT_INITIAL_BACKOFF_MILLIS = 5000L;
|
||||
|
||||
/**
|
||||
* Default type of backoff.
|
||||
* @hide
|
||||
*/
|
||||
public static final int DEFAULT_BACKOFF_POLICY = BackoffPolicy.EXPONENTIAL;
|
||||
|
||||
/**
|
||||
* Linear: retry_time(failure_time, t) = failure_time + initial_retry_delay * t, t >= 1
|
||||
* Expon: retry_time(failure_time, t) = failure_time + initial_retry_delay ^ t, t >= 1
|
||||
@@ -47,7 +60,7 @@ public class Task implements Parcelable {
|
||||
|
||||
private final int taskId;
|
||||
// TODO: Change this to use PersistableBundle when that lands in master.
|
||||
private final Bundle extras;
|
||||
private final PersistableBundle extras;
|
||||
private final ComponentName service;
|
||||
private final boolean requireCharging;
|
||||
private final boolean requireDeviceIdle;
|
||||
@@ -71,7 +84,7 @@ public class Task implements Parcelable {
|
||||
/**
|
||||
* Bundle of extras which are returned to your application at execution time.
|
||||
*/
|
||||
public Bundle getExtras() {
|
||||
public PersistableBundle getExtras() {
|
||||
return extras;
|
||||
}
|
||||
|
||||
@@ -171,7 +184,7 @@ public class Task implements Parcelable {
|
||||
|
||||
private Task(Parcel in) {
|
||||
taskId = in.readInt();
|
||||
extras = in.readBundle();
|
||||
extras = in.readPersistableBundle();
|
||||
service = ComponentName.readFromParcel(in);
|
||||
requireCharging = in.readInt() == 1;
|
||||
requireDeviceIdle = in.readInt() == 1;
|
||||
@@ -188,7 +201,7 @@ public class Task implements Parcelable {
|
||||
|
||||
private Task(Task.Builder b) {
|
||||
taskId = b.mTaskId;
|
||||
extras = new Bundle(b.mExtras);
|
||||
extras = new PersistableBundle(b.mExtras);
|
||||
service = b.mTaskService;
|
||||
requireCharging = b.mRequiresCharging;
|
||||
requireDeviceIdle = b.mRequiresDeviceIdle;
|
||||
@@ -211,7 +224,7 @@ public class Task implements Parcelable {
|
||||
@Override
|
||||
public void writeToParcel(Parcel out, int flags) {
|
||||
out.writeInt(taskId);
|
||||
out.writeBundle(extras);
|
||||
out.writePersistableBundle(extras);
|
||||
ComponentName.writeToParcel(service, out);
|
||||
out.writeInt(requireCharging ? 1 : 0);
|
||||
out.writeInt(requireDeviceIdle ? 1 : 0);
|
||||
@@ -238,12 +251,10 @@ public class Task implements Parcelable {
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Builder class for constructing {@link Task} objects.
|
||||
*/
|
||||
/** Builder class for constructing {@link Task} objects. */
|
||||
public static final class Builder {
|
||||
private int mTaskId;
|
||||
private Bundle mExtras;
|
||||
private PersistableBundle mExtras = PersistableBundle.EMPTY;
|
||||
private ComponentName mTaskService;
|
||||
// Requirements.
|
||||
private boolean mRequiresCharging;
|
||||
@@ -258,8 +269,8 @@ public class Task implements Parcelable {
|
||||
private boolean mHasLateConstraint;
|
||||
private long mIntervalMillis;
|
||||
// Back-off parameters.
|
||||
private long mInitialBackoffMillis = 5000L;
|
||||
private int mBackoffPolicy = BackoffPolicy.EXPONENTIAL;
|
||||
private long mInitialBackoffMillis = DEFAULT_INITIAL_BACKOFF_MILLIS;
|
||||
private int mBackoffPolicy = DEFAULT_BACKOFF_POLICY;
|
||||
/** Easy way to track whether the client has tried to set a back-off policy. */
|
||||
private boolean mBackoffPolicySet = false;
|
||||
|
||||
@@ -279,7 +290,7 @@ public class Task implements Parcelable {
|
||||
* Set optional extras. This is persisted, so we only allow primitive types.
|
||||
* @param extras Bundle containing extras you want the scheduler to hold on to for you.
|
||||
*/
|
||||
public Builder setExtras(Bundle extras) {
|
||||
public Builder setExtras(PersistableBundle extras) {
|
||||
mExtras = extras;
|
||||
return this;
|
||||
}
|
||||
@@ -394,18 +405,13 @@ public class Task implements Parcelable {
|
||||
* @return The task object to hand to the TaskManager. This object is immutable.
|
||||
*/
|
||||
public Task build() {
|
||||
if (mExtras == null) {
|
||||
mExtras = Bundle.EMPTY;
|
||||
}
|
||||
if (mTaskId < 0) {
|
||||
throw new IllegalArgumentException("Task id must be greater than 0.");
|
||||
}
|
||||
mExtras = new PersistableBundle(mExtras); // Make our own copy.
|
||||
// Check that a deadline was not set on a periodic task.
|
||||
if (mIsPeriodic && mHasLateConstraint) {
|
||||
if (mIsPeriodic && (mMaxExecutionDelayMillis != 0L)) {
|
||||
throw new IllegalArgumentException("Can't call setOverrideDeadline() on a " +
|
||||
"periodic task.");
|
||||
}
|
||||
if (mIsPeriodic && mHasEarlyConstraint) {
|
||||
if (mIsPeriodic && (mMinLatencyMillis != 0L)) {
|
||||
throw new IllegalArgumentException("Can't call setMinimumLatency() on a " +
|
||||
"periodic task");
|
||||
}
|
||||
|
||||
@@ -16,10 +16,10 @@
|
||||
|
||||
package android.app.task;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.os.IBinder;
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
import android.os.PersistableBundle;
|
||||
|
||||
/**
|
||||
* Contains the parameters used to configure/identify your task. You do not create this object
|
||||
@@ -28,11 +28,11 @@ import android.os.Parcelable;
|
||||
public class TaskParams implements Parcelable {
|
||||
|
||||
private final int taskId;
|
||||
private final Bundle extras;
|
||||
private final PersistableBundle extras;
|
||||
private final IBinder callback;
|
||||
|
||||
/** @hide */
|
||||
public TaskParams(int taskId, Bundle extras, IBinder callback) {
|
||||
public TaskParams(int taskId, PersistableBundle extras, IBinder callback) {
|
||||
this.taskId = taskId;
|
||||
this.extras = extras;
|
||||
this.callback = callback;
|
||||
@@ -47,10 +47,10 @@ public class TaskParams implements Parcelable {
|
||||
|
||||
/**
|
||||
* @return The extras you passed in when constructing this task with
|
||||
* {@link android.app.task.Task.Builder#setExtras(android.os.Bundle)}. This will
|
||||
* {@link android.app.task.Task.Builder#setExtras(android.os.PersistableBundle)}. This will
|
||||
* never be null. If you did not set any extras this will be an empty bundle.
|
||||
*/
|
||||
public Bundle getExtras() {
|
||||
public PersistableBundle getExtras() {
|
||||
return extras;
|
||||
}
|
||||
|
||||
@@ -61,7 +61,7 @@ public class TaskParams implements Parcelable {
|
||||
|
||||
private TaskParams(Parcel in) {
|
||||
taskId = in.readInt();
|
||||
extras = in.readBundle();
|
||||
extras = in.readPersistableBundle();
|
||||
callback = in.readStrongBinder();
|
||||
}
|
||||
|
||||
@@ -73,7 +73,7 @@ public class TaskParams implements Parcelable {
|
||||
@Override
|
||||
public void writeToParcel(Parcel dest, int flags) {
|
||||
dest.writeInt(taskId);
|
||||
dest.writeBundle(extras);
|
||||
dest.writePersistableBundle(extras);
|
||||
dest.writeStrongBinder(callback);
|
||||
}
|
||||
|
||||
|
||||
@@ -36,6 +36,7 @@ import android.os.SystemClock;
|
||||
import android.util.Slog;
|
||||
import android.util.SparseArray;
|
||||
|
||||
import com.android.server.task.controllers.BatteryController;
|
||||
import com.android.server.task.controllers.ConnectivityController;
|
||||
import com.android.server.task.controllers.IdleController;
|
||||
import com.android.server.task.controllers.StateController;
|
||||
@@ -48,12 +49,19 @@ import java.util.LinkedList;
|
||||
* Responsible for taking tasks representing work to be performed by a client app, and determining
|
||||
* based on the criteria specified when that task should be run against the client application's
|
||||
* endpoint.
|
||||
* Implements logic for scheduling, and rescheduling tasks. The TaskManagerService knows nothing
|
||||
* about constraints, or the state of active tasks. It receives callbacks from the various
|
||||
* controllers and completed tasks and operates accordingly.
|
||||
*
|
||||
* Note on locking: Any operations that manipulate {@link #mTasks} need to lock on that object, and
|
||||
* similarly for {@link #mActiveServices}. If both locks need to be held take mTasksSet first and then
|
||||
* mActiveService afterwards.
|
||||
* @hide
|
||||
*/
|
||||
public class TaskManagerService extends com.android.server.SystemService
|
||||
implements StateChangedListener, TaskCompletedListener {
|
||||
implements StateChangedListener, TaskCompletedListener, TaskMapReadFinishedListener {
|
||||
// TODO: Switch this off for final version.
|
||||
private static final boolean DEBUG = true;
|
||||
static final boolean DEBUG = true;
|
||||
/** The number of concurrent tasks we run at one time. */
|
||||
private static final int MAX_TASK_CONTEXTS_COUNT = 3;
|
||||
static final String TAG = "TaskManager";
|
||||
@@ -113,8 +121,8 @@ public class TaskManagerService extends com.android.server.SystemService
|
||||
*/
|
||||
public int schedule(Task task, int uId, boolean canPersistTask) {
|
||||
TaskStatus taskStatus = new TaskStatus(task, uId, canPersistTask);
|
||||
return startTrackingTask(taskStatus) ?
|
||||
TaskManager.RESULT_SUCCESS : TaskManager.RESULT_FAILURE;
|
||||
startTrackingTask(taskStatus);
|
||||
return TaskManager.RESULT_SUCCESS;
|
||||
}
|
||||
|
||||
public List<Task> getPendingTasks(int uid) {
|
||||
@@ -210,7 +218,7 @@ public class TaskManagerService extends com.android.server.SystemService
|
||||
*/
|
||||
public TaskManagerService(Context context) {
|
||||
super(context);
|
||||
mTasks = new TaskStore(context);
|
||||
mTasks = TaskStore.initAndGet(this);
|
||||
mHandler = new TaskHandler(context.getMainLooper());
|
||||
mTaskManagerStub = new TaskManagerStub();
|
||||
// Create the "runners".
|
||||
@@ -218,12 +226,12 @@ public class TaskManagerService extends com.android.server.SystemService
|
||||
mActiveServices.add(
|
||||
new TaskServiceContext(this, context.getMainLooper()));
|
||||
}
|
||||
|
||||
// Create the controllers.
|
||||
mControllers = new LinkedList<StateController>();
|
||||
mControllers.add(ConnectivityController.get(this));
|
||||
mControllers.add(TimeController.get(this));
|
||||
mControllers.add(IdleController.get(this));
|
||||
// TODO: Add BatteryStateController when implemented.
|
||||
mControllers.add(BatteryController.get(this));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -236,17 +244,14 @@ public class TaskManagerService extends com.android.server.SystemService
|
||||
* {@link com.android.server.task.TaskStore}, and make sure all the relevant controllers know
|
||||
* about.
|
||||
*/
|
||||
private boolean startTrackingTask(TaskStatus taskStatus) {
|
||||
boolean added = false;
|
||||
private void startTrackingTask(TaskStatus taskStatus) {
|
||||
synchronized (mTasks) {
|
||||
added = mTasks.add(taskStatus);
|
||||
mTasks.add(taskStatus);
|
||||
}
|
||||
if (added) {
|
||||
for (StateController controller : mControllers) {
|
||||
controller.maybeStartTrackingTask(taskStatus);
|
||||
}
|
||||
for (StateController controller : mControllers) {
|
||||
controller.maybeStartTrackingTask(taskStatus);
|
||||
|
||||
}
|
||||
return added;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -404,6 +409,27 @@ public class TaskManagerService extends com.android.server.SystemService
|
||||
mHandler.obtainMessage(MSG_TASK_EXPIRED, taskStatus);
|
||||
}
|
||||
|
||||
/**
|
||||
* Disk I/O is finished, take the list of tasks we read from disk and add them to our
|
||||
* {@link TaskStore}.
|
||||
* This is run on the {@link com.android.server.IoThread} instance, which is a separate thread,
|
||||
* and is called once at boot.
|
||||
*/
|
||||
@Override
|
||||
public void onTaskMapReadFinished(List<TaskStatus> tasks) {
|
||||
synchronized (mTasks) {
|
||||
for (TaskStatus ts : tasks) {
|
||||
if (mTasks.contains(ts)) {
|
||||
// An app with BOOT_COMPLETED *might* have decided to reschedule their task, in
|
||||
// the same amount of time it took us to read it from disk. If this is the case
|
||||
// we leave it be.
|
||||
continue;
|
||||
}
|
||||
startTrackingTask(ts);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class TaskHandler extends Handler {
|
||||
|
||||
public TaskHandler(Looper looper) {
|
||||
|
||||
@@ -16,17 +16,37 @@
|
||||
|
||||
package com.android.server.task;
|
||||
|
||||
import android.content.ComponentName;
|
||||
import android.app.task.Task;
|
||||
import android.content.Context;
|
||||
import android.os.Environment;
|
||||
import android.os.Handler;
|
||||
import android.os.PersistableBundle;
|
||||
import android.os.SystemClock;
|
||||
import android.util.AtomicFile;
|
||||
import android.util.ArraySet;
|
||||
import android.util.Pair;
|
||||
import android.util.Slog;
|
||||
import android.util.SparseArray;
|
||||
import android.util.Xml;
|
||||
|
||||
import com.android.internal.annotations.VisibleForTesting;
|
||||
import com.android.internal.util.FastXmlSerializer;
|
||||
import com.android.server.IoThread;
|
||||
import com.android.server.task.controllers.TaskStatus;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Iterator;
|
||||
import java.util.Set;
|
||||
import java.util.List;
|
||||
|
||||
import org.xmlpull.v1.XmlPullParser;
|
||||
import org.xmlpull.v1.XmlPullParserException;
|
||||
import org.xmlpull.v1.XmlSerializer;
|
||||
|
||||
/**
|
||||
* Maintain a list of classes, and accessor methods/logic for these tasks.
|
||||
@@ -38,57 +58,108 @@ import java.util.Set;
|
||||
* - Handles rescheduling of tasks.
|
||||
* - When a periodic task is executed and must be re-added.
|
||||
* - When a task fails and the client requests that it be retried with backoff.
|
||||
* - This class is <strong>not</strong> thread-safe.
|
||||
* - This class <strong>is not</strong> thread-safe.
|
||||
*
|
||||
* Note on locking:
|
||||
* All callers to this class must <strong>lock on the class object they are calling</strong>.
|
||||
* This is important b/c {@link com.android.server.task.TaskStore.WriteTasksMapToDiskRunnable}
|
||||
* and {@link com.android.server.task.TaskStore.ReadTaskMapFromDiskRunnable} lock on that
|
||||
* object.
|
||||
*/
|
||||
public class TaskStore {
|
||||
private static final String TAG = "TaskManagerStore";
|
||||
private static final boolean DEBUG = TaskManagerService.DEBUG;
|
||||
|
||||
/** Threshold to adjust how often we want to write to the db. */
|
||||
private static final int MAX_OPS_BEFORE_WRITE = 1;
|
||||
final ArraySet<TaskStatus> mTasks;
|
||||
final ArraySet<TaskStatus> mTasksSet;
|
||||
final Context mContext;
|
||||
|
||||
private int mDirtyOperations;
|
||||
|
||||
TaskStore(Context context) {
|
||||
mTasks = intialiseTasksFromDisk();
|
||||
private static final Object sSingletonLock = new Object();
|
||||
private final AtomicFile mTasksFile;
|
||||
/** Handler backed by IoThread for writing to disk. */
|
||||
private final Handler mIoHandler = IoThread.getHandler();
|
||||
private static TaskStore sSingleton;
|
||||
|
||||
/** Used by the {@Link TaskManagerService} to instantiate the TaskStore. */
|
||||
static TaskStore initAndGet(TaskManagerService taskManagerService) {
|
||||
synchronized (sSingletonLock) {
|
||||
if (sSingleton == null) {
|
||||
sSingleton = new TaskStore(taskManagerService.getContext(),
|
||||
Environment.getDataDirectory(), taskManagerService);
|
||||
}
|
||||
return sSingleton;
|
||||
}
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
public static TaskStore initAndGetForTesting(Context context, File dataDir,
|
||||
TaskMapReadFinishedListener callback) {
|
||||
return new TaskStore(context, dataDir, callback);
|
||||
}
|
||||
|
||||
private TaskStore(Context context, File dataDir, TaskMapReadFinishedListener callback) {
|
||||
mContext = context;
|
||||
mDirtyOperations = 0;
|
||||
|
||||
File systemDir = new File(dataDir, "system");
|
||||
File taskDir = new File(systemDir, "task");
|
||||
taskDir.mkdirs();
|
||||
mTasksFile = new AtomicFile(new File(taskDir, "tasks.xml"));
|
||||
|
||||
mTasksSet = new ArraySet<TaskStatus>();
|
||||
|
||||
readTaskMapFromDiskAsync(callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a task to the master list, persisting it if necessary. If the TaskStatus already exists,
|
||||
* it will be replaced.
|
||||
* @param taskStatus Task to add.
|
||||
* @return true if the operation succeeded.
|
||||
* @return Whether or not an equivalent TaskStatus was replaced by this operation.
|
||||
*/
|
||||
public boolean add(TaskStatus taskStatus) {
|
||||
boolean replaced = mTasksSet.remove(taskStatus);
|
||||
mTasksSet.add(taskStatus);
|
||||
if (taskStatus.isPersisted()) {
|
||||
if (!maybeWriteStatusToDisk()) {
|
||||
return false;
|
||||
}
|
||||
maybeWriteStatusToDiskAsync();
|
||||
}
|
||||
mTasks.remove(taskStatus);
|
||||
mTasks.add(taskStatus);
|
||||
return true;
|
||||
return replaced;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether this taskStatus object already exists in the TaskStore.
|
||||
*/
|
||||
public boolean contains(TaskStatus taskStatus) {
|
||||
return mTasksSet.contains(taskStatus);
|
||||
}
|
||||
|
||||
public int size() {
|
||||
return mTasks.size();
|
||||
return mTasksSet.size();
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the provided task. Will also delete the task if it was persisted.
|
||||
* @return The TaskStatus that was removed, or null if an invalid token was provided.
|
||||
* @return Whether or not the task existed to be removed.
|
||||
*/
|
||||
public boolean remove(TaskStatus taskStatus) {
|
||||
boolean removed = mTasks.remove(taskStatus);
|
||||
boolean removed = mTasksSet.remove(taskStatus);
|
||||
if (!removed) {
|
||||
Slog.e(TAG, "Error removing task: " + taskStatus);
|
||||
if (DEBUG) {
|
||||
Slog.d(TAG, "Couldn't remove task: didn't exist: " + taskStatus);
|
||||
}
|
||||
return false;
|
||||
} else {
|
||||
maybeWriteStatusToDisk();
|
||||
}
|
||||
return true;
|
||||
maybeWriteStatusToDiskAsync();
|
||||
return removed;
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
public void clear() {
|
||||
mTasksSet.clear();
|
||||
maybeWriteStatusToDiskAsync();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -100,19 +171,16 @@ public class TaskStore {
|
||||
* was found.
|
||||
*/
|
||||
public boolean removeAllByUid(int uid) {
|
||||
Iterator<TaskStatus> it = mTasks.iterator();
|
||||
boolean removed = false;
|
||||
Iterator<TaskStatus> it = mTasksSet.iterator();
|
||||
while (it.hasNext()) {
|
||||
TaskStatus ts = it.next();
|
||||
if (ts.getUid() == uid) {
|
||||
it.remove();
|
||||
removed = true;
|
||||
maybeWriteStatusToDiskAsync();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if (removed) {
|
||||
maybeWriteStatusToDisk();
|
||||
}
|
||||
return removed;
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -124,48 +192,464 @@ public class TaskStore {
|
||||
* @return true if a removal occurred, false if the provided parameters didn't match anything.
|
||||
*/
|
||||
public boolean remove(int uid, int taskId) {
|
||||
Iterator<TaskStatus> it = mTasks.iterator();
|
||||
boolean changed = false;
|
||||
Iterator<TaskStatus> it = mTasksSet.iterator();
|
||||
while (it.hasNext()) {
|
||||
TaskStatus ts = it.next();
|
||||
if (ts.getUid() == uid && ts.getTaskId() == taskId) {
|
||||
it.remove();
|
||||
maybeWriteStatusToDisk();
|
||||
return true;
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
if (changed) {
|
||||
maybeWriteStatusToDiskAsync();
|
||||
}
|
||||
return changed;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The live array of TaskStatus objects.
|
||||
*/
|
||||
public Set<TaskStatus> getTasks() {
|
||||
return mTasks;
|
||||
public ArraySet<TaskStatus> getTasks() {
|
||||
return mTasksSet;
|
||||
}
|
||||
|
||||
/** Version of the db schema. */
|
||||
private static final int TASKS_FILE_VERSION = 0;
|
||||
/** Tag corresponds to constraints this task needs. */
|
||||
private static final String XML_TAG_PARAMS_CONSTRAINTS = "constraints";
|
||||
/** Tag corresponds to execution parameters. */
|
||||
private static final String XML_TAG_PERIODIC = "periodic";
|
||||
private static final String XML_TAG_ONEOFF = "one-off";
|
||||
private static final String XML_TAG_EXTRAS = "extras";
|
||||
|
||||
/**
|
||||
* Every time the state changes we write all the tasks in one swathe, instead of trying to
|
||||
* track incremental changes.
|
||||
* @return Whether the operation was successful. This will only fail for e.g. if the system is
|
||||
* low on storage. If this happens, we continue as normal
|
||||
*/
|
||||
private boolean maybeWriteStatusToDisk() {
|
||||
private void maybeWriteStatusToDiskAsync() {
|
||||
mDirtyOperations++;
|
||||
if (mDirtyOperations > MAX_OPS_BEFORE_WRITE) {
|
||||
for (TaskStatus ts : mTasks) {
|
||||
//
|
||||
if (mDirtyOperations >= MAX_OPS_BEFORE_WRITE) {
|
||||
if (DEBUG) {
|
||||
Slog.v(TAG, "Writing tasks to disk.");
|
||||
}
|
||||
mDirtyOperations = 0;
|
||||
mIoHandler.post(new WriteTasksMapToDiskRunnable());
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private void readTaskMapFromDiskAsync(TaskMapReadFinishedListener callback) {
|
||||
mIoHandler.post(new ReadTaskMapFromDiskRunnable(callback));
|
||||
}
|
||||
|
||||
public void readTaskMapFromDisk(TaskMapReadFinishedListener callback) {
|
||||
new ReadTaskMapFromDiskRunnable(callback).run();
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @return
|
||||
* Runnable that writes {@link #mTasksSet} out to xml.
|
||||
* NOTE: This Runnable locks on TaskStore.this
|
||||
*/
|
||||
// TODO: Implement this.
|
||||
private ArraySet<TaskStatus> intialiseTasksFromDisk() {
|
||||
return new ArraySet<TaskStatus>();
|
||||
private class WriteTasksMapToDiskRunnable implements Runnable {
|
||||
@Override
|
||||
public void run() {
|
||||
final long startElapsed = SystemClock.elapsedRealtime();
|
||||
synchronized (TaskStore.this) {
|
||||
writeTasksMapImpl();
|
||||
}
|
||||
if (TaskManagerService.DEBUG) {
|
||||
Slog.v(TAG, "Finished writing, took " + (SystemClock.elapsedRealtime()
|
||||
- startElapsed) + "ms");
|
||||
}
|
||||
}
|
||||
|
||||
private void writeTasksMapImpl() {
|
||||
try {
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
XmlSerializer out = new FastXmlSerializer();
|
||||
out.setOutput(baos, "utf-8");
|
||||
out.startDocument(null, true);
|
||||
out.setFeature("http://xmlpull.org/v1/doc/features.html#indent-output", true);
|
||||
|
||||
out.startTag(null, "task-info");
|
||||
out.attribute(null, "version", Integer.toString(TASKS_FILE_VERSION));
|
||||
for (int i = 0; i < mTasksSet.size(); i++) {
|
||||
final TaskStatus taskStatus = mTasksSet.valueAt(i);
|
||||
if (DEBUG) {
|
||||
Slog.d(TAG, "Saving task " + taskStatus.getTaskId());
|
||||
}
|
||||
out.startTag(null, "task");
|
||||
addIdentifierAttributesToTaskTag(out, taskStatus);
|
||||
writeConstraintsToXml(out, taskStatus);
|
||||
writeExecutionCriteriaToXml(out, taskStatus);
|
||||
writeBundleToXml(taskStatus.getExtras(), out);
|
||||
out.endTag(null, "task");
|
||||
}
|
||||
out.endTag(null, "task-info");
|
||||
out.endDocument();
|
||||
|
||||
// Write out to disk in one fell sweep.
|
||||
FileOutputStream fos = mTasksFile.startWrite();
|
||||
fos.write(baos.toByteArray());
|
||||
mTasksFile.finishWrite(fos);
|
||||
mDirtyOperations = 0;
|
||||
} catch (IOException e) {
|
||||
if (DEBUG) {
|
||||
Slog.v(TAG, "Error writing out task data.", e);
|
||||
}
|
||||
} catch (XmlPullParserException e) {
|
||||
if (DEBUG) {
|
||||
Slog.d(TAG, "Error persisting bundle.", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Write out a tag with data comprising the required fields of this task and its client. */
|
||||
private void addIdentifierAttributesToTaskTag(XmlSerializer out, TaskStatus taskStatus)
|
||||
throws IOException {
|
||||
out.attribute(null, "taskid", Integer.toString(taskStatus.getTaskId()));
|
||||
out.attribute(null, "package", taskStatus.getServiceComponent().getPackageName());
|
||||
out.attribute(null, "class", taskStatus.getServiceComponent().getClassName());
|
||||
out.attribute(null, "uid", Integer.toString(taskStatus.getUid()));
|
||||
}
|
||||
|
||||
private void writeBundleToXml(PersistableBundle extras, XmlSerializer out)
|
||||
throws IOException, XmlPullParserException {
|
||||
out.startTag(null, XML_TAG_EXTRAS);
|
||||
extras.saveToXml(out);
|
||||
out.endTag(null, XML_TAG_EXTRAS);
|
||||
}
|
||||
/**
|
||||
* Write out a tag with data identifying this tasks constraints. If the constraint isn't here
|
||||
* it doesn't apply.
|
||||
*/
|
||||
private void writeConstraintsToXml(XmlSerializer out, TaskStatus taskStatus) throws IOException {
|
||||
out.startTag(null, XML_TAG_PARAMS_CONSTRAINTS);
|
||||
if (taskStatus.hasMeteredConstraint()) {
|
||||
out.attribute(null, "unmetered", Boolean.toString(true));
|
||||
}
|
||||
if (taskStatus.hasConnectivityConstraint()) {
|
||||
out.attribute(null, "connectivity", Boolean.toString(true));
|
||||
}
|
||||
if (taskStatus.hasIdleConstraint()) {
|
||||
out.attribute(null, "idle", Boolean.toString(true));
|
||||
}
|
||||
if (taskStatus.hasChargingConstraint()) {
|
||||
out.attribute(null, "charging", Boolean.toString(true));
|
||||
}
|
||||
out.endTag(null, XML_TAG_PARAMS_CONSTRAINTS);
|
||||
}
|
||||
|
||||
private void writeExecutionCriteriaToXml(XmlSerializer out, TaskStatus taskStatus)
|
||||
throws IOException {
|
||||
final Task task = taskStatus.getTask();
|
||||
if (taskStatus.getTask().isPeriodic()) {
|
||||
out.startTag(null, XML_TAG_PERIODIC);
|
||||
out.attribute(null, "period", Long.toString(task.getIntervalMillis()));
|
||||
} else {
|
||||
out.startTag(null, XML_TAG_ONEOFF);
|
||||
}
|
||||
|
||||
if (taskStatus.hasDeadlineConstraint()) {
|
||||
// Wall clock deadline.
|
||||
final long deadlineWallclock = System.currentTimeMillis() +
|
||||
(taskStatus.getLatestRunTimeElapsed() - SystemClock.elapsedRealtime());
|
||||
out.attribute(null, "deadline", Long.toString(deadlineWallclock));
|
||||
}
|
||||
if (taskStatus.hasTimingDelayConstraint()) {
|
||||
final long delayWallclock = System.currentTimeMillis() +
|
||||
(taskStatus.getEarliestRunTime() - SystemClock.elapsedRealtime());
|
||||
out.attribute(null, "delay", Long.toString(delayWallclock));
|
||||
}
|
||||
|
||||
// Only write out back-off policy if it differs from the default.
|
||||
// This also helps the case where the task is idle -> these aren't allowed to specify
|
||||
// back-off.
|
||||
if (taskStatus.getTask().getInitialBackoffMillis() != Task.DEFAULT_INITIAL_BACKOFF_MILLIS
|
||||
|| taskStatus.getTask().getBackoffPolicy() != Task.DEFAULT_BACKOFF_POLICY) {
|
||||
out.attribute(null, "backoff-policy", Integer.toString(task.getBackoffPolicy()));
|
||||
out.attribute(null, "initial-backoff", Long.toString(task.getInitialBackoffMillis()));
|
||||
}
|
||||
if (task.isPeriodic()) {
|
||||
out.endTag(null, XML_TAG_PERIODIC);
|
||||
} else {
|
||||
out.endTag(null, XML_TAG_ONEOFF);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Runnable that reads list of persisted task from xml.
|
||||
* NOTE: This Runnable locks on TaskStore.this
|
||||
*/
|
||||
private class ReadTaskMapFromDiskRunnable implements Runnable {
|
||||
private TaskMapReadFinishedListener mCallback;
|
||||
public ReadTaskMapFromDiskRunnable(TaskMapReadFinishedListener callback) {
|
||||
mCallback = callback;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
List<TaskStatus> tasks;
|
||||
synchronized (TaskStore.this) {
|
||||
tasks = readTaskMapImpl();
|
||||
}
|
||||
if (tasks != null) {
|
||||
mCallback.onTaskMapReadFinished(tasks);
|
||||
}
|
||||
} catch (FileNotFoundException e) {
|
||||
if (TaskManagerService.DEBUG) {
|
||||
Slog.d(TAG, "Could not find tasks file, probably there was nothing to load.");
|
||||
}
|
||||
} catch (XmlPullParserException e) {
|
||||
if (TaskManagerService.DEBUG) {
|
||||
Slog.d(TAG, "Error parsing xml.", e);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
if (TaskManagerService.DEBUG) {
|
||||
Slog.d(TAG, "Error parsing xml.", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private List<TaskStatus> readTaskMapImpl() throws XmlPullParserException, IOException {
|
||||
FileInputStream fis = mTasksFile.openRead();
|
||||
XmlPullParser parser = Xml.newPullParser();
|
||||
parser.setInput(fis, null);
|
||||
|
||||
int eventType = parser.getEventType();
|
||||
while (eventType != XmlPullParser.START_TAG &&
|
||||
eventType != XmlPullParser.END_DOCUMENT) {
|
||||
eventType = parser.next();
|
||||
Slog.d(TAG, parser.getName());
|
||||
}
|
||||
if (eventType == XmlPullParser.END_DOCUMENT) {
|
||||
if (DEBUG) {
|
||||
Slog.d(TAG, "No persisted tasks.");
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
String tagName = parser.getName();
|
||||
if ("task-info".equals(tagName)) {
|
||||
final List<TaskStatus> tasks = new ArrayList<TaskStatus>();
|
||||
// Read in version info.
|
||||
try {
|
||||
int version = Integer.valueOf(parser.getAttributeValue(null, "version"));
|
||||
if (version != TASKS_FILE_VERSION) {
|
||||
Slog.d(TAG, "Invalid version number, aborting tasks file read.");
|
||||
return null;
|
||||
}
|
||||
} catch (NumberFormatException e) {
|
||||
Slog.e(TAG, "Invalid version number, aborting tasks file read.");
|
||||
return null;
|
||||
}
|
||||
eventType = parser.next();
|
||||
do {
|
||||
// Read each <task/>
|
||||
if (eventType == XmlPullParser.START_TAG) {
|
||||
tagName = parser.getName();
|
||||
// Start reading task.
|
||||
if ("task".equals(tagName)) {
|
||||
TaskStatus persistedTask = restoreTaskFromXml(parser);
|
||||
if (persistedTask != null) {
|
||||
if (DEBUG) {
|
||||
Slog.d(TAG, "Read out " + persistedTask);
|
||||
}
|
||||
tasks.add(persistedTask);
|
||||
} else {
|
||||
Slog.d(TAG, "Error reading task from file.");
|
||||
}
|
||||
}
|
||||
}
|
||||
eventType = parser.next();
|
||||
} while (eventType != XmlPullParser.END_DOCUMENT);
|
||||
return tasks;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param parser Xml parser at the beginning of a "<task/>" tag. The next "parser.next()" call
|
||||
* will take the parser into the body of the task tag.
|
||||
* @return Newly instantiated task holding all the information we just read out of the xml tag.
|
||||
*/
|
||||
private TaskStatus restoreTaskFromXml(XmlPullParser parser) throws XmlPullParserException,
|
||||
IOException {
|
||||
Task.Builder taskBuilder;
|
||||
int uid;
|
||||
|
||||
// Read out task identifier attributes.
|
||||
try {
|
||||
taskBuilder = buildBuilderFromXml(parser);
|
||||
uid = Integer.valueOf(parser.getAttributeValue(null, "uid"));
|
||||
} catch (NumberFormatException e) {
|
||||
Slog.e(TAG, "Error parsing task's required fields, skipping");
|
||||
return null;
|
||||
}
|
||||
|
||||
int eventType;
|
||||
// Read out constraints tag.
|
||||
do {
|
||||
eventType = parser.next();
|
||||
} while (eventType == XmlPullParser.TEXT); // Push through to next START_TAG.
|
||||
|
||||
if (!(eventType == XmlPullParser.START_TAG &&
|
||||
XML_TAG_PARAMS_CONSTRAINTS.equals(parser.getName()))) {
|
||||
// Expecting a <constraints> start tag.
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
buildConstraintsFromXml(taskBuilder, parser);
|
||||
} catch (NumberFormatException e) {
|
||||
Slog.d(TAG, "Error reading constraints, skipping.");
|
||||
return null;
|
||||
}
|
||||
parser.next(); // Consume </constraints>
|
||||
|
||||
// Read out execution parameters tag.
|
||||
do {
|
||||
eventType = parser.next();
|
||||
} while (eventType == XmlPullParser.TEXT);
|
||||
if (eventType != XmlPullParser.START_TAG) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Pair<Long, Long> runtimes;
|
||||
try {
|
||||
runtimes = buildExecutionTimesFromXml(parser);
|
||||
} catch (NumberFormatException e) {
|
||||
if (DEBUG) {
|
||||
Slog.d(TAG, "Error parsing execution time parameters, skipping.");
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
if (XML_TAG_PERIODIC.equals(parser.getName())) {
|
||||
try {
|
||||
String val = parser.getAttributeValue(null, "period");
|
||||
taskBuilder.setPeriodic(Long.valueOf(val));
|
||||
} catch (NumberFormatException e) {
|
||||
Slog.d(TAG, "Error reading periodic execution criteria, skipping.");
|
||||
return null;
|
||||
}
|
||||
} else if (XML_TAG_ONEOFF.equals(parser.getName())) {
|
||||
try {
|
||||
if (runtimes.first != TaskStatus.DEFAULT_EARLIEST_RUNTIME) {
|
||||
taskBuilder.setMinimumLatency(runtimes.first - SystemClock.elapsedRealtime());
|
||||
}
|
||||
if (runtimes.second != TaskStatus.DEFAULT_LATEST_RUNTIME) {
|
||||
taskBuilder.setOverrideDeadline(
|
||||
runtimes.second - SystemClock.elapsedRealtime());
|
||||
}
|
||||
} catch (NumberFormatException e) {
|
||||
Slog.d(TAG, "Error reading task execution criteria, skipping.");
|
||||
return null;
|
||||
}
|
||||
} else {
|
||||
if (DEBUG) {
|
||||
Slog.d(TAG, "Invalid parameter tag, skipping - " + parser.getName());
|
||||
}
|
||||
// Expecting a parameters start tag.
|
||||
return null;
|
||||
}
|
||||
maybeBuildBackoffPolicyFromXml(taskBuilder, parser);
|
||||
|
||||
parser.nextTag(); // Consume parameters end tag.
|
||||
|
||||
// Read out extras Bundle.
|
||||
do {
|
||||
eventType = parser.next();
|
||||
} while (eventType == XmlPullParser.TEXT);
|
||||
if (!(eventType == XmlPullParser.START_TAG && XML_TAG_EXTRAS.equals(parser.getName()))) {
|
||||
if (DEBUG) {
|
||||
Slog.d(TAG, "Error reading extras, skipping.");
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
PersistableBundle extras = PersistableBundle.restoreFromXml(parser);
|
||||
taskBuilder.setExtras(extras);
|
||||
parser.nextTag(); // Consume </extras>
|
||||
|
||||
return new TaskStatus(taskBuilder.build(), uid, runtimes.first, runtimes.second);
|
||||
}
|
||||
|
||||
private Task.Builder buildBuilderFromXml(XmlPullParser parser) throws NumberFormatException {
|
||||
// Pull out required fields from <task> attributes.
|
||||
int taskId = Integer.valueOf(parser.getAttributeValue(null, "taskid"));
|
||||
String packageName = parser.getAttributeValue(null, "package");
|
||||
String className = parser.getAttributeValue(null, "class");
|
||||
ComponentName cname = new ComponentName(packageName, className);
|
||||
|
||||
return new Task.Builder(taskId, cname);
|
||||
}
|
||||
|
||||
private void buildConstraintsFromXml(Task.Builder taskBuilder, XmlPullParser parser) {
|
||||
String val = parser.getAttributeValue(null, "unmetered");
|
||||
if (val != null) {
|
||||
taskBuilder.setRequiredNetworkCapabilities(Task.NetworkType.UNMETERED);
|
||||
}
|
||||
val = parser.getAttributeValue(null, "connectivity");
|
||||
if (val != null) {
|
||||
taskBuilder.setRequiredNetworkCapabilities(Task.NetworkType.ANY);
|
||||
}
|
||||
val = parser.getAttributeValue(null, "idle");
|
||||
if (val != null) {
|
||||
taskBuilder.setRequiresDeviceIdle(true);
|
||||
}
|
||||
val = parser.getAttributeValue(null, "charging");
|
||||
if (val != null) {
|
||||
taskBuilder.setRequiresCharging(true);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the back-off policy out of the params tag. These attributes may not exist, depending
|
||||
* on whether the back-off was set when the task was first scheduled.
|
||||
*/
|
||||
private void maybeBuildBackoffPolicyFromXml(Task.Builder taskBuilder, XmlPullParser parser) {
|
||||
String val = parser.getAttributeValue(null, "initial-backoff");
|
||||
if (val != null) {
|
||||
long initialBackoff = Long.valueOf(val);
|
||||
val = parser.getAttributeValue(null, "backoff-policy");
|
||||
int backoffPolicy = Integer.valueOf(val); // Will throw NFE which we catch higher up.
|
||||
taskBuilder.setBackoffCriteria(initialBackoff, backoffPolicy);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience function to read out and convert deadline and delay from xml into elapsed real
|
||||
* time.
|
||||
* @return A {@link android.util.Pair}, where the first value is the earliest elapsed runtime
|
||||
* and the second is the latest elapsed runtime.
|
||||
*/
|
||||
private Pair<Long, Long> buildExecutionTimesFromXml(XmlPullParser parser)
|
||||
throws NumberFormatException {
|
||||
// Pull out execution time data.
|
||||
final long nowWallclock = System.currentTimeMillis();
|
||||
final long nowElapsed = SystemClock.elapsedRealtime();
|
||||
|
||||
long earliestRunTimeElapsed = TaskStatus.DEFAULT_EARLIEST_RUNTIME;
|
||||
long latestRunTimeElapsed = TaskStatus.DEFAULT_LATEST_RUNTIME;
|
||||
String val = parser.getAttributeValue(null, "deadline");
|
||||
if (val != null) {
|
||||
long latestRuntimeWallclock = Long.valueOf(val);
|
||||
long maxDelayElapsed =
|
||||
Math.max(latestRuntimeWallclock - nowWallclock, 0);
|
||||
latestRunTimeElapsed = nowElapsed + maxDelayElapsed;
|
||||
}
|
||||
val = parser.getAttributeValue(null, "delay");
|
||||
if (val != null) {
|
||||
long earliestRuntimeWallclock = Long.valueOf(val);
|
||||
long minDelayElapsed =
|
||||
Math.max(earliestRuntimeWallclock - nowWallclock, 0);
|
||||
earliestRunTimeElapsed = nowElapsed + minDelayElapsed;
|
||||
|
||||
}
|
||||
return Pair.create(earliestRunTimeElapsed, latestRunTimeElapsed);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,213 @@
|
||||
/*
|
||||
* 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.ComponentName;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.os.BatteryManager;
|
||||
import android.os.BatteryProperty;
|
||||
import android.os.RemoteException;
|
||||
import android.os.ServiceManager;
|
||||
import android.os.SystemClock;
|
||||
import android.util.Slog;
|
||||
|
||||
import com.android.internal.annotations.VisibleForTesting;
|
||||
import com.android.server.BatteryService;
|
||||
import com.android.server.task.StateChangedListener;
|
||||
import com.android.server.task.TaskManagerService;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Simple controller that tracks whether the phone is charging or not. The phone is considered to
|
||||
* be charging when it's been plugged in for more than two minutes, and the system has broadcast
|
||||
* ACTION_BATTERY_OK.
|
||||
*/
|
||||
public class BatteryController extends StateController {
|
||||
private static final String TAG = "BatteryController";
|
||||
|
||||
private static final Object sCreationLock = new Object();
|
||||
private static volatile BatteryController sController;
|
||||
private static final String ACTION_CHARGING_STABLE =
|
||||
"com.android.server.task.controllers.BatteryController.ACTION_CHARGING_STABLE";
|
||||
/** Wait this long after phone is plugged in before doing any work. */
|
||||
private static final long STABLE_CHARGING_THRESHOLD_MILLIS = 2 * 60 * 1000; // 2 minutes.
|
||||
|
||||
private List<TaskStatus> mTrackedTasks = new ArrayList<TaskStatus>();
|
||||
private ChargingTracker mChargeTracker;
|
||||
|
||||
public static BatteryController get(TaskManagerService taskManagerService) {
|
||||
synchronized (sCreationLock) {
|
||||
if (sController == null) {
|
||||
sController = new BatteryController(taskManagerService,
|
||||
taskManagerService.getContext());
|
||||
}
|
||||
}
|
||||
return sController;
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
public ChargingTracker getTracker() {
|
||||
return mChargeTracker;
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
public static BatteryController getForTesting(StateChangedListener stateChangedListener,
|
||||
Context context) {
|
||||
return new BatteryController(stateChangedListener, context);
|
||||
}
|
||||
|
||||
private BatteryController(StateChangedListener stateChangedListener, Context context) {
|
||||
super(stateChangedListener, context);
|
||||
mChargeTracker = new ChargingTracker();
|
||||
mChargeTracker.startTracking();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void maybeStartTrackingTask(TaskStatus taskStatus) {
|
||||
if (taskStatus.hasChargingConstraint()) {
|
||||
synchronized (mTrackedTasks) {
|
||||
mTrackedTasks.add(taskStatus);
|
||||
taskStatus.chargingConstraintSatisfied.set(mChargeTracker.isOnStablePower());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void maybeStopTrackingTask(TaskStatus taskStatus) {
|
||||
if (taskStatus.hasChargingConstraint()) {
|
||||
synchronized (mTrackedTasks) {
|
||||
mTrackedTasks.remove(taskStatus);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void maybeReportNewChargingState() {
|
||||
final boolean stablePower = mChargeTracker.isOnStablePower();
|
||||
boolean reportChange = false;
|
||||
synchronized (mTrackedTasks) {
|
||||
for (TaskStatus ts : mTrackedTasks) {
|
||||
boolean previous = ts.chargingConstraintSatisfied.getAndSet(stablePower);
|
||||
if (previous != stablePower) {
|
||||
reportChange = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (reportChange) {
|
||||
mStateChangedListener.onControllerStateChanged();
|
||||
}
|
||||
}
|
||||
|
||||
public class ChargingTracker extends BroadcastReceiver {
|
||||
private final AlarmManager mAlarm;
|
||||
private final PendingIntent mStableChargingTriggerIntent;
|
||||
/**
|
||||
* Track whether we're "charging", where charging means that we're ready to commit to
|
||||
* doing work.
|
||||
*/
|
||||
private boolean mCharging;
|
||||
/** Keep track of whether the battery is charged enough that we want to do work. */
|
||||
private boolean mBatteryHealthy;
|
||||
|
||||
public ChargingTracker() {
|
||||
mAlarm = (AlarmManager) mContext.getSystemService(Context.ALARM_SERVICE);
|
||||
Intent intent = new Intent(ACTION_CHARGING_STABLE)
|
||||
.setComponent(new ComponentName(mContext, this.getClass()));
|
||||
mStableChargingTriggerIntent = PendingIntent.getBroadcast(mContext, 0, intent, 0);
|
||||
}
|
||||
|
||||
public void startTracking() {
|
||||
IntentFilter filter = new IntentFilter();
|
||||
|
||||
// Battery health.
|
||||
filter.addAction(Intent.ACTION_BATTERY_LOW);
|
||||
filter.addAction(Intent.ACTION_BATTERY_OKAY);
|
||||
// Charging/not charging.
|
||||
filter.addAction(Intent.ACTION_POWER_CONNECTED);
|
||||
filter.addAction(Intent.ACTION_POWER_DISCONNECTED);
|
||||
mContext.registerReceiver(this, filter);
|
||||
|
||||
// Initialise tracker state.
|
||||
BatteryService batteryService = (BatteryService) ServiceManager.getService("battery");
|
||||
if (batteryService != null) {
|
||||
mBatteryHealthy = !batteryService.isBatteryLow();
|
||||
mCharging = batteryService.isPowered(BatteryManager.BATTERY_PLUGGED_ANY);
|
||||
} else {
|
||||
// Unavailable for some reason, we default to false and let ACTION_BATTERY_[OK,LOW]
|
||||
// sort it out.
|
||||
}
|
||||
}
|
||||
|
||||
boolean isOnStablePower() {
|
||||
return mCharging && mBatteryHealthy;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
onReceiveInternal(intent);
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
public void onReceiveInternal(Intent intent) {
|
||||
final String action = intent.getAction();
|
||||
if (Intent.ACTION_BATTERY_LOW.equals(action)) {
|
||||
if (DEBUG) {
|
||||
Slog.d(TAG, "Battery life too low to do work. @ "
|
||||
+ SystemClock.elapsedRealtime());
|
||||
}
|
||||
// If we get this action, the battery is discharging => it isn't plugged in so
|
||||
// there's no work to cancel. We track this variable for the case where it is
|
||||
// charging, but hasn't been for long enough to be healthy.
|
||||
mBatteryHealthy = false;
|
||||
} else if (Intent.ACTION_BATTERY_OKAY.equals(action)) {
|
||||
if (DEBUG) {
|
||||
Slog.d(TAG, "Battery life healthy enough to do work. @ "
|
||||
+ SystemClock.elapsedRealtime());
|
||||
}
|
||||
mBatteryHealthy = true;
|
||||
maybeReportNewChargingState();
|
||||
} else if (Intent.ACTION_POWER_CONNECTED.equals(action)) {
|
||||
// Set up an alarm for ACTION_CHARGING_STABLE - we don't want to kick off tasks
|
||||
// here if the user unplugs the phone immediately.
|
||||
mAlarm.set(AlarmManager.ELAPSED_REALTIME_WAKEUP,
|
||||
SystemClock.elapsedRealtime() + STABLE_CHARGING_THRESHOLD_MILLIS,
|
||||
mStableChargingTriggerIntent);
|
||||
mCharging = true;
|
||||
} else if (Intent.ACTION_POWER_DISCONNECTED.equals(action)) {
|
||||
// If an alarm is set, breathe a sigh of relief and cancel it - crisis averted.
|
||||
mAlarm.cancel(mStableChargingTriggerIntent);
|
||||
mCharging = false;
|
||||
maybeReportNewChargingState();
|
||||
}else if (ACTION_CHARGING_STABLE.equals(action)) {
|
||||
// Here's where we actually do the notify for a task being ready.
|
||||
if (DEBUG) {
|
||||
Slog.d(TAG, "Battery connected fired @ " + SystemClock.elapsedRealtime());
|
||||
}
|
||||
if (mCharging) { // Should never receive this intent if mCharging is false.
|
||||
maybeReportNewChargingState();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -27,6 +27,7 @@ import android.os.UserHandle;
|
||||
import android.util.Log;
|
||||
import android.util.Slog;
|
||||
|
||||
import com.android.server.task.StateChangedListener;
|
||||
import com.android.server.task.TaskManagerService;
|
||||
|
||||
import java.util.LinkedList;
|
||||
@@ -39,7 +40,6 @@ import java.util.List;
|
||||
*/
|
||||
public class ConnectivityController extends StateController {
|
||||
private static final String TAG = "TaskManager.Connectivity";
|
||||
private static final boolean DEBUG = true;
|
||||
|
||||
private final List<TaskStatus> mTrackedTasks = new LinkedList<TaskStatus>();
|
||||
private final BroadcastReceiver mConnectivityChangedReceiver =
|
||||
@@ -54,13 +54,13 @@ public class ConnectivityController extends StateController {
|
||||
|
||||
public static synchronized ConnectivityController get(TaskManagerService taskManager) {
|
||||
if (mSingleton == null) {
|
||||
mSingleton = new ConnectivityController(taskManager);
|
||||
mSingleton = new ConnectivityController(taskManager, taskManager.getContext());
|
||||
}
|
||||
return mSingleton;
|
||||
}
|
||||
|
||||
private ConnectivityController(TaskManagerService service) {
|
||||
super(service);
|
||||
private ConnectivityController(StateChangedListener stateChangedListener, Context context) {
|
||||
super(stateChangedListener, context);
|
||||
// Register connectivity changed BR.
|
||||
IntentFilter intentFilter = new IntentFilter();
|
||||
intentFilter.addAction(ConnectivityManager.CONNECTIVITY_ACTION);
|
||||
|
||||
@@ -28,11 +28,11 @@ import android.content.IntentFilter;
|
||||
import android.os.SystemClock;
|
||||
import android.util.Slog;
|
||||
|
||||
import com.android.server.task.StateChangedListener;
|
||||
import com.android.server.task.TaskManagerService;
|
||||
|
||||
public class IdleController extends StateController {
|
||||
private static final String TAG = "IdleController";
|
||||
private static final boolean DEBUG = false;
|
||||
|
||||
// Policy: we decide that we're "idle" if the device has been unused /
|
||||
// screen off or dreaming for at least this long
|
||||
@@ -52,14 +52,14 @@ public class IdleController extends StateController {
|
||||
public static IdleController get(TaskManagerService service) {
|
||||
synchronized (sCreationLock) {
|
||||
if (sController == null) {
|
||||
sController = new IdleController(service);
|
||||
sController = new IdleController(service, service.getContext());
|
||||
}
|
||||
return sController;
|
||||
}
|
||||
}
|
||||
|
||||
private IdleController(TaskManagerService service) {
|
||||
super(service);
|
||||
private IdleController(StateChangedListener stateChangedListener, Context context) {
|
||||
super(stateChangedListener, context);
|
||||
initIdleStateTracking();
|
||||
}
|
||||
|
||||
|
||||
@@ -27,13 +27,13 @@ import com.android.server.task.TaskManagerService;
|
||||
* are ready to run, or whether they must be stopped.
|
||||
*/
|
||||
public abstract class StateController {
|
||||
|
||||
protected static final boolean DEBUG = true;
|
||||
protected Context mContext;
|
||||
protected StateChangedListener mStateChangedListener;
|
||||
|
||||
public StateController(TaskManagerService service) {
|
||||
mStateChangedListener = service;
|
||||
mContext = service.getContext();
|
||||
public StateController(StateChangedListener stateChangedListener, Context context) {
|
||||
mStateChangedListener = stateChangedListener;
|
||||
mContext = context;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -18,7 +18,7 @@ package com.android.server.task.controllers;
|
||||
|
||||
import android.app.task.Task;
|
||||
import android.content.ComponentName;
|
||||
import android.os.Bundle;
|
||||
import android.os.PersistableBundle;
|
||||
import android.os.SystemClock;
|
||||
import android.os.UserHandle;
|
||||
|
||||
@@ -37,6 +37,9 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
||||
* @hide
|
||||
*/
|
||||
public class TaskStatus {
|
||||
public static final long DEFAULT_LATEST_RUNTIME = Long.MAX_VALUE;
|
||||
public static final long DEFAULT_EARLIEST_RUNTIME = 0L;
|
||||
|
||||
final Task task;
|
||||
final int uId;
|
||||
|
||||
@@ -61,7 +64,7 @@ public class TaskStatus {
|
||||
* indicates there is no deadline constraint. See {@link #hasDeadlineConstraint()}.
|
||||
*/
|
||||
private long latestRunTimeElapsedMillis;
|
||||
|
||||
/** How many times this task has failed, used to compute back-off. */
|
||||
private final int numFailures;
|
||||
|
||||
/** Provide a handle to the service that this task will be run on. */
|
||||
@@ -69,36 +72,52 @@ public class TaskStatus {
|
||||
return uId;
|
||||
}
|
||||
|
||||
/** Create a newly scheduled task. */
|
||||
public TaskStatus(Task task, int uId, boolean persisted) {
|
||||
private TaskStatus(Task task, int uId, boolean persisted, int numFailures) {
|
||||
this.task = task;
|
||||
this.uId = uId;
|
||||
this.numFailures = 0;
|
||||
this.numFailures = numFailures;
|
||||
this.persisted = persisted;
|
||||
}
|
||||
|
||||
/** Create a newly scheduled task. */
|
||||
public TaskStatus(Task task, int uId, boolean persisted) {
|
||||
this(task, uId, persisted, 0);
|
||||
|
||||
final long elapsedNow = SystemClock.elapsedRealtime();
|
||||
// Timing constraints
|
||||
|
||||
if (task.isPeriodic()) {
|
||||
earliestRunTimeElapsedMillis = elapsedNow;
|
||||
latestRunTimeElapsedMillis = elapsedNow + task.getIntervalMillis();
|
||||
} else {
|
||||
earliestRunTimeElapsedMillis = task.hasEarlyConstraint() ?
|
||||
elapsedNow + task.getMinLatencyMillis() : 0L;
|
||||
elapsedNow + task.getMinLatencyMillis() : DEFAULT_EARLIEST_RUNTIME;
|
||||
latestRunTimeElapsedMillis = task.hasLateConstraint() ?
|
||||
elapsedNow + task.getMaxExecutionDelayMillis() : Long.MAX_VALUE;
|
||||
elapsedNow + task.getMaxExecutionDelayMillis() : DEFAULT_LATEST_RUNTIME;
|
||||
}
|
||||
}
|
||||
|
||||
public TaskStatus(TaskStatus rescheduling, long newEarliestRuntimeElapsed,
|
||||
long newLatestRuntimeElapsed, int backoffAttempt) {
|
||||
this.task = rescheduling.task;
|
||||
/**
|
||||
* Create a new TaskStatus that was loaded from disk. We ignore the provided
|
||||
* {@link android.app.task.Task} time criteria because we can load a persisted periodic task
|
||||
* from the {@link com.android.server.task.TaskStore} and still want to respect its
|
||||
* wallclock runtime rather than resetting it on every boot.
|
||||
* We consider a freshly loaded task to no longer be in back-off.
|
||||
*/
|
||||
public TaskStatus(Task task, int uId, long earliestRunTimeElapsedMillis,
|
||||
long latestRunTimeElapsedMillis) {
|
||||
this(task, uId, true, 0);
|
||||
|
||||
this.uId = rescheduling.getUid();
|
||||
this.persisted = rescheduling.isPersisted();
|
||||
this.numFailures = backoffAttempt;
|
||||
this.earliestRunTimeElapsedMillis = earliestRunTimeElapsedMillis;
|
||||
this.latestRunTimeElapsedMillis = latestRunTimeElapsedMillis;
|
||||
}
|
||||
|
||||
earliestRunTimeElapsedMillis = newEarliestRuntimeElapsed;
|
||||
latestRunTimeElapsedMillis = newLatestRuntimeElapsed;
|
||||
/** Create a new task to be rescheduled with the provided parameters. */
|
||||
public TaskStatus(TaskStatus rescheduling, long newEarliestRuntimeElapsedMillis,
|
||||
long newLatestRuntimeElapsedMillis, int backoffAttempt) {
|
||||
this(rescheduling.task, rescheduling.getUid(), rescheduling.isPersisted(), backoffAttempt);
|
||||
|
||||
earliestRunTimeElapsedMillis = newEarliestRuntimeElapsedMillis;
|
||||
latestRunTimeElapsedMillis = newLatestRuntimeElapsedMillis;
|
||||
}
|
||||
|
||||
public Task getTask() {
|
||||
@@ -125,7 +144,7 @@ public class TaskStatus {
|
||||
return uId;
|
||||
}
|
||||
|
||||
public Bundle getExtras() {
|
||||
public PersistableBundle getExtras() {
|
||||
return task.getExtras();
|
||||
}
|
||||
|
||||
@@ -142,11 +161,11 @@ public class TaskStatus {
|
||||
}
|
||||
|
||||
public boolean hasTimingDelayConstraint() {
|
||||
return earliestRunTimeElapsedMillis != 0L;
|
||||
return earliestRunTimeElapsedMillis != DEFAULT_EARLIEST_RUNTIME;
|
||||
}
|
||||
|
||||
public boolean hasDeadlineConstraint() {
|
||||
return latestRunTimeElapsedMillis != Long.MAX_VALUE;
|
||||
return latestRunTimeElapsedMillis != DEFAULT_LATEST_RUNTIME;
|
||||
}
|
||||
|
||||
public boolean hasIdleConstraint() {
|
||||
|
||||
@@ -24,6 +24,7 @@ import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.os.SystemClock;
|
||||
|
||||
import com.android.server.task.StateChangedListener;
|
||||
import com.android.server.task.TaskManagerService;
|
||||
|
||||
import java.util.Iterator;
|
||||
@@ -58,13 +59,13 @@ public class TimeController extends StateController {
|
||||
|
||||
public static synchronized TimeController get(TaskManagerService taskManager) {
|
||||
if (mSingleton == null) {
|
||||
mSingleton = new TimeController(taskManager);
|
||||
mSingleton = new TimeController(taskManager, taskManager.getContext());
|
||||
}
|
||||
return mSingleton;
|
||||
}
|
||||
|
||||
private TimeController(TaskManagerService service) {
|
||||
super(service);
|
||||
private TimeController(StateChangedListener stateChangedListener, Context context) {
|
||||
super(stateChangedListener, context);
|
||||
mTaskExpiredAlarmIntent =
|
||||
PendingIntent.getBroadcast(mContext, 0 /* ignored */,
|
||||
new Intent(ACTION_TASK_EXPIRED), 0);
|
||||
|
||||
@@ -0,0 +1,199 @@
|
||||
package com.android.server.task;
|
||||
|
||||
|
||||
import android.content.ComponentName;
|
||||
import android.content.Context;
|
||||
import android.app.task.Task;
|
||||
import android.app.task.Task.Builder;
|
||||
import android.os.PersistableBundle;
|
||||
import android.test.AndroidTestCase;
|
||||
import android.test.RenamingDelegatingContext;
|
||||
import android.util.Log;
|
||||
|
||||
import com.android.server.task.controllers.TaskStatus;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static com.android.server.task.TaskStore.initAndGet;
|
||||
/**
|
||||
* Test reading and writing correctly from file.
|
||||
*/
|
||||
public class TaskStoreTest extends AndroidTestCase {
|
||||
private static final String TAG = "TaskStoreTest";
|
||||
private static final String TEST_PREFIX = "_test_";
|
||||
// private static final int USER_NON_0 = 3;
|
||||
private static final int SOME_UID = 34234;
|
||||
private ComponentName mComponent;
|
||||
private static final long IO_WAIT = 600L;
|
||||
|
||||
TaskStore mTaskStoreUnderTest;
|
||||
Context mTestContext;
|
||||
TaskMapReadFinishedListener mTaskMapReadFinishedListenerStub =
|
||||
new TaskMapReadFinishedListener() {
|
||||
@Override
|
||||
public void onTaskMapReadFinished(List<TaskStatus> tasks) {
|
||||
// do nothing.
|
||||
}
|
||||
};
|
||||
|
||||
@Override
|
||||
public void setUp() throws Exception {
|
||||
mTestContext = new RenamingDelegatingContext(getContext(), TEST_PREFIX);
|
||||
Log.d(TAG, "Saving tasks to '" + mTestContext.getFilesDir() + "'");
|
||||
mTaskStoreUnderTest = TaskStore.initAndGetForTesting(mTestContext,
|
||||
mTestContext.getFilesDir(), mTaskMapReadFinishedListenerStub);
|
||||
mComponent = new ComponentName(getContext().getPackageName(), StubClass.class.getName());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void tearDown() throws Exception {
|
||||
mTaskStoreUnderTest.clear();
|
||||
}
|
||||
|
||||
public void testMaybeWriteStatusToDisk() throws Exception {
|
||||
int taskId = 5;
|
||||
long runByMillis = 20000L; // 20s
|
||||
long runFromMillis = 2000L; // 2s
|
||||
long initialBackoff = 10000L; // 10s
|
||||
|
||||
final Task task = new Builder(taskId, mComponent)
|
||||
.setRequiresCharging(true)
|
||||
.setRequiredNetworkCapabilities(Task.NetworkType.ANY)
|
||||
.setBackoffCriteria(initialBackoff, Task.BackoffPolicy.EXPONENTIAL)
|
||||
.setOverrideDeadline(runByMillis)
|
||||
.setMinimumLatency(runFromMillis)
|
||||
.build();
|
||||
final TaskStatus ts = new TaskStatus(task, SOME_UID, true /* persisted */);
|
||||
mTaskStoreUnderTest.add(ts);
|
||||
Thread.sleep(IO_WAIT);
|
||||
// Manually load tasks from xml file.
|
||||
mTaskStoreUnderTest.readTaskMapFromDisk(new TaskMapReadFinishedListener() {
|
||||
@Override
|
||||
public void onTaskMapReadFinished(List<TaskStatus> tasks) {
|
||||
assertEquals("Didn't get expected number of persisted tasks.", 1, tasks.size());
|
||||
TaskStatus loadedTaskStatus = tasks.get(0);
|
||||
assertTasksEqual(task, loadedTaskStatus.getTask());
|
||||
assertEquals("Different uids.", SOME_UID, tasks.get(0).getUid());
|
||||
compareTimestampsSubjectToIoLatency("Early run-times not the same after read.",
|
||||
ts.getEarliestRunTime(), loadedTaskStatus.getEarliestRunTime());
|
||||
compareTimestampsSubjectToIoLatency("Late run-times not the same after read.",
|
||||
ts.getLatestRunTimeElapsed(), loadedTaskStatus.getLatestRunTimeElapsed());
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
public void testWritingTwoFilesToDisk() throws Exception {
|
||||
final Task task1 = new Builder(8, mComponent)
|
||||
.setRequiresDeviceIdle(true)
|
||||
.setPeriodic(10000L)
|
||||
.setRequiresCharging(true)
|
||||
.build();
|
||||
final Task task2 = new Builder(12, mComponent)
|
||||
.setMinimumLatency(5000L)
|
||||
.setBackoffCriteria(15000L, Task.BackoffPolicy.LINEAR)
|
||||
.setOverrideDeadline(30000L)
|
||||
.setRequiredNetworkCapabilities(Task.NetworkType.UNMETERED)
|
||||
.build();
|
||||
final TaskStatus taskStatus1 = new TaskStatus(task1, SOME_UID, true /* persisted */);
|
||||
final TaskStatus taskStatus2 = new TaskStatus(task2, SOME_UID, true /* persisted */);
|
||||
mTaskStoreUnderTest.add(taskStatus1);
|
||||
mTaskStoreUnderTest.add(taskStatus2);
|
||||
Thread.sleep(IO_WAIT);
|
||||
mTaskStoreUnderTest.readTaskMapFromDisk(new TaskMapReadFinishedListener() {
|
||||
@Override
|
||||
public void onTaskMapReadFinished(List<TaskStatus> tasks) {
|
||||
assertEquals("Incorrect # of persisted tasks.", 2, tasks.size());
|
||||
TaskStatus loaded1 = tasks.get(0);
|
||||
TaskStatus loaded2 = tasks.get(1);
|
||||
assertTasksEqual(task1, loaded1.getTask());
|
||||
assertTasksEqual(task2, loaded2.getTask());
|
||||
|
||||
// Check that the loaded task has the correct runtimes.
|
||||
compareTimestampsSubjectToIoLatency("Early run-times not the same after read.",
|
||||
taskStatus1.getEarliestRunTime(), loaded1.getEarliestRunTime());
|
||||
compareTimestampsSubjectToIoLatency("Late run-times not the same after read.",
|
||||
taskStatus1.getLatestRunTimeElapsed(), loaded1.getLatestRunTimeElapsed());
|
||||
compareTimestampsSubjectToIoLatency("Early run-times not the same after read.",
|
||||
taskStatus2.getEarliestRunTime(), loaded2.getEarliestRunTime());
|
||||
compareTimestampsSubjectToIoLatency("Late run-times not the same after read.",
|
||||
taskStatus2.getLatestRunTimeElapsed(), loaded2.getLatestRunTimeElapsed());
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
public void testWritingTaskWithExtras() throws Exception {
|
||||
Task.Builder b = new Builder(8, mComponent)
|
||||
.setRequiresDeviceIdle(true)
|
||||
.setPeriodic(10000L)
|
||||
.setRequiresCharging(true);
|
||||
|
||||
PersistableBundle extras = new PersistableBundle();
|
||||
extras.putDouble("hello", 3.2);
|
||||
extras.putString("hi", "there");
|
||||
extras.putInt("into", 3);
|
||||
b.setExtras(extras);
|
||||
final Task task = b.build();
|
||||
TaskStatus taskStatus = new TaskStatus(task, SOME_UID, true /* persisted */);
|
||||
|
||||
mTaskStoreUnderTest.add(taskStatus);
|
||||
Thread.sleep(IO_WAIT);
|
||||
mTaskStoreUnderTest.readTaskMapFromDisk(new TaskMapReadFinishedListener() {
|
||||
@Override
|
||||
public void onTaskMapReadFinished(List<TaskStatus> tasks) {
|
||||
assertEquals("Incorrect # of persisted tasks.", 1, tasks.size());
|
||||
TaskStatus loaded = tasks.get(0);
|
||||
assertTasksEqual(task, loaded.getTask());
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to throw an error if the provided task and TaskStatus objects are not equal.
|
||||
*/
|
||||
private void assertTasksEqual(Task first, Task second) {
|
||||
assertEquals("Different task ids.", first.getId(), second.getId());
|
||||
assertEquals("Different components.", first.getService(), second.getService());
|
||||
assertEquals("Different periodic status.", first.isPeriodic(), second.isPeriodic());
|
||||
assertEquals("Different period.", first.getIntervalMillis(), second.getIntervalMillis());
|
||||
assertEquals("Different inital backoff.", first.getInitialBackoffMillis(),
|
||||
second.getInitialBackoffMillis());
|
||||
assertEquals("Different backoff policy.", first.getBackoffPolicy(),
|
||||
second.getBackoffPolicy());
|
||||
|
||||
assertEquals("Invalid charging constraint.", first.isRequireCharging(),
|
||||
second.isRequireCharging());
|
||||
assertEquals("Invalid idle constraint.", first.isRequireDeviceIdle(),
|
||||
second.isRequireDeviceIdle());
|
||||
assertEquals("Invalid unmetered constraint.",
|
||||
first.getNetworkCapabilities() == Task.NetworkType.UNMETERED,
|
||||
second.getNetworkCapabilities() == Task.NetworkType.UNMETERED);
|
||||
assertEquals("Invalid connectivity constraint.",
|
||||
first.getNetworkCapabilities() == Task.NetworkType.ANY,
|
||||
second.getNetworkCapabilities() == Task.NetworkType.ANY);
|
||||
assertEquals("Invalid deadline constraint.",
|
||||
first.hasLateConstraint(),
|
||||
second.hasLateConstraint());
|
||||
assertEquals("Invalid delay constraint.",
|
||||
first.hasEarlyConstraint(),
|
||||
second.hasEarlyConstraint());
|
||||
assertEquals("Extras don't match",
|
||||
first.getExtras().toString(), second.getExtras().toString());
|
||||
}
|
||||
|
||||
/**
|
||||
* When comparing timestamps before and after DB read/writes (to make sure we're saving/loading
|
||||
* the correct values), there is some latency involved that terrorises a naive assertEquals().
|
||||
* We define a <code>DELTA_MILLIS</code> as a function variable here to make this comparision
|
||||
* more reasonable.
|
||||
*/
|
||||
private void compareTimestampsSubjectToIoLatency(String error, long ts1, long ts2) {
|
||||
final long DELTA_MILLIS = 700L; // We allow up to 700ms of latency for IO read/writes.
|
||||
assertTrue(error, Math.abs(ts1 - ts2) < DELTA_MILLIS + IO_WAIT);
|
||||
}
|
||||
|
||||
private static class StubClass {}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
/*
|
||||
* 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.content.ComponentName;
|
||||
import android.content.Intent;
|
||||
import android.test.AndroidTestCase;
|
||||
|
||||
import com.android.server.task.StateChangedListener;
|
||||
|
||||
import static com.android.server.task.controllers.BatteryController.getForTesting;
|
||||
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
public class BatteryControllerTest extends AndroidTestCase {
|
||||
BatteryController mBatteryControllerUnderTest;
|
||||
|
||||
StateChangedListener mStateChangedListenerStub = new StateChangedListener() {
|
||||
@Override
|
||||
public void onControllerStateChanged() {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTaskDeadlineExpired(TaskStatus taskStatus) {
|
||||
|
||||
}
|
||||
};
|
||||
BatteryController.ChargingTracker mTrackerUnderTest;
|
||||
|
||||
public void setUp() throws Exception {
|
||||
mBatteryControllerUnderTest = getForTesting(mStateChangedListenerStub, getTestContext());
|
||||
mTrackerUnderTest = mBatteryControllerUnderTest.getTracker();
|
||||
}
|
||||
|
||||
public void testSendBatteryChargingIntent() throws Exception {
|
||||
Intent batteryConnectedIntent = new Intent(Intent.ACTION_POWER_CONNECTED)
|
||||
.setComponent(new ComponentName(getContext(), mTrackerUnderTest.getClass()));
|
||||
Intent batteryHealthyIntent = new Intent(Intent.ACTION_BATTERY_OKAY)
|
||||
.setComponent(new ComponentName(getContext(), mTrackerUnderTest.getClass()));
|
||||
|
||||
mTrackerUnderTest.onReceiveInternal(batteryConnectedIntent);
|
||||
mTrackerUnderTest.onReceiveInternal(batteryHealthyIntent);
|
||||
|
||||
assertTrue(mTrackerUnderTest.isOnStablePower());
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user