JobStore.java revision f07c7b9fd0a640bff4bf7690373613da217fe69b
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.text.format.DateUtils; 28import android.util.AtomicFile; 29import android.util.ArraySet; 30import android.util.Pair; 31import android.util.Slog; 32import android.util.SparseArray; 33import android.util.Xml; 34 35import com.android.internal.annotations.VisibleForTesting; 36import com.android.internal.util.FastXmlSerializer; 37import com.android.server.IoThread; 38import com.android.server.job.controllers.JobStatus; 39 40import java.io.ByteArrayOutputStream; 41import java.io.File; 42import java.io.FileInputStream; 43import java.io.FileNotFoundException; 44import java.io.FileOutputStream; 45import java.io.IOException; 46import java.nio.charset.StandardCharsets; 47import java.util.ArrayList; 48import java.util.List; 49import java.util.Set; 50 51import org.xmlpull.v1.XmlPullParser; 52import org.xmlpull.v1.XmlPullParserException; 53import org.xmlpull.v1.XmlSerializer; 54 55/** 56 * Maintains the master list of jobs that the job scheduler is tracking. These jobs are compared by 57 * reference, so none of the functions in this class should make a copy. 58 * Also handles read/write of persisted jobs. 59 * 60 * Note on locking: 61 * All callers to this class must <strong>lock on the class object they are calling</strong>. 62 * This is important b/c {@link com.android.server.job.JobStore.WriteJobsMapToDiskRunnable} 63 * and {@link com.android.server.job.JobStore.ReadJobMapFromDiskRunnable} lock on that 64 * object. 65 */ 66public class JobStore { 67 private static final String TAG = "JobStore"; 68 private static final boolean DEBUG = JobSchedulerService.DEBUG; 69 70 /** Threshold to adjust how often we want to write to the db. */ 71 private static final int MAX_OPS_BEFORE_WRITE = 1; 72 final Object mLock; 73 final JobSet mJobSet; // per-caller-uid tracking 74 final Context mContext; 75 76 private int mDirtyOperations; 77 78 private static final Object sSingletonLock = new Object(); 79 private final AtomicFile mJobsFile; 80 /** Handler backed by IoThread for writing to disk. */ 81 private final Handler mIoHandler = IoThread.getHandler(); 82 private static JobStore sSingleton; 83 84 /** Used by the {@link JobSchedulerService} to instantiate the JobStore. */ 85 static JobStore initAndGet(JobSchedulerService jobManagerService) { 86 synchronized (sSingletonLock) { 87 if (sSingleton == null) { 88 sSingleton = new JobStore(jobManagerService.getContext(), 89 jobManagerService.getLock(), Environment.getDataDirectory()); 90 } 91 return sSingleton; 92 } 93 } 94 95 /** 96 * @return A freshly initialized job store object, with no loaded jobs. 97 */ 98 @VisibleForTesting 99 public static JobStore initAndGetForTesting(Context context, File dataDir) { 100 JobStore jobStoreUnderTest = new JobStore(context, new Object(), dataDir); 101 jobStoreUnderTest.clear(); 102 return jobStoreUnderTest; 103 } 104 105 /** 106 * Construct the instance of the job store. This results in a blocking read from disk. 107 */ 108 private JobStore(Context context, Object lock, File dataDir) { 109 mLock = lock; 110 mContext = context; 111 mDirtyOperations = 0; 112 113 File systemDir = new File(dataDir, "system"); 114 File jobDir = new File(systemDir, "job"); 115 jobDir.mkdirs(); 116 mJobsFile = new AtomicFile(new File(jobDir, "jobs.xml")); 117 118 mJobSet = new JobSet(); 119 120 readJobMapFromDisk(mJobSet); 121 } 122 123 /** 124 * Add a job to the master list, persisting it if necessary. If the JobStatus already exists, 125 * it will be replaced. 126 * @param jobStatus Job to add. 127 * @return Whether or not an equivalent JobStatus was replaced by this operation. 128 */ 129 public boolean add(JobStatus jobStatus) { 130 boolean replaced = mJobSet.remove(jobStatus); 131 mJobSet.add(jobStatus); 132 if (jobStatus.isPersisted()) { 133 maybeWriteStatusToDiskAsync(); 134 } 135 if (DEBUG) { 136 Slog.d(TAG, "Added job status to store: " + jobStatus); 137 } 138 return replaced; 139 } 140 141 boolean containsJob(JobStatus jobStatus) { 142 return mJobSet.contains(jobStatus); 143 } 144 145 public int size() { 146 return mJobSet.size(); 147 } 148 149 public int countJobsForUid(int uid) { 150 return mJobSet.countJobsForUid(uid); 151 } 152 153 /** 154 * Remove the provided job. Will also delete the job if it was persisted. 155 * @param writeBack If true, the job will be deleted (if it was persisted) immediately. 156 * @return Whether or not the job existed to be removed. 157 */ 158 public boolean remove(JobStatus jobStatus, boolean writeBack) { 159 boolean removed = mJobSet.remove(jobStatus); 160 if (!removed) { 161 if (DEBUG) { 162 Slog.d(TAG, "Couldn't remove job: didn't exist: " + jobStatus); 163 } 164 return false; 165 } 166 if (writeBack && jobStatus.isPersisted()) { 167 maybeWriteStatusToDiskAsync(); 168 } 169 return removed; 170 } 171 172 @VisibleForTesting 173 public void clear() { 174 mJobSet.clear(); 175 maybeWriteStatusToDiskAsync(); 176 } 177 178 /** 179 * @param userHandle User for whom we are querying the list of jobs. 180 * @return A list of all the jobs scheduled by the provided user. Never null. 181 */ 182 public List<JobStatus> getJobsByUser(int userHandle) { 183 return mJobSet.getJobsByUser(userHandle); 184 } 185 186 /** 187 * @param uid Uid of the requesting app. 188 * @return All JobStatus objects for a given uid from the master list. Never null. 189 */ 190 public List<JobStatus> getJobsByUid(int uid) { 191 return mJobSet.getJobsByUid(uid); 192 } 193 194 /** 195 * @param uid Uid of the requesting app. 196 * @param jobId Job id, specified at schedule-time. 197 * @return the JobStatus that matches the provided uId and jobId, or null if none found. 198 */ 199 public JobStatus getJobByUidAndJobId(int uid, int jobId) { 200 return mJobSet.get(uid, jobId); 201 } 202 203 /** 204 * Iterate over the set of all jobs, invoking the supplied functor on each. This is for 205 * customers who need to examine each job; we'd much rather not have to generate 206 * transient unified collections for them to iterate over and then discard, or creating 207 * iterators every time a client needs to perform a sweep. 208 */ 209 public void forEachJob(JobStatusFunctor functor) { 210 mJobSet.forEachJob(functor); 211 } 212 213 public void forEachJob(int uid, JobStatusFunctor functor) { 214 mJobSet.forEachJob(uid, functor); 215 } 216 217 public interface JobStatusFunctor { 218 public void process(JobStatus jobStatus); 219 } 220 221 /** Version of the db schema. */ 222 private static final int JOBS_FILE_VERSION = 0; 223 /** Tag corresponds to constraints this job needs. */ 224 private static final String XML_TAG_PARAMS_CONSTRAINTS = "constraints"; 225 /** Tag corresponds to execution parameters. */ 226 private static final String XML_TAG_PERIODIC = "periodic"; 227 private static final String XML_TAG_ONEOFF = "one-off"; 228 private static final String XML_TAG_EXTRAS = "extras"; 229 230 /** 231 * Every time the state changes we write all the jobs in one swath, instead of trying to 232 * track incremental changes. 233 * @return Whether the operation was successful. This will only fail for e.g. if the system is 234 * low on storage. If this happens, we continue as normal 235 */ 236 private void maybeWriteStatusToDiskAsync() { 237 mDirtyOperations++; 238 if (mDirtyOperations >= MAX_OPS_BEFORE_WRITE) { 239 if (DEBUG) { 240 Slog.v(TAG, "Writing jobs to disk."); 241 } 242 mIoHandler.post(new WriteJobsMapToDiskRunnable()); 243 } 244 } 245 246 @VisibleForTesting 247 public void readJobMapFromDisk(JobSet jobSet) { 248 new ReadJobMapFromDiskRunnable(jobSet).run(); 249 } 250 251 /** 252 * Runnable that writes {@link #mJobSet} out to xml. 253 * NOTE: This Runnable locks on mLock 254 */ 255 private class WriteJobsMapToDiskRunnable implements Runnable { 256 @Override 257 public void run() { 258 final long startElapsed = SystemClock.elapsedRealtime(); 259 final List<JobStatus> storeCopy = new ArrayList<JobStatus>(); 260 synchronized (mLock) { 261 // Clone the jobs so we can release the lock before writing. 262 mJobSet.forEachJob(new JobStatusFunctor() { 263 @Override 264 public void process(JobStatus job) { 265 if (job.isPersisted()) { 266 storeCopy.add(new JobStatus(job)); 267 } 268 } 269 }); 270 } 271 writeJobsMapImpl(storeCopy); 272 if (JobSchedulerService.DEBUG) { 273 Slog.v(TAG, "Finished writing, took " + (SystemClock.elapsedRealtime() 274 - startElapsed) + "ms"); 275 } 276 } 277 278 private void writeJobsMapImpl(List<JobStatus> jobList) { 279 try { 280 ByteArrayOutputStream baos = new ByteArrayOutputStream(); 281 XmlSerializer out = new FastXmlSerializer(); 282 out.setOutput(baos, StandardCharsets.UTF_8.name()); 283 out.startDocument(null, true); 284 out.setFeature("http://xmlpull.org/v1/doc/features.html#indent-output", true); 285 286 out.startTag(null, "job-info"); 287 out.attribute(null, "version", Integer.toString(JOBS_FILE_VERSION)); 288 for (int i=0; i<jobList.size(); i++) { 289 JobStatus jobStatus = jobList.get(i); 290 if (DEBUG) { 291 Slog.d(TAG, "Saving job " + jobStatus.getJobId()); 292 } 293 out.startTag(null, "job"); 294 addAttributesToJobTag(out, jobStatus); 295 writeConstraintsToXml(out, jobStatus); 296 writeExecutionCriteriaToXml(out, jobStatus); 297 writeBundleToXml(jobStatus.getExtras(), out); 298 out.endTag(null, "job"); 299 } 300 out.endTag(null, "job-info"); 301 out.endDocument(); 302 303 // Write out to disk in one fell sweep. 304 FileOutputStream fos = mJobsFile.startWrite(); 305 fos.write(baos.toByteArray()); 306 mJobsFile.finishWrite(fos); 307 mDirtyOperations = 0; 308 } catch (IOException e) { 309 if (DEBUG) { 310 Slog.v(TAG, "Error writing out job data.", e); 311 } 312 } catch (XmlPullParserException e) { 313 if (DEBUG) { 314 Slog.d(TAG, "Error persisting bundle.", e); 315 } 316 } 317 } 318 319 /** Write out a tag with data comprising the required fields and priority of this job and 320 * its client. 321 */ 322 private void addAttributesToJobTag(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 if (jobStatus.getSourcePackageName() != null) { 328 out.attribute(null, "sourcePackageName", jobStatus.getSourcePackageName()); 329 } 330 if (jobStatus.getSourceTag() != null) { 331 out.attribute(null, "sourceTag", jobStatus.getSourceTag()); 332 } 333 out.attribute(null, "sourceUserId", String.valueOf(jobStatus.getSourceUserId())); 334 out.attribute(null, "uid", Integer.toString(jobStatus.getUid())); 335 out.attribute(null, "priority", String.valueOf(jobStatus.getPriority())); 336 } 337 338 private void writeBundleToXml(PersistableBundle extras, XmlSerializer out) 339 throws IOException, XmlPullParserException { 340 out.startTag(null, XML_TAG_EXTRAS); 341 PersistableBundle extrasCopy = deepCopyBundle(extras, 10); 342 extrasCopy.saveToXml(out); 343 out.endTag(null, XML_TAG_EXTRAS); 344 } 345 346 private PersistableBundle deepCopyBundle(PersistableBundle bundle, int maxDepth) { 347 if (maxDepth <= 0) { 348 return null; 349 } 350 PersistableBundle copy = (PersistableBundle) bundle.clone(); 351 Set<String> keySet = bundle.keySet(); 352 for (String key: keySet) { 353 Object o = copy.get(key); 354 if (o instanceof PersistableBundle) { 355 PersistableBundle bCopy = deepCopyBundle((PersistableBundle) o, maxDepth-1); 356 copy.putPersistableBundle(key, bCopy); 357 } 358 } 359 return copy; 360 } 361 362 /** 363 * Write out a tag with data identifying this job's constraints. If the constraint isn't here 364 * it doesn't apply. 365 */ 366 private void writeConstraintsToXml(XmlSerializer out, JobStatus jobStatus) throws IOException { 367 out.startTag(null, XML_TAG_PARAMS_CONSTRAINTS); 368 if (jobStatus.hasConnectivityConstraint()) { 369 out.attribute(null, "connectivity", Boolean.toString(true)); 370 } 371 if (jobStatus.hasUnmeteredConstraint()) { 372 out.attribute(null, "unmetered", Boolean.toString(true)); 373 } 374 if (jobStatus.hasNotRoamingConstraint()) { 375 out.attribute(null, "not-roaming", Boolean.toString(true)); 376 } 377 if (jobStatus.hasIdleConstraint()) { 378 out.attribute(null, "idle", Boolean.toString(true)); 379 } 380 if (jobStatus.hasChargingConstraint()) { 381 out.attribute(null, "charging", Boolean.toString(true)); 382 } 383 out.endTag(null, XML_TAG_PARAMS_CONSTRAINTS); 384 } 385 386 private void writeExecutionCriteriaToXml(XmlSerializer out, JobStatus jobStatus) 387 throws IOException { 388 final JobInfo job = jobStatus.getJob(); 389 if (jobStatus.getJob().isPeriodic()) { 390 out.startTag(null, XML_TAG_PERIODIC); 391 out.attribute(null, "period", Long.toString(job.getIntervalMillis())); 392 out.attribute(null, "flex", Long.toString(job.getFlexMillis())); 393 } else { 394 out.startTag(null, XML_TAG_ONEOFF); 395 } 396 397 if (jobStatus.hasDeadlineConstraint()) { 398 // Wall clock deadline. 399 final long deadlineWallclock = System.currentTimeMillis() + 400 (jobStatus.getLatestRunTimeElapsed() - SystemClock.elapsedRealtime()); 401 out.attribute(null, "deadline", Long.toString(deadlineWallclock)); 402 } 403 if (jobStatus.hasTimingDelayConstraint()) { 404 final long delayWallclock = System.currentTimeMillis() + 405 (jobStatus.getEarliestRunTime() - SystemClock.elapsedRealtime()); 406 out.attribute(null, "delay", Long.toString(delayWallclock)); 407 } 408 409 // Only write out back-off policy if it differs from the default. 410 // This also helps the case where the job is idle -> these aren't allowed to specify 411 // back-off. 412 if (jobStatus.getJob().getInitialBackoffMillis() != JobInfo.DEFAULT_INITIAL_BACKOFF_MILLIS 413 || jobStatus.getJob().getBackoffPolicy() != JobInfo.DEFAULT_BACKOFF_POLICY) { 414 out.attribute(null, "backoff-policy", Integer.toString(job.getBackoffPolicy())); 415 out.attribute(null, "initial-backoff", Long.toString(job.getInitialBackoffMillis())); 416 } 417 if (job.isPeriodic()) { 418 out.endTag(null, XML_TAG_PERIODIC); 419 } else { 420 out.endTag(null, XML_TAG_ONEOFF); 421 } 422 } 423 } 424 425 /** 426 * Runnable that reads list of persisted job from xml. This is run once at start up, so doesn't 427 * need to go through {@link JobStore#add(com.android.server.job.controllers.JobStatus)}. 428 */ 429 private class ReadJobMapFromDiskRunnable implements Runnable { 430 private final JobSet jobSet; 431 432 /** 433 * @param jobSet Reference to the (empty) set of JobStatus objects that back the JobStore, 434 * so that after disk read we can populate it directly. 435 */ 436 ReadJobMapFromDiskRunnable(JobSet jobSet) { 437 this.jobSet = jobSet; 438 } 439 440 @Override 441 public void run() { 442 try { 443 List<JobStatus> jobs; 444 FileInputStream fis = mJobsFile.openRead(); 445 synchronized (mLock) { 446 jobs = readJobMapImpl(fis); 447 if (jobs != null) { 448 for (int i=0; i<jobs.size(); i++) { 449 this.jobSet.add(jobs.get(i)); 450 } 451 } 452 } 453 fis.close(); 454 } catch (FileNotFoundException e) { 455 if (JobSchedulerService.DEBUG) { 456 Slog.d(TAG, "Could not find jobs file, probably there was nothing to load."); 457 } 458 } catch (XmlPullParserException e) { 459 if (JobSchedulerService.DEBUG) { 460 Slog.d(TAG, "Error parsing xml.", e); 461 } 462 } catch (IOException e) { 463 if (JobSchedulerService.DEBUG) { 464 Slog.d(TAG, "Error parsing xml.", e); 465 } 466 } 467 } 468 469 private List<JobStatus> readJobMapImpl(FileInputStream fis) 470 throws XmlPullParserException, IOException { 471 XmlPullParser parser = Xml.newPullParser(); 472 parser.setInput(fis, StandardCharsets.UTF_8.name()); 473 474 int eventType = parser.getEventType(); 475 while (eventType != XmlPullParser.START_TAG && 476 eventType != XmlPullParser.END_DOCUMENT) { 477 eventType = parser.next(); 478 Slog.d(TAG, "Start tag: " + parser.getName()); 479 } 480 if (eventType == XmlPullParser.END_DOCUMENT) { 481 if (DEBUG) { 482 Slog.d(TAG, "No persisted jobs."); 483 } 484 return null; 485 } 486 487 String tagName = parser.getName(); 488 if ("job-info".equals(tagName)) { 489 final List<JobStatus> jobs = new ArrayList<JobStatus>(); 490 // Read in version info. 491 try { 492 int version = Integer.parseInt(parser.getAttributeValue(null, "version")); 493 if (version != JOBS_FILE_VERSION) { 494 Slog.d(TAG, "Invalid version number, aborting jobs file read."); 495 return null; 496 } 497 } catch (NumberFormatException e) { 498 Slog.e(TAG, "Invalid version number, aborting jobs file read."); 499 return null; 500 } 501 eventType = parser.next(); 502 do { 503 // Read each <job/> 504 if (eventType == XmlPullParser.START_TAG) { 505 tagName = parser.getName(); 506 // Start reading job. 507 if ("job".equals(tagName)) { 508 JobStatus persistedJob = restoreJobFromXml(parser); 509 if (persistedJob != null) { 510 if (DEBUG) { 511 Slog.d(TAG, "Read out " + persistedJob); 512 } 513 jobs.add(persistedJob); 514 } else { 515 Slog.d(TAG, "Error reading job from file."); 516 } 517 } 518 } 519 eventType = parser.next(); 520 } while (eventType != XmlPullParser.END_DOCUMENT); 521 return jobs; 522 } 523 return null; 524 } 525 526 /** 527 * @param parser Xml parser at the beginning of a "<job/>" tag. The next "parser.next()" call 528 * will take the parser into the body of the job tag. 529 * @return Newly instantiated job holding all the information we just read out of the xml tag. 530 */ 531 private JobStatus restoreJobFromXml(XmlPullParser parser) throws XmlPullParserException, 532 IOException { 533 JobInfo.Builder jobBuilder; 534 int uid, sourceUserId; 535 536 // Read out job identifier attributes and priority. 537 try { 538 jobBuilder = buildBuilderFromXml(parser); 539 jobBuilder.setPersisted(true); 540 uid = Integer.parseInt(parser.getAttributeValue(null, "uid")); 541 542 String val = parser.getAttributeValue(null, "priority"); 543 if (val != null) { 544 jobBuilder.setPriority(Integer.parseInt(val)); 545 } 546 val = parser.getAttributeValue(null, "sourceUserId"); 547 sourceUserId = val == null ? -1 : Integer.parseInt(val); 548 } catch (NumberFormatException e) { 549 Slog.e(TAG, "Error parsing job's required fields, skipping"); 550 return null; 551 } 552 553 String sourcePackageName = parser.getAttributeValue(null, "sourcePackageName"); 554 555 final String sourceTag = parser.getAttributeValue(null, "sourceTag"); 556 557 int eventType; 558 // Read out constraints tag. 559 do { 560 eventType = parser.next(); 561 } while (eventType == XmlPullParser.TEXT); // Push through to next START_TAG. 562 563 if (!(eventType == XmlPullParser.START_TAG && 564 XML_TAG_PARAMS_CONSTRAINTS.equals(parser.getName()))) { 565 // Expecting a <constraints> start tag. 566 return null; 567 } 568 try { 569 buildConstraintsFromXml(jobBuilder, parser); 570 } catch (NumberFormatException e) { 571 Slog.d(TAG, "Error reading constraints, skipping."); 572 return null; 573 } 574 parser.next(); // Consume </constraints> 575 576 // Read out execution parameters tag. 577 do { 578 eventType = parser.next(); 579 } while (eventType == XmlPullParser.TEXT); 580 if (eventType != XmlPullParser.START_TAG) { 581 return null; 582 } 583 584 // Tuple of (earliest runtime, latest runtime) in elapsed realtime after disk load. 585 Pair<Long, Long> elapsedRuntimes; 586 try { 587 elapsedRuntimes = buildExecutionTimesFromXml(parser); 588 } catch (NumberFormatException e) { 589 if (DEBUG) { 590 Slog.d(TAG, "Error parsing execution time parameters, skipping."); 591 } 592 return null; 593 } 594 595 final long elapsedNow = SystemClock.elapsedRealtime(); 596 if (XML_TAG_PERIODIC.equals(parser.getName())) { 597 try { 598 String val = parser.getAttributeValue(null, "period"); 599 final long periodMillis = Long.valueOf(val); 600 val = parser.getAttributeValue(null, "flex"); 601 final long flexMillis = (val != null) ? Long.valueOf(val) : periodMillis; 602 jobBuilder.setPeriodic(periodMillis, flexMillis); 603 // As a sanity check, cap the recreated run time to be no later than flex+period 604 // from now. This is the latest the periodic could be pushed out. This could 605 // happen if the periodic ran early (at flex time before period), and then the 606 // device rebooted. 607 if (elapsedRuntimes.second > elapsedNow + periodMillis + flexMillis) { 608 final long clampedLateRuntimeElapsed = elapsedNow + flexMillis 609 + periodMillis; 610 final long clampedEarlyRuntimeElapsed = clampedLateRuntimeElapsed 611 - flexMillis; 612 Slog.w(TAG, 613 String.format("Periodic job for uid='%d' persisted run-time is" + 614 " too big [%s, %s]. Clamping to [%s,%s]", 615 uid, 616 DateUtils.formatElapsedTime(elapsedRuntimes.first / 1000), 617 DateUtils.formatElapsedTime(elapsedRuntimes.second / 1000), 618 DateUtils.formatElapsedTime( 619 clampedEarlyRuntimeElapsed / 1000), 620 DateUtils.formatElapsedTime( 621 clampedLateRuntimeElapsed / 1000)) 622 ); 623 elapsedRuntimes = 624 Pair.create(clampedEarlyRuntimeElapsed, clampedLateRuntimeElapsed); 625 } 626 } catch (NumberFormatException e) { 627 Slog.d(TAG, "Error reading periodic execution criteria, skipping."); 628 return null; 629 } 630 } else if (XML_TAG_ONEOFF.equals(parser.getName())) { 631 try { 632 if (elapsedRuntimes.first != JobStatus.NO_EARLIEST_RUNTIME) { 633 jobBuilder.setMinimumLatency(elapsedRuntimes.first - elapsedNow); 634 } 635 if (elapsedRuntimes.second != JobStatus.NO_LATEST_RUNTIME) { 636 jobBuilder.setOverrideDeadline( 637 elapsedRuntimes.second - elapsedNow); 638 } 639 } catch (NumberFormatException e) { 640 Slog.d(TAG, "Error reading job execution criteria, skipping."); 641 return null; 642 } 643 } else { 644 if (DEBUG) { 645 Slog.d(TAG, "Invalid parameter tag, skipping - " + parser.getName()); 646 } 647 // Expecting a parameters start tag. 648 return null; 649 } 650 maybeBuildBackoffPolicyFromXml(jobBuilder, parser); 651 652 parser.nextTag(); // Consume parameters end tag. 653 654 // Read out extras Bundle. 655 do { 656 eventType = parser.next(); 657 } while (eventType == XmlPullParser.TEXT); 658 if (!(eventType == XmlPullParser.START_TAG 659 && XML_TAG_EXTRAS.equals(parser.getName()))) { 660 if (DEBUG) { 661 Slog.d(TAG, "Error reading extras, skipping."); 662 } 663 return null; 664 } 665 666 PersistableBundle extras = PersistableBundle.restoreFromXml(parser); 667 jobBuilder.setExtras(extras); 668 parser.nextTag(); // Consume </extras> 669 670 // Migrate sync jobs forward from earlier, incomplete representation 671 if ("android".equals(sourcePackageName) 672 && extras != null 673 && extras.getBoolean("SyncManagerJob", false)) { 674 sourcePackageName = extras.getString("owningPackage", sourcePackageName); 675 if (DEBUG) { 676 Slog.i(TAG, "Fixing up sync job source package name from 'android' to '" 677 + sourcePackageName + "'"); 678 } 679 } 680 681 // And now we're done 682 JobStatus js = new JobStatus( 683 jobBuilder.build(), uid, sourcePackageName, sourceUserId, sourceTag, 684 elapsedRuntimes.first, elapsedRuntimes.second); 685 return js; 686 } 687 688 private JobInfo.Builder buildBuilderFromXml(XmlPullParser parser) throws NumberFormatException { 689 // Pull out required fields from <job> attributes. 690 int jobId = Integer.parseInt(parser.getAttributeValue(null, "jobid")); 691 String packageName = parser.getAttributeValue(null, "package"); 692 String className = parser.getAttributeValue(null, "class"); 693 ComponentName cname = new ComponentName(packageName, className); 694 695 return new JobInfo.Builder(jobId, cname); 696 } 697 698 private void buildConstraintsFromXml(JobInfo.Builder jobBuilder, XmlPullParser parser) { 699 String val = parser.getAttributeValue(null, "connectivity"); 700 if (val != null) { 701 jobBuilder.setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY); 702 } 703 val = parser.getAttributeValue(null, "unmetered"); 704 if (val != null) { 705 jobBuilder.setRequiredNetworkType(JobInfo.NETWORK_TYPE_UNMETERED); 706 } 707 val = parser.getAttributeValue(null, "not-roaming"); 708 if (val != null) { 709 jobBuilder.setRequiredNetworkType(JobInfo.NETWORK_TYPE_NOT_ROAMING); 710 } 711 val = parser.getAttributeValue(null, "idle"); 712 if (val != null) { 713 jobBuilder.setRequiresDeviceIdle(true); 714 } 715 val = parser.getAttributeValue(null, "charging"); 716 if (val != null) { 717 jobBuilder.setRequiresCharging(true); 718 } 719 } 720 721 /** 722 * Builds the back-off policy out of the params tag. These attributes may not exist, depending 723 * on whether the back-off was set when the job was first scheduled. 724 */ 725 private void maybeBuildBackoffPolicyFromXml(JobInfo.Builder jobBuilder, XmlPullParser parser) { 726 String val = parser.getAttributeValue(null, "initial-backoff"); 727 if (val != null) { 728 long initialBackoff = Long.valueOf(val); 729 val = parser.getAttributeValue(null, "backoff-policy"); 730 int backoffPolicy = Integer.parseInt(val); // Will throw NFE which we catch higher up. 731 jobBuilder.setBackoffCriteria(initialBackoff, backoffPolicy); 732 } 733 } 734 735 /** 736 * Convenience function to read out and convert deadline and delay from xml into elapsed real 737 * time. 738 * @return A {@link android.util.Pair}, where the first value is the earliest elapsed runtime 739 * and the second is the latest elapsed runtime. 740 */ 741 private Pair<Long, Long> buildExecutionTimesFromXml(XmlPullParser parser) 742 throws NumberFormatException { 743 // Pull out execution time data. 744 final long nowWallclock = System.currentTimeMillis(); 745 final long nowElapsed = SystemClock.elapsedRealtime(); 746 747 long earliestRunTimeElapsed = JobStatus.NO_EARLIEST_RUNTIME; 748 long latestRunTimeElapsed = JobStatus.NO_LATEST_RUNTIME; 749 String val = parser.getAttributeValue(null, "deadline"); 750 if (val != null) { 751 long latestRuntimeWallclock = Long.valueOf(val); 752 long maxDelayElapsed = 753 Math.max(latestRuntimeWallclock - nowWallclock, 0); 754 latestRunTimeElapsed = nowElapsed + maxDelayElapsed; 755 } 756 val = parser.getAttributeValue(null, "delay"); 757 if (val != null) { 758 long earliestRuntimeWallclock = Long.valueOf(val); 759 long minDelayElapsed = 760 Math.max(earliestRuntimeWallclock - nowWallclock, 0); 761 earliestRunTimeElapsed = nowElapsed + minDelayElapsed; 762 763 } 764 return Pair.create(earliestRunTimeElapsed, latestRunTimeElapsed); 765 } 766 } 767 768 static class JobSet { 769 // Key is the getUid() originator of the jobs in each sheaf 770 private SparseArray<ArraySet<JobStatus>> mJobs; 771 772 public JobSet() { 773 mJobs = new SparseArray<ArraySet<JobStatus>>(); 774 } 775 776 public List<JobStatus> getJobsByUid(int uid) { 777 ArrayList<JobStatus> matchingJobs = new ArrayList<JobStatus>(); 778 ArraySet<JobStatus> jobs = mJobs.get(uid); 779 if (jobs != null) { 780 matchingJobs.addAll(jobs); 781 } 782 return matchingJobs; 783 } 784 785 // By user, not by uid, so we need to traverse by key and check 786 public List<JobStatus> getJobsByUser(int userId) { 787 ArrayList<JobStatus> result = new ArrayList<JobStatus>(); 788 for (int i = mJobs.size() - 1; i >= 0; i--) { 789 if (UserHandle.getUserId(mJobs.keyAt(i)) == userId) { 790 ArraySet<JobStatus> jobs = mJobs.get(i); 791 if (jobs != null) { 792 result.addAll(jobs); 793 } 794 } 795 } 796 return result; 797 } 798 799 public boolean add(JobStatus job) { 800 final int uid = job.getUid(); 801 ArraySet<JobStatus> jobs = mJobs.get(uid); 802 if (jobs == null) { 803 jobs = new ArraySet<JobStatus>(); 804 mJobs.put(uid, jobs); 805 } 806 return jobs.add(job); 807 } 808 809 public boolean remove(JobStatus job) { 810 final int uid = job.getUid(); 811 ArraySet<JobStatus> jobs = mJobs.get(uid); 812 boolean didRemove = (jobs != null) ? jobs.remove(job) : false; 813 if (didRemove && jobs.size() == 0) { 814 // no more jobs for this uid; let the now-empty set object be GC'd. 815 mJobs.remove(uid); 816 } 817 return didRemove; 818 } 819 820 public boolean contains(JobStatus job) { 821 final int uid = job.getUid(); 822 ArraySet<JobStatus> jobs = mJobs.get(uid); 823 return jobs != null && jobs.contains(job); 824 } 825 826 public JobStatus get(int uid, int jobId) { 827 ArraySet<JobStatus> jobs = mJobs.get(uid); 828 if (jobs != null) { 829 for (int i = jobs.size() - 1; i >= 0; i--) { 830 JobStatus job = jobs.valueAt(i); 831 if (job.getJobId() == jobId) { 832 return job; 833 } 834 } 835 } 836 return null; 837 } 838 839 // Inefficient; use only for testing 840 public List<JobStatus> getAllJobs() { 841 ArrayList<JobStatus> allJobs = new ArrayList<JobStatus>(size()); 842 for (int i = mJobs.size(); i >= 0; i--) { 843 allJobs.addAll(mJobs.valueAt(i)); 844 } 845 return allJobs; 846 } 847 848 public void clear() { 849 mJobs.clear(); 850 } 851 852 public int size() { 853 int total = 0; 854 for (int i = mJobs.size() - 1; i >= 0; i--) { 855 total += mJobs.valueAt(i).size(); 856 } 857 return total; 858 } 859 860 // We only want to count the jobs that this uid has scheduled on its own 861 // behalf, not those that the app has scheduled on someone else's behalf. 862 public int countJobsForUid(int uid) { 863 int total = 0; 864 ArraySet<JobStatus> jobs = mJobs.get(uid); 865 if (jobs != null) { 866 for (int i = jobs.size() - 1; i >= 0; i--) { 867 JobStatus job = jobs.valueAt(i); 868 if (job.getUid() == job.getSourceUid()) { 869 total++; 870 } 871 } 872 } 873 return total; 874 } 875 876 public void forEachJob(JobStatusFunctor functor) { 877 for (int uidIndex = mJobs.size() - 1; uidIndex >= 0; uidIndex--) { 878 ArraySet<JobStatus> jobs = mJobs.valueAt(uidIndex); 879 for (int i = jobs.size() - 1; i >= 0; i--) { 880 functor.process(jobs.valueAt(i)); 881 } 882 } 883 } 884 885 public void forEachJob(int uid, JobStatusFunctor functor) { 886 ArraySet<JobStatus> jobs = mJobs.get(uid); 887 if (jobs != null) { 888 for (int i = jobs.size() - 1; i >= 0; i--) { 889 functor.process(jobs.valueAt(i)); 890 } 891 } 892 } 893 } 894} 895