Out with the old; in with the new
Switch to the official "JobScheduler" etc naming.
Bug 14997851
Change-Id: I73a61aaa9af0740c114d08188bd97c52f3ac86b7
diff --git a/services/core/java/com/android/server/job/JobCompletedListener.java b/services/core/java/com/android/server/job/JobCompletedListener.java
new file mode 100644
index 0000000..a7af9cd
--- /dev/null
+++ b/services/core/java/com/android/server/job/JobCompletedListener.java
@@ -0,0 +1,32 @@
+/*
+ * 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;
+
+import com.android.server.job.controllers.JobStatus;
+
+/**
+ * Used for communication between {@link com.android.server.job.JobServiceContext} and the
+ * {@link com.android.server.job.JobSchedulerService}.
+ */
+public interface JobCompletedListener {
+
+ /**
+ * Callback for when a job is completed.
+ * @param needsReschedule Whether the implementing class should reschedule this job.
+ */
+ public void onJobCompleted(JobStatus jobStatus, boolean needsReschedule);
+}
diff --git a/services/core/java/com/android/server/job/JobMapReadFinishedListener.java b/services/core/java/com/android/server/job/JobMapReadFinishedListener.java
new file mode 100644
index 0000000..f3e77e6
--- /dev/null
+++ b/services/core/java/com/android/server/job/JobMapReadFinishedListener.java
@@ -0,0 +1,34 @@
+/*
+ * 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;
+
+import java.util.List;
+
+import com.android.server.job.controllers.JobStatus;
+
+/**
+ * Callback definition for I/O thread to let the JobManagerService know when
+ * I/O read has completed. Done this way so we don't stall the main thread on
+ * boot.
+ */
+public interface JobMapReadFinishedListener {
+
+ /**
+ * Called by the {@link JobStore} at boot, when the disk read is finished.
+ */
+ public void onJobMapReadFinished(List<JobStatus> jobs);
+}
diff --git a/services/core/java/com/android/server/job/JobSchedulerService.java b/services/core/java/com/android/server/job/JobSchedulerService.java
new file mode 100644
index 0000000..0e9a9cc
--- /dev/null
+++ b/services/core/java/com/android/server/job/JobSchedulerService.java
@@ -0,0 +1,764 @@
+/*
+ * 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;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+
+import android.app.job.JobInfo;
+import android.app.job.JobScheduler;
+import android.app.job.JobService;
+import android.app.job.IJobScheduler;
+import android.content.BroadcastReceiver;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.content.pm.ServiceInfo;
+import android.os.Binder;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import android.os.RemoteException;
+import android.os.SystemClock;
+import android.os.UserHandle;
+import android.util.Slog;
+import android.util.SparseArray;
+
+import com.android.server.job.controllers.BatteryController;
+import com.android.server.job.controllers.ConnectivityController;
+import com.android.server.job.controllers.IdleController;
+import com.android.server.job.controllers.JobStatus;
+import com.android.server.job.controllers.StateController;
+import com.android.server.job.controllers.TimeController;
+
+import java.util.LinkedList;
+
+/**
+ * Responsible for taking jobs representing work to be performed by a client app, and determining
+ * based on the criteria specified when that job should be run against the client application's
+ * endpoint.
+ * Implements logic for scheduling, and rescheduling jobs. The JobSchedulerService knows nothing
+ * about constraints, or the state of active jobs. It receives callbacks from the various
+ * controllers and completed jobs and operates accordingly.
+ *
+ * Note on locking: Any operations that manipulate {@link #mJobs} need to lock on that object.
+ * Any function with the suffix 'Locked' also needs to lock on {@link #mJobs}.
+ * @hide
+ */
+public class JobSchedulerService extends com.android.server.SystemService
+ implements StateChangedListener, JobCompletedListener, JobMapReadFinishedListener {
+ // TODO: Switch this off for final version.
+ static final boolean DEBUG = true;
+ /** The number of concurrent jobs we run at one time. */
+ private static final int MAX_JOB_CONTEXTS_COUNT = 3;
+ static final String TAG = "JobManagerService";
+ /** Master list of jobs. */
+ private final JobStore mJobs;
+
+ static final int MSG_JOB_EXPIRED = 0;
+ static final int MSG_CHECK_JOB = 1;
+
+ // Policy constants
+ /**
+ * Minimum # of idle jobs that must be ready in order to force the JMS to schedule things
+ * early.
+ */
+ private static final int MIN_IDLE_COUNT = 1;
+ /**
+ * Minimum # of connectivity jobs that must be ready in order to force the JMS to schedule
+ * things early.
+ */
+ private static final int MIN_CONNECTIVITY_COUNT = 2;
+ /**
+ * Minimum # of jobs (with no particular constraints) for which the JMS will be happy running
+ * some work early.
+ */
+ private static final int MIN_READY_JOBS_COUNT = 4;
+
+ /**
+ * Track Services that have currently active or pending jobs. The index is provided by
+ * {@link JobStatus#getServiceToken()}
+ */
+ private final List<JobServiceContext> mActiveServices = new LinkedList<JobServiceContext>();
+ /** List of controllers that will notify this service of updates to jobs. */
+ private List<StateController> mControllers;
+ /**
+ * Queue of pending jobs. The JobServiceContext class will receive jobs from this list
+ * when ready to execute them.
+ */
+ private final LinkedList<JobStatus> mPendingJobs = new LinkedList<JobStatus>();
+
+ private final JobHandler mHandler;
+ private final JobSchedulerStub mJobSchedulerStub;
+ /**
+ * Cleans up outstanding jobs when a package is removed. Even if it's being replaced later we
+ * still clean up. On reinstall the package will have a new uid.
+ */
+ private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ Slog.d(TAG, "Receieved: " + intent.getAction());
+ if (Intent.ACTION_PACKAGE_REMOVED.equals(intent.getAction())) {
+ int uidRemoved = intent.getIntExtra(Intent.EXTRA_UID, -1);
+ if (DEBUG) {
+ Slog.d(TAG, "Removing jobs for uid: " + uidRemoved);
+ }
+ cancelJobsForUid(uidRemoved);
+ } else if (Intent.ACTION_USER_REMOVED.equals(intent.getAction())) {
+ final int userId = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, 0);
+ if (DEBUG) {
+ Slog.d(TAG, "Removing jobs for user: " + userId);
+ }
+ cancelJobsForUser(userId);
+ }
+ }
+ };
+
+ /**
+ * Entry point from client to schedule the provided job.
+ * This cancels the job if it's already been scheduled, and replaces it with the one provided.
+ * @param job JobInfo object containing execution parameters
+ * @param uId The package identifier of the application this job is for.
+ * @param canPersistJob Whether or not the client has the appropriate permissions for
+ * persisting this job.
+ * @return Result of this operation. See <code>JobScheduler#RESULT_*</code> return codes.
+ */
+ public int schedule(JobInfo job, int uId, boolean canPersistJob) {
+ JobStatus jobStatus = new JobStatus(job, uId, canPersistJob);
+ cancelJob(uId, job.getId());
+ startTrackingJob(jobStatus);
+ return JobScheduler.RESULT_SUCCESS;
+ }
+
+ public List<JobInfo> getPendingJobs(int uid) {
+ ArrayList<JobInfo> outList = new ArrayList<JobInfo>();
+ synchronized (mJobs) {
+ for (JobStatus job : mJobs.getJobs()) {
+ if (job.getUid() == uid) {
+ outList.add(job.getJob());
+ }
+ }
+ }
+ return outList;
+ }
+
+ private void cancelJobsForUser(int userHandle) {
+ synchronized (mJobs) {
+ List<JobStatus> jobsForUser = mJobs.getJobsByUser(userHandle);
+ for (JobStatus toRemove : jobsForUser) {
+ if (DEBUG) {
+ Slog.d(TAG, "Cancelling: " + toRemove);
+ }
+ cancelJobLocked(toRemove);
+ }
+ }
+ }
+
+ /**
+ * Entry point from client to cancel all jobs originating from their uid.
+ * This will remove the job from the master list, and cancel the job if it was staged for
+ * execution or being executed.
+ * @param uid To check against for removal of a job.
+ */
+ public void cancelJobsForUid(int uid) {
+ // Remove from master list.
+ synchronized (mJobs) {
+ List<JobStatus> jobsForUid = mJobs.getJobsByUid(uid);
+ for (JobStatus toRemove : jobsForUid) {
+ if (DEBUG) {
+ Slog.d(TAG, "Cancelling: " + toRemove);
+ }
+ cancelJobLocked(toRemove);
+ }
+ }
+ }
+
+ /**
+ * Entry point from client to cancel the job corresponding to the jobId provided.
+ * This will remove the job from the master list, and cancel the job if it was staged for
+ * execution or being executed.
+ * @param uid Uid of the calling client.
+ * @param jobId Id of the job, provided at schedule-time.
+ */
+ public void cancelJob(int uid, int jobId) {
+ JobStatus toCancel;
+ synchronized (mJobs) {
+ toCancel = mJobs.getJobByUidAndJobId(uid, jobId);
+ if (toCancel != null) {
+ cancelJobLocked(toCancel);
+ }
+ }
+ }
+
+ private void cancelJobLocked(JobStatus cancelled) {
+ // Remove from store.
+ stopTrackingJob(cancelled);
+ // Remove from pending queue.
+ mPendingJobs.remove(cancelled);
+ // Cancel if running.
+ stopJobOnServiceContextLocked(cancelled);
+ }
+
+ /**
+ * Initializes the system service.
+ * <p>
+ * Subclasses must define a single argument constructor that accepts the context
+ * and passes it to super.
+ * </p>
+ *
+ * @param context The system server context.
+ */
+ public JobSchedulerService(Context context) {
+ super(context);
+ // Create the controllers.
+ mControllers = new LinkedList<StateController>();
+ mControllers.add(ConnectivityController.get(this));
+ mControllers.add(TimeController.get(this));
+ mControllers.add(IdleController.get(this));
+ mControllers.add(BatteryController.get(this));
+
+ mHandler = new JobHandler(context.getMainLooper());
+ mJobSchedulerStub = new JobSchedulerStub();
+ // Create the "runners".
+ for (int i = 0; i < MAX_JOB_CONTEXTS_COUNT; i++) {
+ mActiveServices.add(
+ new JobServiceContext(this, context.getMainLooper()));
+ }
+ mJobs = JobStore.initAndGet(this);
+ }
+
+ @Override
+ public void onStart() {
+ publishBinderService(Context.JOB_SCHEDULER_SERVICE, mJobSchedulerStub);
+ }
+
+ @Override
+ public void onBootPhase(int phase) {
+ if (PHASE_SYSTEM_SERVICES_READY == phase) {
+ // Register br for package removals and user removals.
+ final IntentFilter filter = new IntentFilter(Intent.ACTION_PACKAGE_REMOVED);
+ filter.addDataScheme("package");
+ getContext().registerReceiverAsUser(
+ mBroadcastReceiver, UserHandle.ALL, filter, null, null);
+ final IntentFilter userFilter = new IntentFilter(Intent.ACTION_USER_REMOVED);
+ getContext().registerReceiverAsUser(
+ mBroadcastReceiver, UserHandle.ALL, userFilter, null, null);
+ }
+ }
+
+ /**
+ * Called when we have a job status object that we need to insert in our
+ * {@link com.android.server.job.JobStore}, and make sure all the relevant controllers know
+ * about.
+ */
+ private void startTrackingJob(JobStatus jobStatus) {
+ boolean update;
+ synchronized (mJobs) {
+ update = mJobs.add(jobStatus);
+ }
+ for (StateController controller : mControllers) {
+ if (update) {
+ controller.maybeStopTrackingJob(jobStatus);
+ }
+ controller.maybeStartTrackingJob(jobStatus);
+ }
+ }
+
+ /**
+ * Called when we want to remove a JobStatus object that we've finished executing. Returns the
+ * object removed.
+ */
+ private boolean stopTrackingJob(JobStatus jobStatus) {
+ boolean removed;
+ synchronized (mJobs) {
+ // Remove from store as well as controllers.
+ removed = mJobs.remove(jobStatus);
+ }
+ if (removed) {
+ for (StateController controller : mControllers) {
+ controller.maybeStopTrackingJob(jobStatus);
+ }
+ }
+ return removed;
+ }
+
+ private boolean stopJobOnServiceContextLocked(JobStatus job) {
+ for (JobServiceContext jsc : mActiveServices) {
+ final JobStatus executing = jsc.getRunningJob();
+ if (executing != null && executing.matches(job.getUid(), job.getJobId())) {
+ jsc.cancelExecutingJob();
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * @param job JobStatus we are querying against.
+ * @return Whether or not the job represented by the status object is currently being run or
+ * is pending.
+ */
+ private boolean isCurrentlyActiveLocked(JobStatus job) {
+ for (JobServiceContext serviceContext : mActiveServices) {
+ final JobStatus running = serviceContext.getRunningJob();
+ if (running != null && running.matches(job.getUid(), job.getJobId())) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * A job is rescheduled with exponential back-off if the client requests this from their
+ * execution logic.
+ * A caveat is for idle-mode jobs, for which the idle-mode constraint will usurp the
+ * timeliness of the reschedule. For an idle-mode job, no deadline is given.
+ * @param failureToReschedule Provided job status that we will reschedule.
+ * @return A newly instantiated JobStatus with the same constraints as the last job except
+ * with adjusted timing constraints.
+ */
+ private JobStatus getRescheduleJobForFailure(JobStatus failureToReschedule) {
+ final long elapsedNowMillis = SystemClock.elapsedRealtime();
+ final JobInfo job = failureToReschedule.getJob();
+
+ final long initialBackoffMillis = job.getInitialBackoffMillis();
+ final int backoffAttempt = failureToReschedule.getNumFailures() + 1;
+ long newEarliestRuntimeElapsed = elapsedNowMillis;
+
+ switch (job.getBackoffPolicy()) {
+ case JobInfo.BackoffPolicy.LINEAR:
+ newEarliestRuntimeElapsed += initialBackoffMillis * backoffAttempt;
+ break;
+ default:
+ if (DEBUG) {
+ Slog.v(TAG, "Unrecognised back-off policy, defaulting to exponential.");
+ }
+ case JobInfo.BackoffPolicy.EXPONENTIAL:
+ newEarliestRuntimeElapsed +=
+ Math.pow(initialBackoffMillis * 0.001, backoffAttempt) * 1000;
+ break;
+ }
+ newEarliestRuntimeElapsed =
+ Math.min(newEarliestRuntimeElapsed, JobInfo.MAX_BACKOFF_DELAY_MILLIS);
+ return new JobStatus(failureToReschedule, newEarliestRuntimeElapsed,
+ JobStatus.NO_LATEST_RUNTIME, backoffAttempt);
+ }
+
+ /**
+ * Called after a periodic has executed so we can to re-add it. We take the last execution time
+ * of the job to be the time of completion (i.e. the time at which this function is called).
+ * This could be inaccurate b/c the job can run for as long as
+ * {@link com.android.server.job.JobServiceContext#EXECUTING_TIMESLICE_MILLIS}, but will lead
+ * to underscheduling at least, rather than if we had taken the last execution time to be the
+ * start of the execution.
+ * @return A new job representing the execution criteria for this instantiation of the
+ * recurring job.
+ */
+ private JobStatus getRescheduleJobForPeriodic(JobStatus periodicToReschedule) {
+ final long elapsedNow = SystemClock.elapsedRealtime();
+ // Compute how much of the period is remaining.
+ long runEarly = Math.max(periodicToReschedule.getLatestRunTimeElapsed() - elapsedNow, 0);
+ long newEarliestRunTimeElapsed = elapsedNow + runEarly;
+ long period = periodicToReschedule.getJob().getIntervalMillis();
+ long newLatestRuntimeElapsed = newEarliestRunTimeElapsed + period;
+
+ if (DEBUG) {
+ Slog.v(TAG, "Rescheduling executed periodic. New execution window [" +
+ newEarliestRunTimeElapsed/1000 + ", " + newLatestRuntimeElapsed/1000 + "]s");
+ }
+ return new JobStatus(periodicToReschedule, newEarliestRunTimeElapsed,
+ newLatestRuntimeElapsed, 0 /* backoffAttempt */);
+ }
+
+ // JobCompletedListener implementations.
+
+ /**
+ * A job just finished executing. We fetch the
+ * {@link com.android.server.job.controllers.JobStatus} from the store and depending on
+ * whether we want to reschedule we readd it to the controllers.
+ * @param jobStatus Completed job.
+ * @param needsReschedule Whether the implementing class should reschedule this job.
+ */
+ @Override
+ public void onJobCompleted(JobStatus jobStatus, boolean needsReschedule) {
+ if (DEBUG) {
+ Slog.d(TAG, "Completed " + jobStatus + ", reschedule=" + needsReschedule);
+ }
+ if (!stopTrackingJob(jobStatus)) {
+ if (DEBUG) {
+ Slog.e(TAG, "Error removing job: could not find job to remove. Was job " +
+ "removed while executing?");
+ }
+ return;
+ }
+ if (needsReschedule) {
+ JobStatus rescheduled = getRescheduleJobForFailure(jobStatus);
+ startTrackingJob(rescheduled);
+ } else if (jobStatus.getJob().isPeriodic()) {
+ JobStatus rescheduledPeriodic = getRescheduleJobForPeriodic(jobStatus);
+ startTrackingJob(rescheduledPeriodic);
+ }
+ mHandler.obtainMessage(MSG_CHECK_JOB).sendToTarget();
+ }
+
+ // StateChangedListener implementations.
+
+ /**
+ * Off-board work to our handler thread as quickly as possible, b/c this call is probably being
+ * made on the main thread.
+ * For now this takes the job and if it's ready to run it will run it. In future we might not
+ * provide the job, so that the StateChangedListener has to run through its list of jobs to
+ * see which are ready. This will further decouple the controllers from the execution logic.
+ */
+ @Override
+ public void onControllerStateChanged() {
+ // Post a message to to run through the list of jobs and start/stop any that are eligible.
+ mHandler.obtainMessage(MSG_CHECK_JOB).sendToTarget();
+ }
+
+ @Override
+ public void onRunJobNow(JobStatus jobStatus) {
+ mHandler.obtainMessage(MSG_JOB_EXPIRED, jobStatus).sendToTarget();
+ }
+
+ /**
+ * Disk I/O is finished, take the list of jobs we read from disk and add them to our
+ * {@link JobStore}.
+ * 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 onJobMapReadFinished(List<JobStatus> jobs) {
+ synchronized (mJobs) {
+ for (JobStatus js : jobs) {
+ if (mJobs.containsJobIdForUid(js.getJobId(), js.getUid())) {
+ // An app with BOOT_COMPLETED *might* have decided to reschedule their job, in
+ // the same amount of time it took us to read it from disk. If this is the case
+ // we leave it be.
+ continue;
+ }
+ startTrackingJob(js);
+ }
+ }
+ }
+
+ private class JobHandler extends Handler {
+
+ public JobHandler(Looper looper) {
+ super(looper);
+ }
+
+ @Override
+ public void handleMessage(Message message) {
+ switch (message.what) {
+ case MSG_JOB_EXPIRED:
+ synchronized (mJobs) {
+ JobStatus runNow = (JobStatus) message.obj;
+ if (!mPendingJobs.contains(runNow)) {
+ mPendingJobs.add(runNow);
+ }
+ }
+ queueReadyJobsForExecutionH();
+ break;
+ case MSG_CHECK_JOB:
+ // Check the list of jobs and run some of them if we feel inclined.
+ maybeQueueReadyJobsForExecutionH();
+ break;
+ }
+ maybeRunPendingJobsH();
+ // Don't remove JOB_EXPIRED in case one came along while processing the queue.
+ removeMessages(MSG_CHECK_JOB);
+ }
+
+ /**
+ * Run through list of jobs and execute all possible - at least one is expired so we do
+ * as many as we can.
+ */
+ private void queueReadyJobsForExecutionH() {
+ synchronized (mJobs) {
+ for (JobStatus job : mJobs.getJobs()) {
+ if (isReadyToBeExecutedLocked(job)) {
+ mPendingJobs.add(job);
+ } else if (isReadyToBeCancelledLocked(job)) {
+ stopJobOnServiceContextLocked(job);
+ }
+ }
+ }
+ }
+
+ /**
+ * The state of at least one job has changed. Here is where we could enforce various
+ * policies on when we want to execute jobs.
+ * Right now the policy is such:
+ * If >1 of the ready jobs is idle mode we send all of them off
+ * if more than 2 network connectivity jobs are ready we send them all off.
+ * If more than 4 jobs total are ready we send them all off.
+ * TODO: It would be nice to consolidate these sort of high-level policies somewhere.
+ */
+ private void maybeQueueReadyJobsForExecutionH() {
+ synchronized (mJobs) {
+ int idleCount = 0;
+ int backoffCount = 0;
+ int connectivityCount = 0;
+ List<JobStatus> runnableJobs = new ArrayList<JobStatus>();
+ for (JobStatus job : mJobs.getJobs()) {
+ if (isReadyToBeExecutedLocked(job)) {
+ if (job.getNumFailures() > 0) {
+ backoffCount++;
+ }
+ if (job.hasIdleConstraint()) {
+ idleCount++;
+ }
+ if (job.hasConnectivityConstraint() || job.hasUnmeteredConstraint()) {
+ connectivityCount++;
+ }
+ runnableJobs.add(job);
+ } else if (isReadyToBeCancelledLocked(job)) {
+ stopJobOnServiceContextLocked(job);
+ }
+ }
+ if (backoffCount > 0 || idleCount >= MIN_IDLE_COUNT ||
+ connectivityCount >= MIN_CONNECTIVITY_COUNT ||
+ runnableJobs.size() >= MIN_READY_JOBS_COUNT) {
+ for (JobStatus job : runnableJobs) {
+ mPendingJobs.add(job);
+ }
+ }
+ }
+ }
+
+ /**
+ * Criteria for moving a job into the pending queue:
+ * - It's ready.
+ * - It's not pending.
+ * - It's not already running on a JSC.
+ */
+ private boolean isReadyToBeExecutedLocked(JobStatus job) {
+ return job.isReady() && !mPendingJobs.contains(job) && !isCurrentlyActiveLocked(job);
+ }
+
+ /**
+ * Criteria for cancelling an active job:
+ * - It's not ready
+ * - It's running on a JSC.
+ */
+ private boolean isReadyToBeCancelledLocked(JobStatus job) {
+ return !job.isReady() && isCurrentlyActiveLocked(job);
+ }
+
+ /**
+ * Reconcile jobs in the pending queue against available execution contexts.
+ * A controller can force a job into the pending queue even if it's already running, but
+ * here is where we decide whether to actually execute it.
+ */
+ private void maybeRunPendingJobsH() {
+ synchronized (mJobs) {
+ Iterator<JobStatus> it = mPendingJobs.iterator();
+ while (it.hasNext()) {
+ JobStatus nextPending = it.next();
+ JobServiceContext availableContext = null;
+ for (JobServiceContext jsc : mActiveServices) {
+ final JobStatus running = jsc.getRunningJob();
+ if (running != null && running.matches(nextPending.getUid(),
+ nextPending.getJobId())) {
+ // Already running this tId for this uId, skip.
+ availableContext = null;
+ break;
+ }
+ if (jsc.isAvailable()) {
+ availableContext = jsc;
+ }
+ }
+ if (availableContext != null) {
+ if (!availableContext.executeRunnableJob(nextPending)) {
+ if (DEBUG) {
+ Slog.d(TAG, "Error executing " + nextPending);
+ }
+ mJobs.remove(nextPending);
+ }
+ it.remove();
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Binder stub trampoline implementation
+ */
+ final class JobSchedulerStub extends IJobScheduler.Stub {
+ /** Cache determination of whether a given app can persist jobs
+ * key is uid of the calling app; value is undetermined/true/false
+ */
+ private final SparseArray<Boolean> mPersistCache = new SparseArray<Boolean>();
+
+ // Enforce that only the app itself (or shared uid participant) can schedule a
+ // job that runs one of the app's services, as well as verifying that the
+ // named service properly requires the BIND_JOB_SERVICE permission
+ private void enforceValidJobRequest(int uid, JobInfo job) {
+ final PackageManager pm = getContext().getPackageManager();
+ final ComponentName service = job.getService();
+ try {
+ ServiceInfo si = pm.getServiceInfo(service, 0);
+ if (si.applicationInfo.uid != uid) {
+ throw new IllegalArgumentException("uid " + uid +
+ " cannot schedule job in " + service.getPackageName());
+ }
+ if (!JobService.PERMISSION_BIND.equals(si.permission)) {
+ throw new IllegalArgumentException("Scheduled service " + service
+ + " does not require android.permission.BIND_JOB_SERVICE permission");
+ }
+ } catch (NameNotFoundException e) {
+ throw new IllegalArgumentException("No such service: " + service);
+ }
+ }
+
+ private boolean canPersistJobs(int pid, int uid) {
+ // If we get this far we're good to go; all we need to do now is check
+ // whether the app is allowed to persist its scheduled work.
+ final boolean canPersist;
+ synchronized (mPersistCache) {
+ Boolean cached = mPersistCache.get(uid);
+ if (cached != null) {
+ canPersist = cached.booleanValue();
+ } else {
+ // Persisting jobs is tantamount to running at boot, so we permit
+ // it when the app has declared that it uses the RECEIVE_BOOT_COMPLETED
+ // permission
+ int result = getContext().checkPermission(
+ android.Manifest.permission.RECEIVE_BOOT_COMPLETED, pid, uid);
+ canPersist = (result == PackageManager.PERMISSION_GRANTED);
+ mPersistCache.put(uid, canPersist);
+ }
+ }
+ return canPersist;
+ }
+
+ // IJobScheduler implementation
+ @Override
+ public int schedule(JobInfo job) throws RemoteException {
+ if (DEBUG) {
+ Slog.d(TAG, "Scheduling job: " + job);
+ }
+ final int pid = Binder.getCallingPid();
+ final int uid = Binder.getCallingUid();
+
+ enforceValidJobRequest(uid, job);
+ final boolean canPersist = canPersistJobs(pid, uid);
+
+ long ident = Binder.clearCallingIdentity();
+ try {
+ return JobSchedulerService.this.schedule(job, uid, canPersist);
+ } finally {
+ Binder.restoreCallingIdentity(ident);
+ }
+ }
+
+ @Override
+ public List<JobInfo> getAllPendingJobs() throws RemoteException {
+ final int uid = Binder.getCallingUid();
+
+ long ident = Binder.clearCallingIdentity();
+ try {
+ return JobSchedulerService.this.getPendingJobs(uid);
+ } finally {
+ Binder.restoreCallingIdentity(ident);
+ }
+ }
+
+ @Override
+ public void cancelAll() throws RemoteException {
+ final int uid = Binder.getCallingUid();
+
+ long ident = Binder.clearCallingIdentity();
+ try {
+ JobSchedulerService.this.cancelJobsForUid(uid);
+ } finally {
+ Binder.restoreCallingIdentity(ident);
+ }
+ }
+
+ @Override
+ public void cancel(int jobId) throws RemoteException {
+ final int uid = Binder.getCallingUid();
+
+ long ident = Binder.clearCallingIdentity();
+ try {
+ JobSchedulerService.this.cancelJob(uid, jobId);
+ } finally {
+ Binder.restoreCallingIdentity(ident);
+ }
+ }
+
+ /**
+ * "dumpsys" infrastructure
+ */
+ @Override
+ public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
+ getContext().enforceCallingOrSelfPermission(android.Manifest.permission.DUMP, TAG);
+
+ long identityToken = Binder.clearCallingIdentity();
+ try {
+ JobSchedulerService.this.dumpInternal(pw);
+ } finally {
+ Binder.restoreCallingIdentity(identityToken);
+ }
+ }
+ };
+
+ void dumpInternal(PrintWriter pw) {
+ synchronized (mJobs) {
+ pw.println("Registered jobs:");
+ if (mJobs.size() > 0) {
+ for (JobStatus job : mJobs.getJobs()) {
+ job.dump(pw, " ");
+ }
+ } else {
+ pw.println();
+ pw.println("No jobs scheduled.");
+ }
+ for (StateController controller : mControllers) {
+ pw.println();
+ controller.dumpControllerState(pw);
+ }
+ pw.println();
+ pw.println("Pending");
+ for (JobStatus jobStatus : mPendingJobs) {
+ pw.println(jobStatus.hashCode());
+ }
+ pw.println();
+ pw.println("Active jobs:");
+ for (JobServiceContext jsc : mActiveServices) {
+ if (jsc.isAvailable()) {
+ continue;
+ } else {
+ pw.println(jsc.getRunningJob().hashCode() + " for: " +
+ (SystemClock.elapsedRealtime()
+ - jsc.getExecutionStartTimeElapsed())/1000 + "s " +
+ "timeout: " + jsc.getTimeoutElapsed());
+ }
+ }
+ }
+ pw.println();
+ }
+}
diff --git a/services/core/java/com/android/server/job/JobServiceContext.java b/services/core/java/com/android/server/job/JobServiceContext.java
new file mode 100644
index 0000000..92b643c
--- /dev/null
+++ b/services/core/java/com/android/server/job/JobServiceContext.java
@@ -0,0 +1,519 @@
+/*
+ * 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;
+
+import android.app.ActivityManager;
+import android.app.job.JobParameters;
+import android.app.job.IJobCallback;
+import android.app.job.IJobService;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.os.Binder;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Looper;
+import android.os.Message;
+import android.os.PowerManager;
+import android.os.RemoteException;
+import android.os.SystemClock;
+import android.os.UserHandle;
+import android.os.WorkSource;
+import android.util.Log;
+import android.util.Slog;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.job.controllers.JobStatus;
+
+import java.util.concurrent.atomic.AtomicBoolean;
+
+/**
+ * Handles client binding and lifecycle of a job. A job will only execute one at a time on an
+ * instance of this class.
+ */
+public class JobServiceContext extends IJobCallback.Stub implements ServiceConnection {
+ private static final boolean DEBUG = true;
+ private static final String TAG = "JobServiceContext";
+ /** Define the maximum # of jobs allowed to run on a service at once. */
+ private static final int defaultMaxActiveJobsPerService =
+ ActivityManager.isLowRamDeviceStatic() ? 1 : 3;
+ /** Amount of time a job is allowed to execute for before being considered timed-out. */
+ private static final long EXECUTING_TIMESLICE_MILLIS = 60 * 1000;
+ /** Amount of time the JobScheduler will wait for a response from an app for a message. */
+ private static final long OP_TIMEOUT_MILLIS = 8 * 1000;
+ /** String prefix for all wakelock names. */
+ private static final String JS_WAKELOCK_PREFIX = "*job*/";
+
+ private static final String[] VERB_STRINGS = {
+ "VERB_STARTING", "VERB_EXECUTING", "VERB_STOPPING", "VERB_PENDING"
+ };
+
+ // States that a job occupies while interacting with the client.
+ static final int VERB_BINDING = 0;
+ static final int VERB_STARTING = 1;
+ static final int VERB_EXECUTING = 2;
+ static final int VERB_STOPPING = 3;
+
+ // Messages that result from interactions with the client service.
+ /** System timed out waiting for a response. */
+ private static final int MSG_TIMEOUT = 0;
+ /** Received a callback from client. */
+ private static final int MSG_CALLBACK = 1;
+ /** Run through list and start any ready jobs.*/
+ private static final int MSG_SERVICE_BOUND = 2;
+ /** Cancel a job. */
+ private static final int MSG_CANCEL = 3;
+ /** Shutdown the job. Used when the client crashes and we can't die gracefully.*/
+ private static final int MSG_SHUTDOWN_EXECUTION = 4;
+
+ private final Handler mCallbackHandler;
+ /** Make callbacks to {@link JobSchedulerService} to inform on job completion status. */
+ private final JobCompletedListener mCompletedListener;
+ /** Used for service binding, etc. */
+ private final Context mContext;
+ private PowerManager.WakeLock mWakeLock;
+
+ // Execution state.
+ private JobParameters mParams;
+ @VisibleForTesting
+ int mVerb;
+ private AtomicBoolean mCancelled = new AtomicBoolean();
+
+ /** All the information maintained about the job currently being executed. */
+ private JobStatus mRunningJob;
+ /** Binder to the client service. */
+ IJobService service;
+
+ private final Object mLock = new Object();
+ /** Whether this context is free. */
+ @GuardedBy("mLock")
+ private boolean mAvailable;
+ /** Track start time. */
+ private long mExecutionStartTimeElapsed;
+ /** Track when job will timeout. */
+ private long mTimeoutElapsed;
+
+ JobServiceContext(JobSchedulerService service, Looper looper) {
+ this(service.getContext(), service, looper);
+ }
+
+ @VisibleForTesting
+ JobServiceContext(Context context, JobCompletedListener completedListener, Looper looper) {
+ mContext = context;
+ mCallbackHandler = new JobServiceHandler(looper);
+ mCompletedListener = completedListener;
+ mAvailable = true;
+ }
+
+ /**
+ * Give a job to this context for execution. Callers must first check {@link #isAvailable()}
+ * to make sure this is a valid context.
+ * @param job The status of the job that we are going to run.
+ * @return True if the job is valid and is running. False if the job cannot be executed.
+ */
+ boolean executeRunnableJob(JobStatus job) {
+ synchronized (mLock) {
+ if (!mAvailable) {
+ Slog.e(TAG, "Starting new runnable but context is unavailable > Error.");
+ return false;
+ }
+
+ mRunningJob = job;
+ mParams = new JobParameters(job.getJobId(), job.getExtras(), this);
+ mExecutionStartTimeElapsed = SystemClock.elapsedRealtime();
+
+ mVerb = VERB_BINDING;
+ final Intent intent = new Intent().setComponent(job.getServiceComponent());
+ boolean binding = mContext.bindServiceAsUser(intent, this,
+ Context.BIND_AUTO_CREATE | Context.BIND_NOT_FOREGROUND,
+ new UserHandle(job.getUserId()));
+ if (!binding) {
+ if (DEBUG) {
+ Slog.d(TAG, job.getServiceComponent().getShortClassName() + " unavailable.");
+ }
+ mRunningJob = null;
+ mParams = null;
+ mExecutionStartTimeElapsed = 0L;
+ return false;
+ }
+ mAvailable = false;
+ return true;
+ }
+ }
+
+ /** Used externally to query the running job. Will return null if there is no job running. */
+ JobStatus getRunningJob() {
+ return mRunningJob;
+ }
+
+ /** Called externally when a job that was scheduled for execution should be cancelled. */
+ void cancelExecutingJob() {
+ mCallbackHandler.obtainMessage(MSG_CANCEL).sendToTarget();
+ }
+
+ /**
+ * @return Whether this context is available to handle incoming work.
+ */
+ boolean isAvailable() {
+ synchronized (mLock) {
+ return mAvailable;
+ }
+ }
+
+ long getExecutionStartTimeElapsed() {
+ return mExecutionStartTimeElapsed;
+ }
+
+ long getTimeoutElapsed() {
+ return mTimeoutElapsed;
+ }
+
+ @Override
+ public void jobFinished(int jobId, boolean reschedule) {
+ if (!verifyCallingUid()) {
+ return;
+ }
+ mCallbackHandler.obtainMessage(MSG_CALLBACK, jobId, reschedule ? 1 : 0)
+ .sendToTarget();
+ }
+
+ @Override
+ public void acknowledgeStopMessage(int jobId, boolean reschedule) {
+ if (!verifyCallingUid()) {
+ return;
+ }
+ mCallbackHandler.obtainMessage(MSG_CALLBACK, jobId, reschedule ? 1 : 0)
+ .sendToTarget();
+ }
+
+ @Override
+ public void acknowledgeStartMessage(int jobId, boolean ongoing) {
+ if (!verifyCallingUid()) {
+ return;
+ }
+ mCallbackHandler.obtainMessage(MSG_CALLBACK, jobId, ongoing ? 1 : 0).sendToTarget();
+ }
+
+ /**
+ * We acquire/release a wakelock on onServiceConnected/unbindService. This mirrors the work
+ * we intend to send to the client - we stop sending work when the service is unbound so until
+ * then we keep the wakelock.
+ * @param name The concrete component name of the service that has been connected.
+ * @param service The IBinder of the Service's communication channel,
+ */
+ @Override
+ public void onServiceConnected(ComponentName name, IBinder service) {
+ if (!name.equals(mRunningJob.getServiceComponent())) {
+ mCallbackHandler.obtainMessage(MSG_SHUTDOWN_EXECUTION).sendToTarget();
+ return;
+ }
+ this.service = IJobService.Stub.asInterface(service);
+ // Remove all timeouts.
+ mCallbackHandler.removeMessages(MSG_TIMEOUT);
+ final PowerManager pm =
+ (PowerManager) mContext.getSystemService(Context.POWER_SERVICE);
+ mWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK,
+ JS_WAKELOCK_PREFIX + mRunningJob.getServiceComponent().getPackageName());
+ mWakeLock.setWorkSource(new WorkSource(mRunningJob.getUid()));
+ mWakeLock.setReferenceCounted(false);
+ mWakeLock.acquire();
+ mCallbackHandler.obtainMessage(MSG_SERVICE_BOUND).sendToTarget();
+ }
+
+ /**
+ * If the client service crashes we reschedule this job and clean up.
+ * @param name The concrete component name of the service whose
+ */
+ @Override
+ public void onServiceDisconnected(ComponentName name) {
+ mCallbackHandler.obtainMessage(MSG_SHUTDOWN_EXECUTION).sendToTarget();
+ }
+
+ /**
+ * This class is reused across different clients, and passes itself in as a callback. Check
+ * whether the client exercising the callback is the client we expect.
+ * @return True if the binder calling is coming from the client we expect.
+ */
+ private boolean verifyCallingUid() {
+ if (mRunningJob == null || Binder.getCallingUid() != mRunningJob.getUid()) {
+ if (DEBUG) {
+ Slog.d(TAG, "Stale callback received, ignoring.");
+ }
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Handles the lifecycle of the JobService binding/callbacks, etc. The convention within this
+ * class is to append 'H' to each function name that can only be called on this handler. This
+ * isn't strictly necessary because all of these functions are private, but helps clarity.
+ */
+ private class JobServiceHandler extends Handler {
+ JobServiceHandler(Looper looper) {
+ super(looper);
+ }
+
+ @Override
+ public void handleMessage(Message message) {
+ switch (message.what) {
+ case MSG_SERVICE_BOUND:
+ handleServiceBoundH();
+ break;
+ case MSG_CALLBACK:
+ if (DEBUG) {
+ Slog.d(TAG, "MSG_CALLBACK of : " + mRunningJob + " v:" +
+ VERB_STRINGS[mVerb]);
+ }
+ removeMessages(MSG_TIMEOUT);
+
+ if (mVerb == VERB_STARTING) {
+ final boolean workOngoing = message.arg2 == 1;
+ handleStartedH(workOngoing);
+ } else if (mVerb == VERB_EXECUTING ||
+ mVerb == VERB_STOPPING) {
+ final boolean reschedule = message.arg2 == 1;
+ handleFinishedH(reschedule);
+ } else {
+ if (DEBUG) {
+ Slog.d(TAG, "Unrecognised callback: " + mRunningJob);
+ }
+ }
+ break;
+ case MSG_CANCEL:
+ handleCancelH();
+ break;
+ case MSG_TIMEOUT:
+ handleOpTimeoutH();
+ break;
+ case MSG_SHUTDOWN_EXECUTION:
+ closeAndCleanupJobH(true /* needsReschedule */);
+ break;
+ default:
+ Log.e(TAG, "Unrecognised message: " + message);
+ }
+ }
+
+ /** Start the job on the service. */
+ private void handleServiceBoundH() {
+ if (mVerb != VERB_BINDING) {
+ Slog.e(TAG, "Sending onStartJob for a job that isn't pending. "
+ + VERB_STRINGS[mVerb]);
+ closeAndCleanupJobH(false /* reschedule */);
+ return;
+ }
+ if (mCancelled.get()) {
+ if (DEBUG) {
+ Slog.d(TAG, "Job cancelled while waiting for bind to complete. "
+ + mRunningJob);
+ }
+ closeAndCleanupJobH(true /* reschedule */);
+ return;
+ }
+ try {
+ mVerb = VERB_STARTING;
+ scheduleOpTimeOut();
+ service.startJob(mParams);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Error sending onStart message to '" +
+ mRunningJob.getServiceComponent().getShortClassName() + "' ", e);
+ }
+ }
+
+ /**
+ * State behaviours.
+ * VERB_STARTING -> Successful start, change job to VERB_EXECUTING and post timeout.
+ * _PENDING -> Error
+ * _EXECUTING -> Error
+ * _STOPPING -> Error
+ */
+ private void handleStartedH(boolean workOngoing) {
+ switch (mVerb) {
+ case VERB_STARTING:
+ mVerb = VERB_EXECUTING;
+ if (!workOngoing) {
+ // Job is finished already so fast-forward to handleFinished.
+ handleFinishedH(false);
+ return;
+ }
+ if (mCancelled.get()) {
+ // Cancelled *while* waiting for acknowledgeStartMessage from client.
+ handleCancelH();
+ return;
+ }
+ scheduleOpTimeOut();
+ break;
+ default:
+ Log.e(TAG, "Handling started job but job wasn't starting! Was "
+ + VERB_STRINGS[mVerb] + ".");
+ return;
+ }
+ }
+
+ /**
+ * VERB_EXECUTING -> Client called jobFinished(), clean up and notify done.
+ * _STOPPING -> Successful finish, clean up and notify done.
+ * _STARTING -> Error
+ * _PENDING -> Error
+ */
+ private void handleFinishedH(boolean reschedule) {
+ switch (mVerb) {
+ case VERB_EXECUTING:
+ case VERB_STOPPING:
+ closeAndCleanupJobH(reschedule);
+ break;
+ default:
+ Slog.e(TAG, "Got an execution complete message for a job that wasn't being" +
+ "executed. Was " + VERB_STRINGS[mVerb] + ".");
+ }
+ }
+
+ /**
+ * A job can be in various states when a cancel request comes in:
+ * VERB_BINDING -> Cancelled before bind completed. Mark as cancelled and wait for
+ * {@link #onServiceConnected(android.content.ComponentName, android.os.IBinder)}
+ * _STARTING -> Mark as cancelled and wait for
+ * {@link JobServiceContext#acknowledgeStartMessage(int, boolean)}
+ * _EXECUTING -> call {@link #sendStopMessageH}}.
+ * _ENDING -> No point in doing anything here, so we ignore.
+ */
+ private void handleCancelH() {
+ switch (mVerb) {
+ case VERB_BINDING:
+ case VERB_STARTING:
+ mCancelled.set(true);
+ break;
+ case VERB_EXECUTING:
+ sendStopMessageH();
+ break;
+ case VERB_STOPPING:
+ // Nada.
+ break;
+ default:
+ Slog.e(TAG, "Cancelling a job without a valid verb: " + mVerb);
+ break;
+ }
+ }
+
+ /** Process MSG_TIMEOUT here. */
+ private void handleOpTimeoutH() {
+ if (Log.isLoggable(JobSchedulerService.TAG, Log.DEBUG)) {
+ Log.d(TAG, "MSG_TIMEOUT of " +
+ mRunningJob.getServiceComponent().getShortClassName() + " : "
+ + mParams.getJobId());
+ }
+
+ final int jobId = mParams.getJobId();
+ switch (mVerb) {
+ case VERB_STARTING:
+ // Client unresponsive - wedged or failed to respond in time. We don't really
+ // know what happened so let's log it and notify the JobScheduler
+ // FINISHED/NO-RETRY.
+ Log.e(TAG, "No response from client for onStartJob '" +
+ mRunningJob.getServiceComponent().getShortClassName() + "' tId: "
+ + jobId);
+ closeAndCleanupJobH(false /* needsReschedule */);
+ break;
+ case VERB_STOPPING:
+ // At least we got somewhere, so fail but ask the JobScheduler to reschedule.
+ Log.e(TAG, "No response from client for onStopJob, '" +
+ mRunningJob.getServiceComponent().getShortClassName() + "' tId: "
+ + jobId);
+ closeAndCleanupJobH(true /* needsReschedule */);
+ break;
+ case VERB_EXECUTING:
+ // Not an error - client ran out of time.
+ Log.i(TAG, "Client timed out while executing (no jobFinished received)." +
+ " sending onStop. " +
+ mRunningJob.getServiceComponent().getShortClassName() + "' tId: "
+ + jobId);
+ sendStopMessageH();
+ break;
+ default:
+ Log.e(TAG, "Handling timeout for an unknown active job state: "
+ + mRunningJob);
+ return;
+ }
+ }
+
+ /**
+ * Already running, need to stop. Will switch {@link #mVerb} from VERB_EXECUTING ->
+ * VERB_STOPPING.
+ */
+ private void sendStopMessageH() {
+ mCallbackHandler.removeMessages(MSG_TIMEOUT);
+ if (mVerb != VERB_EXECUTING) {
+ Log.e(TAG, "Sending onStopJob for a job that isn't started. " + mRunningJob);
+ closeAndCleanupJobH(false /* reschedule */);
+ return;
+ }
+ try {
+ mVerb = VERB_STOPPING;
+ scheduleOpTimeOut();
+ service.stopJob(mParams);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Error sending onStopJob to client.", e);
+ closeAndCleanupJobH(false /* reschedule */);
+ }
+ }
+
+ /**
+ * The provided job has finished, either by calling
+ * {@link android.app.job.JobService#jobFinished(android.app.job.JobParameters, boolean)}
+ * or from acknowledging the stop message we sent. Either way, we're done tracking it and
+ * we want to clean up internally.
+ */
+ private void closeAndCleanupJobH(boolean reschedule) {
+ removeMessages(MSG_TIMEOUT);
+ synchronized (mLock) {
+ mWakeLock.release();
+ mContext.unbindService(JobServiceContext.this);
+ mCompletedListener.onJobCompleted(mRunningJob, reschedule);
+
+ mWakeLock = null;
+ mRunningJob = null;
+ mParams = null;
+ mVerb = -1;
+ mCancelled.set(false);
+ service = null;
+ mAvailable = true;
+ }
+ }
+
+ /**
+ * Called when sending a message to the client, over whose execution we have no control. If we
+ * haven't received a response in a certain amount of time, we want to give up and carry on
+ * with life.
+ */
+ private void scheduleOpTimeOut() {
+ mCallbackHandler.removeMessages(MSG_TIMEOUT);
+
+ final long timeoutMillis = (mVerb == VERB_EXECUTING) ?
+ EXECUTING_TIMESLICE_MILLIS : OP_TIMEOUT_MILLIS;
+ if (DEBUG) {
+ Slog.d(TAG, "Scheduling time out for '" +
+ mRunningJob.getServiceComponent().getShortClassName() + "' tId: " +
+ mParams.getJobId() + ", in " + (timeoutMillis / 1000) + " s");
+ }
+ Message m = mCallbackHandler.obtainMessage(MSG_TIMEOUT);
+ mCallbackHandler.sendMessageDelayed(m, timeoutMillis);
+ mTimeoutElapsed = SystemClock.elapsedRealtime() + timeoutMillis;
+ }
+ }
+}
diff --git a/services/core/java/com/android/server/job/JobStore.java b/services/core/java/com/android/server/job/JobStore.java
new file mode 100644
index 0000000..3ea7171
--- /dev/null
+++ b/services/core/java/com/android/server/job/JobStore.java
@@ -0,0 +1,663 @@
+/*
+ * 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;
+
+import android.content.ComponentName;
+import android.app.job.JobInfo;
+import android.content.Context;
+import android.os.Environment;
+import android.os.Handler;
+import android.os.PersistableBundle;
+import android.os.SystemClock;
+import android.os.UserHandle;
+import android.util.AtomicFile;
+import android.util.ArraySet;
+import android.util.Pair;
+import android.util.Slog;
+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.job.controllers.JobStatus;
+
+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.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 jobs.
+ * This class offers the following functionality:
+ * - When a job is added, it will determine if the job requirements have changed (update) and
+ * whether the controllers need to be updated.
+ * - Persists JobInfos, figures out when to to rewrite the JobInfo to disk.
+ * - Handles rescheduling of jobs.
+ * - When a periodic job is executed and must be re-added.
+ * - When a job fails and the client requests that it be retried with backoff.
+ * - 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.job.JobStore.WriteJobsMapToDiskRunnable}
+ * and {@link com.android.server.job.JobStore.ReadJobMapFromDiskRunnable} lock on that
+ * object.
+ */
+public class JobStore {
+ private static final String TAG = "JobStore";
+ private static final boolean DEBUG = JobSchedulerService.DEBUG;
+
+ /** Threshold to adjust how often we want to write to the db. */
+ private static final int MAX_OPS_BEFORE_WRITE = 1;
+ final ArraySet<JobStatus> mJobSet;
+ final Context mContext;
+
+ private int mDirtyOperations;
+
+ private static final Object sSingletonLock = new Object();
+ private final AtomicFile mJobsFile;
+ /** Handler backed by IoThread for writing to disk. */
+ private final Handler mIoHandler = IoThread.getHandler();
+ private static JobStore sSingleton;
+
+ /** Used by the {@link JobSchedulerService} to instantiate the JobStore. */
+ static JobStore initAndGet(JobSchedulerService jobManagerService) {
+ synchronized (sSingletonLock) {
+ if (sSingleton == null) {
+ sSingleton = new JobStore(jobManagerService.getContext(),
+ Environment.getDataDirectory(), jobManagerService);
+ }
+ return sSingleton;
+ }
+ }
+
+ @VisibleForTesting
+ public static JobStore initAndGetForTesting(Context context, File dataDir,
+ JobMapReadFinishedListener callback) {
+ return new JobStore(context, dataDir, callback);
+ }
+
+ private JobStore(Context context, File dataDir, JobMapReadFinishedListener callback) {
+ mContext = context;
+ mDirtyOperations = 0;
+
+ File systemDir = new File(dataDir, "system");
+ File jobDir = new File(systemDir, "job");
+ jobDir.mkdirs();
+ mJobsFile = new AtomicFile(new File(jobDir, "jobs.xml"));
+
+ mJobSet = new ArraySet<JobStatus>();
+
+ readJobMapFromDiskAsync(callback);
+ }
+
+ /**
+ * Add a job to the master list, persisting it if necessary. If the JobStatus already exists,
+ * it will be replaced.
+ * @param jobStatus Job to add.
+ * @return Whether or not an equivalent JobStatus was replaced by this operation.
+ */
+ public boolean add(JobStatus jobStatus) {
+ boolean replaced = mJobSet.remove(jobStatus);
+ mJobSet.add(jobStatus);
+ if (jobStatus.isPersisted()) {
+ maybeWriteStatusToDiskAsync();
+ }
+ if (DEBUG) {
+ Slog.d(TAG, "Added job status to store: " + jobStatus);
+ }
+ return replaced;
+ }
+
+ /**
+ * Whether this jobStatus object already exists in the JobStore.
+ */
+ public boolean containsJobIdForUid(int jobId, int uId) {
+ for (JobStatus ts : mJobSet) {
+ if (ts.getUid() == uId && ts.getJobId() == jobId) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ public int size() {
+ return mJobSet.size();
+ }
+
+ /**
+ * Remove the provided job. Will also delete the job if it was persisted.
+ * @return Whether or not the job existed to be removed.
+ */
+ public boolean remove(JobStatus jobStatus) {
+ boolean removed = mJobSet.remove(jobStatus);
+ if (!removed) {
+ if (DEBUG) {
+ Slog.d(TAG, "Couldn't remove job: didn't exist: " + jobStatus);
+ }
+ return false;
+ }
+ maybeWriteStatusToDiskAsync();
+ return removed;
+ }
+
+ @VisibleForTesting
+ public void clear() {
+ mJobSet.clear();
+ maybeWriteStatusToDiskAsync();
+ }
+
+ public List<JobStatus> getJobsByUser(int userHandle) {
+ List<JobStatus> matchingJobs = new ArrayList<JobStatus>();
+ Iterator<JobStatus> it = mJobSet.iterator();
+ while (it.hasNext()) {
+ JobStatus ts = it.next();
+ if (UserHandle.getUserId(ts.getUid()) == userHandle) {
+ matchingJobs.add(ts);
+ }
+ }
+ return matchingJobs;
+ }
+
+ /**
+ * @param uid Uid of the requesting app.
+ * @return All JobStatus objects for a given uid from the master list.
+ */
+ public List<JobStatus> getJobsByUid(int uid) {
+ List<JobStatus> matchingJobs = new ArrayList<JobStatus>();
+ Iterator<JobStatus> it = mJobSet.iterator();
+ while (it.hasNext()) {
+ JobStatus ts = it.next();
+ if (ts.getUid() == uid) {
+ matchingJobs.add(ts);
+ }
+ }
+ return matchingJobs;
+ }
+
+ /**
+ * @param uid Uid of the requesting app.
+ * @param jobId Job id, specified at schedule-time.
+ * @return the JobStatus that matches the provided uId and jobId, or null if none found.
+ */
+ public JobStatus getJobByUidAndJobId(int uid, int jobId) {
+ Iterator<JobStatus> it = mJobSet.iterator();
+ while (it.hasNext()) {
+ JobStatus ts = it.next();
+ if (ts.getUid() == uid && ts.getJobId() == jobid) {
+ return ts;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * @return The live array of JobStatus objects.
+ */
+ public ArraySet<JobStatus> getJobs() {
+ return mJobSet;
+ }
+
+ /** Version of the db schema. */
+ private static final int JOBS_FILE_VERSION = 0;
+ /** Tag corresponds to constraints this job 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 jobs in one swath, 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 void maybeWriteStatusToDiskAsync() {
+ mDirtyOperations++;
+ if (mDirtyOperations >= MAX_OPS_BEFORE_WRITE) {
+ if (DEBUG) {
+ Slog.v(TAG, "Writing jobs to disk.");
+ }
+ mIoHandler.post(new WriteJobsMapToDiskRunnable());
+ }
+ }
+
+ private void readJobMapFromDiskAsync(JobMapReadFinishedListener callback) {
+ mIoHandler.post(new ReadJobMapFromDiskRunnable(callback));
+ }
+
+ public void readJobMapFromDisk(JobMapReadFinishedListener callback) {
+ new ReadJobMapFromDiskRunnable(callback).run();
+ }
+
+ /**
+ * Runnable that writes {@link #mJobSet} out to xml.
+ * NOTE: This Runnable locks on JobStore.this
+ */
+ private class WriteJobsMapToDiskRunnable implements Runnable {
+ @Override
+ public void run() {
+ final long startElapsed = SystemClock.elapsedRealtime();
+ synchronized (JobStore.this) {
+ writeJobsMapImpl();
+ }
+ if (JobSchedulerService.DEBUG) {
+ Slog.v(TAG, "Finished writing, took " + (SystemClock.elapsedRealtime()
+ - startElapsed) + "ms");
+ }
+ }
+
+ private void writeJobsMapImpl() {
+ 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, "job-info");
+ out.attribute(null, "version", Integer.toString(JOBS_FILE_VERSION));
+ for (int i = 0; i < mJobSet.size(); i++) {
+ final JobStatus jobStatus = mJobSet.valueAt(i);
+ if (DEBUG) {
+ Slog.d(TAG, "Saving job " + jobStatus.getJobId());
+ }
+ out.startTag(null, "job");
+ addIdentifierAttributesToJobTag(out, jobStatus);
+ writeConstraintsToXml(out, jobStatus);
+ writeExecutionCriteriaToXml(out, jobStatus);
+ writeBundleToXml(jobStatus.getExtras(), out);
+ out.endTag(null, "job");
+ }
+ out.endTag(null, "job-info");
+ out.endDocument();
+
+ // Write out to disk in one fell sweep.
+ FileOutputStream fos = mJobsFile.startWrite();
+ fos.write(baos.toByteArray());
+ mJobsFile.finishWrite(fos);
+ mDirtyOperations = 0;
+ } catch (IOException e) {
+ if (DEBUG) {
+ Slog.v(TAG, "Error writing out job 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 job and its client. */
+ private void addIdentifierAttributesToJobTag(XmlSerializer out, JobStatus jobStatus)
+ throws IOException {
+ out.attribute(null, "jobid", Integer.toString(jobStatus.getJobId()));
+ out.attribute(null, "package", jobStatus.getServiceComponent().getPackageName());
+ out.attribute(null, "class", jobStatus.getServiceComponent().getClassName());
+ out.attribute(null, "uid", Integer.toString(jobStatus.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 job's constraints. If the constraint isn't here
+ * it doesn't apply.
+ */
+ private void writeConstraintsToXml(XmlSerializer out, JobStatus jobStatus) throws IOException {
+ out.startTag(null, XML_TAG_PARAMS_CONSTRAINTS);
+ if (jobStatus.hasUnmeteredConstraint()) {
+ out.attribute(null, "unmetered", Boolean.toString(true));
+ }
+ if (jobStatus.hasConnectivityConstraint()) {
+ out.attribute(null, "connectivity", Boolean.toString(true));
+ }
+ if (jobStatus.hasIdleConstraint()) {
+ out.attribute(null, "idle", Boolean.toString(true));
+ }
+ if (jobStatus.hasChargingConstraint()) {
+ out.attribute(null, "charging", Boolean.toString(true));
+ }
+ out.endTag(null, XML_TAG_PARAMS_CONSTRAINTS);
+ }
+
+ private void writeExecutionCriteriaToXml(XmlSerializer out, JobStatus jobStatus)
+ throws IOException {
+ final JobInfo job = jobStatus.getJob();
+ if (jobStatus.getJob().isPeriodic()) {
+ out.startTag(null, XML_TAG_PERIODIC);
+ out.attribute(null, "period", Long.toString(job.getIntervalMillis()));
+ } else {
+ out.startTag(null, XML_TAG_ONEOFF);
+ }
+
+ if (jobStatus.hasDeadlineConstraint()) {
+ // Wall clock deadline.
+ final long deadlineWallclock = System.currentTimeMillis() +
+ (jobStatus.getLatestRunTimeElapsed() - SystemClock.elapsedRealtime());
+ out.attribute(null, "deadline", Long.toString(deadlineWallclock));
+ }
+ if (jobStatus.hasTimingDelayConstraint()) {
+ final long delayWallclock = System.currentTimeMillis() +
+ (jobStatus.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 job is idle -> these aren't allowed to specify
+ // back-off.
+ if (jobStatus.getJob().getInitialBackoffMillis() != JobInfo.DEFAULT_INITIAL_BACKOFF_MILLIS
+ || jobStatus.getJob().getBackoffPolicy() != JobInfo.DEFAULT_BACKOFF_POLICY) {
+ out.attribute(null, "backoff-policy", Integer.toString(job.getBackoffPolicy()));
+ out.attribute(null, "initial-backoff", Long.toString(job.getInitialBackoffMillis()));
+ }
+ if (job.isPeriodic()) {
+ out.endTag(null, XML_TAG_PERIODIC);
+ } else {
+ out.endTag(null, XML_TAG_ONEOFF);
+ }
+ }
+ }
+
+ /**
+ * Runnable that reads list of persisted job from xml.
+ * NOTE: This Runnable locks on JobStore.this
+ */
+ private class ReadJobMapFromDiskRunnable implements Runnable {
+ private JobMapReadFinishedListener mCallback;
+ public ReadJobMapFromDiskRunnable(JobMapReadFinishedListener callback) {
+ mCallback = callback;
+ }
+
+ @Override
+ public void run() {
+ try {
+ List<JobStatus> jobs;
+ FileInputStream fis = mJobsFile.openRead();
+ synchronized (JobStore.this) {
+ jobs = readJobMapImpl(fis);
+ }
+ fis.close();
+ if (jobs != null) {
+ mCallback.onJobMapReadFinished(jobs);
+ }
+ } catch (FileNotFoundException e) {
+ if (JobSchedulerService.DEBUG) {
+ Slog.d(TAG, "Could not find jobs file, probably there was nothing to load.");
+ }
+ } catch (XmlPullParserException e) {
+ if (JobSchedulerService.DEBUG) {
+ Slog.d(TAG, "Error parsing xml.", e);
+ }
+ } catch (IOException e) {
+ if (JobSchedulerService.DEBUG) {
+ Slog.d(TAG, "Error parsing xml.", e);
+ }
+ }
+ }
+
+ private List<JobStatus> readJobMapImpl(FileInputStream fis) throws XmlPullParserException, IOException {
+ 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 jobs.");
+ }
+ return null;
+ }
+
+ String tagName = parser.getName();
+ if ("job-info".equals(tagName)) {
+ final List<JobStatus> jobs = new ArrayList<JobStatus>();
+ // Read in version info.
+ try {
+ int version = Integer.valueOf(parser.getAttributeValue(null, "version"));
+ if (version != JOBS_FILE_VERSION) {
+ Slog.d(TAG, "Invalid version number, aborting jobs file read.");
+ return null;
+ }
+ } catch (NumberFormatException e) {
+ Slog.e(TAG, "Invalid version number, aborting jobs file read.");
+ return null;
+ }
+ eventType = parser.next();
+ do {
+ // Read each <job/>
+ if (eventType == XmlPullParser.START_TAG) {
+ tagName = parser.getName();
+ // Start reading job.
+ if ("job".equals(tagName)) {
+ JobStatus persistedJob = restoreJobFromXml(parser);
+ if (persistedJob != null) {
+ if (DEBUG) {
+ Slog.d(TAG, "Read out " + persistedJob);
+ }
+ jobs.add(persistedJob);
+ } else {
+ Slog.d(TAG, "Error reading job from file.");
+ }
+ }
+ }
+ eventType = parser.next();
+ } while (eventType != XmlPullParser.END_DOCUMENT);
+ return jobs;
+ }
+ return null;
+ }
+
+ /**
+ * @param parser Xml parser at the beginning of a "<job/>" tag. The next "parser.next()" call
+ * will take the parser into the body of the job tag.
+ * @return Newly instantiated job holding all the information we just read out of the xml tag.
+ */
+ private JobStatus restoreJobFromXml(XmlPullParser parser) throws XmlPullParserException,
+ IOException {
+ JobInfo.Builder jobBuilder;
+ int uid;
+
+ // Read out job identifier attributes.
+ try {
+ jobBuilder = buildBuilderFromXml(parser);
+ uid = Integer.valueOf(parser.getAttributeValue(null, "uid"));
+ } catch (NumberFormatException e) {
+ Slog.e(TAG, "Error parsing job'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(jobBuilder, 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");
+ jobBuilder.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 != JobStatus.NO_EARLIEST_RUNTIME) {
+ jobBuilder.setMinimumLatency(runtimes.first - SystemClock.elapsedRealtime());
+ }
+ if (runtimes.second != JobStatus.NO_LATEST_RUNTIME) {
+ jobBuilder.setOverrideDeadline(
+ runtimes.second - SystemClock.elapsedRealtime());
+ }
+ } catch (NumberFormatException e) {
+ Slog.d(TAG, "Error reading job 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(jobBuilder, 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);
+ jobBuilder.setExtras(extras);
+ parser.nextTag(); // Consume </extras>
+
+ return new JobStatus(jobBuilder.build(), uid, runtimes.first, runtimes.second);
+ }
+
+ private JobInfo.Builder buildBuilderFromXml(XmlPullParser parser) throws NumberFormatException {
+ // Pull out required fields from <job> attributes.
+ int jobId = Integer.valueOf(parser.getAttributeValue(null, "jobid"));
+ String packageName = parser.getAttributeValue(null, "package");
+ String className = parser.getAttributeValue(null, "class");
+ ComponentName cname = new ComponentName(packageName, className);
+
+ return new JobInfo.Builder(jobId, cname);
+ }
+
+ private void buildConstraintsFromXml(JobInfo.Builder jobBuilder, XmlPullParser parser) {
+ String val = parser.getAttributeValue(null, "unmetered");
+ if (val != null) {
+ jobBuilder.setRequiredNetworkCapabilities(JobInfo.NetworkType.UNMETERED);
+ }
+ val = parser.getAttributeValue(null, "connectivity");
+ if (val != null) {
+ jobBuilder.setRequiredNetworkCapabilities(JobInfo.NetworkType.ANY);
+ }
+ val = parser.getAttributeValue(null, "idle");
+ if (val != null) {
+ jobBuilder.setRequiresDeviceIdle(true);
+ }
+ val = parser.getAttributeValue(null, "charging");
+ if (val != null) {
+ jobBuilder.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 job was first scheduled.
+ */
+ private void maybeBuildBackoffPolicyFromXml(JobInfo.Builder jobBuilder, 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.
+ jobBuilder.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 = JobStatus.NO_EARLIEST_RUNTIME;
+ long latestRunTimeElapsed = JobStatus.NO_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);
+ }
+ }
+}
\ No newline at end of file
diff --git a/services/core/java/com/android/server/job/StateChangedListener.java b/services/core/java/com/android/server/job/StateChangedListener.java
new file mode 100644
index 0000000..90c203a
--- /dev/null
+++ b/services/core/java/com/android/server/job/StateChangedListener.java
@@ -0,0 +1,39 @@
+/*
+ * 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;
+
+import com.android.server.job.controllers.JobStatus;
+
+/**
+ * Interface through which a {@link com.android.server.job.controllers.StateController} informs
+ * the {@link com.android.server.job.JobSchedulerService} that there are some tasks potentially
+ * ready to be run.
+ */
+public interface StateChangedListener {
+ /**
+ * Called by the controller to notify the JobManager that it should check on the state of a
+ * task.
+ */
+ public void onControllerStateChanged();
+
+ /**
+ * Called by the controller to notify the JobManager that regardless of the state of the task,
+ * it must be run immediately.
+ * @param jobStatus The state of the task which is to be run immediately.
+ */
+ public void onRunJobNow(JobStatus jobStatus);
+}
diff --git a/services/core/java/com/android/server/job/controllers/BatteryController.java b/services/core/java/com/android/server/job/controllers/BatteryController.java
new file mode 100644
index 0000000..010de5c
--- /dev/null
+++ b/services/core/java/com/android/server/job/controllers/BatteryController.java
@@ -0,0 +1,219 @@
+/*
+ * 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.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.job.JobSchedulerService;
+import com.android.server.job.StateChangedListener;
+
+import java.io.PrintWriter;
+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<JobStatus> mTrackedTasks = new ArrayList<JobStatus>();
+ private ChargingTracker mChargeTracker;
+
+ public static BatteryController get(JobSchedulerService 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 maybeStartTrackingJob(JobStatus taskStatus) {
+ if (taskStatus.hasChargingConstraint()) {
+ synchronized (mTrackedTasks) {
+ mTrackedTasks.add(taskStatus);
+ taskStatus.chargingConstraintSatisfied.set(mChargeTracker.isOnStablePower());
+ }
+ }
+
+ }
+
+ @Override
+ public void maybeStopTrackingJob(JobStatus taskStatus) {
+ if (taskStatus.hasChargingConstraint()) {
+ synchronized (mTrackedTasks) {
+ mTrackedTasks.remove(taskStatus);
+ }
+ }
+ }
+
+ private void maybeReportNewChargingState() {
+ final boolean stablePower = mChargeTracker.isOnStablePower();
+ boolean reportChange = false;
+ synchronized (mTrackedTasks) {
+ for (JobStatus 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.getBatteryLevelLow();
+ 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();
+ }
+ }
+ }
+ }
+
+ @Override
+ public void dumpControllerState(PrintWriter pw) {
+
+ }
+}
diff --git a/services/core/java/com/android/server/job/controllers/ConnectivityController.java b/services/core/java/com/android/server/job/controllers/ConnectivityController.java
new file mode 100644
index 0000000..7e79ff7
--- /dev/null
+++ b/services/core/java/com/android/server/job/controllers/ConnectivityController.java
@@ -0,0 +1,200 @@
+/*
+ * 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.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.net.ConnectivityManager;
+import android.net.NetworkInfo;
+import android.os.ServiceManager;
+import android.os.UserHandle;
+import android.util.Slog;
+
+import com.android.server.ConnectivityService;
+import com.android.server.job.JobSchedulerService;
+import com.android.server.job.StateChangedListener;
+
+import java.io.PrintWriter;
+import java.util.LinkedList;
+import java.util.List;
+
+/**
+ * Handles changes in connectivity.
+ * We are only interested in metered vs. unmetered networks, and we're interested in them on a
+ * per-user basis.
+ */
+public class ConnectivityController extends StateController implements
+ ConnectivityManager.OnNetworkActiveListener {
+ private static final String TAG = "JobScheduler.Conn";
+
+ private final List<JobStatus> mTrackedJobs = new LinkedList<JobStatus>();
+ private final BroadcastReceiver mConnectivityChangedReceiver =
+ new ConnectivityChangedReceiver();
+ /** Singleton. */
+ private static ConnectivityController mSingleton;
+ private static Object sCreationLock = new Object();
+ /** Track whether the latest active network is metered. */
+ private boolean mNetworkUnmetered;
+ /** Track whether the latest active network is connected. */
+ private boolean mNetworkConnected;
+
+ public static ConnectivityController get(JobSchedulerService jms) {
+ synchronized (sCreationLock) {
+ if (mSingleton == null) {
+ mSingleton = new ConnectivityController(jms, jms.getContext());
+ }
+ return mSingleton;
+ }
+ }
+
+ private ConnectivityController(StateChangedListener stateChangedListener, Context context) {
+ super(stateChangedListener, context);
+ // Register connectivity changed BR.
+ IntentFilter intentFilter = new IntentFilter();
+ intentFilter.addAction(ConnectivityManager.CONNECTIVITY_ACTION);
+ mContext.registerReceiverAsUser(
+ mConnectivityChangedReceiver, UserHandle.ALL, intentFilter, null, null);
+ ConnectivityService cs =
+ (ConnectivityService)ServiceManager.getService(Context.CONNECTIVITY_SERVICE);
+ if (cs != null) {
+ if (cs.getActiveNetworkInfo() != null) {
+ mNetworkConnected = cs.getActiveNetworkInfo().isConnected();
+ }
+ mNetworkUnmetered = mNetworkConnected && !cs.isActiveNetworkMetered();
+ }
+ }
+
+ @Override
+ public void maybeStartTrackingJob(JobStatus jobStatus) {
+ if (jobStatus.hasConnectivityConstraint() || jobStatus.hasUnmeteredConstraint()) {
+ synchronized (mTrackedJobs) {
+ jobStatus.connectivityConstraintSatisfied.set(mNetworkConnected);
+ jobStatus.unmeteredConstraintSatisfied.set(mNetworkUnmetered);
+ mTrackedJobs.add(jobStatus);
+ }
+ }
+ }
+
+ @Override
+ public void maybeStopTrackingJob(JobStatus jobStatus) {
+ if (jobStatus.hasConnectivityConstraint() || jobStatus.hasUnmeteredConstraint()) {
+ synchronized (mTrackedJobs) {
+ mTrackedJobs.remove(jobStatus);
+ }
+ }
+ }
+
+ /**
+ * @param userId Id of the user for whom we are updating the connectivity state.
+ */
+ private void updateTrackedJobs(int userId) {
+ synchronized (mTrackedJobs) {
+ boolean changed = false;
+ for (JobStatus js : mTrackedJobs) {
+ if (js.getUserId() != userId) {
+ continue;
+ }
+ boolean prevIsConnected =
+ js.connectivityConstraintSatisfied.getAndSet(mNetworkConnected);
+ boolean prevIsMetered = js.unmeteredConstraintSatisfied.getAndSet(mNetworkUnmetered);
+ if (prevIsConnected != mNetworkConnected || prevIsMetered != mNetworkUnmetered) {
+ changed = true;
+ }
+ }
+ if (changed) {
+ mStateChangedListener.onControllerStateChanged();
+ }
+ }
+ }
+
+ /**
+ * We know the network has just come up. We want to run any jobs that are ready.
+ */
+ public synchronized void onNetworkActive() {
+ synchronized (mTrackedJobs) {
+ for (JobStatus js : mTrackedJobs) {
+ if (js.isReady()) {
+ if (DEBUG) {
+ Slog.d(TAG, "Running " + js + " due to network activity.");
+ }
+ mStateChangedListener.onRunJobNow(js);
+ }
+ }
+ }
+ }
+
+ class ConnectivityChangedReceiver extends BroadcastReceiver {
+ /**
+ * We'll receive connectivity changes for each user here, which we process independently.
+ * We are only interested in the active network here. We're only interested in the active
+ * network, b/c the end result of this will be for apps to try to hit the network.
+ * @param context The Context in which the receiver is running.
+ * @param intent The Intent being received.
+ */
+ // TODO: Test whether this will be called twice for each user.
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (DEBUG) {
+ Slog.d(TAG, "Received connectivity event: " + intent.getAction() + " u"
+ + context.getUserId());
+ }
+ final String action = intent.getAction();
+ if (action.equals(ConnectivityManager.CONNECTIVITY_ACTION)) {
+ final int networkType =
+ intent.getIntExtra(ConnectivityManager.EXTRA_NETWORK_TYPE,
+ ConnectivityManager.TYPE_NONE);
+ // Connectivity manager for THIS context - important!
+ final ConnectivityManager connManager = (ConnectivityManager)
+ context.getSystemService(Context.CONNECTIVITY_SERVICE);
+ final NetworkInfo activeNetwork = connManager.getActiveNetworkInfo();
+ final int userid = context.getUserId();
+ // This broadcast gets sent a lot, only update if the active network has changed.
+ if (activeNetwork == null) {
+ mNetworkUnmetered = false;
+ mNetworkConnected = false;
+ updateTrackedJobs(userid);
+ } else if (activeNetwork.getType() == networkType) {
+ mNetworkUnmetered = false;
+ mNetworkConnected = !intent.getBooleanExtra(
+ ConnectivityManager.EXTRA_NO_CONNECTIVITY, false);
+ if (mNetworkConnected) { // No point making the call if we know there's no conn.
+ mNetworkUnmetered = !connManager.isActiveNetworkMetered();
+ }
+ updateTrackedJobs(userid);
+ }
+ } else {
+ if (DEBUG) {
+ Slog.d(TAG, "Unrecognised action in intent: " + action);
+ }
+ }
+ }
+ };
+
+ @Override
+ public void dumpControllerState(PrintWriter pw) {
+ pw.println("Conn.");
+ pw.println("connected: " + mNetworkConnected + " unmetered: " + mNetworkUnmetered);
+ for (JobStatus js: mTrackedJobs) {
+ pw.println(String.valueOf(js.hashCode()).substring(0, 3) + ".."
+ + ": C=" + js.hasConnectivityConstraint()
+ + ", UM=" + js.hasUnmeteredConstraint());
+ }
+ }
+}
\ No newline at end of file
diff --git a/services/core/java/com/android/server/job/controllers/IdleController.java b/services/core/java/com/android/server/job/controllers/IdleController.java
new file mode 100644
index 0000000..07ffe4d
--- /dev/null
+++ b/services/core/java/com/android/server/job/controllers/IdleController.java
@@ -0,0 +1,186 @@
+/*
+ * 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 java.io.PrintWriter;
+import java.util.ArrayList;
+
+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.SystemClock;
+import android.util.Slog;
+
+import com.android.server.job.JobSchedulerService;
+import com.android.server.job.StateChangedListener;
+
+public class IdleController extends StateController {
+ private static final String TAG = "IdleController";
+
+ // Policy: we decide that we're "idle" if the device has been unused /
+ // screen off or dreaming for at least this long
+ private static final long INACTIVITY_IDLE_THRESHOLD = 71 * 60 * 1000; // millis; 71 min
+ private static final long IDLE_WINDOW_SLOP = 5 * 60 * 1000; // 5 minute window, to be nice
+
+ private static final String ACTION_TRIGGER_IDLE =
+ "com.android.server.task.controllers.IdleController.ACTION_TRIGGER_IDLE";
+
+ final ArrayList<JobStatus> mTrackedTasks = new ArrayList<JobStatus>();
+ IdlenessTracker mIdleTracker;
+
+ // Singleton factory
+ private static Object sCreationLock = new Object();
+ private static volatile IdleController sController;
+
+ public static IdleController get(JobSchedulerService service) {
+ synchronized (sCreationLock) {
+ if (sController == null) {
+ sController = new IdleController(service, service.getContext());
+ }
+ return sController;
+ }
+ }
+
+ private IdleController(StateChangedListener stateChangedListener, Context context) {
+ super(stateChangedListener, context);
+ initIdleStateTracking();
+ }
+
+ /**
+ * StateController interface
+ */
+ @Override
+ public void maybeStartTrackingJob(JobStatus taskStatus) {
+ if (taskStatus.hasIdleConstraint()) {
+ synchronized (mTrackedTasks) {
+ mTrackedTasks.add(taskStatus);
+ taskStatus.idleConstraintSatisfied.set(mIdleTracker.isIdle());
+ }
+ }
+ }
+
+ @Override
+ public void maybeStopTrackingJob(JobStatus taskStatus) {
+ synchronized (mTrackedTasks) {
+ mTrackedTasks.remove(taskStatus);
+ }
+ }
+
+ /**
+ * Interaction with the task manager service
+ */
+ void reportNewIdleState(boolean isIdle) {
+ synchronized (mTrackedTasks) {
+ for (JobStatus task : mTrackedTasks) {
+ task.idleConstraintSatisfied.set(isIdle);
+ }
+ }
+ mStateChangedListener.onControllerStateChanged();
+ }
+
+ /**
+ * Idle state tracking, and messaging with the task manager when
+ * significant state changes occur
+ */
+ private void initIdleStateTracking() {
+ mIdleTracker = new IdlenessTracker();
+ mIdleTracker.startTracking();
+ }
+
+ class IdlenessTracker extends BroadcastReceiver {
+ private AlarmManager mAlarm;
+ private PendingIntent mIdleTriggerIntent;
+ boolean mIdle;
+
+ public IdlenessTracker() {
+ mAlarm = (AlarmManager) mContext.getSystemService(Context.ALARM_SERVICE);
+
+ Intent intent = new Intent(ACTION_TRIGGER_IDLE);
+ intent.setComponent(new ComponentName(mContext, this.getClass()));
+ mIdleTriggerIntent = PendingIntent.getBroadcast(mContext, 0, intent, 0);
+
+ // at boot we presume that the user has just "interacted" with the
+ // device in some meaningful way
+ mIdle = false;
+ }
+
+ public boolean isIdle() {
+ return mIdle;
+ }
+
+ public void startTracking() {
+ IntentFilter filter = new IntentFilter();
+
+ // Screen state
+ filter.addAction(Intent.ACTION_SCREEN_ON);
+ filter.addAction(Intent.ACTION_SCREEN_OFF);
+
+ // Dreaming state
+ filter.addAction(Intent.ACTION_DREAMING_STARTED);
+ filter.addAction(Intent.ACTION_DREAMING_STOPPED);
+
+ mContext.registerReceiver(this, filter);
+ }
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ final String action = intent.getAction();
+
+ if (action.equals(Intent.ACTION_SCREEN_ON)
+ || action.equals(Intent.ACTION_DREAMING_STOPPED)) {
+ // possible transition to not-idle
+ if (mIdle) {
+ if (DEBUG) {
+ Slog.v(TAG, "exiting idle : " + action);
+ }
+ mAlarm.cancel(mIdleTriggerIntent);
+ mIdle = false;
+ reportNewIdleState(mIdle);
+ }
+ } else if (action.equals(Intent.ACTION_SCREEN_OFF)
+ || action.equals(Intent.ACTION_DREAMING_STARTED)) {
+ // when the screen goes off or dreaming starts, we schedule the
+ // alarm that will tell us when we have decided the device is
+ // truly idle.
+ long when = SystemClock.elapsedRealtime() + INACTIVITY_IDLE_THRESHOLD;
+ if (DEBUG) {
+ Slog.v(TAG, "Scheduling idle : " + action + " when=" + when);
+ }
+ mAlarm.setWindow(AlarmManager.ELAPSED_REALTIME_WAKEUP,
+ when, IDLE_WINDOW_SLOP, mIdleTriggerIntent);
+ } else if (action.equals(ACTION_TRIGGER_IDLE)) {
+ // idle time starts now
+ if (!mIdle) {
+ if (DEBUG) {
+ Slog.v(TAG, "Idle trigger fired @ " + SystemClock.elapsedRealtime());
+ }
+ mIdle = true;
+ reportNewIdleState(mIdle);
+ }
+ }
+ }
+ }
+
+ @Override
+ public void dumpControllerState(PrintWriter pw) {
+
+ }
+}
diff --git a/services/core/java/com/android/server/job/controllers/JobStatus.java b/services/core/java/com/android/server/job/controllers/JobStatus.java
new file mode 100644
index 0000000..15a6b25
--- /dev/null
+++ b/services/core/java/com/android/server/job/controllers/JobStatus.java
@@ -0,0 +1,217 @@
+/*
+ * 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.job.JobInfo;
+import android.content.ComponentName;
+import android.os.PersistableBundle;
+import android.os.SystemClock;
+import android.os.UserHandle;
+
+import java.io.PrintWriter;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+/**
+ * Uniquely identifies a job internally.
+ * Created from the public {@link android.app.job.JobInfo} object when it lands on the scheduler.
+ * Contains current state of the requirements of the job, as well as a function to evaluate
+ * whether it's ready to run.
+ * This object is shared among the various controllers - hence why the different fields are atomic.
+ * This isn't strictly necessary because each controller is only interested in a specific field,
+ * and the receivers that are listening for global state change will all run on the main looper,
+ * but we don't enforce that so this is safer.
+ * @hide
+ */
+public class JobStatus {
+ public static final long NO_LATEST_RUNTIME = Long.MAX_VALUE;
+ public static final long NO_EARLIEST_RUNTIME = 0L;
+
+ final JobInfo job;
+ final int uId;
+
+ /** At reschedule time we need to know whether to update job on disk. */
+ final boolean persisted;
+
+ // Constraints.
+ final AtomicBoolean chargingConstraintSatisfied = new AtomicBoolean();
+ final AtomicBoolean timeDelayConstraintSatisfied = new AtomicBoolean();
+ final AtomicBoolean deadlineConstraintSatisfied = new AtomicBoolean();
+ final AtomicBoolean idleConstraintSatisfied = new AtomicBoolean();
+ final AtomicBoolean unmeteredConstraintSatisfied = new AtomicBoolean();
+ final AtomicBoolean connectivityConstraintSatisfied = new AtomicBoolean();
+
+ /**
+ * Earliest point in the future at which this job will be eligible to run. A value of 0
+ * indicates there is no delay constraint. See {@link #hasTimingDelayConstraint()}.
+ */
+ private long earliestRunTimeElapsedMillis;
+ /**
+ * Latest point in the future at which this job must be run. A value of {@link Long#MAX_VALUE}
+ * indicates there is no deadline constraint. See {@link #hasDeadlineConstraint()}.
+ */
+ private long latestRunTimeElapsedMillis;
+ /** How many times this job has failed, used to compute back-off. */
+ private final int numFailures;
+
+ /** Provide a handle to the service that this job will be run on. */
+ public int getServiceToken() {
+ return uId;
+ }
+
+ private JobStatus(JobInfo job, int uId, boolean persisted, int numFailures) {
+ this.job = job;
+ this.uId = uId;
+ this.numFailures = numFailures;
+ this.persisted = persisted;
+ }
+
+ /** Create a newly scheduled job. */
+ public JobStatus(JobInfo job, int uId, boolean persisted) {
+ this(job, uId, persisted, 0);
+
+ final long elapsedNow = SystemClock.elapsedRealtime();
+
+ if (job.isPeriodic()) {
+ earliestRunTimeElapsedMillis = elapsedNow;
+ latestRunTimeElapsedMillis = elapsedNow + job.getIntervalMillis();
+ } else {
+ earliestRunTimeElapsedMillis = job.hasEarlyConstraint() ?
+ elapsedNow + job.getMinLatencyMillis() : NO_EARLIEST_RUNTIME;
+ latestRunTimeElapsedMillis = job.hasLateConstraint() ?
+ elapsedNow + job.getMaxExecutionDelayMillis() : NO_LATEST_RUNTIME;
+ }
+ }
+
+ /**
+ * Create a new JobStatus that was loaded from disk. We ignore the provided
+ * {@link android.app.job.JobInfo} time criteria because we can load a persisted periodic job
+ * from the {@link com.android.server.job.JobStore} and still want to respect its
+ * wallclock runtime rather than resetting it on every boot.
+ * We consider a freshly loaded job to no longer be in back-off.
+ */
+ public JobStatus(JobInfo job, int uId, long earliestRunTimeElapsedMillis,
+ long latestRunTimeElapsedMillis) {
+ this(job, uId, true, 0);
+
+ this.earliestRunTimeElapsedMillis = earliestRunTimeElapsedMillis;
+ this.latestRunTimeElapsedMillis = latestRunTimeElapsedMillis;
+ }
+
+ /** Create a new job to be rescheduled with the provided parameters. */
+ public JobStatus(JobStatus rescheduling, long newEarliestRuntimeElapsedMillis,
+ long newLatestRuntimeElapsedMillis, int backoffAttempt) {
+ this(rescheduling.job, rescheduling.getUid(), rescheduling.isPersisted(), backoffAttempt);
+
+ earliestRunTimeElapsedMillis = newEarliestRuntimeElapsedMillis;
+ latestRunTimeElapsedMillis = newLatestRuntimeElapsedMillis;
+ }
+
+ public JobInfo getJob() {
+ return job;
+ }
+
+ public int getJobId() {
+ return job.getId();
+ }
+
+ public int getNumFailures() {
+ return numFailures;
+ }
+
+ public ComponentName getServiceComponent() {
+ return job.getService();
+ }
+
+ public int getUserId() {
+ return UserHandle.getUserId(uId);
+ }
+
+ public int getUid() {
+ return uId;
+ }
+
+ public PersistableBundle getExtras() {
+ return job.getExtras();
+ }
+
+ public boolean hasConnectivityConstraint() {
+ return job.getNetworkCapabilities() == JobInfo.NetworkType.ANY;
+ }
+
+ public boolean hasUnmeteredConstraint() {
+ return job.getNetworkCapabilities() == JobInfo.NetworkType.UNMETERED;
+ }
+
+ public boolean hasChargingConstraint() {
+ return job.isRequireCharging();
+ }
+
+ public boolean hasTimingDelayConstraint() {
+ return earliestRunTimeElapsedMillis != NO_EARLIEST_RUNTIME;
+ }
+
+ public boolean hasDeadlineConstraint() {
+ return latestRunTimeElapsedMillis != NO_LATEST_RUNTIME;
+ }
+
+ public boolean hasIdleConstraint() {
+ return job.isRequireDeviceIdle();
+ }
+
+ public long getEarliestRunTime() {
+ return earliestRunTimeElapsedMillis;
+ }
+
+ public long getLatestRunTimeElapsed() {
+ return latestRunTimeElapsedMillis;
+ }
+
+ public boolean isPersisted() {
+ return persisted;
+ }
+ /**
+ * @return Whether or not this job is ready to run, based on its requirements.
+ */
+ public synchronized boolean isReady() {
+ return (!hasChargingConstraint() || chargingConstraintSatisfied.get())
+ && (!hasTimingDelayConstraint() || timeDelayConstraintSatisfied.get())
+ && (!hasConnectivityConstraint() || connectivityConstraintSatisfied.get())
+ && (!hasUnmeteredConstraint() || unmeteredConstraintSatisfied.get())
+ && (!hasIdleConstraint() || idleConstraintSatisfied.get())
+ // Also ready if the deadline has expired - special case.
+ || (hasDeadlineConstraint() && deadlineConstraintSatisfied.get());
+ }
+
+ public boolean matches(int uid, int jobId) {
+ return this.job.getId() == jobId && this.uId == uid;
+ }
+
+ @Override
+ public String toString() {
+ return String.valueOf(hashCode()).substring(0, 3) + ".."
+ + ":[" + job.getService().getPackageName() + ",jId=" + job.getId()
+ + ",R=(" + earliestRunTimeElapsedMillis + "," + latestRunTimeElapsedMillis + ")"
+ + ",N=" + job.getNetworkCapabilities() + ",C=" + job.isRequireCharging()
+ + ",I=" + job.isRequireDeviceIdle() + ",F=" + numFailures
+ + (isReady() ? "(READY)" : "")
+ + "]";
+ }
+ // Dumpsys infrastructure
+ public void dump(PrintWriter pw, String prefix) {
+ pw.println(this.toString());
+ }
+}
diff --git a/services/core/java/com/android/server/job/controllers/StateController.java b/services/core/java/com/android/server/job/controllers/StateController.java
new file mode 100644
index 0000000..81658bf
--- /dev/null
+++ b/services/core/java/com/android/server/job/controllers/StateController.java
@@ -0,0 +1,55 @@
+/*
+ * 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.content.Context;
+
+import com.android.server.job.JobSchedulerService;
+import com.android.server.job.StateChangedListener;
+
+import java.io.PrintWriter;
+
+/**
+ * Incorporates shared controller logic between the various controllers of the JobManager.
+ * These are solely responsible for tracking a list of jobs, and notifying the JM when these
+ * 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(StateChangedListener stateChangedListener, Context context) {
+ mStateChangedListener = stateChangedListener;
+ mContext = context;
+ }
+
+ /**
+ * Implement the logic here to decide whether a job should be tracked by this controller.
+ * This logic is put here so the JobManger can be completely agnostic of Controller logic.
+ * Also called when updating a task, so implementing controllers have to be aware of
+ * preexisting tasks.
+ */
+ public abstract void maybeStartTrackingJob(JobStatus jobStatus);
+ /**
+ * Remove task - this will happen if the task is cancelled, completed, etc.
+ */
+ public abstract void maybeStopTrackingJob(JobStatus jobStatus);
+
+ public abstract void dumpControllerState(PrintWriter pw);
+
+}
diff --git a/services/core/java/com/android/server/job/controllers/TimeController.java b/services/core/java/com/android/server/job/controllers/TimeController.java
new file mode 100644
index 0000000..e46226c
--- /dev/null
+++ b/services/core/java/com/android/server/job/controllers/TimeController.java
@@ -0,0 +1,289 @@
+/*
+ * 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.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.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";
+ private static final String ACTION_JOB_EXPIRED =
+ "android.content.jobscheduler.JOB_DEADLINE_EXPIRED";
+ private static final String ACTION_JOB_DELAY_EXPIRED =
+ "android.content.jobscheduler.JOB_DELAY_EXPIRED";
+
+ /** Set an alarm for the next job expiry. */
+ private final PendingIntent mDeadlineExpiredAlarmIntent;
+ /** Set an alarm for the next job 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 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());
+ }
+ return mSingleton;
+ }
+
+ private TimeController(StateChangedListener stateChangedListener, Context context) {
+ super(stateChangedListener, context);
+ mDeadlineExpiredAlarmIntent =
+ PendingIntent.getBroadcast(mContext, 0 /* ignored */,
+ new Intent(ACTION_JOB_EXPIRED), 0);
+ mNextDelayExpiredAlarmIntent =
+ PendingIntent.getBroadcast(mContext, 0 /* ignored */,
+ new Intent(ACTION_JOB_DELAY_EXPIRED), 0);
+ mNextJobExpiredElapsedMillis = Long.MAX_VALUE;
+ mNextDelayExpiredElapsedMillis = Long.MAX_VALUE;
+
+ // Register BR for these intents.
+ IntentFilter intentFilter = new IntentFilter(ACTION_JOB_EXPIRED);
+ intentFilter.addAction(ACTION_JOB_DELAY_EXPIRED);
+ mContext.registerReceiver(mAlarmExpiredReceiver, intentFilter);
+ }
+
+ /**
+ * Check if the job has a timing constraint, and if so determine where to insert it in our
+ * list.
+ */
+ @Override
+ public synchronized void maybeStartTrackingJob(JobStatus job) {
+ if (job.hasTimingDelayConstraint() || job.hasDeadlineConstraint()) {
+ maybeStopTrackingJob(job);
+ ListIterator<JobStatus> it = mTrackedJobs.listIterator(mTrackedJobs.size());
+ while (it.hasPrevious()) {
+ JobStatus ts = it.previous();
+ if (ts.getLatestRunTimeElapsed() < job.getLatestRunTimeElapsed()) {
+ // Insert
+ break;
+ }
+ }
+ it.add(job);
+ maybeUpdateAlarms(
+ job.hasTimingDelayConstraint() ? job.getEarliestRunTime() : Long.MAX_VALUE,
+ job.hasDeadlineConstraint() ? job.getLatestRunTimeElapsed() : Long.MAX_VALUE);
+ }
+ }
+
+ /**
+ * 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 synchronized void maybeStopTrackingJob(JobStatus job) {
+ 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 canStopTrackingJob(JobStatus job) {
+ return (!job.hasTimingDelayConstraint() ||
+ job.timeDelayConstraintSatisfied.get()) &&
+ (!job.hasDeadlineConstraint() ||
+ job.deadlineConstraintSatisfied.get());
+ }
+
+ private void ensureAlarmService() {
+ 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 synchronized void checkExpiredDeadlinesAndResetAlarm() {
+ long nextExpiryTime = Long.MAX_VALUE;
+ 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) {
+ job.deadlineConstraintSatisfied.set(true);
+ mStateChangedListener.onRunJobNow(job);
+ it.remove();
+ } else { // Sorted by expiry time, so take the next one and stop.
+ nextExpiryTime = jobDeadline;
+ break;
+ }
+ }
+ setDeadlineExpiredAlarm(nextExpiryTime);
+ }
+
+ /**
+ * 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 synchronized void checkExpiredDelaysAndResetAlarm() {
+ final long nowElapsedMillis = SystemClock.elapsedRealtime();
+ long nextDelayTime = Long.MAX_VALUE;
+ 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.timeDelayConstraintSatisfied.set(true);
+ if (canStopTrackingJob(job)) {
+ it.remove();
+ }
+ if (job.isReady()) {
+ ready = true;
+ }
+ } else { // Keep going through list to get next delay time.
+ if (nextDelayTime > jobDelayTime) {
+ nextDelayTime = jobDelayTime;
+ }
+ }
+ }
+ if (ready) {
+ mStateChangedListener.onControllerStateChanged();
+ }
+ setDelayExpiredAlarm(nextDelayTime);
+ }
+
+ private void maybeUpdateAlarms(long delayExpiredElapsed, long deadlineExpiredElapsed) {
+ if (delayExpiredElapsed < mNextDelayExpiredElapsedMillis) {
+ setDelayExpiredAlarm(delayExpiredElapsed);
+ }
+ if (deadlineExpiredElapsed < mNextJobExpiredElapsedMillis) {
+ setDeadlineExpiredAlarm(deadlineExpiredElapsed);
+ }
+ }
+
+ /**
+ * 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 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 job'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;
+ }
+ mNextJobExpiredElapsedMillis = alarmTimeElapsedMillis;
+ updateAlarmWithPendingIntent(mDeadlineExpiredAlarmIntent, mNextJobExpiredElapsedMillis);
+ }
+
+ 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());
+ }
+ // A job has just expired, so we run through the list of jobs that we have and
+ // notify our StateChangedListener.
+ if (ACTION_JOB_EXPIRED.equals(intent.getAction())) {
+ checkExpiredDeadlinesAndResetAlarm();
+ } else if (ACTION_JOB_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 " + (mNextJobExpiredElapsedMillis - nowElapsed)/1000
+ + "s");
+ pw.println("Tracking:");
+ for (JobStatus ts : mTrackedJobs) {
+ pw.println(String.valueOf(ts.hashCode()).substring(0, 3) + ".."
+ + ": (" + (ts.hasTimingDelayConstraint() ? ts.getEarliestRunTime() : "N/A")
+ + ", " + (ts.hasDeadlineConstraint() ?ts.getLatestRunTimeElapsed() : "N/A")
+ + ")");
+ }
+ }
+}
\ No newline at end of file