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