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