JobStore.java revision 7060b04f6d92351b67222e636ab378a0273bf3e7
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 synchronized (JobStore.this) { 266 writeJobsMapImpl(); 267 } 268 if (JobSchedulerService.DEBUG) { 269 Slog.v(TAG, "Finished writing, took " + (SystemClock.elapsedRealtime() 270 - startElapsed) + "ms"); 271 } 272 } 273 274 private void writeJobsMapImpl() { 275 try { 276 ByteArrayOutputStream baos = new ByteArrayOutputStream(); 277 XmlSerializer out = new FastXmlSerializer(); 278 out.setOutput(baos, "utf-8"); 279 out.startDocument(null, true); 280 out.setFeature("http://xmlpull.org/v1/doc/features.html#indent-output", true); 281 282 out.startTag(null, "job-info"); 283 out.attribute(null, "version", Integer.toString(JOBS_FILE_VERSION)); 284 for (int i = 0; i < mJobSet.size(); i++) { 285 final JobStatus jobStatus = mJobSet.valueAt(i); 286 if (DEBUG) { 287 Slog.d(TAG, "Saving job " + jobStatus.getJobId()); 288 } 289 out.startTag(null, "job"); 290 addIdentifierAttributesToJobTag(out, jobStatus); 291 writeConstraintsToXml(out, jobStatus); 292 writeExecutionCriteriaToXml(out, jobStatus); 293 writeBundleToXml(jobStatus.getExtras(), out); 294 out.endTag(null, "job"); 295 } 296 out.endTag(null, "job-info"); 297 out.endDocument(); 298 299 // Write out to disk in one fell sweep. 300 FileOutputStream fos = mJobsFile.startWrite(); 301 fos.write(baos.toByteArray()); 302 mJobsFile.finishWrite(fos); 303 mDirtyOperations = 0; 304 } catch (IOException e) { 305 if (DEBUG) { 306 Slog.v(TAG, "Error writing out job data.", e); 307 } 308 } catch (XmlPullParserException e) { 309 if (DEBUG) { 310 Slog.d(TAG, "Error persisting bundle.", e); 311 } 312 } 313 } 314 315 /** Write out a tag with data comprising the required fields of this job and its client. */ 316 private void addIdentifierAttributesToJobTag(XmlSerializer out, JobStatus jobStatus) 317 throws IOException { 318 out.attribute(null, "jobid", Integer.toString(jobStatus.getJobId())); 319 out.attribute(null, "package", jobStatus.getServiceComponent().getPackageName()); 320 out.attribute(null, "class", jobStatus.getServiceComponent().getClassName()); 321 out.attribute(null, "uid", Integer.toString(jobStatus.getUid())); 322 } 323 324 private void writeBundleToXml(PersistableBundle extras, XmlSerializer out) 325 throws IOException, XmlPullParserException { 326 out.startTag(null, XML_TAG_EXTRAS); 327 extras.saveToXml(out); 328 out.endTag(null, XML_TAG_EXTRAS); 329 } 330 /** 331 * Write out a tag with data identifying this job's constraints. If the constraint isn't here 332 * it doesn't apply. 333 */ 334 private void writeConstraintsToXml(XmlSerializer out, JobStatus jobStatus) throws IOException { 335 out.startTag(null, XML_TAG_PARAMS_CONSTRAINTS); 336 if (jobStatus.hasUnmeteredConstraint()) { 337 out.attribute(null, "unmetered", Boolean.toString(true)); 338 } 339 if (jobStatus.hasConnectivityConstraint()) { 340 out.attribute(null, "connectivity", Boolean.toString(true)); 341 } 342 if (jobStatus.hasIdleConstraint()) { 343 out.attribute(null, "idle", Boolean.toString(true)); 344 } 345 if (jobStatus.hasChargingConstraint()) { 346 out.attribute(null, "charging", Boolean.toString(true)); 347 } 348 out.endTag(null, XML_TAG_PARAMS_CONSTRAINTS); 349 } 350 351 private void writeExecutionCriteriaToXml(XmlSerializer out, JobStatus jobStatus) 352 throws IOException { 353 final JobInfo job = jobStatus.getJob(); 354 if (jobStatus.getJob().isPeriodic()) { 355 out.startTag(null, XML_TAG_PERIODIC); 356 out.attribute(null, "period", Long.toString(job.getIntervalMillis())); 357 } else { 358 out.startTag(null, XML_TAG_ONEOFF); 359 } 360 361 if (jobStatus.hasDeadlineConstraint()) { 362 // Wall clock deadline. 363 final long deadlineWallclock = System.currentTimeMillis() + 364 (jobStatus.getLatestRunTimeElapsed() - SystemClock.elapsedRealtime()); 365 out.attribute(null, "deadline", Long.toString(deadlineWallclock)); 366 } 367 if (jobStatus.hasTimingDelayConstraint()) { 368 final long delayWallclock = System.currentTimeMillis() + 369 (jobStatus.getEarliestRunTime() - SystemClock.elapsedRealtime()); 370 out.attribute(null, "delay", Long.toString(delayWallclock)); 371 } 372 373 // Only write out back-off policy if it differs from the default. 374 // This also helps the case where the job is idle -> these aren't allowed to specify 375 // back-off. 376 if (jobStatus.getJob().getInitialBackoffMillis() != JobInfo.DEFAULT_INITIAL_BACKOFF_MILLIS 377 || jobStatus.getJob().getBackoffPolicy() != JobInfo.DEFAULT_BACKOFF_POLICY) { 378 out.attribute(null, "backoff-policy", Integer.toString(job.getBackoffPolicy())); 379 out.attribute(null, "initial-backoff", Long.toString(job.getInitialBackoffMillis())); 380 } 381 if (job.isPeriodic()) { 382 out.endTag(null, XML_TAG_PERIODIC); 383 } else { 384 out.endTag(null, XML_TAG_ONEOFF); 385 } 386 } 387 } 388 389 /** 390 * Runnable that reads list of persisted job from xml. 391 * NOTE: This Runnable locks on JobStore.this 392 */ 393 private class ReadJobMapFromDiskRunnable implements Runnable { 394 private JobMapReadFinishedListener mCallback; 395 public ReadJobMapFromDiskRunnable(JobMapReadFinishedListener callback) { 396 mCallback = callback; 397 } 398 399 @Override 400 public void run() { 401 try { 402 List<JobStatus> jobs; 403 FileInputStream fis = mJobsFile.openRead(); 404 synchronized (JobStore.this) { 405 jobs = readJobMapImpl(fis); 406 } 407 fis.close(); 408 if (jobs != null) { 409 mCallback.onJobMapReadFinished(jobs); 410 } 411 } catch (FileNotFoundException e) { 412 if (JobSchedulerService.DEBUG) { 413 Slog.d(TAG, "Could not find jobs file, probably there was nothing to load."); 414 } 415 } catch (XmlPullParserException e) { 416 if (JobSchedulerService.DEBUG) { 417 Slog.d(TAG, "Error parsing xml.", e); 418 } 419 } catch (IOException e) { 420 if (JobSchedulerService.DEBUG) { 421 Slog.d(TAG, "Error parsing xml.", e); 422 } 423 } 424 } 425 426 private List<JobStatus> readJobMapImpl(FileInputStream fis) throws XmlPullParserException, IOException { 427 XmlPullParser parser = Xml.newPullParser(); 428 parser.setInput(fis, null); 429 430 int eventType = parser.getEventType(); 431 while (eventType != XmlPullParser.START_TAG && 432 eventType != XmlPullParser.END_DOCUMENT) { 433 eventType = parser.next(); 434 Slog.d(TAG, parser.getName()); 435 } 436 if (eventType == XmlPullParser.END_DOCUMENT) { 437 if (DEBUG) { 438 Slog.d(TAG, "No persisted jobs."); 439 } 440 return null; 441 } 442 443 String tagName = parser.getName(); 444 if ("job-info".equals(tagName)) { 445 final List<JobStatus> jobs = new ArrayList<JobStatus>(); 446 // Read in version info. 447 try { 448 int version = Integer.valueOf(parser.getAttributeValue(null, "version")); 449 if (version != JOBS_FILE_VERSION) { 450 Slog.d(TAG, "Invalid version number, aborting jobs file read."); 451 return null; 452 } 453 } catch (NumberFormatException e) { 454 Slog.e(TAG, "Invalid version number, aborting jobs file read."); 455 return null; 456 } 457 eventType = parser.next(); 458 do { 459 // Read each <job/> 460 if (eventType == XmlPullParser.START_TAG) { 461 tagName = parser.getName(); 462 // Start reading job. 463 if ("job".equals(tagName)) { 464 JobStatus persistedJob = restoreJobFromXml(parser); 465 if (persistedJob != null) { 466 if (DEBUG) { 467 Slog.d(TAG, "Read out " + persistedJob); 468 } 469 jobs.add(persistedJob); 470 } else { 471 Slog.d(TAG, "Error reading job from file."); 472 } 473 } 474 } 475 eventType = parser.next(); 476 } while (eventType != XmlPullParser.END_DOCUMENT); 477 return jobs; 478 } 479 return null; 480 } 481 482 /** 483 * @param parser Xml parser at the beginning of a "<job/>" tag. The next "parser.next()" call 484 * will take the parser into the body of the job tag. 485 * @return Newly instantiated job holding all the information we just read out of the xml tag. 486 */ 487 private JobStatus restoreJobFromXml(XmlPullParser parser) throws XmlPullParserException, 488 IOException { 489 JobInfo.Builder jobBuilder; 490 int uid; 491 492 // Read out job identifier attributes. 493 try { 494 jobBuilder = buildBuilderFromXml(parser); 495 uid = Integer.valueOf(parser.getAttributeValue(null, "uid")); 496 } catch (NumberFormatException e) { 497 Slog.e(TAG, "Error parsing job's required fields, skipping"); 498 return null; 499 } 500 501 int eventType; 502 // Read out constraints tag. 503 do { 504 eventType = parser.next(); 505 } while (eventType == XmlPullParser.TEXT); // Push through to next START_TAG. 506 507 if (!(eventType == XmlPullParser.START_TAG && 508 XML_TAG_PARAMS_CONSTRAINTS.equals(parser.getName()))) { 509 // Expecting a <constraints> start tag. 510 return null; 511 } 512 try { 513 buildConstraintsFromXml(jobBuilder, parser); 514 } catch (NumberFormatException e) { 515 Slog.d(TAG, "Error reading constraints, skipping."); 516 return null; 517 } 518 parser.next(); // Consume </constraints> 519 520 // Read out execution parameters tag. 521 do { 522 eventType = parser.next(); 523 } while (eventType == XmlPullParser.TEXT); 524 if (eventType != XmlPullParser.START_TAG) { 525 return null; 526 } 527 528 Pair<Long, Long> runtimes; 529 try { 530 runtimes = buildExecutionTimesFromXml(parser); 531 } catch (NumberFormatException e) { 532 if (DEBUG) { 533 Slog.d(TAG, "Error parsing execution time parameters, skipping."); 534 } 535 return null; 536 } 537 538 if (XML_TAG_PERIODIC.equals(parser.getName())) { 539 try { 540 String val = parser.getAttributeValue(null, "period"); 541 jobBuilder.setPeriodic(Long.valueOf(val)); 542 } catch (NumberFormatException e) { 543 Slog.d(TAG, "Error reading periodic execution criteria, skipping."); 544 return null; 545 } 546 } else if (XML_TAG_ONEOFF.equals(parser.getName())) { 547 try { 548 if (runtimes.first != JobStatus.NO_EARLIEST_RUNTIME) { 549 jobBuilder.setMinimumLatency(runtimes.first - SystemClock.elapsedRealtime()); 550 } 551 if (runtimes.second != JobStatus.NO_LATEST_RUNTIME) { 552 jobBuilder.setOverrideDeadline( 553 runtimes.second - SystemClock.elapsedRealtime()); 554 } 555 } catch (NumberFormatException e) { 556 Slog.d(TAG, "Error reading job execution criteria, skipping."); 557 return null; 558 } 559 } else { 560 if (DEBUG) { 561 Slog.d(TAG, "Invalid parameter tag, skipping - " + parser.getName()); 562 } 563 // Expecting a parameters start tag. 564 return null; 565 } 566 maybeBuildBackoffPolicyFromXml(jobBuilder, parser); 567 568 parser.nextTag(); // Consume parameters end tag. 569 570 // Read out extras Bundle. 571 do { 572 eventType = parser.next(); 573 } while (eventType == XmlPullParser.TEXT); 574 if (!(eventType == XmlPullParser.START_TAG && XML_TAG_EXTRAS.equals(parser.getName()))) { 575 if (DEBUG) { 576 Slog.d(TAG, "Error reading extras, skipping."); 577 } 578 return null; 579 } 580 581 PersistableBundle extras = PersistableBundle.restoreFromXml(parser); 582 jobBuilder.setExtras(extras); 583 parser.nextTag(); // Consume </extras> 584 585 return new JobStatus(jobBuilder.build(), uid, runtimes.first, runtimes.second); 586 } 587 588 private JobInfo.Builder buildBuilderFromXml(XmlPullParser parser) throws NumberFormatException { 589 // Pull out required fields from <job> attributes. 590 int jobId = Integer.valueOf(parser.getAttributeValue(null, "jobid")); 591 String packageName = parser.getAttributeValue(null, "package"); 592 String className = parser.getAttributeValue(null, "class"); 593 ComponentName cname = new ComponentName(packageName, className); 594 595 return new JobInfo.Builder(jobId, cname); 596 } 597 598 private void buildConstraintsFromXml(JobInfo.Builder jobBuilder, XmlPullParser parser) { 599 String val = parser.getAttributeValue(null, "unmetered"); 600 if (val != null) { 601 jobBuilder.setRequiredNetworkCapabilities(JobInfo.NetworkType.UNMETERED); 602 } 603 val = parser.getAttributeValue(null, "connectivity"); 604 if (val != null) { 605 jobBuilder.setRequiredNetworkCapabilities(JobInfo.NetworkType.ANY); 606 } 607 val = parser.getAttributeValue(null, "idle"); 608 if (val != null) { 609 jobBuilder.setRequiresDeviceIdle(true); 610 } 611 val = parser.getAttributeValue(null, "charging"); 612 if (val != null) { 613 jobBuilder.setRequiresCharging(true); 614 } 615 } 616 617 /** 618 * Builds the back-off policy out of the params tag. These attributes may not exist, depending 619 * on whether the back-off was set when the job was first scheduled. 620 */ 621 private void maybeBuildBackoffPolicyFromXml(JobInfo.Builder jobBuilder, XmlPullParser parser) { 622 String val = parser.getAttributeValue(null, "initial-backoff"); 623 if (val != null) { 624 long initialBackoff = Long.valueOf(val); 625 val = parser.getAttributeValue(null, "backoff-policy"); 626 int backoffPolicy = Integer.valueOf(val); // Will throw NFE which we catch higher up. 627 jobBuilder.setBackoffCriteria(initialBackoff, backoffPolicy); 628 } 629 } 630 631 /** 632 * Convenience function to read out and convert deadline and delay from xml into elapsed real 633 * time. 634 * @return A {@link android.util.Pair}, where the first value is the earliest elapsed runtime 635 * and the second is the latest elapsed runtime. 636 */ 637 private Pair<Long, Long> buildExecutionTimesFromXml(XmlPullParser parser) 638 throws NumberFormatException { 639 // Pull out execution time data. 640 final long nowWallclock = System.currentTimeMillis(); 641 final long nowElapsed = SystemClock.elapsedRealtime(); 642 643 long earliestRunTimeElapsed = JobStatus.NO_EARLIEST_RUNTIME; 644 long latestRunTimeElapsed = JobStatus.NO_LATEST_RUNTIME; 645 String val = parser.getAttributeValue(null, "deadline"); 646 if (val != null) { 647 long latestRuntimeWallclock = Long.valueOf(val); 648 long maxDelayElapsed = 649 Math.max(latestRuntimeWallclock - nowWallclock, 0); 650 latestRunTimeElapsed = nowElapsed + maxDelayElapsed; 651 } 652 val = parser.getAttributeValue(null, "delay"); 653 if (val != null) { 654 long earliestRuntimeWallclock = Long.valueOf(val); 655 long minDelayElapsed = 656 Math.max(earliestRuntimeWallclock - nowWallclock, 0); 657 earliestRunTimeElapsed = nowElapsed + minDelayElapsed; 658 659 } 660 return Pair.create(earliestRunTimeElapsed, latestRunTimeElapsed); 661 } 662 } 663}