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