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.content.ComponentName; 20 import android.app.job.JobInfo; 21 import android.content.Context; 22 import android.os.Environment; 23 import android.os.Handler; 24 import android.os.PersistableBundle; 25 import android.os.SystemClock; 26 import android.os.UserHandle; 27 import android.util.AtomicFile; 28 import android.util.ArraySet; 29 import android.util.Pair; 30 import android.util.Slog; 31 import android.util.Xml; 32 33 import com.android.internal.annotations.VisibleForTesting; 34 import com.android.internal.util.FastXmlSerializer; 35 import com.android.server.IoThread; 36 import com.android.server.job.controllers.JobStatus; 37 38 import java.io.ByteArrayOutputStream; 39 import java.io.File; 40 import java.io.FileInputStream; 41 import java.io.FileNotFoundException; 42 import java.io.FileOutputStream; 43 import java.io.IOException; 44 import java.nio.charset.StandardCharsets; 45 import java.util.ArrayList; 46 import java.util.Iterator; 47 import java.util.List; 48 49 import org.xmlpull.v1.XmlPullParser; 50 import org.xmlpull.v1.XmlPullParserException; 51 import org.xmlpull.v1.XmlSerializer; 52 53 /** 54 * Maintains the master list of jobs that the job scheduler is tracking. These jobs are compared by 55 * reference, so none of the functions in this class should make a copy. 56 * Also handles read/write of persisted jobs. 57 * 58 * Note on locking: 59 * All callers to this class must <strong>lock on the class object they are calling</strong>. 60 * This is important b/c {@link com.android.server.job.JobStore.WriteJobsMapToDiskRunnable} 61 * and {@link com.android.server.job.JobStore.ReadJobMapFromDiskRunnable} lock on that 62 * object. 63 */ 64 public class JobStore { 65 private static final String TAG = "JobStore"; 66 private static final boolean DEBUG = JobSchedulerService.DEBUG; 67 68 /** Threshold to adjust how often we want to write to the db. */ 69 private static final int MAX_OPS_BEFORE_WRITE = 1; 70 final ArraySet<JobStatus> mJobSet; 71 final Context mContext; 72 73 private int mDirtyOperations; 74 75 private static final Object sSingletonLock = new Object(); 76 private final AtomicFile mJobsFile; 77 /** Handler backed by IoThread for writing to disk. */ 78 private final Handler mIoHandler = IoThread.getHandler(); 79 private static JobStore sSingleton; 80 81 /** Used by the {@link JobSchedulerService} to instantiate the JobStore. */ 82 static JobStore initAndGet(JobSchedulerService jobManagerService) { 83 synchronized (sSingletonLock) { 84 if (sSingleton == null) { 85 sSingleton = new JobStore(jobManagerService.getContext(), 86 Environment.getDataDirectory()); 87 } 88 return sSingleton; 89 } 90 } 91 92 /** 93 * @return A freshly initialized job store object, with no loaded jobs. 94 */ 95 @VisibleForTesting 96 public static JobStore initAndGetForTesting(Context context, File dataDir) { 97 JobStore jobStoreUnderTest = new JobStore(context, dataDir); 98 jobStoreUnderTest.clear(); 99 return jobStoreUnderTest; 100 } 101 102 /** 103 * Construct the instance of the job store. This results in a blocking read from disk. 104 */ 105 private JobStore(Context context, File dataDir) { 106 mContext = context; 107 mDirtyOperations = 0; 108 109 File systemDir = new File(dataDir, "system"); 110 File jobDir = new File(systemDir, "job"); 111 jobDir.mkdirs(); 112 mJobsFile = new AtomicFile(new File(jobDir, "jobs.xml")); 113 114 mJobSet = new ArraySet<JobStatus>(); 115 116 readJobMapFromDisk(mJobSet); 117 } 118 119 /** 120 * Add a job to the master list, persisting it if necessary. If the JobStatus already exists, 121 * it will be replaced. 122 * @param jobStatus Job to add. 123 * @return Whether or not an equivalent JobStatus was replaced by this operation. 124 */ 125 public boolean add(JobStatus jobStatus) { 126 boolean replaced = mJobSet.remove(jobStatus); 127 mJobSet.add(jobStatus); 128 if (jobStatus.isPersisted()) { 129 maybeWriteStatusToDiskAsync(); 130 } 131 if (DEBUG) { 132 Slog.d(TAG, "Added job status to store: " + jobStatus); 133 } 134 return replaced; 135 } 136 137 /** 138 * Whether this jobStatus object already exists in the JobStore. 139 */ 140 public boolean containsJobIdForUid(int jobId, int uId) { 141 for (int i=mJobSet.size()-1; i>=0; i--) { 142 JobStatus ts = mJobSet.valueAt(i); 143 if (ts.getUid() == uId && ts.getJobId() == jobId) { 144 return true; 145 } 146 } 147 return false; 148 } 149 150 boolean containsJob(JobStatus jobStatus) { 151 return mJobSet.contains(jobStatus); 152 } 153 154 public int size() { 155 return mJobSet.size(); 156 } 157 158 /** 159 * Remove the provided job. Will also delete the job if it was persisted. 160 * @return Whether or not the job existed to be removed. 161 */ 162 public boolean remove(JobStatus jobStatus) { 163 boolean removed = mJobSet.remove(jobStatus); 164 if (!removed) { 165 if (DEBUG) { 166 Slog.d(TAG, "Couldn't remove job: didn't exist: " + jobStatus); 167 } 168 return false; 169 } 170 if (jobStatus.isPersisted()) { 171 maybeWriteStatusToDiskAsync(); 172 } 173 return removed; 174 } 175 176 @VisibleForTesting 177 public void clear() { 178 mJobSet.clear(); 179 maybeWriteStatusToDiskAsync(); 180 } 181 182 /** 183 * @param userHandle User for whom we are querying the list of jobs. 184 * @return A list of all the jobs scheduled by the provided user. Never null. 185 */ 186 public List<JobStatus> getJobsByUser(int userHandle) { 187 List<JobStatus> matchingJobs = new ArrayList<JobStatus>(); 188 Iterator<JobStatus> it = mJobSet.iterator(); 189 while (it.hasNext()) { 190 JobStatus ts = it.next(); 191 if (UserHandle.getUserId(ts.getUid()) == userHandle) { 192 matchingJobs.add(ts); 193 } 194 } 195 return matchingJobs; 196 } 197 198 /** 199 * @param uid Uid of the requesting app. 200 * @return All JobStatus objects for a given uid from the master list. Never null. 201 */ 202 public List<JobStatus> getJobsByUid(int uid) { 203 List<JobStatus> matchingJobs = new ArrayList<JobStatus>(); 204 Iterator<JobStatus> it = mJobSet.iterator(); 205 while (it.hasNext()) { 206 JobStatus ts = it.next(); 207 if (ts.getUid() == uid) { 208 matchingJobs.add(ts); 209 } 210 } 211 return matchingJobs; 212 } 213 214 /** 215 * @param uid Uid of the requesting app. 216 * @param jobId Job id, specified at schedule-time. 217 * @return the JobStatus that matches the provided uId and jobId, or null if none found. 218 */ 219 public JobStatus getJobByUidAndJobId(int uid, int jobId) { 220 Iterator<JobStatus> it = mJobSet.iterator(); 221 while (it.hasNext()) { 222 JobStatus ts = it.next(); 223 if (ts.getUid() == uid && ts.getJobId() == jobId) { 224 return ts; 225 } 226 } 227 return null; 228 } 229 230 /** 231 * @return The live array of JobStatus objects. 232 */ 233 public ArraySet<JobStatus> getJobs() { 234 return mJobSet; 235 } 236 237 /** Version of the db schema. */ 238 private static final int JOBS_FILE_VERSION = 0; 239 /** Tag corresponds to constraints this job needs. */ 240 private static final String XML_TAG_PARAMS_CONSTRAINTS = "constraints"; 241 /** Tag corresponds to execution parameters. */ 242 private static final String XML_TAG_PERIODIC = "periodic"; 243 private static final String XML_TAG_ONEOFF = "one-off"; 244 private static final String XML_TAG_EXTRAS = "extras"; 245 246 /** 247 * Every time the state changes we write all the jobs in one swath, instead of trying to 248 * track incremental changes. 249 * @return Whether the operation was successful. This will only fail for e.g. if the system is 250 * low on storage. If this happens, we continue as normal 251 */ 252 private void maybeWriteStatusToDiskAsync() { 253 mDirtyOperations++; 254 if (mDirtyOperations >= MAX_OPS_BEFORE_WRITE) { 255 if (DEBUG) { 256 Slog.v(TAG, "Writing jobs to disk."); 257 } 258 mIoHandler.post(new WriteJobsMapToDiskRunnable()); 259 } 260 } 261 262 @VisibleForTesting 263 public void readJobMapFromDisk(ArraySet<JobStatus> jobSet) { 264 new ReadJobMapFromDiskRunnable(jobSet).run(); 265 } 266 267 /** 268 * Runnable that writes {@link #mJobSet} out to xml. 269 * NOTE: This Runnable locks on JobStore.this 270 */ 271 private class WriteJobsMapToDiskRunnable implements Runnable { 272 @Override 273 public void run() { 274 final long startElapsed = SystemClock.elapsedRealtime(); 275 List<JobStatus> mStoreCopy = new ArrayList<JobStatus>(); 276 synchronized (JobStore.this) { 277 // Copy over the jobs so we can release the lock before writing. 278 for (int i=0; i<mJobSet.size(); i++) { 279 JobStatus jobStatus = mJobSet.valueAt(i); 280 JobStatus copy = new JobStatus(jobStatus.getJob(), jobStatus.getUid(), 281 jobStatus.getEarliestRunTime(), jobStatus.getLatestRunTimeElapsed()); 282 mStoreCopy.add(copy); 283 } 284 } 285 writeJobsMapImpl(mStoreCopy); 286 if (JobSchedulerService.DEBUG) { 287 Slog.v(TAG, "Finished writing, took " + (SystemClock.elapsedRealtime() 288 - startElapsed) + "ms"); 289 } 290 } 291 292 private void writeJobsMapImpl(List<JobStatus> jobList) { 293 try { 294 ByteArrayOutputStream baos = new ByteArrayOutputStream(); 295 XmlSerializer out = new FastXmlSerializer(); 296 out.setOutput(baos, StandardCharsets.UTF_8.name()); 297 out.startDocument(null, true); 298 out.setFeature("http://xmlpull.org/v1/doc/features.html#indent-output", true); 299 300 out.startTag(null, "job-info"); 301 out.attribute(null, "version", Integer.toString(JOBS_FILE_VERSION)); 302 for (int i=0; i<jobList.size(); i++) { 303 JobStatus jobStatus = jobList.get(i); 304 if (DEBUG) { 305 Slog.d(TAG, "Saving job " + jobStatus.getJobId()); 306 } 307 out.startTag(null, "job"); 308 addIdentifierAttributesToJobTag(out, jobStatus); 309 writeConstraintsToXml(out, jobStatus); 310 writeExecutionCriteriaToXml(out, jobStatus); 311 writeBundleToXml(jobStatus.getExtras(), out); 312 out.endTag(null, "job"); 313 } 314 out.endTag(null, "job-info"); 315 out.endDocument(); 316 317 // Write out to disk in one fell sweep. 318 FileOutputStream fos = mJobsFile.startWrite(); 319 fos.write(baos.toByteArray()); 320 mJobsFile.finishWrite(fos); 321 mDirtyOperations = 0; 322 } catch (IOException e) { 323 if (DEBUG) { 324 Slog.v(TAG, "Error writing out job data.", e); 325 } 326 } catch (XmlPullParserException e) { 327 if (DEBUG) { 328 Slog.d(TAG, "Error persisting bundle.", e); 329 } 330 } 331 } 332 333 /** Write out a tag with data comprising the required fields of this job and its client. */ 334 private void addIdentifierAttributesToJobTag(XmlSerializer out, JobStatus jobStatus) 335 throws IOException { 336 out.attribute(null, "jobid", Integer.toString(jobStatus.getJobId())); 337 out.attribute(null, "package", jobStatus.getServiceComponent().getPackageName()); 338 out.attribute(null, "class", jobStatus.getServiceComponent().getClassName()); 339 out.attribute(null, "uid", Integer.toString(jobStatus.getUid())); 340 } 341 342 private void writeBundleToXml(PersistableBundle extras, XmlSerializer out) 343 throws IOException, XmlPullParserException { 344 out.startTag(null, XML_TAG_EXTRAS); 345 extras.saveToXml(out); 346 out.endTag(null, XML_TAG_EXTRAS); 347 } 348 /** 349 * Write out a tag with data identifying this job's constraints. If the constraint isn't here 350 * it doesn't apply. 351 */ 352 private void writeConstraintsToXml(XmlSerializer out, JobStatus jobStatus) throws IOException { 353 out.startTag(null, XML_TAG_PARAMS_CONSTRAINTS); 354 if (jobStatus.hasUnmeteredConstraint()) { 355 out.attribute(null, "unmetered", Boolean.toString(true)); 356 } 357 if (jobStatus.hasConnectivityConstraint()) { 358 out.attribute(null, "connectivity", Boolean.toString(true)); 359 } 360 if (jobStatus.hasIdleConstraint()) { 361 out.attribute(null, "idle", Boolean.toString(true)); 362 } 363 if (jobStatus.hasChargingConstraint()) { 364 out.attribute(null, "charging", Boolean.toString(true)); 365 } 366 out.endTag(null, XML_TAG_PARAMS_CONSTRAINTS); 367 } 368 369 private void writeExecutionCriteriaToXml(XmlSerializer out, JobStatus jobStatus) 370 throws IOException { 371 final JobInfo job = jobStatus.getJob(); 372 if (jobStatus.getJob().isPeriodic()) { 373 out.startTag(null, XML_TAG_PERIODIC); 374 out.attribute(null, "period", Long.toString(job.getIntervalMillis())); 375 } else { 376 out.startTag(null, XML_TAG_ONEOFF); 377 } 378 379 if (jobStatus.hasDeadlineConstraint()) { 380 // Wall clock deadline. 381 final long deadlineWallclock = System.currentTimeMillis() + 382 (jobStatus.getLatestRunTimeElapsed() - SystemClock.elapsedRealtime()); 383 out.attribute(null, "deadline", Long.toString(deadlineWallclock)); 384 } 385 if (jobStatus.hasTimingDelayConstraint()) { 386 final long delayWallclock = System.currentTimeMillis() + 387 (jobStatus.getEarliestRunTime() - SystemClock.elapsedRealtime()); 388 out.attribute(null, "delay", Long.toString(delayWallclock)); 389 } 390 391 // Only write out back-off policy if it differs from the default. 392 // This also helps the case where the job is idle -> these aren't allowed to specify 393 // back-off. 394 if (jobStatus.getJob().getInitialBackoffMillis() != JobInfo.DEFAULT_INITIAL_BACKOFF_MILLIS 395 || jobStatus.getJob().getBackoffPolicy() != JobInfo.DEFAULT_BACKOFF_POLICY) { 396 out.attribute(null, "backoff-policy", Integer.toString(job.getBackoffPolicy())); 397 out.attribute(null, "initial-backoff", Long.toString(job.getInitialBackoffMillis())); 398 } 399 if (job.isPeriodic()) { 400 out.endTag(null, XML_TAG_PERIODIC); 401 } else { 402 out.endTag(null, XML_TAG_ONEOFF); 403 } 404 } 405 } 406 407 /** 408 * Runnable that reads list of persisted job from xml. This is run once at start up, so doesn't 409 * need to go through {@link JobStore#add(com.android.server.job.controllers.JobStatus)}. 410 */ 411 private class ReadJobMapFromDiskRunnable implements Runnable { 412 private final ArraySet<JobStatus> jobSet; 413 414 /** 415 * @param jobSet Reference to the (empty) set of JobStatus objects that back the JobStore, 416 * so that after disk read we can populate it directly. 417 */ 418 ReadJobMapFromDiskRunnable(ArraySet<JobStatus> jobSet) { 419 this.jobSet = jobSet; 420 } 421 422 @Override 423 public void run() { 424 try { 425 List<JobStatus> jobs; 426 FileInputStream fis = mJobsFile.openRead(); 427 synchronized (JobStore.this) { 428 jobs = readJobMapImpl(fis); 429 if (jobs != null) { 430 for (int i=0; i<jobs.size(); i++) { 431 this.jobSet.add(jobs.get(i)); 432 } 433 } 434 } 435 fis.close(); 436 } catch (FileNotFoundException e) { 437 if (JobSchedulerService.DEBUG) { 438 Slog.d(TAG, "Could not find jobs file, probably there was nothing to load."); 439 } 440 } catch (XmlPullParserException e) { 441 if (JobSchedulerService.DEBUG) { 442 Slog.d(TAG, "Error parsing xml.", e); 443 } 444 } catch (IOException e) { 445 if (JobSchedulerService.DEBUG) { 446 Slog.d(TAG, "Error parsing xml.", e); 447 } 448 } 449 } 450 451 private List<JobStatus> readJobMapImpl(FileInputStream fis) 452 throws XmlPullParserException, IOException { 453 XmlPullParser parser = Xml.newPullParser(); 454 parser.setInput(fis, StandardCharsets.UTF_8.name()); 455 456 int eventType = parser.getEventType(); 457 while (eventType != XmlPullParser.START_TAG && 458 eventType != XmlPullParser.END_DOCUMENT) { 459 eventType = parser.next(); 460 Slog.d(TAG, parser.getName()); 461 } 462 if (eventType == XmlPullParser.END_DOCUMENT) { 463 if (DEBUG) { 464 Slog.d(TAG, "No persisted jobs."); 465 } 466 return null; 467 } 468 469 String tagName = parser.getName(); 470 if ("job-info".equals(tagName)) { 471 final List<JobStatus> jobs = new ArrayList<JobStatus>(); 472 // Read in version info. 473 try { 474 int version = Integer.valueOf(parser.getAttributeValue(null, "version")); 475 if (version != JOBS_FILE_VERSION) { 476 Slog.d(TAG, "Invalid version number, aborting jobs file read."); 477 return null; 478 } 479 } catch (NumberFormatException e) { 480 Slog.e(TAG, "Invalid version number, aborting jobs file read."); 481 return null; 482 } 483 eventType = parser.next(); 484 do { 485 // Read each <job/> 486 if (eventType == XmlPullParser.START_TAG) { 487 tagName = parser.getName(); 488 // Start reading job. 489 if ("job".equals(tagName)) { 490 JobStatus persistedJob = restoreJobFromXml(parser); 491 if (persistedJob != null) { 492 if (DEBUG) { 493 Slog.d(TAG, "Read out " + persistedJob); 494 } 495 jobs.add(persistedJob); 496 } else { 497 Slog.d(TAG, "Error reading job from file."); 498 } 499 } 500 } 501 eventType = parser.next(); 502 } while (eventType != XmlPullParser.END_DOCUMENT); 503 return jobs; 504 } 505 return null; 506 } 507 508 /** 509 * @param parser Xml parser at the beginning of a "<job/>" tag. The next "parser.next()" call 510 * will take the parser into the body of the job tag. 511 * @return Newly instantiated job holding all the information we just read out of the xml tag. 512 */ 513 private JobStatus restoreJobFromXml(XmlPullParser parser) throws XmlPullParserException, 514 IOException { 515 JobInfo.Builder jobBuilder; 516 int uid; 517 518 // Read out job identifier attributes. 519 try { 520 jobBuilder = buildBuilderFromXml(parser); 521 jobBuilder.setPersisted(true); 522 uid = Integer.valueOf(parser.getAttributeValue(null, "uid")); 523 } catch (NumberFormatException e) { 524 Slog.e(TAG, "Error parsing job's required fields, skipping"); 525 return null; 526 } 527 528 int eventType; 529 // Read out constraints tag. 530 do { 531 eventType = parser.next(); 532 } while (eventType == XmlPullParser.TEXT); // Push through to next START_TAG. 533 534 if (!(eventType == XmlPullParser.START_TAG && 535 XML_TAG_PARAMS_CONSTRAINTS.equals(parser.getName()))) { 536 // Expecting a <constraints> start tag. 537 return null; 538 } 539 try { 540 buildConstraintsFromXml(jobBuilder, parser); 541 } catch (NumberFormatException e) { 542 Slog.d(TAG, "Error reading constraints, skipping."); 543 return null; 544 } 545 parser.next(); // Consume </constraints> 546 547 // Read out execution parameters tag. 548 do { 549 eventType = parser.next(); 550 } while (eventType == XmlPullParser.TEXT); 551 if (eventType != XmlPullParser.START_TAG) { 552 return null; 553 } 554 555 Pair<Long, Long> runtimes; 556 try { 557 runtimes = buildExecutionTimesFromXml(parser); 558 } catch (NumberFormatException e) { 559 if (DEBUG) { 560 Slog.d(TAG, "Error parsing execution time parameters, skipping."); 561 } 562 return null; 563 } 564 565 if (XML_TAG_PERIODIC.equals(parser.getName())) { 566 try { 567 String val = parser.getAttributeValue(null, "period"); 568 jobBuilder.setPeriodic(Long.valueOf(val)); 569 } catch (NumberFormatException e) { 570 Slog.d(TAG, "Error reading periodic execution criteria, skipping."); 571 return null; 572 } 573 } else if (XML_TAG_ONEOFF.equals(parser.getName())) { 574 try { 575 if (runtimes.first != JobStatus.NO_EARLIEST_RUNTIME) { 576 jobBuilder.setMinimumLatency(runtimes.first - SystemClock.elapsedRealtime()); 577 } 578 if (runtimes.second != JobStatus.NO_LATEST_RUNTIME) { 579 jobBuilder.setOverrideDeadline( 580 runtimes.second - SystemClock.elapsedRealtime()); 581 } 582 } catch (NumberFormatException e) { 583 Slog.d(TAG, "Error reading job execution criteria, skipping."); 584 return null; 585 } 586 } else { 587 if (DEBUG) { 588 Slog.d(TAG, "Invalid parameter tag, skipping - " + parser.getName()); 589 } 590 // Expecting a parameters start tag. 591 return null; 592 } 593 maybeBuildBackoffPolicyFromXml(jobBuilder, parser); 594 595 parser.nextTag(); // Consume parameters end tag. 596 597 // Read out extras Bundle. 598 do { 599 eventType = parser.next(); 600 } while (eventType == XmlPullParser.TEXT); 601 if (!(eventType == XmlPullParser.START_TAG && XML_TAG_EXTRAS.equals(parser.getName()))) { 602 if (DEBUG) { 603 Slog.d(TAG, "Error reading extras, skipping."); 604 } 605 return null; 606 } 607 608 PersistableBundle extras = PersistableBundle.restoreFromXml(parser); 609 jobBuilder.setExtras(extras); 610 parser.nextTag(); // Consume </extras> 611 612 return new JobStatus(jobBuilder.build(), uid, runtimes.first, runtimes.second); 613 } 614 615 private JobInfo.Builder buildBuilderFromXml(XmlPullParser parser) throws NumberFormatException { 616 // Pull out required fields from <job> attributes. 617 int jobId = Integer.valueOf(parser.getAttributeValue(null, "jobid")); 618 String packageName = parser.getAttributeValue(null, "package"); 619 String className = parser.getAttributeValue(null, "class"); 620 ComponentName cname = new ComponentName(packageName, className); 621 622 return new JobInfo.Builder(jobId, cname); 623 } 624 625 private void buildConstraintsFromXml(JobInfo.Builder jobBuilder, XmlPullParser parser) { 626 String val = parser.getAttributeValue(null, "unmetered"); 627 if (val != null) { 628 jobBuilder.setRequiredNetworkType(JobInfo.NETWORK_TYPE_UNMETERED); 629 } 630 val = parser.getAttributeValue(null, "connectivity"); 631 if (val != null) { 632 jobBuilder.setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY); 633 } 634 val = parser.getAttributeValue(null, "idle"); 635 if (val != null) { 636 jobBuilder.setRequiresDeviceIdle(true); 637 } 638 val = parser.getAttributeValue(null, "charging"); 639 if (val != null) { 640 jobBuilder.setRequiresCharging(true); 641 } 642 } 643 644 /** 645 * Builds the back-off policy out of the params tag. These attributes may not exist, depending 646 * on whether the back-off was set when the job was first scheduled. 647 */ 648 private void maybeBuildBackoffPolicyFromXml(JobInfo.Builder jobBuilder, XmlPullParser parser) { 649 String val = parser.getAttributeValue(null, "initial-backoff"); 650 if (val != null) { 651 long initialBackoff = Long.valueOf(val); 652 val = parser.getAttributeValue(null, "backoff-policy"); 653 int backoffPolicy = Integer.valueOf(val); // Will throw NFE which we catch higher up. 654 jobBuilder.setBackoffCriteria(initialBackoff, backoffPolicy); 655 } 656 } 657 658 /** 659 * Convenience function to read out and convert deadline and delay from xml into elapsed real 660 * time. 661 * @return A {@link android.util.Pair}, where the first value is the earliest elapsed runtime 662 * and the second is the latest elapsed runtime. 663 */ 664 private Pair<Long, Long> buildExecutionTimesFromXml(XmlPullParser parser) 665 throws NumberFormatException { 666 // Pull out execution time data. 667 final long nowWallclock = System.currentTimeMillis(); 668 final long nowElapsed = SystemClock.elapsedRealtime(); 669 670 long earliestRunTimeElapsed = JobStatus.NO_EARLIEST_RUNTIME; 671 long latestRunTimeElapsed = JobStatus.NO_LATEST_RUNTIME; 672 String val = parser.getAttributeValue(null, "deadline"); 673 if (val != null) { 674 long latestRuntimeWallclock = Long.valueOf(val); 675 long maxDelayElapsed = 676 Math.max(latestRuntimeWallclock - nowWallclock, 0); 677 latestRunTimeElapsed = nowElapsed + maxDelayElapsed; 678 } 679 val = parser.getAttributeValue(null, "delay"); 680 if (val != null) { 681 long earliestRuntimeWallclock = Long.valueOf(val); 682 long minDelayElapsed = 683 Math.max(earliestRuntimeWallclock - nowWallclock, 0); 684 earliestRunTimeElapsed = nowElapsed + minDelayElapsed; 685 686 } 687 return Pair.create(earliestRunTimeElapsed, latestRunTimeElapsed); 688 } 689 } 690 } 691