Loading services/core/java/com/android/server/job/JobSchedulerService.java +150 −82 Original line number Diff line number Diff line Loading @@ -49,14 +49,13 @@ import android.os.RemoteException; import android.os.ServiceManager; import android.os.SystemClock; import android.os.UserHandle; import android.os.Process; import android.util.ArraySet; import android.util.Slog; import android.util.SparseArray; import com.android.internal.app.IBatteryStats; import com.android.server.DeviceIdleController; import com.android.server.LocalServices; import com.android.server.job.JobStore.JobStatusFunctor; import com.android.server.job.controllers.AppIdleController; import com.android.server.job.controllers.BatteryController; import com.android.server.job.controllers.ConnectivityController; Loading @@ -80,11 +79,15 @@ import com.android.server.job.controllers.TimeController; */ public final class JobSchedulerService extends com.android.server.SystemService implements StateChangedListener, JobCompletedListener { static final String TAG = "JobSchedulerService"; public static final boolean DEBUG = false; /** The number of concurrent jobs we run at one time. */ private static final int MAX_JOB_CONTEXTS_COUNT = ActivityManager.isLowRamDeviceStatic() ? 3 : 6; static final String TAG = "JobSchedulerService"; /** The maximum number of jobs that we allow an unprivileged app to schedule */ private static final int MAX_JOBS_PER_APP = 100; /** Global local for all job scheduler state. */ final Object mLock = new Object(); /** Master list of jobs. */ Loading Loading @@ -250,6 +253,15 @@ public final class JobSchedulerService extends com.android.server.SystemService if (DEBUG) Slog.d(TAG, "SCHEDULE: " + jobStatus.toShortString()); JobStatus toCancel; synchronized (mLock) { // Jobs on behalf of others don't apply to the per-app job cap if (packageName == null) { if (mJobs.countJobsForUid(uId) > MAX_JOBS_PER_APP) { Slog.w(TAG, "Too many jobs for uid " + uId); throw new IllegalStateException("Apps may not schedule more than " + MAX_JOBS_PER_APP + " distinct jobs"); } } toCancel = mJobs.getJobByUidAndJobId(uId, job.getId()); } startTrackingJob(jobStatus, toCancel); Loading @@ -261,18 +273,16 @@ public final class JobSchedulerService extends com.android.server.SystemService } public List<JobInfo> getPendingJobs(int uid) { ArrayList<JobInfo> outList = new ArrayList<JobInfo>(); synchronized (mLock) { ArraySet<JobStatus> jobs = mJobs.getJobs(); for (int i=0; i<jobs.size(); i++) { JobStatus job = jobs.valueAt(i); if (job.getUid() == uid) { List<JobStatus> jobs = mJobs.getJobsByUid(uid); ArrayList<JobInfo> outList = new ArrayList<JobInfo>(jobs.size()); for (int i = jobs.size() - 1; i >= 0; i--) { JobStatus job = jobs.get(i); outList.add(job.getJob()); } } } return outList; } } void cancelJobsForUser(int userHandle) { List<JobStatus> jobsForUser; Loading Loading @@ -471,14 +481,16 @@ public final class JobSchedulerService extends com.android.server.SystemService getContext().getMainLooper())); } // Attach jobs to their controllers. ArraySet<JobStatus> jobs = mJobs.getJobs(); for (int i=0; i<jobs.size(); i++) { JobStatus job = jobs.valueAt(i); mJobs.forEachJob(new JobStatusFunctor() { @Override public void process(JobStatus job) { for (int controller = 0; controller < mControllers.size(); controller++) { mControllers.get(controller).deviceIdleModeChanged(mDeviceIdleMode); mControllers.get(controller).maybeStartTrackingJobLocked(job, null); final StateController sc = mControllers.get(controller); sc.deviceIdleModeChanged(mDeviceIdleMode); sc.maybeStartTrackingJobLocked(job, null); } } }); // GO GO GO! mHandler.obtainMessage(MSG_CHECK_JOB).sendToTarget(); } Loading Loading @@ -741,32 +753,50 @@ public final class JobSchedulerService extends com.android.server.SystemService * as many as we can. */ private void queueReadyJobsForExecutionLockedH() { ArraySet<JobStatus> jobs = mJobs.getJobs(); mPendingJobs.clear(); if (DEBUG) { Slog.d(TAG, "queuing all ready jobs for execution:"); } for (int i=0; i<jobs.size(); i++) { JobStatus job = jobs.valueAt(i); mPendingJobs.clear(); mJobs.forEachJob(mReadyQueueFunctor); mReadyQueueFunctor.postProcess(); if (DEBUG) { final int queuedJobs = mPendingJobs.size(); if (queuedJobs == 0) { Slog.d(TAG, "No jobs pending."); } else { Slog.d(TAG, queuedJobs + " jobs queued."); } } } class ReadyJobQueueFunctor implements JobStatusFunctor { ArrayList<JobStatus> newReadyJobs; @Override public void process(JobStatus job) { if (isReadyToBeExecutedLocked(job)) { if (DEBUG) { Slog.d(TAG, " queued " + job.toShortString()); } mPendingJobs.add(job); if (newReadyJobs == null) { newReadyJobs = new ArrayList<JobStatus>(); } newReadyJobs.add(job); } else if (areJobConstraintsNotSatisfiedLocked(job)) { stopJobOnServiceContextLocked(job, JobParameters.REASON_CONSTRAINTS_NOT_SATISFIED); } } if (DEBUG) { final int queuedJobs = mPendingJobs.size(); if (queuedJobs == 0) { Slog.d(TAG, "No jobs pending."); } else { Slog.d(TAG, queuedJobs + " jobs queued."); public void postProcess() { if (newReadyJobs != null) { mPendingJobs.addAll(newReadyJobs); } newReadyJobs = null; } } private final ReadyJobQueueFunctor mReadyQueueFunctor = new ReadyJobQueueFunctor(); /** * The state of at least one job has changed. Here is where we could enforce various Loading @@ -777,18 +807,21 @@ public final class JobSchedulerService extends com.android.server.SystemService * 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 maybeQueueReadyJobsForExecutionLockedH() { mPendingJobs.clear(); int chargingCount = 0; int idleCount = 0; int backoffCount = 0; int connectivityCount = 0; int contentCount = 0; List<JobStatus> runnableJobs = null; ArraySet<JobStatus> jobs = mJobs.getJobs(); if (DEBUG) Slog.d(TAG, "Maybe queuing ready jobs..."); for (int i=0; i<jobs.size(); i++) { JobStatus job = jobs.valueAt(i); class MaybeReadyJobQueueFunctor implements JobStatusFunctor { int chargingCount; int idleCount; int backoffCount; int connectivityCount; int contentCount; List<JobStatus> runnableJobs; public MaybeReadyJobQueueFunctor() { reset(); } // Functor method invoked for each job via JobStore.forEachJob() @Override public void process(JobStatus job) { if (isReadyToBeExecutedLocked(job)) { try { if (ActivityManagerNative.getDefault().getAppStartMode(job.getUid(), Loading @@ -797,7 +830,7 @@ public final class JobSchedulerService extends com.android.server.SystemService Slog.w(TAG, "Aborting job " + job.getUid() + ":" + job.getJob().toString() + " -- package not allowed to start"); mHandler.obtainMessage(MSG_STOP_JOB, job).sendToTarget(); continue; return; } } catch (RemoteException e) { } Loading Loading @@ -825,6 +858,8 @@ public final class JobSchedulerService extends com.android.server.SystemService JobParameters.REASON_CONSTRAINTS_NOT_SATISFIED); } } public void postProcess() { if (backoffCount > 0 || idleCount >= MIN_IDLE_COUNT || connectivityCount >= MIN_CONNECTIVITY_COUNT || Loading @@ -834,14 +869,34 @@ public final class JobSchedulerService extends com.android.server.SystemService if (DEBUG) { Slog.d(TAG, "maybeQueueReadyJobsForExecutionLockedH: Running jobs."); } for (int i=0; i<runnableJobs.size(); i++) { mPendingJobs.add(runnableJobs.get(i)); } mPendingJobs.addAll(runnableJobs); } else { if (DEBUG) { Slog.d(TAG, "maybeQueueReadyJobsForExecutionLockedH: Not running anything."); } } // Be ready for next time reset(); } private void reset() { chargingCount = 0; idleCount = 0; backoffCount = 0; connectivityCount = 0; contentCount = 0; runnableJobs = null; } } private final MaybeReadyJobQueueFunctor mMaybeQueueFunctor = new MaybeReadyJobQueueFunctor(); private void maybeQueueReadyJobsForExecutionLockedH() { if (DEBUG) Slog.d(TAG, "Maybe queuing ready jobs..."); mPendingJobs.clear(); mJobs.forEachJob(mMaybeQueueFunctor); mMaybeQueueFunctor.postProcess(); } /** Loading Loading @@ -1090,17 +1145,27 @@ public final class JobSchedulerService extends com.android.server.SystemService @Override public int scheduleAsPackage(JobInfo job, String packageName, int userId) throws RemoteException { final int callerUid = Binder.getCallingUid(); if (DEBUG) { Slog.d(TAG, "Scheduling job: " + job.toString() + " on behalf of " + packageName); Slog.d(TAG, "Caller uid " + callerUid + " scheduling job: " + job.toString() + " on behalf of " + packageName); } final int uid = Binder.getCallingUid(); if (uid != Process.SYSTEM_UID) { throw new IllegalArgumentException("Only system process is allowed" + "to set packageName"); if (packageName == null) { throw new NullPointerException("Must specify a package for scheduleAsPackage()"); } int mayScheduleForOthers = getContext().checkCallingOrSelfPermission( android.Manifest.permission.UPDATE_DEVICE_STATS); if (mayScheduleForOthers != PackageManager.PERMISSION_GRANTED) { throw new SecurityException("Caller uid " + callerUid + " not permitted to schedule jobs for other apps"); } long ident = Binder.clearCallingIdentity(); try { return JobSchedulerService.this.scheduleAsPackage(job, uid, packageName, userId); return JobSchedulerService.this.scheduleAsPackage(job, callerUid, packageName, userId); } finally { Binder.restoreCallingIdentity(ident); } Loading Loading @@ -1183,7 +1248,7 @@ public final class JobSchedulerService extends com.android.server.SystemService return s.toString(); } void dumpInternal(PrintWriter pw) { void dumpInternal(final PrintWriter pw) { final long now = SystemClock.elapsedRealtime(); synchronized (mLock) { pw.print("Started users: "); Loading @@ -1193,10 +1258,12 @@ public final class JobSchedulerService extends com.android.server.SystemService pw.println(); pw.println("Registered jobs:"); if (mJobs.size() > 0) { ArraySet<JobStatus> jobs = mJobs.getJobs(); for (int i=0; i<jobs.size(); i++) { JobStatus job = jobs.valueAt(i); pw.print(" Job #"); pw.print(i); pw.print(": "); mJobs.forEachJob(new JobStatusFunctor() { private int index = 0; @Override public void process(JobStatus job) { pw.print(" Job #"); pw.print(index++); pw.print(": "); pw.println(job.toShortString()); job.dump(pw, " "); pw.print(" Ready: "); Loading @@ -1211,6 +1278,7 @@ public final class JobSchedulerService extends com.android.server.SystemService pw.print(mStartedUsers.contains(job.getUserId())); pw.println(")"); } }); } else { pw.println(" None."); } Loading services/core/java/com/android/server/job/JobStore.java +151 −56 Original line number Diff line number Diff line Loading @@ -29,6 +29,7 @@ import android.util.AtomicFile; import android.util.ArraySet; import android.util.Pair; import android.util.Slog; import android.util.SparseArray; import android.util.Xml; import com.android.internal.annotations.VisibleForTesting; Loading Loading @@ -68,8 +69,8 @@ public class JobStore { /** 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 Object mLock; final JobSet mJobSet; // per-caller-uid tracking final Context mContext; private int mDirtyOperations; Loading Loading @@ -114,7 +115,7 @@ public class JobStore { jobDir.mkdirs(); mJobsFile = new AtomicFile(new File(jobDir, "jobs.xml")); mJobSet = new ArraySet<JobStatus>(); mJobSet = new JobSet(); readJobMapFromDisk(mJobSet); } Loading @@ -137,19 +138,6 @@ public class JobStore { return replaced; } /** * Whether this jobStatus object already exists in the JobStore. */ public boolean containsJobIdForUid(int jobId, int uId) { for (int i=mJobSet.size()-1; i>=0; i--) { JobStatus ts = mJobSet.valueAt(i); if (ts.getUid() == uId && ts.getJobId() == jobId) { return true; } } return false; } boolean containsJob(JobStatus jobStatus) { return mJobSet.contains(jobStatus); } Loading @@ -158,6 +146,10 @@ public class JobStore { return mJobSet.size(); } public int countJobsForUid(int uid) { return mJobSet.countJobsForUid(uid); } /** * Remove the provided job. Will also delete the job if it was persisted. * @param writeBack If true, the job will be deleted (if it was persisted) immediately. Loading Loading @@ -188,14 +180,7 @@ public class JobStore { * @return A list of all the jobs scheduled by the provided user. Never null. */ public List<JobStatus> getJobsByUser(int userHandle) { List<JobStatus> matchingJobs = new ArrayList<JobStatus>(); for (int i=mJobSet.size()-1; i>=0; i--) { JobStatus ts = mJobSet.valueAt(i); if (UserHandle.getUserId(ts.getUid()) == userHandle) { matchingJobs.add(ts); } } return matchingJobs; return mJobSet.getJobsByUser(userHandle); } /** Loading @@ -203,14 +188,7 @@ public class JobStore { * @return All JobStatus objects for a given uid from the master list. Never null. */ public List<JobStatus> getJobsByUid(int uid) { List<JobStatus> matchingJobs = new ArrayList<JobStatus>(); for (int i=mJobSet.size()-1; i>=0; i--) { JobStatus ts = mJobSet.valueAt(i); if (ts.getUid() == uid) { matchingJobs.add(ts); } } return matchingJobs; return mJobSet.getJobsByUid(uid); } /** Loading @@ -219,20 +197,21 @@ public class JobStore { * @return the JobStatus that matches the provided uId and jobId, or null if none found. */ public JobStatus getJobByUidAndJobId(int uid, int jobId) { for (int i=mJobSet.size()-1; i>=0; i--) { JobStatus ts = mJobSet.valueAt(i); if (ts.getUid() == uid && ts.getJobId() == jobId) { return ts; } } return null; return mJobSet.get(uid, jobId); } /** * @return The live array of JobStatus objects. * Iterate over the set of all jobs, invoking the supplied functor on each. This is for * customers who need to examine each job; we'd much rather not have to generate * transient unified collections for them to iterate over and then discard, or creating * iterators every time a client needs to perform a sweep. */ public ArraySet<JobStatus> getJobs() { return mJobSet; public void forEachJob(JobStatusFunctor functor) { mJobSet.forEachJob(functor); } public interface JobStatusFunctor { public void process(JobStatus jobStatus); } /** Version of the db schema. */ Loading Loading @@ -261,7 +240,7 @@ public class JobStore { } @VisibleForTesting public void readJobMapFromDisk(ArraySet<JobStatus> jobSet) { public void readJobMapFromDisk(JobSet jobSet) { new ReadJobMapFromDiskRunnable(jobSet).run(); } Loading @@ -273,21 +252,19 @@ public class JobStore { @Override public void run() { final long startElapsed = SystemClock.elapsedRealtime(); List<JobStatus> mStoreCopy = new ArrayList<JobStatus>(); final List<JobStatus> storeCopy = new ArrayList<JobStatus>(); synchronized (mLock) { // Copy over the jobs so we can release the lock before writing. for (int i=0; i<mJobSet.size(); i++) { JobStatus jobStatus = mJobSet.valueAt(i); if (!jobStatus.isPersisted()){ continue; // Clone the jobs so we can release the lock before writing. mJobSet.forEachJob(new JobStatusFunctor() { @Override public void process(JobStatus job) { if (job.isPersisted()) { storeCopy.add(new JobStatus(job)); } JobStatus copy = new JobStatus(jobStatus); mStoreCopy.add(copy); } }); } writeJobsMapImpl(mStoreCopy); writeJobsMapImpl(storeCopy); if (JobSchedulerService.DEBUG) { Slog.v(TAG, "Finished writing, took " + (SystemClock.elapsedRealtime() - startElapsed) + "ms"); Loading Loading @@ -440,13 +417,13 @@ public class JobStore { * need to go through {@link JobStore#add(com.android.server.job.controllers.JobStatus)}. */ private class ReadJobMapFromDiskRunnable implements Runnable { private final ArraySet<JobStatus> jobSet; private final JobSet jobSet; /** * @param jobSet Reference to the (empty) set of JobStatus objects that back the JobStore, * so that after disk read we can populate it directly. */ ReadJobMapFromDiskRunnable(ArraySet<JobStatus> jobSet) { ReadJobMapFromDiskRunnable(JobSet jobSet) { this.jobSet = jobSet; } Loading Loading @@ -759,4 +736,122 @@ public class JobStore { return Pair.create(earliestRunTimeElapsed, latestRunTimeElapsed); } } static class JobSet { // Key is the getUid() originator of the jobs in each sheaf private SparseArray<ArraySet<JobStatus>> mJobs; public JobSet() { mJobs = new SparseArray<ArraySet<JobStatus>>(); } public List<JobStatus> getJobsByUid(int uid) { ArrayList<JobStatus> matchingJobs = new ArrayList<JobStatus>(); ArraySet<JobStatus> jobs = mJobs.get(uid); if (jobs != null) { matchingJobs.addAll(jobs); } return matchingJobs; } // By user, not by uid, so we need to traverse by key and check public List<JobStatus> getJobsByUser(int userId) { ArrayList<JobStatus> result = new ArrayList<JobStatus>(); for (int i = mJobs.size() - 1; i >= 0; i--) { if (UserHandle.getUserId(mJobs.keyAt(i)) == userId) { ArraySet<JobStatus> jobs = mJobs.get(i); if (jobs != null) { result.addAll(jobs); } } } return result; } public boolean add(JobStatus job) { final int uid = job.getUid(); ArraySet<JobStatus> jobs = mJobs.get(uid); if (jobs == null) { jobs = new ArraySet<JobStatus>(); mJobs.put(uid, jobs); } return jobs.add(job); } public boolean remove(JobStatus job) { final int uid = job.getUid(); ArraySet<JobStatus> jobs = mJobs.get(uid); boolean didRemove = (jobs != null) ? jobs.remove(job) : false; if (didRemove && jobs.size() == 0) { // no more jobs for this uid; let the now-empty set object be GC'd. mJobs.remove(uid); } return didRemove; } public boolean contains(JobStatus job) { final int uid = job.getUid(); ArraySet<JobStatus> jobs = mJobs.get(uid); return jobs != null && jobs.contains(job); } public JobStatus get(int uid, int jobId) { ArraySet<JobStatus> jobs = mJobs.get(uid); if (jobs != null) { for (int i = jobs.size() - 1; i >= 0; i--) { JobStatus job = jobs.valueAt(i); if (job.getJobId() == jobId) { return job; } } } return null; } // Inefficient; use only for testing public List<JobStatus> getAllJobs() { ArrayList<JobStatus> allJobs = new ArrayList<JobStatus>(size()); for (int i = mJobs.size(); i >= 0; i--) { allJobs.addAll(mJobs.valueAt(i)); } return allJobs; } public void clear() { mJobs.clear(); } public int size() { int total = 0; for (int i = mJobs.size() - 1; i >= 0; i--) { total += mJobs.valueAt(i).size(); } return total; } // We only want to count the jobs that this uid has scheduled on its own // behalf, not those that the app has scheduled on someone else's behalf. public int countJobsForUid(int uid) { int total = 0; ArraySet<JobStatus> jobs = mJobs.get(uid); if (jobs != null) { for (int i = jobs.size() - 1; i >= 0; i--) { JobStatus job = jobs.valueAt(i); if (job.getUid() == job.getSourceUid()) { total++; } } } return total; } public void forEachJob(JobStatusFunctor functor) { for (int uidIndex = mJobs.size() - 1; uidIndex >= 0; uidIndex--) { ArraySet<JobStatus> jobs = mJobs.valueAt(uidIndex); for (int i = jobs.size() - 1; i >= 0; i--) { functor.process(jobs.valueAt(i)); } } } } } services/tests/servicestests/src/com/android/server/job/JobStoreTest.java +17 −16 File changed.Preview size limit exceeded, changes collapsed. Show changes Loading
services/core/java/com/android/server/job/JobSchedulerService.java +150 −82 Original line number Diff line number Diff line Loading @@ -49,14 +49,13 @@ import android.os.RemoteException; import android.os.ServiceManager; import android.os.SystemClock; import android.os.UserHandle; import android.os.Process; import android.util.ArraySet; import android.util.Slog; import android.util.SparseArray; import com.android.internal.app.IBatteryStats; import com.android.server.DeviceIdleController; import com.android.server.LocalServices; import com.android.server.job.JobStore.JobStatusFunctor; import com.android.server.job.controllers.AppIdleController; import com.android.server.job.controllers.BatteryController; import com.android.server.job.controllers.ConnectivityController; Loading @@ -80,11 +79,15 @@ import com.android.server.job.controllers.TimeController; */ public final class JobSchedulerService extends com.android.server.SystemService implements StateChangedListener, JobCompletedListener { static final String TAG = "JobSchedulerService"; public static final boolean DEBUG = false; /** The number of concurrent jobs we run at one time. */ private static final int MAX_JOB_CONTEXTS_COUNT = ActivityManager.isLowRamDeviceStatic() ? 3 : 6; static final String TAG = "JobSchedulerService"; /** The maximum number of jobs that we allow an unprivileged app to schedule */ private static final int MAX_JOBS_PER_APP = 100; /** Global local for all job scheduler state. */ final Object mLock = new Object(); /** Master list of jobs. */ Loading Loading @@ -250,6 +253,15 @@ public final class JobSchedulerService extends com.android.server.SystemService if (DEBUG) Slog.d(TAG, "SCHEDULE: " + jobStatus.toShortString()); JobStatus toCancel; synchronized (mLock) { // Jobs on behalf of others don't apply to the per-app job cap if (packageName == null) { if (mJobs.countJobsForUid(uId) > MAX_JOBS_PER_APP) { Slog.w(TAG, "Too many jobs for uid " + uId); throw new IllegalStateException("Apps may not schedule more than " + MAX_JOBS_PER_APP + " distinct jobs"); } } toCancel = mJobs.getJobByUidAndJobId(uId, job.getId()); } startTrackingJob(jobStatus, toCancel); Loading @@ -261,18 +273,16 @@ public final class JobSchedulerService extends com.android.server.SystemService } public List<JobInfo> getPendingJobs(int uid) { ArrayList<JobInfo> outList = new ArrayList<JobInfo>(); synchronized (mLock) { ArraySet<JobStatus> jobs = mJobs.getJobs(); for (int i=0; i<jobs.size(); i++) { JobStatus job = jobs.valueAt(i); if (job.getUid() == uid) { List<JobStatus> jobs = mJobs.getJobsByUid(uid); ArrayList<JobInfo> outList = new ArrayList<JobInfo>(jobs.size()); for (int i = jobs.size() - 1; i >= 0; i--) { JobStatus job = jobs.get(i); outList.add(job.getJob()); } } } return outList; } } void cancelJobsForUser(int userHandle) { List<JobStatus> jobsForUser; Loading Loading @@ -471,14 +481,16 @@ public final class JobSchedulerService extends com.android.server.SystemService getContext().getMainLooper())); } // Attach jobs to their controllers. ArraySet<JobStatus> jobs = mJobs.getJobs(); for (int i=0; i<jobs.size(); i++) { JobStatus job = jobs.valueAt(i); mJobs.forEachJob(new JobStatusFunctor() { @Override public void process(JobStatus job) { for (int controller = 0; controller < mControllers.size(); controller++) { mControllers.get(controller).deviceIdleModeChanged(mDeviceIdleMode); mControllers.get(controller).maybeStartTrackingJobLocked(job, null); final StateController sc = mControllers.get(controller); sc.deviceIdleModeChanged(mDeviceIdleMode); sc.maybeStartTrackingJobLocked(job, null); } } }); // GO GO GO! mHandler.obtainMessage(MSG_CHECK_JOB).sendToTarget(); } Loading Loading @@ -741,32 +753,50 @@ public final class JobSchedulerService extends com.android.server.SystemService * as many as we can. */ private void queueReadyJobsForExecutionLockedH() { ArraySet<JobStatus> jobs = mJobs.getJobs(); mPendingJobs.clear(); if (DEBUG) { Slog.d(TAG, "queuing all ready jobs for execution:"); } for (int i=0; i<jobs.size(); i++) { JobStatus job = jobs.valueAt(i); mPendingJobs.clear(); mJobs.forEachJob(mReadyQueueFunctor); mReadyQueueFunctor.postProcess(); if (DEBUG) { final int queuedJobs = mPendingJobs.size(); if (queuedJobs == 0) { Slog.d(TAG, "No jobs pending."); } else { Slog.d(TAG, queuedJobs + " jobs queued."); } } } class ReadyJobQueueFunctor implements JobStatusFunctor { ArrayList<JobStatus> newReadyJobs; @Override public void process(JobStatus job) { if (isReadyToBeExecutedLocked(job)) { if (DEBUG) { Slog.d(TAG, " queued " + job.toShortString()); } mPendingJobs.add(job); if (newReadyJobs == null) { newReadyJobs = new ArrayList<JobStatus>(); } newReadyJobs.add(job); } else if (areJobConstraintsNotSatisfiedLocked(job)) { stopJobOnServiceContextLocked(job, JobParameters.REASON_CONSTRAINTS_NOT_SATISFIED); } } if (DEBUG) { final int queuedJobs = mPendingJobs.size(); if (queuedJobs == 0) { Slog.d(TAG, "No jobs pending."); } else { Slog.d(TAG, queuedJobs + " jobs queued."); public void postProcess() { if (newReadyJobs != null) { mPendingJobs.addAll(newReadyJobs); } newReadyJobs = null; } } private final ReadyJobQueueFunctor mReadyQueueFunctor = new ReadyJobQueueFunctor(); /** * The state of at least one job has changed. Here is where we could enforce various Loading @@ -777,18 +807,21 @@ public final class JobSchedulerService extends com.android.server.SystemService * 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 maybeQueueReadyJobsForExecutionLockedH() { mPendingJobs.clear(); int chargingCount = 0; int idleCount = 0; int backoffCount = 0; int connectivityCount = 0; int contentCount = 0; List<JobStatus> runnableJobs = null; ArraySet<JobStatus> jobs = mJobs.getJobs(); if (DEBUG) Slog.d(TAG, "Maybe queuing ready jobs..."); for (int i=0; i<jobs.size(); i++) { JobStatus job = jobs.valueAt(i); class MaybeReadyJobQueueFunctor implements JobStatusFunctor { int chargingCount; int idleCount; int backoffCount; int connectivityCount; int contentCount; List<JobStatus> runnableJobs; public MaybeReadyJobQueueFunctor() { reset(); } // Functor method invoked for each job via JobStore.forEachJob() @Override public void process(JobStatus job) { if (isReadyToBeExecutedLocked(job)) { try { if (ActivityManagerNative.getDefault().getAppStartMode(job.getUid(), Loading @@ -797,7 +830,7 @@ public final class JobSchedulerService extends com.android.server.SystemService Slog.w(TAG, "Aborting job " + job.getUid() + ":" + job.getJob().toString() + " -- package not allowed to start"); mHandler.obtainMessage(MSG_STOP_JOB, job).sendToTarget(); continue; return; } } catch (RemoteException e) { } Loading Loading @@ -825,6 +858,8 @@ public final class JobSchedulerService extends com.android.server.SystemService JobParameters.REASON_CONSTRAINTS_NOT_SATISFIED); } } public void postProcess() { if (backoffCount > 0 || idleCount >= MIN_IDLE_COUNT || connectivityCount >= MIN_CONNECTIVITY_COUNT || Loading @@ -834,14 +869,34 @@ public final class JobSchedulerService extends com.android.server.SystemService if (DEBUG) { Slog.d(TAG, "maybeQueueReadyJobsForExecutionLockedH: Running jobs."); } for (int i=0; i<runnableJobs.size(); i++) { mPendingJobs.add(runnableJobs.get(i)); } mPendingJobs.addAll(runnableJobs); } else { if (DEBUG) { Slog.d(TAG, "maybeQueueReadyJobsForExecutionLockedH: Not running anything."); } } // Be ready for next time reset(); } private void reset() { chargingCount = 0; idleCount = 0; backoffCount = 0; connectivityCount = 0; contentCount = 0; runnableJobs = null; } } private final MaybeReadyJobQueueFunctor mMaybeQueueFunctor = new MaybeReadyJobQueueFunctor(); private void maybeQueueReadyJobsForExecutionLockedH() { if (DEBUG) Slog.d(TAG, "Maybe queuing ready jobs..."); mPendingJobs.clear(); mJobs.forEachJob(mMaybeQueueFunctor); mMaybeQueueFunctor.postProcess(); } /** Loading Loading @@ -1090,17 +1145,27 @@ public final class JobSchedulerService extends com.android.server.SystemService @Override public int scheduleAsPackage(JobInfo job, String packageName, int userId) throws RemoteException { final int callerUid = Binder.getCallingUid(); if (DEBUG) { Slog.d(TAG, "Scheduling job: " + job.toString() + " on behalf of " + packageName); Slog.d(TAG, "Caller uid " + callerUid + " scheduling job: " + job.toString() + " on behalf of " + packageName); } final int uid = Binder.getCallingUid(); if (uid != Process.SYSTEM_UID) { throw new IllegalArgumentException("Only system process is allowed" + "to set packageName"); if (packageName == null) { throw new NullPointerException("Must specify a package for scheduleAsPackage()"); } int mayScheduleForOthers = getContext().checkCallingOrSelfPermission( android.Manifest.permission.UPDATE_DEVICE_STATS); if (mayScheduleForOthers != PackageManager.PERMISSION_GRANTED) { throw new SecurityException("Caller uid " + callerUid + " not permitted to schedule jobs for other apps"); } long ident = Binder.clearCallingIdentity(); try { return JobSchedulerService.this.scheduleAsPackage(job, uid, packageName, userId); return JobSchedulerService.this.scheduleAsPackage(job, callerUid, packageName, userId); } finally { Binder.restoreCallingIdentity(ident); } Loading Loading @@ -1183,7 +1248,7 @@ public final class JobSchedulerService extends com.android.server.SystemService return s.toString(); } void dumpInternal(PrintWriter pw) { void dumpInternal(final PrintWriter pw) { final long now = SystemClock.elapsedRealtime(); synchronized (mLock) { pw.print("Started users: "); Loading @@ -1193,10 +1258,12 @@ public final class JobSchedulerService extends com.android.server.SystemService pw.println(); pw.println("Registered jobs:"); if (mJobs.size() > 0) { ArraySet<JobStatus> jobs = mJobs.getJobs(); for (int i=0; i<jobs.size(); i++) { JobStatus job = jobs.valueAt(i); pw.print(" Job #"); pw.print(i); pw.print(": "); mJobs.forEachJob(new JobStatusFunctor() { private int index = 0; @Override public void process(JobStatus job) { pw.print(" Job #"); pw.print(index++); pw.print(": "); pw.println(job.toShortString()); job.dump(pw, " "); pw.print(" Ready: "); Loading @@ -1211,6 +1278,7 @@ public final class JobSchedulerService extends com.android.server.SystemService pw.print(mStartedUsers.contains(job.getUserId())); pw.println(")"); } }); } else { pw.println(" None."); } Loading
services/core/java/com/android/server/job/JobStore.java +151 −56 Original line number Diff line number Diff line Loading @@ -29,6 +29,7 @@ import android.util.AtomicFile; import android.util.ArraySet; import android.util.Pair; import android.util.Slog; import android.util.SparseArray; import android.util.Xml; import com.android.internal.annotations.VisibleForTesting; Loading Loading @@ -68,8 +69,8 @@ public class JobStore { /** 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 Object mLock; final JobSet mJobSet; // per-caller-uid tracking final Context mContext; private int mDirtyOperations; Loading Loading @@ -114,7 +115,7 @@ public class JobStore { jobDir.mkdirs(); mJobsFile = new AtomicFile(new File(jobDir, "jobs.xml")); mJobSet = new ArraySet<JobStatus>(); mJobSet = new JobSet(); readJobMapFromDisk(mJobSet); } Loading @@ -137,19 +138,6 @@ public class JobStore { return replaced; } /** * Whether this jobStatus object already exists in the JobStore. */ public boolean containsJobIdForUid(int jobId, int uId) { for (int i=mJobSet.size()-1; i>=0; i--) { JobStatus ts = mJobSet.valueAt(i); if (ts.getUid() == uId && ts.getJobId() == jobId) { return true; } } return false; } boolean containsJob(JobStatus jobStatus) { return mJobSet.contains(jobStatus); } Loading @@ -158,6 +146,10 @@ public class JobStore { return mJobSet.size(); } public int countJobsForUid(int uid) { return mJobSet.countJobsForUid(uid); } /** * Remove the provided job. Will also delete the job if it was persisted. * @param writeBack If true, the job will be deleted (if it was persisted) immediately. Loading Loading @@ -188,14 +180,7 @@ public class JobStore { * @return A list of all the jobs scheduled by the provided user. Never null. */ public List<JobStatus> getJobsByUser(int userHandle) { List<JobStatus> matchingJobs = new ArrayList<JobStatus>(); for (int i=mJobSet.size()-1; i>=0; i--) { JobStatus ts = mJobSet.valueAt(i); if (UserHandle.getUserId(ts.getUid()) == userHandle) { matchingJobs.add(ts); } } return matchingJobs; return mJobSet.getJobsByUser(userHandle); } /** Loading @@ -203,14 +188,7 @@ public class JobStore { * @return All JobStatus objects for a given uid from the master list. Never null. */ public List<JobStatus> getJobsByUid(int uid) { List<JobStatus> matchingJobs = new ArrayList<JobStatus>(); for (int i=mJobSet.size()-1; i>=0; i--) { JobStatus ts = mJobSet.valueAt(i); if (ts.getUid() == uid) { matchingJobs.add(ts); } } return matchingJobs; return mJobSet.getJobsByUid(uid); } /** Loading @@ -219,20 +197,21 @@ public class JobStore { * @return the JobStatus that matches the provided uId and jobId, or null if none found. */ public JobStatus getJobByUidAndJobId(int uid, int jobId) { for (int i=mJobSet.size()-1; i>=0; i--) { JobStatus ts = mJobSet.valueAt(i); if (ts.getUid() == uid && ts.getJobId() == jobId) { return ts; } } return null; return mJobSet.get(uid, jobId); } /** * @return The live array of JobStatus objects. * Iterate over the set of all jobs, invoking the supplied functor on each. This is for * customers who need to examine each job; we'd much rather not have to generate * transient unified collections for them to iterate over and then discard, or creating * iterators every time a client needs to perform a sweep. */ public ArraySet<JobStatus> getJobs() { return mJobSet; public void forEachJob(JobStatusFunctor functor) { mJobSet.forEachJob(functor); } public interface JobStatusFunctor { public void process(JobStatus jobStatus); } /** Version of the db schema. */ Loading Loading @@ -261,7 +240,7 @@ public class JobStore { } @VisibleForTesting public void readJobMapFromDisk(ArraySet<JobStatus> jobSet) { public void readJobMapFromDisk(JobSet jobSet) { new ReadJobMapFromDiskRunnable(jobSet).run(); } Loading @@ -273,21 +252,19 @@ public class JobStore { @Override public void run() { final long startElapsed = SystemClock.elapsedRealtime(); List<JobStatus> mStoreCopy = new ArrayList<JobStatus>(); final List<JobStatus> storeCopy = new ArrayList<JobStatus>(); synchronized (mLock) { // Copy over the jobs so we can release the lock before writing. for (int i=0; i<mJobSet.size(); i++) { JobStatus jobStatus = mJobSet.valueAt(i); if (!jobStatus.isPersisted()){ continue; // Clone the jobs so we can release the lock before writing. mJobSet.forEachJob(new JobStatusFunctor() { @Override public void process(JobStatus job) { if (job.isPersisted()) { storeCopy.add(new JobStatus(job)); } JobStatus copy = new JobStatus(jobStatus); mStoreCopy.add(copy); } }); } writeJobsMapImpl(mStoreCopy); writeJobsMapImpl(storeCopy); if (JobSchedulerService.DEBUG) { Slog.v(TAG, "Finished writing, took " + (SystemClock.elapsedRealtime() - startElapsed) + "ms"); Loading Loading @@ -440,13 +417,13 @@ public class JobStore { * need to go through {@link JobStore#add(com.android.server.job.controllers.JobStatus)}. */ private class ReadJobMapFromDiskRunnable implements Runnable { private final ArraySet<JobStatus> jobSet; private final JobSet jobSet; /** * @param jobSet Reference to the (empty) set of JobStatus objects that back the JobStore, * so that after disk read we can populate it directly. */ ReadJobMapFromDiskRunnable(ArraySet<JobStatus> jobSet) { ReadJobMapFromDiskRunnable(JobSet jobSet) { this.jobSet = jobSet; } Loading Loading @@ -759,4 +736,122 @@ public class JobStore { return Pair.create(earliestRunTimeElapsed, latestRunTimeElapsed); } } static class JobSet { // Key is the getUid() originator of the jobs in each sheaf private SparseArray<ArraySet<JobStatus>> mJobs; public JobSet() { mJobs = new SparseArray<ArraySet<JobStatus>>(); } public List<JobStatus> getJobsByUid(int uid) { ArrayList<JobStatus> matchingJobs = new ArrayList<JobStatus>(); ArraySet<JobStatus> jobs = mJobs.get(uid); if (jobs != null) { matchingJobs.addAll(jobs); } return matchingJobs; } // By user, not by uid, so we need to traverse by key and check public List<JobStatus> getJobsByUser(int userId) { ArrayList<JobStatus> result = new ArrayList<JobStatus>(); for (int i = mJobs.size() - 1; i >= 0; i--) { if (UserHandle.getUserId(mJobs.keyAt(i)) == userId) { ArraySet<JobStatus> jobs = mJobs.get(i); if (jobs != null) { result.addAll(jobs); } } } return result; } public boolean add(JobStatus job) { final int uid = job.getUid(); ArraySet<JobStatus> jobs = mJobs.get(uid); if (jobs == null) { jobs = new ArraySet<JobStatus>(); mJobs.put(uid, jobs); } return jobs.add(job); } public boolean remove(JobStatus job) { final int uid = job.getUid(); ArraySet<JobStatus> jobs = mJobs.get(uid); boolean didRemove = (jobs != null) ? jobs.remove(job) : false; if (didRemove && jobs.size() == 0) { // no more jobs for this uid; let the now-empty set object be GC'd. mJobs.remove(uid); } return didRemove; } public boolean contains(JobStatus job) { final int uid = job.getUid(); ArraySet<JobStatus> jobs = mJobs.get(uid); return jobs != null && jobs.contains(job); } public JobStatus get(int uid, int jobId) { ArraySet<JobStatus> jobs = mJobs.get(uid); if (jobs != null) { for (int i = jobs.size() - 1; i >= 0; i--) { JobStatus job = jobs.valueAt(i); if (job.getJobId() == jobId) { return job; } } } return null; } // Inefficient; use only for testing public List<JobStatus> getAllJobs() { ArrayList<JobStatus> allJobs = new ArrayList<JobStatus>(size()); for (int i = mJobs.size(); i >= 0; i--) { allJobs.addAll(mJobs.valueAt(i)); } return allJobs; } public void clear() { mJobs.clear(); } public int size() { int total = 0; for (int i = mJobs.size() - 1; i >= 0; i--) { total += mJobs.valueAt(i).size(); } return total; } // We only want to count the jobs that this uid has scheduled on its own // behalf, not those that the app has scheduled on someone else's behalf. public int countJobsForUid(int uid) { int total = 0; ArraySet<JobStatus> jobs = mJobs.get(uid); if (jobs != null) { for (int i = jobs.size() - 1; i >= 0; i--) { JobStatus job = jobs.valueAt(i); if (job.getUid() == job.getSourceUid()) { total++; } } } return total; } public void forEachJob(JobStatusFunctor functor) { for (int uidIndex = mJobs.size() - 1; uidIndex >= 0; uidIndex--) { ArraySet<JobStatus> jobs = mJobs.valueAt(uidIndex); for (int i = jobs.size() - 1; i >= 0; i--) { functor.process(jobs.valueAt(i)); } } } } }
services/tests/servicestests/src/com/android/server/job/JobStoreTest.java +17 −16 File changed.Preview size limit exceeded, changes collapsed. Show changes