1 /* 2 * Copyright (C) 2014 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License 15 */ 16 17 package com.android.server.job; 18 19 import static com.android.server.job.JobSchedulerService.sElapsedRealtimeClock; 20 import static com.android.server.job.JobSchedulerService.sSystemClock; 21 22 import android.annotation.Nullable; 23 import android.app.ActivityManager; 24 import android.app.IActivityManager; 25 import android.app.job.JobInfo; 26 import android.content.ComponentName; 27 import android.content.Context; 28 import android.net.NetworkRequest; 29 import android.os.Environment; 30 import android.os.Handler; 31 import android.os.PersistableBundle; 32 import android.os.Process; 33 import android.os.SystemClock; 34 import android.os.UserHandle; 35 import android.text.format.DateUtils; 36 import android.util.ArraySet; 37 import android.util.AtomicFile; 38 import android.util.Pair; 39 import android.util.Slog; 40 import android.util.SparseArray; 41 import android.util.Xml; 42 43 import com.android.internal.annotations.GuardedBy; 44 import com.android.internal.annotations.VisibleForTesting; 45 import com.android.internal.util.ArrayUtils; 46 import com.android.internal.util.BitUtils; 47 import com.android.internal.util.FastXmlSerializer; 48 import com.android.server.IoThread; 49 import com.android.server.LocalServices; 50 import com.android.server.job.JobSchedulerInternal.JobStorePersistStats; 51 import com.android.server.job.controllers.JobStatus; 52 53 import org.xmlpull.v1.XmlPullParser; 54 import org.xmlpull.v1.XmlPullParserException; 55 import org.xmlpull.v1.XmlSerializer; 56 57 import java.io.ByteArrayOutputStream; 58 import java.io.File; 59 import java.io.FileInputStream; 60 import java.io.FileNotFoundException; 61 import java.io.FileOutputStream; 62 import java.io.IOException; 63 import java.nio.charset.StandardCharsets; 64 import java.util.ArrayList; 65 import java.util.List; 66 import java.util.Set; 67 import java.util.function.Consumer; 68 import java.util.function.Predicate; 69 70 /** 71 * Maintains the master list of jobs that the job scheduler is tracking. These jobs are compared by 72 * reference, so none of the functions in this class should make a copy. 73 * Also handles read/write of persisted jobs. 74 * 75 * Note on locking: 76 * All callers to this class must <strong>lock on the class object they are calling</strong>. 77 * This is important b/c {@link com.android.server.job.JobStore.WriteJobsMapToDiskRunnable} 78 * and {@link com.android.server.job.JobStore.ReadJobMapFromDiskRunnable} lock on that 79 * object. 80 * 81 * Test: 82 * atest $ANDROID_BUILD_TOP/frameworks/base/services/tests/servicestests/src/com/android/server/job/JobStoreTest.java 83 */ 84 public final class JobStore { 85 private static final String TAG = "JobStore"; 86 private static final boolean DEBUG = JobSchedulerService.DEBUG; 87 88 /** Threshold to adjust how often we want to write to the db. */ 89 private static final long JOB_PERSIST_DELAY = 2000L; 90 91 final Object mLock; 92 final Object mWriteScheduleLock; // used solely for invariants around write scheduling 93 final JobSet mJobSet; // per-caller-uid and per-source-uid tracking 94 final Context mContext; 95 96 // Bookkeeping around incorrect boot-time system clock 97 private final long mXmlTimestamp; 98 private boolean mRtcGood; 99 100 @GuardedBy("mWriteScheduleLock") 101 private boolean mWriteScheduled; 102 103 @GuardedBy("mWriteScheduleLock") 104 private boolean mWriteInProgress; 105 106 private static final Object sSingletonLock = new Object(); 107 private final AtomicFile mJobsFile; 108 /** Handler backed by IoThread for writing to disk. */ 109 private final Handler mIoHandler = IoThread.getHandler(); 110 private static JobStore sSingleton; 111 112 private JobStorePersistStats mPersistInfo = new JobStorePersistStats(); 113 114 /** Used by the {@link JobSchedulerService} to instantiate the JobStore. */ initAndGet(JobSchedulerService jobManagerService)115 static JobStore initAndGet(JobSchedulerService jobManagerService) { 116 synchronized (sSingletonLock) { 117 if (sSingleton == null) { 118 sSingleton = new JobStore(jobManagerService.getContext(), 119 jobManagerService.getLock(), Environment.getDataDirectory()); 120 } 121 return sSingleton; 122 } 123 } 124 125 /** 126 * @return A freshly initialized job store object, with no loaded jobs. 127 */ 128 @VisibleForTesting initAndGetForTesting(Context context, File dataDir)129 public static JobStore initAndGetForTesting(Context context, File dataDir) { 130 JobStore jobStoreUnderTest = new JobStore(context, new Object(), dataDir); 131 jobStoreUnderTest.clear(); 132 return jobStoreUnderTest; 133 } 134 135 /** 136 * Construct the instance of the job store. This results in a blocking read from disk. 137 */ JobStore(Context context, Object lock, File dataDir)138 private JobStore(Context context, Object lock, File dataDir) { 139 mLock = lock; 140 mWriteScheduleLock = new Object(); 141 mContext = context; 142 143 File systemDir = new File(dataDir, "system"); 144 File jobDir = new File(systemDir, "job"); 145 jobDir.mkdirs(); 146 mJobsFile = new AtomicFile(new File(jobDir, "jobs.xml"), "jobs"); 147 148 mJobSet = new JobSet(); 149 150 // If the current RTC is earlier than the timestamp on our persisted jobs file, 151 // we suspect that the RTC is uninitialized and so we cannot draw conclusions 152 // about persisted job scheduling. 153 // 154 // Note that if the persisted jobs file does not exist, we proceed with the 155 // assumption that the RTC is good. This is less work and is safe: if the 156 // clock updates to sanity then we'll be saving the persisted jobs file in that 157 // correct state, which is normal; or we'll wind up writing the jobs file with 158 // an incorrect historical timestamp. That's fine; at worst we'll reboot with 159 // a *correct* timestamp, see a bunch of overdue jobs, and run them; then 160 // settle into normal operation. 161 mXmlTimestamp = mJobsFile.getLastModifiedTime(); 162 mRtcGood = (sSystemClock.millis() > mXmlTimestamp); 163 164 readJobMapFromDisk(mJobSet, mRtcGood); 165 } 166 jobTimesInflatedValid()167 public boolean jobTimesInflatedValid() { 168 return mRtcGood; 169 } 170 clockNowValidToInflate(long now)171 public boolean clockNowValidToInflate(long now) { 172 return now >= mXmlTimestamp; 173 } 174 175 /** 176 * Find all the jobs that were affected by RTC clock uncertainty at boot time. Returns 177 * parallel lists of the existing JobStatus objects and of new, equivalent JobStatus instances 178 * with now-corrected time bounds. 179 */ getRtcCorrectedJobsLocked(final ArrayList<JobStatus> toAdd, final ArrayList<JobStatus> toRemove)180 public void getRtcCorrectedJobsLocked(final ArrayList<JobStatus> toAdd, 181 final ArrayList<JobStatus> toRemove) { 182 final long elapsedNow = sElapsedRealtimeClock.millis(); 183 final IActivityManager am = ActivityManager.getService(); 184 185 // Find the jobs that need to be fixed up, collecting them for post-iteration 186 // replacement with their new versions 187 forEachJob(job -> { 188 final Pair<Long, Long> utcTimes = job.getPersistedUtcTimes(); 189 if (utcTimes != null) { 190 Pair<Long, Long> elapsedRuntimes = 191 convertRtcBoundsToElapsed(utcTimes, elapsedNow); 192 JobStatus newJob = new JobStatus(job, job.getBaseHeartbeat(), 193 elapsedRuntimes.first, elapsedRuntimes.second, 194 0, job.getLastSuccessfulRunTime(), job.getLastFailedRunTime()); 195 newJob.prepareLocked(am); 196 toAdd.add(newJob); 197 toRemove.add(job); 198 } 199 }); 200 } 201 202 /** 203 * Add a job to the master list, persisting it if necessary. If the JobStatus already exists, 204 * it will be replaced. 205 * @param jobStatus Job to add. 206 * @return Whether or not an equivalent JobStatus was replaced by this operation. 207 */ add(JobStatus jobStatus)208 public boolean add(JobStatus jobStatus) { 209 boolean replaced = mJobSet.remove(jobStatus); 210 mJobSet.add(jobStatus); 211 if (jobStatus.isPersisted()) { 212 maybeWriteStatusToDiskAsync(); 213 } 214 if (DEBUG) { 215 Slog.d(TAG, "Added job status to store: " + jobStatus); 216 } 217 return replaced; 218 } 219 containsJob(JobStatus jobStatus)220 boolean containsJob(JobStatus jobStatus) { 221 return mJobSet.contains(jobStatus); 222 } 223 size()224 public int size() { 225 return mJobSet.size(); 226 } 227 getPersistStats()228 public JobStorePersistStats getPersistStats() { 229 return mPersistInfo; 230 } 231 countJobsForUid(int uid)232 public int countJobsForUid(int uid) { 233 return mJobSet.countJobsForUid(uid); 234 } 235 236 /** 237 * Remove the provided job. Will also delete the job if it was persisted. 238 * @param writeBack If true, the job will be deleted (if it was persisted) immediately. 239 * @return Whether or not the job existed to be removed. 240 */ remove(JobStatus jobStatus, boolean writeBack)241 public boolean remove(JobStatus jobStatus, boolean writeBack) { 242 boolean removed = mJobSet.remove(jobStatus); 243 if (!removed) { 244 if (DEBUG) { 245 Slog.d(TAG, "Couldn't remove job: didn't exist: " + jobStatus); 246 } 247 return false; 248 } 249 if (writeBack && jobStatus.isPersisted()) { 250 maybeWriteStatusToDiskAsync(); 251 } 252 return removed; 253 } 254 255 /** 256 * Remove the jobs of users not specified in the whitelist. 257 * @param whitelist Array of User IDs whose jobs are not to be removed. 258 */ removeJobsOfNonUsers(int[] whitelist)259 public void removeJobsOfNonUsers(int[] whitelist) { 260 mJobSet.removeJobsOfNonUsers(whitelist); 261 } 262 263 @VisibleForTesting clear()264 public void clear() { 265 mJobSet.clear(); 266 maybeWriteStatusToDiskAsync(); 267 } 268 269 /** 270 * @param userHandle User for whom we are querying the list of jobs. 271 * @return A list of all the jobs scheduled for the provided user. Never null. 272 */ getJobsByUser(int userHandle)273 public List<JobStatus> getJobsByUser(int userHandle) { 274 return mJobSet.getJobsByUser(userHandle); 275 } 276 277 /** 278 * @param uid Uid of the requesting app. 279 * @return All JobStatus objects for a given uid from the master list. Never null. 280 */ getJobsByUid(int uid)281 public List<JobStatus> getJobsByUid(int uid) { 282 return mJobSet.getJobsByUid(uid); 283 } 284 285 /** 286 * @param uid Uid of the requesting app. 287 * @param jobId Job id, specified at schedule-time. 288 * @return the JobStatus that matches the provided uId and jobId, or null if none found. 289 */ getJobByUidAndJobId(int uid, int jobId)290 public JobStatus getJobByUidAndJobId(int uid, int jobId) { 291 return mJobSet.get(uid, jobId); 292 } 293 294 /** 295 * Iterate over the set of all jobs, invoking the supplied functor on each. This is for 296 * customers who need to examine each job; we'd much rather not have to generate 297 * transient unified collections for them to iterate over and then discard, or creating 298 * iterators every time a client needs to perform a sweep. 299 */ forEachJob(Consumer<JobStatus> functor)300 public void forEachJob(Consumer<JobStatus> functor) { 301 mJobSet.forEachJob(null, functor); 302 } 303 forEachJob(@ullable Predicate<JobStatus> filterPredicate, Consumer<JobStatus> functor)304 public void forEachJob(@Nullable Predicate<JobStatus> filterPredicate, 305 Consumer<JobStatus> functor) { 306 mJobSet.forEachJob(filterPredicate, functor); 307 } 308 forEachJob(int uid, Consumer<JobStatus> functor)309 public void forEachJob(int uid, Consumer<JobStatus> functor) { 310 mJobSet.forEachJob(uid, functor); 311 } 312 forEachJobForSourceUid(int sourceUid, Consumer<JobStatus> functor)313 public void forEachJobForSourceUid(int sourceUid, Consumer<JobStatus> functor) { 314 mJobSet.forEachJobForSourceUid(sourceUid, functor); 315 } 316 317 /** Version of the db schema. */ 318 private static final int JOBS_FILE_VERSION = 0; 319 /** Tag corresponds to constraints this job needs. */ 320 private static final String XML_TAG_PARAMS_CONSTRAINTS = "constraints"; 321 /** Tag corresponds to execution parameters. */ 322 private static final String XML_TAG_PERIODIC = "periodic"; 323 private static final String XML_TAG_ONEOFF = "one-off"; 324 private static final String XML_TAG_EXTRAS = "extras"; 325 326 /** 327 * Every time the state changes we write all the jobs in one swath, instead of trying to 328 * track incremental changes. 329 */ maybeWriteStatusToDiskAsync()330 private void maybeWriteStatusToDiskAsync() { 331 synchronized (mWriteScheduleLock) { 332 if (!mWriteScheduled) { 333 if (DEBUG) { 334 Slog.v(TAG, "Scheduling persist of jobs to disk."); 335 } 336 mIoHandler.postDelayed(mWriteRunnable, JOB_PERSIST_DELAY); 337 mWriteScheduled = mWriteInProgress = true; 338 } 339 } 340 } 341 342 @VisibleForTesting readJobMapFromDisk(JobSet jobSet, boolean rtcGood)343 public void readJobMapFromDisk(JobSet jobSet, boolean rtcGood) { 344 new ReadJobMapFromDiskRunnable(jobSet, rtcGood).run(); 345 } 346 347 /** 348 * Wait for any pending write to the persistent store to clear 349 * @param maxWaitMillis Maximum time from present to wait 350 * @return {@code true} if I/O cleared as expected, {@code false} if the wait 351 * timed out before the pending write completed. 352 */ 353 @VisibleForTesting waitForWriteToCompleteForTesting(long maxWaitMillis)354 public boolean waitForWriteToCompleteForTesting(long maxWaitMillis) { 355 final long start = SystemClock.uptimeMillis(); 356 final long end = start + maxWaitMillis; 357 synchronized (mWriteScheduleLock) { 358 while (mWriteInProgress) { 359 final long now = SystemClock.uptimeMillis(); 360 if (now >= end) { 361 // still not done and we've hit the end; failure 362 return false; 363 } 364 try { 365 mWriteScheduleLock.wait(now - start + maxWaitMillis); 366 } catch (InterruptedException e) { 367 // Spurious; keep waiting 368 break; 369 } 370 } 371 } 372 return true; 373 } 374 375 /** 376 * Runnable that writes {@link #mJobSet} out to xml. 377 * NOTE: This Runnable locks on mLock 378 */ 379 private final Runnable mWriteRunnable = new Runnable() { 380 @Override 381 public void run() { 382 final long startElapsed = sElapsedRealtimeClock.millis(); 383 final List<JobStatus> storeCopy = new ArrayList<JobStatus>(); 384 // Intentionally allow new scheduling of a write operation *before* we clone 385 // the job set. If we reset it to false after cloning, there's a window in 386 // which no new write will be scheduled but mLock is not held, i.e. a new 387 // job might appear and fail to be recognized as needing a persist. The 388 // potential cost is one redundant write of an identical set of jobs in the 389 // rare case of that specific race, but by doing it this way we avoid quite 390 // a bit of lock contention. 391 synchronized (mWriteScheduleLock) { 392 mWriteScheduled = false; 393 } 394 synchronized (mLock) { 395 // Clone the jobs so we can release the lock before writing. 396 mJobSet.forEachJob(null, (job) -> { 397 if (job.isPersisted()) { 398 storeCopy.add(new JobStatus(job)); 399 } 400 }); 401 } 402 writeJobsMapImpl(storeCopy); 403 if (DEBUG) { 404 Slog.v(TAG, "Finished writing, took " + (sElapsedRealtimeClock.millis() 405 - startElapsed) + "ms"); 406 } 407 synchronized (mWriteScheduleLock) { 408 mWriteInProgress = false; 409 mWriteScheduleLock.notifyAll(); 410 } 411 } 412 413 private void writeJobsMapImpl(List<JobStatus> jobList) { 414 int numJobs = 0; 415 int numSystemJobs = 0; 416 int numSyncJobs = 0; 417 try { 418 final long startTime = SystemClock.uptimeMillis(); 419 ByteArrayOutputStream baos = new ByteArrayOutputStream(); 420 XmlSerializer out = new FastXmlSerializer(); 421 out.setOutput(baos, StandardCharsets.UTF_8.name()); 422 out.startDocument(null, true); 423 out.setFeature("http://xmlpull.org/v1/doc/features.html#indent-output", true); 424 425 out.startTag(null, "job-info"); 426 out.attribute(null, "version", Integer.toString(JOBS_FILE_VERSION)); 427 for (int i=0; i<jobList.size(); i++) { 428 JobStatus jobStatus = jobList.get(i); 429 if (DEBUG) { 430 Slog.d(TAG, "Saving job " + jobStatus.getJobId()); 431 } 432 out.startTag(null, "job"); 433 addAttributesToJobTag(out, jobStatus); 434 writeConstraintsToXml(out, jobStatus); 435 writeExecutionCriteriaToXml(out, jobStatus); 436 writeBundleToXml(jobStatus.getJob().getExtras(), out); 437 out.endTag(null, "job"); 438 439 numJobs++; 440 if (jobStatus.getUid() == Process.SYSTEM_UID) { 441 numSystemJobs++; 442 if (isSyncJob(jobStatus)) { 443 numSyncJobs++; 444 } 445 } 446 } 447 out.endTag(null, "job-info"); 448 out.endDocument(); 449 450 // Write out to disk in one fell swoop. 451 FileOutputStream fos = mJobsFile.startWrite(startTime); 452 fos.write(baos.toByteArray()); 453 mJobsFile.finishWrite(fos); 454 } catch (IOException e) { 455 if (DEBUG) { 456 Slog.v(TAG, "Error writing out job data.", e); 457 } 458 } catch (XmlPullParserException e) { 459 if (DEBUG) { 460 Slog.d(TAG, "Error persisting bundle.", e); 461 } 462 } finally { 463 mPersistInfo.countAllJobsSaved = numJobs; 464 mPersistInfo.countSystemServerJobsSaved = numSystemJobs; 465 mPersistInfo.countSystemSyncManagerJobsSaved = numSyncJobs; 466 } 467 } 468 469 /** Write out a tag with data comprising the required fields and priority of this job and 470 * its client. 471 */ 472 private void addAttributesToJobTag(XmlSerializer out, JobStatus jobStatus) 473 throws IOException { 474 out.attribute(null, "jobid", Integer.toString(jobStatus.getJobId())); 475 out.attribute(null, "package", jobStatus.getServiceComponent().getPackageName()); 476 out.attribute(null, "class", jobStatus.getServiceComponent().getClassName()); 477 if (jobStatus.getSourcePackageName() != null) { 478 out.attribute(null, "sourcePackageName", jobStatus.getSourcePackageName()); 479 } 480 if (jobStatus.getSourceTag() != null) { 481 out.attribute(null, "sourceTag", jobStatus.getSourceTag()); 482 } 483 out.attribute(null, "sourceUserId", String.valueOf(jobStatus.getSourceUserId())); 484 out.attribute(null, "uid", Integer.toString(jobStatus.getUid())); 485 out.attribute(null, "priority", String.valueOf(jobStatus.getPriority())); 486 out.attribute(null, "flags", String.valueOf(jobStatus.getFlags())); 487 if (jobStatus.getInternalFlags() != 0) { 488 out.attribute(null, "internalFlags", String.valueOf(jobStatus.getInternalFlags())); 489 } 490 491 out.attribute(null, "lastSuccessfulRunTime", 492 String.valueOf(jobStatus.getLastSuccessfulRunTime())); 493 out.attribute(null, "lastFailedRunTime", 494 String.valueOf(jobStatus.getLastFailedRunTime())); 495 } 496 497 private void writeBundleToXml(PersistableBundle extras, XmlSerializer out) 498 throws IOException, XmlPullParserException { 499 out.startTag(null, XML_TAG_EXTRAS); 500 PersistableBundle extrasCopy = deepCopyBundle(extras, 10); 501 extrasCopy.saveToXml(out); 502 out.endTag(null, XML_TAG_EXTRAS); 503 } 504 505 private PersistableBundle deepCopyBundle(PersistableBundle bundle, int maxDepth) { 506 if (maxDepth <= 0) { 507 return null; 508 } 509 PersistableBundle copy = (PersistableBundle) bundle.clone(); 510 Set<String> keySet = bundle.keySet(); 511 for (String key: keySet) { 512 Object o = copy.get(key); 513 if (o instanceof PersistableBundle) { 514 PersistableBundle bCopy = deepCopyBundle((PersistableBundle) o, maxDepth-1); 515 copy.putPersistableBundle(key, bCopy); 516 } 517 } 518 return copy; 519 } 520 521 /** 522 * Write out a tag with data identifying this job's constraints. If the constraint isn't here 523 * it doesn't apply. 524 */ 525 private void writeConstraintsToXml(XmlSerializer out, JobStatus jobStatus) throws IOException { 526 out.startTag(null, XML_TAG_PARAMS_CONSTRAINTS); 527 if (jobStatus.hasConnectivityConstraint()) { 528 final NetworkRequest network = jobStatus.getJob().getRequiredNetwork(); 529 out.attribute(null, "net-capabilities", Long.toString( 530 BitUtils.packBits(network.networkCapabilities.getCapabilities()))); 531 out.attribute(null, "net-unwanted-capabilities", Long.toString( 532 BitUtils.packBits(network.networkCapabilities.getUnwantedCapabilities()))); 533 534 out.attribute(null, "net-transport-types", Long.toString( 535 BitUtils.packBits(network.networkCapabilities.getTransportTypes()))); 536 } 537 if (jobStatus.hasIdleConstraint()) { 538 out.attribute(null, "idle", Boolean.toString(true)); 539 } 540 if (jobStatus.hasChargingConstraint()) { 541 out.attribute(null, "charging", Boolean.toString(true)); 542 } 543 if (jobStatus.hasBatteryNotLowConstraint()) { 544 out.attribute(null, "battery-not-low", Boolean.toString(true)); 545 } 546 if (jobStatus.hasStorageNotLowConstraint()) { 547 out.attribute(null, "storage-not-low", Boolean.toString(true)); 548 } 549 out.endTag(null, XML_TAG_PARAMS_CONSTRAINTS); 550 } 551 552 private void writeExecutionCriteriaToXml(XmlSerializer out, JobStatus jobStatus) 553 throws IOException { 554 final JobInfo job = jobStatus.getJob(); 555 if (jobStatus.getJob().isPeriodic()) { 556 out.startTag(null, XML_TAG_PERIODIC); 557 out.attribute(null, "period", Long.toString(job.getIntervalMillis())); 558 out.attribute(null, "flex", Long.toString(job.getFlexMillis())); 559 } else { 560 out.startTag(null, XML_TAG_ONEOFF); 561 } 562 563 // If we still have the persisted times, we need to record those directly because 564 // we haven't yet been able to calculate the usual elapsed-timebase bounds 565 // correctly due to wall-clock uncertainty. 566 Pair <Long, Long> utcJobTimes = jobStatus.getPersistedUtcTimes(); 567 if (DEBUG && utcJobTimes != null) { 568 Slog.i(TAG, "storing original UTC timestamps for " + jobStatus); 569 } 570 571 final long nowRTC = sSystemClock.millis(); 572 final long nowElapsed = sElapsedRealtimeClock.millis(); 573 if (jobStatus.hasDeadlineConstraint()) { 574 // Wall clock deadline. 575 final long deadlineWallclock = (utcJobTimes == null) 576 ? nowRTC + (jobStatus.getLatestRunTimeElapsed() - nowElapsed) 577 : utcJobTimes.second; 578 out.attribute(null, "deadline", Long.toString(deadlineWallclock)); 579 } 580 if (jobStatus.hasTimingDelayConstraint()) { 581 final long delayWallclock = (utcJobTimes == null) 582 ? nowRTC + (jobStatus.getEarliestRunTime() - nowElapsed) 583 : utcJobTimes.first; 584 out.attribute(null, "delay", Long.toString(delayWallclock)); 585 } 586 587 // Only write out back-off policy if it differs from the default. 588 // This also helps the case where the job is idle -> these aren't allowed to specify 589 // back-off. 590 if (jobStatus.getJob().getInitialBackoffMillis() != JobInfo.DEFAULT_INITIAL_BACKOFF_MILLIS 591 || jobStatus.getJob().getBackoffPolicy() != JobInfo.DEFAULT_BACKOFF_POLICY) { 592 out.attribute(null, "backoff-policy", Integer.toString(job.getBackoffPolicy())); 593 out.attribute(null, "initial-backoff", Long.toString(job.getInitialBackoffMillis())); 594 } 595 if (job.isPeriodic()) { 596 out.endTag(null, XML_TAG_PERIODIC); 597 } else { 598 out.endTag(null, XML_TAG_ONEOFF); 599 } 600 } 601 }; 602 603 /** 604 * Translate the supplied RTC times to the elapsed timebase, with clamping appropriate 605 * to interpreting them as a job's delay + deadline times for alarm-setting purposes. 606 * @param rtcTimes a Pair<Long, Long> in which {@code first} is the "delay" earliest 607 * allowable runtime for the job, and {@code second} is the "deadline" time at which 608 * the job becomes overdue. 609 */ convertRtcBoundsToElapsed(Pair<Long, Long> rtcTimes, long nowElapsed)610 private static Pair<Long, Long> convertRtcBoundsToElapsed(Pair<Long, Long> rtcTimes, 611 long nowElapsed) { 612 final long nowWallclock = sSystemClock.millis(); 613 final long earliest = (rtcTimes.first > JobStatus.NO_EARLIEST_RUNTIME) 614 ? nowElapsed + Math.max(rtcTimes.first - nowWallclock, 0) 615 : JobStatus.NO_EARLIEST_RUNTIME; 616 final long latest = (rtcTimes.second < JobStatus.NO_LATEST_RUNTIME) 617 ? nowElapsed + Math.max(rtcTimes.second - nowWallclock, 0) 618 : JobStatus.NO_LATEST_RUNTIME; 619 return Pair.create(earliest, latest); 620 } 621 isSyncJob(JobStatus status)622 private static boolean isSyncJob(JobStatus status) { 623 return com.android.server.content.SyncJobService.class.getName() 624 .equals(status.getServiceComponent().getClassName()); 625 } 626 627 /** 628 * Runnable that reads list of persisted job from xml. This is run once at start up, so doesn't 629 * need to go through {@link JobStore#add(com.android.server.job.controllers.JobStatus)}. 630 */ 631 private final class ReadJobMapFromDiskRunnable implements Runnable { 632 private final JobSet jobSet; 633 private final boolean rtcGood; 634 635 /** 636 * @param jobSet Reference to the (empty) set of JobStatus objects that back the JobStore, 637 * so that after disk read we can populate it directly. 638 */ ReadJobMapFromDiskRunnable(JobSet jobSet, boolean rtcIsGood)639 ReadJobMapFromDiskRunnable(JobSet jobSet, boolean rtcIsGood) { 640 this.jobSet = jobSet; 641 this.rtcGood = rtcIsGood; 642 } 643 644 @Override run()645 public void run() { 646 int numJobs = 0; 647 int numSystemJobs = 0; 648 int numSyncJobs = 0; 649 try { 650 List<JobStatus> jobs; 651 FileInputStream fis = mJobsFile.openRead(); 652 synchronized (mLock) { 653 jobs = readJobMapImpl(fis, rtcGood); 654 if (jobs != null) { 655 long now = sElapsedRealtimeClock.millis(); 656 IActivityManager am = ActivityManager.getService(); 657 for (int i=0; i<jobs.size(); i++) { 658 JobStatus js = jobs.get(i); 659 js.prepareLocked(am); 660 js.enqueueTime = now; 661 this.jobSet.add(js); 662 663 numJobs++; 664 if (js.getUid() == Process.SYSTEM_UID) { 665 numSystemJobs++; 666 if (isSyncJob(js)) { 667 numSyncJobs++; 668 } 669 } 670 } 671 } 672 } 673 fis.close(); 674 } catch (FileNotFoundException e) { 675 if (DEBUG) { 676 Slog.d(TAG, "Could not find jobs file, probably there was nothing to load."); 677 } 678 } catch (XmlPullParserException | IOException e) { 679 Slog.wtf(TAG, "Error jobstore xml.", e); 680 } finally { 681 if (mPersistInfo.countAllJobsLoaded < 0) { // Only set them once. 682 mPersistInfo.countAllJobsLoaded = numJobs; 683 mPersistInfo.countSystemServerJobsLoaded = numSystemJobs; 684 mPersistInfo.countSystemSyncManagerJobsLoaded = numSyncJobs; 685 } 686 } 687 Slog.i(TAG, "Read " + numJobs + " jobs"); 688 } 689 readJobMapImpl(FileInputStream fis, boolean rtcIsGood)690 private List<JobStatus> readJobMapImpl(FileInputStream fis, boolean rtcIsGood) 691 throws XmlPullParserException, IOException { 692 XmlPullParser parser = Xml.newPullParser(); 693 parser.setInput(fis, StandardCharsets.UTF_8.name()); 694 695 int eventType = parser.getEventType(); 696 while (eventType != XmlPullParser.START_TAG && 697 eventType != XmlPullParser.END_DOCUMENT) { 698 eventType = parser.next(); 699 Slog.d(TAG, "Start tag: " + parser.getName()); 700 } 701 if (eventType == XmlPullParser.END_DOCUMENT) { 702 if (DEBUG) { 703 Slog.d(TAG, "No persisted jobs."); 704 } 705 return null; 706 } 707 708 String tagName = parser.getName(); 709 if ("job-info".equals(tagName)) { 710 final List<JobStatus> jobs = new ArrayList<JobStatus>(); 711 // Read in version info. 712 try { 713 int version = Integer.parseInt(parser.getAttributeValue(null, "version")); 714 if (version != JOBS_FILE_VERSION) { 715 Slog.d(TAG, "Invalid version number, aborting jobs file read."); 716 return null; 717 } 718 } catch (NumberFormatException e) { 719 Slog.e(TAG, "Invalid version number, aborting jobs file read."); 720 return null; 721 } 722 eventType = parser.next(); 723 do { 724 // Read each <job/> 725 if (eventType == XmlPullParser.START_TAG) { 726 tagName = parser.getName(); 727 // Start reading job. 728 if ("job".equals(tagName)) { 729 JobStatus persistedJob = restoreJobFromXml(rtcIsGood, parser); 730 if (persistedJob != null) { 731 if (DEBUG) { 732 Slog.d(TAG, "Read out " + persistedJob); 733 } 734 jobs.add(persistedJob); 735 } else { 736 Slog.d(TAG, "Error reading job from file."); 737 } 738 } 739 } 740 eventType = parser.next(); 741 } while (eventType != XmlPullParser.END_DOCUMENT); 742 return jobs; 743 } 744 return null; 745 } 746 747 /** 748 * @param parser Xml parser at the beginning of a "<job/>" tag. The next "parser.next()" call 749 * will take the parser into the body of the job tag. 750 * @return Newly instantiated job holding all the information we just read out of the xml tag. 751 */ restoreJobFromXml(boolean rtcIsGood, XmlPullParser parser)752 private JobStatus restoreJobFromXml(boolean rtcIsGood, XmlPullParser parser) 753 throws XmlPullParserException, IOException { 754 JobInfo.Builder jobBuilder; 755 int uid, sourceUserId; 756 long lastSuccessfulRunTime; 757 long lastFailedRunTime; 758 int internalFlags = 0; 759 760 // Read out job identifier attributes and priority. 761 try { 762 jobBuilder = buildBuilderFromXml(parser); 763 jobBuilder.setPersisted(true); 764 uid = Integer.parseInt(parser.getAttributeValue(null, "uid")); 765 766 String val = parser.getAttributeValue(null, "priority"); 767 if (val != null) { 768 jobBuilder.setPriority(Integer.parseInt(val)); 769 } 770 val = parser.getAttributeValue(null, "flags"); 771 if (val != null) { 772 jobBuilder.setFlags(Integer.parseInt(val)); 773 } 774 val = parser.getAttributeValue(null, "internalFlags"); 775 if (val != null) { 776 internalFlags = Integer.parseInt(val); 777 } 778 val = parser.getAttributeValue(null, "sourceUserId"); 779 sourceUserId = val == null ? -1 : Integer.parseInt(val); 780 781 val = parser.getAttributeValue(null, "lastSuccessfulRunTime"); 782 lastSuccessfulRunTime = val == null ? 0 : Long.parseLong(val); 783 784 val = parser.getAttributeValue(null, "lastFailedRunTime"); 785 lastFailedRunTime = val == null ? 0 : Long.parseLong(val); 786 } catch (NumberFormatException e) { 787 Slog.e(TAG, "Error parsing job's required fields, skipping"); 788 return null; 789 } 790 791 String sourcePackageName = parser.getAttributeValue(null, "sourcePackageName"); 792 final String sourceTag = parser.getAttributeValue(null, "sourceTag"); 793 794 int eventType; 795 // Read out constraints tag. 796 do { 797 eventType = parser.next(); 798 } while (eventType == XmlPullParser.TEXT); // Push through to next START_TAG. 799 800 if (!(eventType == XmlPullParser.START_TAG && 801 XML_TAG_PARAMS_CONSTRAINTS.equals(parser.getName()))) { 802 // Expecting a <constraints> start tag. 803 return null; 804 } 805 try { 806 buildConstraintsFromXml(jobBuilder, parser); 807 } catch (NumberFormatException e) { 808 Slog.d(TAG, "Error reading constraints, skipping."); 809 return null; 810 } 811 parser.next(); // Consume </constraints> 812 813 // Read out execution parameters tag. 814 do { 815 eventType = parser.next(); 816 } while (eventType == XmlPullParser.TEXT); 817 if (eventType != XmlPullParser.START_TAG) { 818 return null; 819 } 820 821 // Tuple of (earliest runtime, latest runtime) in UTC. 822 final Pair<Long, Long> rtcRuntimes; 823 try { 824 rtcRuntimes = buildRtcExecutionTimesFromXml(parser); 825 } catch (NumberFormatException e) { 826 if (DEBUG) { 827 Slog.d(TAG, "Error parsing execution time parameters, skipping."); 828 } 829 return null; 830 } 831 832 final long elapsedNow = sElapsedRealtimeClock.millis(); 833 Pair<Long, Long> elapsedRuntimes = convertRtcBoundsToElapsed(rtcRuntimes, elapsedNow); 834 835 if (XML_TAG_PERIODIC.equals(parser.getName())) { 836 try { 837 String val = parser.getAttributeValue(null, "period"); 838 final long periodMillis = Long.parseLong(val); 839 val = parser.getAttributeValue(null, "flex"); 840 final long flexMillis = (val != null) ? Long.valueOf(val) : periodMillis; 841 jobBuilder.setPeriodic(periodMillis, flexMillis); 842 // As a sanity check, cap the recreated run time to be no later than flex+period 843 // from now. This is the latest the periodic could be pushed out. This could 844 // happen if the periodic ran early (at flex time before period), and then the 845 // device rebooted. 846 if (elapsedRuntimes.second > elapsedNow + periodMillis + flexMillis) { 847 final long clampedLateRuntimeElapsed = elapsedNow + flexMillis 848 + periodMillis; 849 final long clampedEarlyRuntimeElapsed = clampedLateRuntimeElapsed 850 - flexMillis; 851 Slog.w(TAG, 852 String.format("Periodic job for uid='%d' persisted run-time is" + 853 " too big [%s, %s]. Clamping to [%s,%s]", 854 uid, 855 DateUtils.formatElapsedTime(elapsedRuntimes.first / 1000), 856 DateUtils.formatElapsedTime(elapsedRuntimes.second / 1000), 857 DateUtils.formatElapsedTime( 858 clampedEarlyRuntimeElapsed / 1000), 859 DateUtils.formatElapsedTime( 860 clampedLateRuntimeElapsed / 1000)) 861 ); 862 elapsedRuntimes = 863 Pair.create(clampedEarlyRuntimeElapsed, clampedLateRuntimeElapsed); 864 } 865 } catch (NumberFormatException e) { 866 Slog.d(TAG, "Error reading periodic execution criteria, skipping."); 867 return null; 868 } 869 } else if (XML_TAG_ONEOFF.equals(parser.getName())) { 870 try { 871 if (elapsedRuntimes.first != JobStatus.NO_EARLIEST_RUNTIME) { 872 jobBuilder.setMinimumLatency(elapsedRuntimes.first - elapsedNow); 873 } 874 if (elapsedRuntimes.second != JobStatus.NO_LATEST_RUNTIME) { 875 jobBuilder.setOverrideDeadline( 876 elapsedRuntimes.second - elapsedNow); 877 } 878 } catch (NumberFormatException e) { 879 Slog.d(TAG, "Error reading job execution criteria, skipping."); 880 return null; 881 } 882 } else { 883 if (DEBUG) { 884 Slog.d(TAG, "Invalid parameter tag, skipping - " + parser.getName()); 885 } 886 // Expecting a parameters start tag. 887 return null; 888 } 889 maybeBuildBackoffPolicyFromXml(jobBuilder, parser); 890 891 parser.nextTag(); // Consume parameters end tag. 892 893 // Read out extras Bundle. 894 do { 895 eventType = parser.next(); 896 } while (eventType == XmlPullParser.TEXT); 897 if (!(eventType == XmlPullParser.START_TAG 898 && XML_TAG_EXTRAS.equals(parser.getName()))) { 899 if (DEBUG) { 900 Slog.d(TAG, "Error reading extras, skipping."); 901 } 902 return null; 903 } 904 905 PersistableBundle extras = PersistableBundle.restoreFromXml(parser); 906 jobBuilder.setExtras(extras); 907 parser.nextTag(); // Consume </extras> 908 909 final JobInfo builtJob; 910 try { 911 builtJob = jobBuilder.build(); 912 } catch (Exception e) { 913 Slog.w(TAG, "Unable to build job from XML, ignoring: " 914 + jobBuilder.summarize()); 915 return null; 916 } 917 918 // Migrate sync jobs forward from earlier, incomplete representation 919 if ("android".equals(sourcePackageName) 920 && extras != null 921 && extras.getBoolean("SyncManagerJob", false)) { 922 sourcePackageName = extras.getString("owningPackage", sourcePackageName); 923 if (DEBUG) { 924 Slog.i(TAG, "Fixing up sync job source package name from 'android' to '" 925 + sourcePackageName + "'"); 926 } 927 } 928 929 // And now we're done 930 JobSchedulerInternal service = LocalServices.getService(JobSchedulerInternal.class); 931 final int appBucket = JobSchedulerService.standbyBucketForPackage(sourcePackageName, 932 sourceUserId, elapsedNow); 933 long currentHeartbeat = service != null ? service.currentHeartbeat() : 0; 934 JobStatus js = new JobStatus( 935 jobBuilder.build(), uid, sourcePackageName, sourceUserId, 936 appBucket, currentHeartbeat, sourceTag, 937 elapsedRuntimes.first, elapsedRuntimes.second, 938 lastSuccessfulRunTime, lastFailedRunTime, 939 (rtcIsGood) ? null : rtcRuntimes, internalFlags); 940 return js; 941 } 942 buildBuilderFromXml(XmlPullParser parser)943 private JobInfo.Builder buildBuilderFromXml(XmlPullParser parser) throws NumberFormatException { 944 // Pull out required fields from <job> attributes. 945 int jobId = Integer.parseInt(parser.getAttributeValue(null, "jobid")); 946 String packageName = parser.getAttributeValue(null, "package"); 947 String className = parser.getAttributeValue(null, "class"); 948 ComponentName cname = new ComponentName(packageName, className); 949 950 return new JobInfo.Builder(jobId, cname); 951 } 952 buildConstraintsFromXml(JobInfo.Builder jobBuilder, XmlPullParser parser)953 private void buildConstraintsFromXml(JobInfo.Builder jobBuilder, XmlPullParser parser) { 954 String val; 955 956 final String netCapabilities = parser.getAttributeValue(null, "net-capabilities"); 957 final String netUnwantedCapabilities = parser.getAttributeValue( 958 null, "net-unwanted-capabilities"); 959 final String netTransportTypes = parser.getAttributeValue(null, "net-transport-types"); 960 if (netCapabilities != null && netTransportTypes != null) { 961 final NetworkRequest request = new NetworkRequest.Builder().build(); 962 final long unwantedCapabilities = netUnwantedCapabilities != null 963 ? Long.parseLong(netUnwantedCapabilities) 964 : BitUtils.packBits(request.networkCapabilities.getUnwantedCapabilities()); 965 966 // We're okay throwing NFE here; caught by caller 967 request.networkCapabilities.setCapabilities( 968 BitUtils.unpackBits(Long.parseLong(netCapabilities)), 969 BitUtils.unpackBits(unwantedCapabilities)); 970 request.networkCapabilities.setTransportTypes( 971 BitUtils.unpackBits(Long.parseLong(netTransportTypes))); 972 jobBuilder.setRequiredNetwork(request); 973 } else { 974 // Read legacy values 975 val = parser.getAttributeValue(null, "connectivity"); 976 if (val != null) { 977 jobBuilder.setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY); 978 } 979 val = parser.getAttributeValue(null, "metered"); 980 if (val != null) { 981 jobBuilder.setRequiredNetworkType(JobInfo.NETWORK_TYPE_METERED); 982 } 983 val = parser.getAttributeValue(null, "unmetered"); 984 if (val != null) { 985 jobBuilder.setRequiredNetworkType(JobInfo.NETWORK_TYPE_UNMETERED); 986 } 987 val = parser.getAttributeValue(null, "not-roaming"); 988 if (val != null) { 989 jobBuilder.setRequiredNetworkType(JobInfo.NETWORK_TYPE_NOT_ROAMING); 990 } 991 } 992 993 val = parser.getAttributeValue(null, "idle"); 994 if (val != null) { 995 jobBuilder.setRequiresDeviceIdle(true); 996 } 997 val = parser.getAttributeValue(null, "charging"); 998 if (val != null) { 999 jobBuilder.setRequiresCharging(true); 1000 } 1001 val = parser.getAttributeValue(null, "battery-not-low"); 1002 if (val != null) { 1003 jobBuilder.setRequiresBatteryNotLow(true); 1004 } 1005 val = parser.getAttributeValue(null, "storage-not-low"); 1006 if (val != null) { 1007 jobBuilder.setRequiresStorageNotLow(true); 1008 } 1009 } 1010 1011 /** 1012 * Builds the back-off policy out of the params tag. These attributes may not exist, depending 1013 * on whether the back-off was set when the job was first scheduled. 1014 */ maybeBuildBackoffPolicyFromXml(JobInfo.Builder jobBuilder, XmlPullParser parser)1015 private void maybeBuildBackoffPolicyFromXml(JobInfo.Builder jobBuilder, XmlPullParser parser) { 1016 String val = parser.getAttributeValue(null, "initial-backoff"); 1017 if (val != null) { 1018 long initialBackoff = Long.parseLong(val); 1019 val = parser.getAttributeValue(null, "backoff-policy"); 1020 int backoffPolicy = Integer.parseInt(val); // Will throw NFE which we catch higher up. 1021 jobBuilder.setBackoffCriteria(initialBackoff, backoffPolicy); 1022 } 1023 } 1024 1025 /** 1026 * Extract a job's earliest/latest run time data from XML. These are returned in 1027 * unadjusted UTC wall clock time, because we do not yet know whether the system 1028 * clock is reliable for purposes of calculating deltas from 'now'. 1029 * 1030 * @param parser 1031 * @return A Pair of timestamps in UTC wall-clock time. The first is the earliest 1032 * time at which the job is to become runnable, and the second is the deadline at 1033 * which it becomes overdue to execute. 1034 * @throws NumberFormatException 1035 */ buildRtcExecutionTimesFromXml(XmlPullParser parser)1036 private Pair<Long, Long> buildRtcExecutionTimesFromXml(XmlPullParser parser) 1037 throws NumberFormatException { 1038 String val; 1039 // Pull out execution time data. 1040 val = parser.getAttributeValue(null, "delay"); 1041 final long earliestRunTimeRtc = (val != null) 1042 ? Long.parseLong(val) 1043 : JobStatus.NO_EARLIEST_RUNTIME; 1044 val = parser.getAttributeValue(null, "deadline"); 1045 final long latestRunTimeRtc = (val != null) 1046 ? Long.parseLong(val) 1047 : JobStatus.NO_LATEST_RUNTIME; 1048 return Pair.create(earliestRunTimeRtc, latestRunTimeRtc); 1049 } 1050 } 1051 1052 static final class JobSet { 1053 @VisibleForTesting // Key is the getUid() originator of the jobs in each sheaf 1054 final SparseArray<ArraySet<JobStatus>> mJobs; 1055 1056 @VisibleForTesting // Same data but with the key as getSourceUid() of the jobs in each sheaf 1057 final SparseArray<ArraySet<JobStatus>> mJobsPerSourceUid; 1058 JobSet()1059 public JobSet() { 1060 mJobs = new SparseArray<ArraySet<JobStatus>>(); 1061 mJobsPerSourceUid = new SparseArray<>(); 1062 } 1063 getJobsByUid(int uid)1064 public List<JobStatus> getJobsByUid(int uid) { 1065 ArrayList<JobStatus> matchingJobs = new ArrayList<JobStatus>(); 1066 ArraySet<JobStatus> jobs = mJobs.get(uid); 1067 if (jobs != null) { 1068 matchingJobs.addAll(jobs); 1069 } 1070 return matchingJobs; 1071 } 1072 1073 // By user, not by uid, so we need to traverse by key and check getJobsByUser(int userId)1074 public List<JobStatus> getJobsByUser(int userId) { 1075 final ArrayList<JobStatus> result = new ArrayList<JobStatus>(); 1076 for (int i = mJobsPerSourceUid.size() - 1; i >= 0; i--) { 1077 if (UserHandle.getUserId(mJobsPerSourceUid.keyAt(i)) == userId) { 1078 final ArraySet<JobStatus> jobs = mJobsPerSourceUid.valueAt(i); 1079 if (jobs != null) { 1080 result.addAll(jobs); 1081 } 1082 } 1083 } 1084 return result; 1085 } 1086 add(JobStatus job)1087 public boolean add(JobStatus job) { 1088 final int uid = job.getUid(); 1089 final int sourceUid = job.getSourceUid(); 1090 ArraySet<JobStatus> jobs = mJobs.get(uid); 1091 if (jobs == null) { 1092 jobs = new ArraySet<JobStatus>(); 1093 mJobs.put(uid, jobs); 1094 } 1095 ArraySet<JobStatus> jobsForSourceUid = mJobsPerSourceUid.get(sourceUid); 1096 if (jobsForSourceUid == null) { 1097 jobsForSourceUid = new ArraySet<>(); 1098 mJobsPerSourceUid.put(sourceUid, jobsForSourceUid); 1099 } 1100 final boolean added = jobs.add(job); 1101 final boolean addedInSource = jobsForSourceUid.add(job); 1102 if (added != addedInSource) { 1103 Slog.wtf(TAG, "mJobs and mJobsPerSourceUid mismatch; caller= " + added 1104 + " source= " + addedInSource); 1105 } 1106 return added || addedInSource; 1107 } 1108 remove(JobStatus job)1109 public boolean remove(JobStatus job) { 1110 final int uid = job.getUid(); 1111 final ArraySet<JobStatus> jobs = mJobs.get(uid); 1112 final int sourceUid = job.getSourceUid(); 1113 final ArraySet<JobStatus> jobsForSourceUid = mJobsPerSourceUid.get(sourceUid); 1114 final boolean didRemove = jobs != null && jobs.remove(job); 1115 final boolean sourceRemove = jobsForSourceUid != null && jobsForSourceUid.remove(job); 1116 if (didRemove != sourceRemove) { 1117 Slog.wtf(TAG, "Job presence mismatch; caller=" + didRemove 1118 + " source=" + sourceRemove); 1119 } 1120 if (didRemove || sourceRemove) { 1121 // no more jobs for this uid? let the now-empty set objects be GC'd. 1122 if (jobs != null && jobs.size() == 0) { 1123 mJobs.remove(uid); 1124 } 1125 if (jobsForSourceUid != null && jobsForSourceUid.size() == 0) { 1126 mJobsPerSourceUid.remove(sourceUid); 1127 } 1128 return true; 1129 } 1130 return false; 1131 } 1132 1133 /** 1134 * Removes the jobs of all users not specified by the whitelist of user ids. 1135 * This will remove jobs scheduled *by* non-existent users as well as jobs scheduled *for* 1136 * non-existent users 1137 */ removeJobsOfNonUsers(final int[] whitelist)1138 public void removeJobsOfNonUsers(final int[] whitelist) { 1139 final Predicate<JobStatus> noSourceUser = 1140 job -> !ArrayUtils.contains(whitelist, job.getSourceUserId()); 1141 final Predicate<JobStatus> noCallingUser = 1142 job -> !ArrayUtils.contains(whitelist, job.getUserId()); 1143 removeAll(noSourceUser.or(noCallingUser)); 1144 } 1145 removeAll(Predicate<JobStatus> predicate)1146 private void removeAll(Predicate<JobStatus> predicate) { 1147 for (int jobSetIndex = mJobs.size() - 1; jobSetIndex >= 0; jobSetIndex--) { 1148 final ArraySet<JobStatus> jobs = mJobs.valueAt(jobSetIndex); 1149 for (int jobIndex = jobs.size() - 1; jobIndex >= 0; jobIndex--) { 1150 if (predicate.test(jobs.valueAt(jobIndex))) { 1151 jobs.removeAt(jobIndex); 1152 } 1153 } 1154 if (jobs.size() == 0) { 1155 mJobs.removeAt(jobSetIndex); 1156 } 1157 } 1158 for (int jobSetIndex = mJobsPerSourceUid.size() - 1; jobSetIndex >= 0; jobSetIndex--) { 1159 final ArraySet<JobStatus> jobs = mJobsPerSourceUid.valueAt(jobSetIndex); 1160 for (int jobIndex = jobs.size() - 1; jobIndex >= 0; jobIndex--) { 1161 if (predicate.test(jobs.valueAt(jobIndex))) { 1162 jobs.removeAt(jobIndex); 1163 } 1164 } 1165 if (jobs.size() == 0) { 1166 mJobsPerSourceUid.removeAt(jobSetIndex); 1167 } 1168 } 1169 } 1170 contains(JobStatus job)1171 public boolean contains(JobStatus job) { 1172 final int uid = job.getUid(); 1173 ArraySet<JobStatus> jobs = mJobs.get(uid); 1174 return jobs != null && jobs.contains(job); 1175 } 1176 get(int uid, int jobId)1177 public JobStatus get(int uid, int jobId) { 1178 ArraySet<JobStatus> jobs = mJobs.get(uid); 1179 if (jobs != null) { 1180 for (int i = jobs.size() - 1; i >= 0; i--) { 1181 JobStatus job = jobs.valueAt(i); 1182 if (job.getJobId() == jobId) { 1183 return job; 1184 } 1185 } 1186 } 1187 return null; 1188 } 1189 1190 // Inefficient; use only for testing getAllJobs()1191 public List<JobStatus> getAllJobs() { 1192 ArrayList<JobStatus> allJobs = new ArrayList<JobStatus>(size()); 1193 for (int i = mJobs.size() - 1; i >= 0; i--) { 1194 ArraySet<JobStatus> jobs = mJobs.valueAt(i); 1195 if (jobs != null) { 1196 // Use a for loop over the ArraySet, so we don't need to make its 1197 // optional collection class iterator implementation or have to go 1198 // through a temporary array from toArray(). 1199 for (int j = jobs.size() - 1; j >= 0; j--) { 1200 allJobs.add(jobs.valueAt(j)); 1201 } 1202 } 1203 } 1204 return allJobs; 1205 } 1206 clear()1207 public void clear() { 1208 mJobs.clear(); 1209 mJobsPerSourceUid.clear(); 1210 } 1211 size()1212 public int size() { 1213 int total = 0; 1214 for (int i = mJobs.size() - 1; i >= 0; i--) { 1215 total += mJobs.valueAt(i).size(); 1216 } 1217 return total; 1218 } 1219 1220 // We only want to count the jobs that this uid has scheduled on its own 1221 // behalf, not those that the app has scheduled on someone else's behalf. countJobsForUid(int uid)1222 public int countJobsForUid(int uid) { 1223 int total = 0; 1224 ArraySet<JobStatus> jobs = mJobs.get(uid); 1225 if (jobs != null) { 1226 for (int i = jobs.size() - 1; i >= 0; i--) { 1227 JobStatus job = jobs.valueAt(i); 1228 if (job.getUid() == job.getSourceUid()) { 1229 total++; 1230 } 1231 } 1232 } 1233 return total; 1234 } 1235 forEachJob(@ullable Predicate<JobStatus> filterPredicate, Consumer<JobStatus> functor)1236 public void forEachJob(@Nullable Predicate<JobStatus> filterPredicate, 1237 Consumer<JobStatus> functor) { 1238 for (int uidIndex = mJobs.size() - 1; uidIndex >= 0; uidIndex--) { 1239 ArraySet<JobStatus> jobs = mJobs.valueAt(uidIndex); 1240 if (jobs != null) { 1241 for (int i = jobs.size() - 1; i >= 0; i--) { 1242 final JobStatus jobStatus = jobs.valueAt(i); 1243 if ((filterPredicate == null) || filterPredicate.test(jobStatus)) { 1244 functor.accept(jobStatus); 1245 } 1246 } 1247 } 1248 } 1249 } 1250 forEachJob(int callingUid, Consumer<JobStatus> functor)1251 public void forEachJob(int callingUid, Consumer<JobStatus> functor) { 1252 ArraySet<JobStatus> jobs = mJobs.get(callingUid); 1253 if (jobs != null) { 1254 for (int i = jobs.size() - 1; i >= 0; i--) { 1255 functor.accept(jobs.valueAt(i)); 1256 } 1257 } 1258 } 1259 forEachJobForSourceUid(int sourceUid, Consumer<JobStatus> functor)1260 public void forEachJobForSourceUid(int sourceUid, Consumer<JobStatus> functor) { 1261 final ArraySet<JobStatus> jobs = mJobsPerSourceUid.get(sourceUid); 1262 if (jobs != null) { 1263 for (int i = jobs.size() - 1; i >= 0; i--) { 1264 functor.accept(jobs.valueAt(i)); 1265 } 1266 } 1267 } 1268 } 1269 } 1270