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