1 /* 2 * Copyright (C) 2014 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License 15 */ 16 17 package com.android.server.job; 18 19 import android.content.ComponentName; 20 import android.app.job.JobInfo; 21 import android.content.Context; 22 import android.os.Environment; 23 import android.os.Handler; 24 import android.os.PersistableBundle; 25 import android.os.SystemClock; 26 import android.os.UserHandle; 27 import android.text.format.DateUtils; 28 import android.util.AtomicFile; 29 import android.util.ArraySet; 30 import android.util.Pair; 31 import android.util.Slog; 32 import android.util.SparseArray; 33 import android.util.Xml; 34 35 import com.android.internal.annotations.VisibleForTesting; 36 import com.android.internal.util.FastXmlSerializer; 37 import com.android.server.IoThread; 38 import com.android.server.job.controllers.JobStatus; 39 40 import java.io.ByteArrayOutputStream; 41 import java.io.File; 42 import java.io.FileInputStream; 43 import java.io.FileNotFoundException; 44 import java.io.FileOutputStream; 45 import java.io.IOException; 46 import java.nio.charset.StandardCharsets; 47 import java.util.ArrayList; 48 import java.util.List; 49 import java.util.Set; 50 51 import org.xmlpull.v1.XmlPullParser; 52 import org.xmlpull.v1.XmlPullParserException; 53 import 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 */ 66 public 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. */ initAndGet(JobSchedulerService jobManagerService)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 initAndGetForTesting(Context context, File dataDir)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 */ JobStore(Context context, Object lock, File dataDir)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 */ add(JobStatus jobStatus)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 containsJob(JobStatus jobStatus)141 boolean containsJob(JobStatus jobStatus) { 142 return mJobSet.contains(jobStatus); 143 } 144 size()145 public int size() { 146 return mJobSet.size(); 147 } 148 countJobsForUid(int uid)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 */ remove(JobStatus jobStatus, boolean writeBack)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 clear()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 */ getJobsByUser(int userHandle)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 */ getJobsByUid(int uid)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 */ getJobByUidAndJobId(int uid, int jobId)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 */ forEachJob(JobStatusFunctor functor)209 public void forEachJob(JobStatusFunctor functor) { 210 mJobSet.forEachJob(functor); 211 } 212 forEachJob(int uid, JobStatusFunctor functor)213 public void forEachJob(int uid, JobStatusFunctor functor) { 214 mJobSet.forEachJob(uid, functor); 215 } 216 217 public interface JobStatusFunctor { process(JobStatus jobStatus)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 */ maybeWriteStatusToDiskAsync()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 readJobMapFromDisk(JobSet jobSet)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 run()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 writeJobsMapImpl(List<JobStatus> jobList)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 */ addAttributesToJobTag(XmlSerializer out, JobStatus jobStatus)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 out.attribute(null, "flags", String.valueOf(jobStatus.getFlags())); 337 } 338 writeBundleToXml(PersistableBundle extras, XmlSerializer out)339 private void writeBundleToXml(PersistableBundle extras, XmlSerializer out) 340 throws IOException, XmlPullParserException { 341 out.startTag(null, XML_TAG_EXTRAS); 342 PersistableBundle extrasCopy = deepCopyBundle(extras, 10); 343 extrasCopy.saveToXml(out); 344 out.endTag(null, XML_TAG_EXTRAS); 345 } 346 deepCopyBundle(PersistableBundle bundle, int maxDepth)347 private PersistableBundle deepCopyBundle(PersistableBundle bundle, int maxDepth) { 348 if (maxDepth <= 0) { 349 return null; 350 } 351 PersistableBundle copy = (PersistableBundle) bundle.clone(); 352 Set<String> keySet = bundle.keySet(); 353 for (String key: keySet) { 354 Object o = copy.get(key); 355 if (o instanceof PersistableBundle) { 356 PersistableBundle bCopy = deepCopyBundle((PersistableBundle) o, maxDepth-1); 357 copy.putPersistableBundle(key, bCopy); 358 } 359 } 360 return copy; 361 } 362 363 /** 364 * Write out a tag with data identifying this job's constraints. If the constraint isn't here 365 * it doesn't apply. 366 */ writeConstraintsToXml(XmlSerializer out, JobStatus jobStatus)367 private void writeConstraintsToXml(XmlSerializer out, JobStatus jobStatus) throws IOException { 368 out.startTag(null, XML_TAG_PARAMS_CONSTRAINTS); 369 if (jobStatus.hasConnectivityConstraint()) { 370 out.attribute(null, "connectivity", Boolean.toString(true)); 371 } 372 if (jobStatus.hasUnmeteredConstraint()) { 373 out.attribute(null, "unmetered", Boolean.toString(true)); 374 } 375 if (jobStatus.hasNotRoamingConstraint()) { 376 out.attribute(null, "not-roaming", Boolean.toString(true)); 377 } 378 if (jobStatus.hasIdleConstraint()) { 379 out.attribute(null, "idle", Boolean.toString(true)); 380 } 381 if (jobStatus.hasChargingConstraint()) { 382 out.attribute(null, "charging", Boolean.toString(true)); 383 } 384 out.endTag(null, XML_TAG_PARAMS_CONSTRAINTS); 385 } 386 writeExecutionCriteriaToXml(XmlSerializer out, JobStatus jobStatus)387 private void writeExecutionCriteriaToXml(XmlSerializer out, JobStatus jobStatus) 388 throws IOException { 389 final JobInfo job = jobStatus.getJob(); 390 if (jobStatus.getJob().isPeriodic()) { 391 out.startTag(null, XML_TAG_PERIODIC); 392 out.attribute(null, "period", Long.toString(job.getIntervalMillis())); 393 out.attribute(null, "flex", Long.toString(job.getFlexMillis())); 394 } else { 395 out.startTag(null, XML_TAG_ONEOFF); 396 } 397 398 if (jobStatus.hasDeadlineConstraint()) { 399 // Wall clock deadline. 400 final long deadlineWallclock = System.currentTimeMillis() + 401 (jobStatus.getLatestRunTimeElapsed() - SystemClock.elapsedRealtime()); 402 out.attribute(null, "deadline", Long.toString(deadlineWallclock)); 403 } 404 if (jobStatus.hasTimingDelayConstraint()) { 405 final long delayWallclock = System.currentTimeMillis() + 406 (jobStatus.getEarliestRunTime() - SystemClock.elapsedRealtime()); 407 out.attribute(null, "delay", Long.toString(delayWallclock)); 408 } 409 410 // Only write out back-off policy if it differs from the default. 411 // This also helps the case where the job is idle -> these aren't allowed to specify 412 // back-off. 413 if (jobStatus.getJob().getInitialBackoffMillis() != JobInfo.DEFAULT_INITIAL_BACKOFF_MILLIS 414 || jobStatus.getJob().getBackoffPolicy() != JobInfo.DEFAULT_BACKOFF_POLICY) { 415 out.attribute(null, "backoff-policy", Integer.toString(job.getBackoffPolicy())); 416 out.attribute(null, "initial-backoff", Long.toString(job.getInitialBackoffMillis())); 417 } 418 if (job.isPeriodic()) { 419 out.endTag(null, XML_TAG_PERIODIC); 420 } else { 421 out.endTag(null, XML_TAG_ONEOFF); 422 } 423 } 424 } 425 426 /** 427 * Runnable that reads list of persisted job from xml. This is run once at start up, so doesn't 428 * need to go through {@link JobStore#add(com.android.server.job.controllers.JobStatus)}. 429 */ 430 private class ReadJobMapFromDiskRunnable implements Runnable { 431 private final JobSet jobSet; 432 433 /** 434 * @param jobSet Reference to the (empty) set of JobStatus objects that back the JobStore, 435 * so that after disk read we can populate it directly. 436 */ ReadJobMapFromDiskRunnable(JobSet jobSet)437 ReadJobMapFromDiskRunnable(JobSet jobSet) { 438 this.jobSet = jobSet; 439 } 440 441 @Override run()442 public void run() { 443 try { 444 List<JobStatus> jobs; 445 FileInputStream fis = mJobsFile.openRead(); 446 synchronized (mLock) { 447 jobs = readJobMapImpl(fis); 448 if (jobs != null) { 449 for (int i=0; i<jobs.size(); i++) { 450 this.jobSet.add(jobs.get(i)); 451 } 452 } 453 } 454 fis.close(); 455 } catch (FileNotFoundException e) { 456 if (JobSchedulerService.DEBUG) { 457 Slog.d(TAG, "Could not find jobs file, probably there was nothing to load."); 458 } 459 } catch (XmlPullParserException e) { 460 if (JobSchedulerService.DEBUG) { 461 Slog.d(TAG, "Error parsing xml.", e); 462 } 463 } catch (IOException e) { 464 if (JobSchedulerService.DEBUG) { 465 Slog.d(TAG, "Error parsing xml.", e); 466 } 467 } 468 } 469 readJobMapImpl(FileInputStream fis)470 private List<JobStatus> readJobMapImpl(FileInputStream fis) 471 throws XmlPullParserException, IOException { 472 XmlPullParser parser = Xml.newPullParser(); 473 parser.setInput(fis, StandardCharsets.UTF_8.name()); 474 475 int eventType = parser.getEventType(); 476 while (eventType != XmlPullParser.START_TAG && 477 eventType != XmlPullParser.END_DOCUMENT) { 478 eventType = parser.next(); 479 Slog.d(TAG, "Start tag: " + parser.getName()); 480 } 481 if (eventType == XmlPullParser.END_DOCUMENT) { 482 if (DEBUG) { 483 Slog.d(TAG, "No persisted jobs."); 484 } 485 return null; 486 } 487 488 String tagName = parser.getName(); 489 if ("job-info".equals(tagName)) { 490 final List<JobStatus> jobs = new ArrayList<JobStatus>(); 491 // Read in version info. 492 try { 493 int version = Integer.parseInt(parser.getAttributeValue(null, "version")); 494 if (version != JOBS_FILE_VERSION) { 495 Slog.d(TAG, "Invalid version number, aborting jobs file read."); 496 return null; 497 } 498 } catch (NumberFormatException e) { 499 Slog.e(TAG, "Invalid version number, aborting jobs file read."); 500 return null; 501 } 502 eventType = parser.next(); 503 do { 504 // Read each <job/> 505 if (eventType == XmlPullParser.START_TAG) { 506 tagName = parser.getName(); 507 // Start reading job. 508 if ("job".equals(tagName)) { 509 JobStatus persistedJob = restoreJobFromXml(parser); 510 if (persistedJob != null) { 511 if (DEBUG) { 512 Slog.d(TAG, "Read out " + persistedJob); 513 } 514 jobs.add(persistedJob); 515 } else { 516 Slog.d(TAG, "Error reading job from file."); 517 } 518 } 519 } 520 eventType = parser.next(); 521 } while (eventType != XmlPullParser.END_DOCUMENT); 522 return jobs; 523 } 524 return null; 525 } 526 527 /** 528 * @param parser Xml parser at the beginning of a "<job/>" tag. The next "parser.next()" call 529 * will take the parser into the body of the job tag. 530 * @return Newly instantiated job holding all the information we just read out of the xml tag. 531 */ restoreJobFromXml(XmlPullParser parser)532 private JobStatus restoreJobFromXml(XmlPullParser parser) throws XmlPullParserException, 533 IOException { 534 JobInfo.Builder jobBuilder; 535 int uid, sourceUserId; 536 537 // Read out job identifier attributes and priority. 538 try { 539 jobBuilder = buildBuilderFromXml(parser); 540 jobBuilder.setPersisted(true); 541 uid = Integer.parseInt(parser.getAttributeValue(null, "uid")); 542 543 String val = parser.getAttributeValue(null, "priority"); 544 if (val != null) { 545 jobBuilder.setPriority(Integer.parseInt(val)); 546 } 547 val = parser.getAttributeValue(null, "flags"); 548 if (val != null) { 549 jobBuilder.setFlags(Integer.parseInt(val)); 550 } 551 val = parser.getAttributeValue(null, "sourceUserId"); 552 sourceUserId = val == null ? -1 : Integer.parseInt(val); 553 } catch (NumberFormatException e) { 554 Slog.e(TAG, "Error parsing job's required fields, skipping"); 555 return null; 556 } 557 558 String sourcePackageName = parser.getAttributeValue(null, "sourcePackageName"); 559 560 final String sourceTag = parser.getAttributeValue(null, "sourceTag"); 561 562 int eventType; 563 // Read out constraints tag. 564 do { 565 eventType = parser.next(); 566 } while (eventType == XmlPullParser.TEXT); // Push through to next START_TAG. 567 568 if (!(eventType == XmlPullParser.START_TAG && 569 XML_TAG_PARAMS_CONSTRAINTS.equals(parser.getName()))) { 570 // Expecting a <constraints> start tag. 571 return null; 572 } 573 try { 574 buildConstraintsFromXml(jobBuilder, parser); 575 } catch (NumberFormatException e) { 576 Slog.d(TAG, "Error reading constraints, skipping."); 577 return null; 578 } 579 parser.next(); // Consume </constraints> 580 581 // Read out execution parameters tag. 582 do { 583 eventType = parser.next(); 584 } while (eventType == XmlPullParser.TEXT); 585 if (eventType != XmlPullParser.START_TAG) { 586 return null; 587 } 588 589 // Tuple of (earliest runtime, latest runtime) in elapsed realtime after disk load. 590 Pair<Long, Long> elapsedRuntimes; 591 try { 592 elapsedRuntimes = buildExecutionTimesFromXml(parser); 593 } catch (NumberFormatException e) { 594 if (DEBUG) { 595 Slog.d(TAG, "Error parsing execution time parameters, skipping."); 596 } 597 return null; 598 } 599 600 final long elapsedNow = SystemClock.elapsedRealtime(); 601 if (XML_TAG_PERIODIC.equals(parser.getName())) { 602 try { 603 String val = parser.getAttributeValue(null, "period"); 604 final long periodMillis = Long.valueOf(val); 605 val = parser.getAttributeValue(null, "flex"); 606 final long flexMillis = (val != null) ? Long.valueOf(val) : periodMillis; 607 jobBuilder.setPeriodic(periodMillis, flexMillis); 608 // As a sanity check, cap the recreated run time to be no later than flex+period 609 // from now. This is the latest the periodic could be pushed out. This could 610 // happen if the periodic ran early (at flex time before period), and then the 611 // device rebooted. 612 if (elapsedRuntimes.second > elapsedNow + periodMillis + flexMillis) { 613 final long clampedLateRuntimeElapsed = elapsedNow + flexMillis 614 + periodMillis; 615 final long clampedEarlyRuntimeElapsed = clampedLateRuntimeElapsed 616 - flexMillis; 617 Slog.w(TAG, 618 String.format("Periodic job for uid='%d' persisted run-time is" + 619 " too big [%s, %s]. Clamping to [%s,%s]", 620 uid, 621 DateUtils.formatElapsedTime(elapsedRuntimes.first / 1000), 622 DateUtils.formatElapsedTime(elapsedRuntimes.second / 1000), 623 DateUtils.formatElapsedTime( 624 clampedEarlyRuntimeElapsed / 1000), 625 DateUtils.formatElapsedTime( 626 clampedLateRuntimeElapsed / 1000)) 627 ); 628 elapsedRuntimes = 629 Pair.create(clampedEarlyRuntimeElapsed, clampedLateRuntimeElapsed); 630 } 631 } catch (NumberFormatException e) { 632 Slog.d(TAG, "Error reading periodic execution criteria, skipping."); 633 return null; 634 } 635 } else if (XML_TAG_ONEOFF.equals(parser.getName())) { 636 try { 637 if (elapsedRuntimes.first != JobStatus.NO_EARLIEST_RUNTIME) { 638 jobBuilder.setMinimumLatency(elapsedRuntimes.first - elapsedNow); 639 } 640 if (elapsedRuntimes.second != JobStatus.NO_LATEST_RUNTIME) { 641 jobBuilder.setOverrideDeadline( 642 elapsedRuntimes.second - elapsedNow); 643 } 644 } catch (NumberFormatException e) { 645 Slog.d(TAG, "Error reading job execution criteria, skipping."); 646 return null; 647 } 648 } else { 649 if (DEBUG) { 650 Slog.d(TAG, "Invalid parameter tag, skipping - " + parser.getName()); 651 } 652 // Expecting a parameters start tag. 653 return null; 654 } 655 maybeBuildBackoffPolicyFromXml(jobBuilder, parser); 656 657 parser.nextTag(); // Consume parameters end tag. 658 659 // Read out extras Bundle. 660 do { 661 eventType = parser.next(); 662 } while (eventType == XmlPullParser.TEXT); 663 if (!(eventType == XmlPullParser.START_TAG 664 && XML_TAG_EXTRAS.equals(parser.getName()))) { 665 if (DEBUG) { 666 Slog.d(TAG, "Error reading extras, skipping."); 667 } 668 return null; 669 } 670 671 PersistableBundle extras = PersistableBundle.restoreFromXml(parser); 672 jobBuilder.setExtras(extras); 673 parser.nextTag(); // Consume </extras> 674 675 // Migrate sync jobs forward from earlier, incomplete representation 676 if ("android".equals(sourcePackageName) 677 && extras != null 678 && extras.getBoolean("SyncManagerJob", false)) { 679 sourcePackageName = extras.getString("owningPackage", sourcePackageName); 680 if (DEBUG) { 681 Slog.i(TAG, "Fixing up sync job source package name from 'android' to '" 682 + sourcePackageName + "'"); 683 } 684 } 685 686 // And now we're done 687 JobStatus js = new JobStatus( 688 jobBuilder.build(), uid, sourcePackageName, sourceUserId, sourceTag, 689 elapsedRuntimes.first, elapsedRuntimes.second); 690 return js; 691 } 692 buildBuilderFromXml(XmlPullParser parser)693 private JobInfo.Builder buildBuilderFromXml(XmlPullParser parser) throws NumberFormatException { 694 // Pull out required fields from <job> attributes. 695 int jobId = Integer.parseInt(parser.getAttributeValue(null, "jobid")); 696 String packageName = parser.getAttributeValue(null, "package"); 697 String className = parser.getAttributeValue(null, "class"); 698 ComponentName cname = new ComponentName(packageName, className); 699 700 return new JobInfo.Builder(jobId, cname); 701 } 702 buildConstraintsFromXml(JobInfo.Builder jobBuilder, XmlPullParser parser)703 private void buildConstraintsFromXml(JobInfo.Builder jobBuilder, XmlPullParser parser) { 704 String val = parser.getAttributeValue(null, "connectivity"); 705 if (val != null) { 706 jobBuilder.setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY); 707 } 708 val = parser.getAttributeValue(null, "unmetered"); 709 if (val != null) { 710 jobBuilder.setRequiredNetworkType(JobInfo.NETWORK_TYPE_UNMETERED); 711 } 712 val = parser.getAttributeValue(null, "not-roaming"); 713 if (val != null) { 714 jobBuilder.setRequiredNetworkType(JobInfo.NETWORK_TYPE_NOT_ROAMING); 715 } 716 val = parser.getAttributeValue(null, "idle"); 717 if (val != null) { 718 jobBuilder.setRequiresDeviceIdle(true); 719 } 720 val = parser.getAttributeValue(null, "charging"); 721 if (val != null) { 722 jobBuilder.setRequiresCharging(true); 723 } 724 } 725 726 /** 727 * Builds the back-off policy out of the params tag. These attributes may not exist, depending 728 * on whether the back-off was set when the job was first scheduled. 729 */ maybeBuildBackoffPolicyFromXml(JobInfo.Builder jobBuilder, XmlPullParser parser)730 private void maybeBuildBackoffPolicyFromXml(JobInfo.Builder jobBuilder, XmlPullParser parser) { 731 String val = parser.getAttributeValue(null, "initial-backoff"); 732 if (val != null) { 733 long initialBackoff = Long.valueOf(val); 734 val = parser.getAttributeValue(null, "backoff-policy"); 735 int backoffPolicy = Integer.parseInt(val); // Will throw NFE which we catch higher up. 736 jobBuilder.setBackoffCriteria(initialBackoff, backoffPolicy); 737 } 738 } 739 740 /** 741 * Convenience function to read out and convert deadline and delay from xml into elapsed real 742 * time. 743 * @return A {@link android.util.Pair}, where the first value is the earliest elapsed runtime 744 * and the second is the latest elapsed runtime. 745 */ buildExecutionTimesFromXml(XmlPullParser parser)746 private Pair<Long, Long> buildExecutionTimesFromXml(XmlPullParser parser) 747 throws NumberFormatException { 748 // Pull out execution time data. 749 final long nowWallclock = System.currentTimeMillis(); 750 final long nowElapsed = SystemClock.elapsedRealtime(); 751 752 long earliestRunTimeElapsed = JobStatus.NO_EARLIEST_RUNTIME; 753 long latestRunTimeElapsed = JobStatus.NO_LATEST_RUNTIME; 754 String val = parser.getAttributeValue(null, "deadline"); 755 if (val != null) { 756 long latestRuntimeWallclock = Long.valueOf(val); 757 long maxDelayElapsed = 758 Math.max(latestRuntimeWallclock - nowWallclock, 0); 759 latestRunTimeElapsed = nowElapsed + maxDelayElapsed; 760 } 761 val = parser.getAttributeValue(null, "delay"); 762 if (val != null) { 763 long earliestRuntimeWallclock = Long.valueOf(val); 764 long minDelayElapsed = 765 Math.max(earliestRuntimeWallclock - nowWallclock, 0); 766 earliestRunTimeElapsed = nowElapsed + minDelayElapsed; 767 768 } 769 return Pair.create(earliestRunTimeElapsed, latestRunTimeElapsed); 770 } 771 } 772 773 static class JobSet { 774 // Key is the getUid() originator of the jobs in each sheaf 775 private SparseArray<ArraySet<JobStatus>> mJobs; 776 JobSet()777 public JobSet() { 778 mJobs = new SparseArray<ArraySet<JobStatus>>(); 779 } 780 getJobsByUid(int uid)781 public List<JobStatus> getJobsByUid(int uid) { 782 ArrayList<JobStatus> matchingJobs = new ArrayList<JobStatus>(); 783 ArraySet<JobStatus> jobs = mJobs.get(uid); 784 if (jobs != null) { 785 matchingJobs.addAll(jobs); 786 } 787 return matchingJobs; 788 } 789 790 // By user, not by uid, so we need to traverse by key and check getJobsByUser(int userId)791 public List<JobStatus> getJobsByUser(int userId) { 792 ArrayList<JobStatus> result = new ArrayList<JobStatus>(); 793 for (int i = mJobs.size() - 1; i >= 0; i--) { 794 if (UserHandle.getUserId(mJobs.keyAt(i)) == userId) { 795 ArraySet<JobStatus> jobs = mJobs.get(i); 796 if (jobs != null) { 797 result.addAll(jobs); 798 } 799 } 800 } 801 return result; 802 } 803 add(JobStatus job)804 public boolean add(JobStatus job) { 805 final int uid = job.getUid(); 806 ArraySet<JobStatus> jobs = mJobs.get(uid); 807 if (jobs == null) { 808 jobs = new ArraySet<JobStatus>(); 809 mJobs.put(uid, jobs); 810 } 811 return jobs.add(job); 812 } 813 remove(JobStatus job)814 public boolean remove(JobStatus job) { 815 final int uid = job.getUid(); 816 ArraySet<JobStatus> jobs = mJobs.get(uid); 817 boolean didRemove = (jobs != null) ? jobs.remove(job) : false; 818 if (didRemove && jobs.size() == 0) { 819 // no more jobs for this uid; let the now-empty set object be GC'd. 820 mJobs.remove(uid); 821 } 822 return didRemove; 823 } 824 contains(JobStatus job)825 public boolean contains(JobStatus job) { 826 final int uid = job.getUid(); 827 ArraySet<JobStatus> jobs = mJobs.get(uid); 828 return jobs != null && jobs.contains(job); 829 } 830 get(int uid, int jobId)831 public JobStatus get(int uid, int jobId) { 832 ArraySet<JobStatus> jobs = mJobs.get(uid); 833 if (jobs != null) { 834 for (int i = jobs.size() - 1; i >= 0; i--) { 835 JobStatus job = jobs.valueAt(i); 836 if (job.getJobId() == jobId) { 837 return job; 838 } 839 } 840 } 841 return null; 842 } 843 844 // Inefficient; use only for testing getAllJobs()845 public List<JobStatus> getAllJobs() { 846 ArrayList<JobStatus> allJobs = new ArrayList<JobStatus>(size()); 847 for (int i = mJobs.size() - 1; i >= 0; i--) { 848 ArraySet<JobStatus> jobs = mJobs.valueAt(i); 849 if (jobs != null) { 850 // Use a for loop over the ArraySet, so we don't need to make its 851 // optional collection class iterator implementation or have to go 852 // through a temporary array from toArray(). 853 for (int j = jobs.size() - 1; j >= 0; j--) { 854 allJobs.add(jobs.valueAt(j)); 855 } 856 } 857 } 858 return allJobs; 859 } 860 clear()861 public void clear() { 862 mJobs.clear(); 863 } 864 size()865 public int size() { 866 int total = 0; 867 for (int i = mJobs.size() - 1; i >= 0; i--) { 868 total += mJobs.valueAt(i).size(); 869 } 870 return total; 871 } 872 873 // We only want to count the jobs that this uid has scheduled on its own 874 // behalf, not those that the app has scheduled on someone else's behalf. countJobsForUid(int uid)875 public int countJobsForUid(int uid) { 876 int total = 0; 877 ArraySet<JobStatus> jobs = mJobs.get(uid); 878 if (jobs != null) { 879 for (int i = jobs.size() - 1; i >= 0; i--) { 880 JobStatus job = jobs.valueAt(i); 881 if (job.getUid() == job.getSourceUid()) { 882 total++; 883 } 884 } 885 } 886 return total; 887 } 888 forEachJob(JobStatusFunctor functor)889 public void forEachJob(JobStatusFunctor functor) { 890 for (int uidIndex = mJobs.size() - 1; uidIndex >= 0; uidIndex--) { 891 ArraySet<JobStatus> jobs = mJobs.valueAt(uidIndex); 892 for (int i = jobs.size() - 1; i >= 0; i--) { 893 functor.process(jobs.valueAt(i)); 894 } 895 } 896 } 897 forEachJob(int uid, JobStatusFunctor functor)898 public void forEachJob(int uid, JobStatusFunctor functor) { 899 ArraySet<JobStatus> jobs = mJobs.get(uid); 900 if (jobs != null) { 901 for (int i = jobs.size() - 1; i >= 0; i--) { 902 functor.process(jobs.valueAt(i)); 903 } 904 } 905 } 906 } 907 } 908