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