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