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