1 /* 2 * Copyright (C) 2016 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.controllers; 18 19 import android.annotation.UserIdInt; 20 import android.app.job.JobInfo; 21 import android.database.ContentObserver; 22 import android.net.Uri; 23 import android.os.Handler; 24 import android.os.UserHandle; 25 import android.util.ArrayMap; 26 import android.util.ArraySet; 27 import android.util.Log; 28 import android.util.Slog; 29 import android.util.SparseArray; 30 import android.util.TimeUtils; 31 import android.util.proto.ProtoOutputStream; 32 33 import com.android.internal.util.IndentingPrintWriter; 34 import com.android.server.job.JobSchedulerService; 35 import com.android.server.job.StateControllerProto; 36 import com.android.server.job.StateControllerProto.ContentObserverController.Observer.TriggerContentData; 37 38 import java.util.ArrayList; 39 import java.util.function.Predicate; 40 41 /** 42 * Controller for monitoring changes to content URIs through a ContentObserver. 43 */ 44 public final class ContentObserverController extends StateController { 45 private static final String TAG = "JobScheduler.ContentObserver"; 46 private static final boolean DEBUG = JobSchedulerService.DEBUG 47 || Log.isLoggable(TAG, Log.DEBUG); 48 49 /** 50 * Maximum number of changing URIs we will batch together to report. 51 * XXX Should be smarter about this, restricting it by the maximum number 52 * of characters we will retain. 53 */ 54 private static final int MAX_URIS_REPORTED = 50; 55 56 /** 57 * At this point we consider it urgent to schedule the job ASAP. 58 */ 59 private static final int URIS_URGENT_THRESHOLD = 40; 60 61 final private ArraySet<JobStatus> mTrackedTasks = new ArraySet<>(); 62 /** 63 * Per-userid {@link JobInfo.TriggerContentUri} keyed ContentObserver cache. 64 */ 65 final SparseArray<ArrayMap<JobInfo.TriggerContentUri, ObserverInstance>> mObservers = 66 new SparseArray<>(); 67 final Handler mHandler; 68 69 public ContentObserverController(JobSchedulerService service) { 70 super(service); 71 mHandler = new Handler(mContext.getMainLooper()); 72 } 73 74 @Override 75 public void maybeStartTrackingJobLocked(JobStatus taskStatus, JobStatus lastJob) { 76 if (taskStatus.hasContentTriggerConstraint()) { 77 if (taskStatus.contentObserverJobInstance == null) { 78 taskStatus.contentObserverJobInstance = new JobInstance(taskStatus); 79 } 80 if (DEBUG) { 81 Slog.i(TAG, "Tracking content-trigger job " + taskStatus); 82 } 83 mTrackedTasks.add(taskStatus); 84 taskStatus.setTrackingController(JobStatus.TRACKING_CONTENT); 85 boolean havePendingUris = false; 86 // If there is a previous job associated with the new job, propagate over 87 // any pending content URI trigger reports. 88 if (taskStatus.contentObserverJobInstance.mChangedAuthorities != null) { 89 havePendingUris = true; 90 } 91 // If we have previously reported changed authorities/uris, then we failed 92 // to complete the job with them so will re-record them to report again. 93 if (taskStatus.changedAuthorities != null) { 94 havePendingUris = true; 95 if (taskStatus.contentObserverJobInstance.mChangedAuthorities == null) { 96 taskStatus.contentObserverJobInstance.mChangedAuthorities 97 = new ArraySet<>(); 98 } 99 for (String auth : taskStatus.changedAuthorities) { 100 taskStatus.contentObserverJobInstance.mChangedAuthorities.add(auth); 101 } 102 if (taskStatus.changedUris != null) { 103 if (taskStatus.contentObserverJobInstance.mChangedUris == null) { 104 taskStatus.contentObserverJobInstance.mChangedUris = new ArraySet<>(); 105 } 106 for (Uri uri : taskStatus.changedUris) { 107 taskStatus.contentObserverJobInstance.mChangedUris.add(uri); 108 } 109 } 110 taskStatus.changedAuthorities = null; 111 taskStatus.changedUris = null; 112 } 113 taskStatus.changedAuthorities = null; 114 taskStatus.changedUris = null; 115 taskStatus.setContentTriggerConstraintSatisfied(havePendingUris); 116 } 117 if (lastJob != null && lastJob.contentObserverJobInstance != null) { 118 // And now we can detach the instance state from the last job. 119 lastJob.contentObserverJobInstance.detachLocked(); 120 lastJob.contentObserverJobInstance = null; 121 } 122 } 123 124 @Override 125 public void prepareForExecutionLocked(JobStatus taskStatus) { 126 if (taskStatus.hasContentTriggerConstraint()) { 127 if (taskStatus.contentObserverJobInstance != null) { 128 taskStatus.changedUris = taskStatus.contentObserverJobInstance.mChangedUris; 129 taskStatus.changedAuthorities 130 = taskStatus.contentObserverJobInstance.mChangedAuthorities; 131 taskStatus.contentObserverJobInstance.mChangedUris = null; 132 taskStatus.contentObserverJobInstance.mChangedAuthorities = null; 133 } 134 } 135 } 136 137 @Override 138 public void maybeStopTrackingJobLocked(JobStatus taskStatus, JobStatus incomingJob, 139 boolean forUpdate) { 140 if (taskStatus.clearTrackingController(JobStatus.TRACKING_CONTENT)) { 141 mTrackedTasks.remove(taskStatus); 142 if (taskStatus.contentObserverJobInstance != null) { 143 taskStatus.contentObserverJobInstance.unscheduleLocked(); 144 if (incomingJob != null) { 145 if (taskStatus.contentObserverJobInstance != null 146 && taskStatus.contentObserverJobInstance.mChangedAuthorities != null) { 147 // We are stopping this job, but it is going to be replaced by this given 148 // incoming job. We want to propagate our state over to it, so we don't 149 // lose any content changes that had happened since the last one started. 150 // If there is a previous job associated with the new job, propagate over 151 // any pending content URI trigger reports. 152 if (incomingJob.contentObserverJobInstance == null) { 153 incomingJob.contentObserverJobInstance = new JobInstance(incomingJob); 154 } 155 incomingJob.contentObserverJobInstance.mChangedAuthorities 156 = taskStatus.contentObserverJobInstance.mChangedAuthorities; 157 incomingJob.contentObserverJobInstance.mChangedUris 158 = taskStatus.contentObserverJobInstance.mChangedUris; 159 taskStatus.contentObserverJobInstance.mChangedAuthorities = null; 160 taskStatus.contentObserverJobInstance.mChangedUris = null; 161 } 162 // We won't detach the content observers here, because we want to 163 // allow them to continue monitoring so we don't miss anything... and 164 // since we are giving an incomingJob here, we know this will be 165 // immediately followed by a start tracking of that job. 166 } else { 167 // But here there is no incomingJob, so nothing coming up, so time to detach. 168 taskStatus.contentObserverJobInstance.detachLocked(); 169 taskStatus.contentObserverJobInstance = null; 170 } 171 } 172 if (DEBUG) { 173 Slog.i(TAG, "No longer tracking job " + taskStatus); 174 } 175 } 176 } 177 178 @Override 179 public void rescheduleForFailureLocked(JobStatus newJob, JobStatus failureToReschedule) { 180 if (failureToReschedule.hasContentTriggerConstraint() 181 && newJob.hasContentTriggerConstraint()) { 182 // Our job has failed, and we are scheduling a new job for it. 183 // Copy the last reported content changes in to the new job, so when 184 // we schedule the new one we will pick them up and report them again. 185 newJob.changedAuthorities = failureToReschedule.changedAuthorities; 186 newJob.changedUris = failureToReschedule.changedUris; 187 } 188 } 189 190 final class ObserverInstance extends ContentObserver { 191 final JobInfo.TriggerContentUri mUri; 192 final @UserIdInt int mUserId; 193 final ArraySet<JobInstance> mJobs = new ArraySet<>(); 194 195 public ObserverInstance(Handler handler, JobInfo.TriggerContentUri uri, 196 @UserIdInt int userId) { 197 super(handler); 198 mUri = uri; 199 mUserId = userId; 200 } 201 202 @Override 203 public void onChange(boolean selfChange, Uri uri) { 204 if (DEBUG) { 205 Slog.i(TAG, "onChange(self=" + selfChange + ") for " + uri 206 + " when mUri=" + mUri + " mUserId=" + mUserId); 207 } 208 synchronized (mLock) { 209 final int N = mJobs.size(); 210 for (int i=0; i<N; i++) { 211 JobInstance inst = mJobs.valueAt(i); 212 if (inst.mChangedUris == null) { 213 inst.mChangedUris = new ArraySet<>(); 214 } 215 if (inst.mChangedUris.size() < MAX_URIS_REPORTED) { 216 inst.mChangedUris.add(uri); 217 } 218 if (inst.mChangedAuthorities == null) { 219 inst.mChangedAuthorities = new ArraySet<>(); 220 } 221 inst.mChangedAuthorities.add(uri.getAuthority()); 222 inst.scheduleLocked(); 223 } 224 } 225 } 226 } 227 228 static final class TriggerRunnable implements Runnable { 229 final JobInstance mInstance; 230 231 TriggerRunnable(JobInstance instance) { 232 mInstance = instance; 233 } 234 235 @Override public void run() { 236 mInstance.trigger(); 237 } 238 } 239 240 final class JobInstance { 241 final ArrayList<ObserverInstance> mMyObservers = new ArrayList<>(); 242 final JobStatus mJobStatus; 243 final Runnable mExecuteRunner; 244 final Runnable mTimeoutRunner; 245 ArraySet<Uri> mChangedUris; 246 ArraySet<String> mChangedAuthorities; 247 248 boolean mTriggerPending; 249 250 // This constructor must be called with the master job scheduler lock held. 251 JobInstance(JobStatus jobStatus) { 252 mJobStatus = jobStatus; 253 mExecuteRunner = new TriggerRunnable(this); 254 mTimeoutRunner = new TriggerRunnable(this); 255 final JobInfo.TriggerContentUri[] uris = jobStatus.getJob().getTriggerContentUris(); 256 final int sourceUserId = jobStatus.getSourceUserId(); 257 ArrayMap<JobInfo.TriggerContentUri, ObserverInstance> observersOfUser = 258 mObservers.get(sourceUserId); 259 if (observersOfUser == null) { 260 observersOfUser = new ArrayMap<>(); 261 mObservers.put(sourceUserId, observersOfUser); 262 } 263 if (uris != null) { 264 for (JobInfo.TriggerContentUri uri : uris) { 265 ObserverInstance obs = observersOfUser.get(uri); 266 if (obs == null) { 267 obs = new ObserverInstance(mHandler, uri, jobStatus.getSourceUserId()); 268 observersOfUser.put(uri, obs); 269 final boolean andDescendants = (uri.getFlags() & 270 JobInfo.TriggerContentUri.FLAG_NOTIFY_FOR_DESCENDANTS) != 0; 271 if (DEBUG) { 272 Slog.v(TAG, "New observer " + obs + " for " + uri.getUri() 273 + " andDescendants=" + andDescendants 274 + " sourceUserId=" + sourceUserId); 275 } 276 mContext.getContentResolver().registerContentObserver( 277 uri.getUri(), 278 andDescendants, 279 obs, 280 sourceUserId 281 ); 282 } else { 283 if (DEBUG) { 284 final boolean andDescendants = (uri.getFlags() & 285 JobInfo.TriggerContentUri.FLAG_NOTIFY_FOR_DESCENDANTS) != 0; 286 Slog.v(TAG, "Reusing existing observer " + obs + " for " + uri.getUri() 287 + " andDescendants=" + andDescendants); 288 } 289 } 290 obs.mJobs.add(this); 291 mMyObservers.add(obs); 292 } 293 } 294 } 295 296 void trigger() { 297 boolean reportChange = false; 298 synchronized (mLock) { 299 if (mTriggerPending) { 300 if (mJobStatus.setContentTriggerConstraintSatisfied(true)) { 301 reportChange = true; 302 } 303 unscheduleLocked(); 304 } 305 } 306 // Let the scheduler know that state has changed. This may or may not result in an 307 // execution. 308 if (reportChange) { 309 mStateChangedListener.onControllerStateChanged(); 310 } 311 } 312 313 void scheduleLocked() { 314 if (!mTriggerPending) { 315 mTriggerPending = true; 316 mHandler.postDelayed(mTimeoutRunner, mJobStatus.getTriggerContentMaxDelay()); 317 } 318 mHandler.removeCallbacks(mExecuteRunner); 319 if (mChangedUris.size() >= URIS_URGENT_THRESHOLD) { 320 // If we start getting near the limit, GO NOW! 321 mHandler.post(mExecuteRunner); 322 } else { 323 mHandler.postDelayed(mExecuteRunner, mJobStatus.getTriggerContentUpdateDelay()); 324 } 325 } 326 327 void unscheduleLocked() { 328 if (mTriggerPending) { 329 mHandler.removeCallbacks(mExecuteRunner); 330 mHandler.removeCallbacks(mTimeoutRunner); 331 mTriggerPending = false; 332 } 333 } 334 335 void detachLocked() { 336 final int N = mMyObservers.size(); 337 for (int i=0; i<N; i++) { 338 final ObserverInstance obs = mMyObservers.get(i); 339 obs.mJobs.remove(this); 340 if (obs.mJobs.size() == 0) { 341 if (DEBUG) { 342 Slog.i(TAG, "Unregistering observer " + obs + " for " + obs.mUri.getUri()); 343 } 344 mContext.getContentResolver().unregisterContentObserver(obs); 345 ArrayMap<JobInfo.TriggerContentUri, ObserverInstance> observerOfUser = 346 mObservers.get(obs.mUserId); 347 if (observerOfUser != null) { 348 observerOfUser.remove(obs.mUri); 349 } 350 } 351 } 352 } 353 } 354 355 @Override 356 public void dumpControllerStateLocked(IndentingPrintWriter pw, 357 Predicate<JobStatus> predicate) { 358 for (int i = 0; i < mTrackedTasks.size(); i++) { 359 JobStatus js = mTrackedTasks.valueAt(i); 360 if (!predicate.test(js)) { 361 continue; 362 } 363 pw.print("#"); 364 js.printUniqueId(pw); 365 pw.print(" from "); 366 UserHandle.formatUid(pw, js.getSourceUid()); 367 pw.println(); 368 } 369 pw.println(); 370 371 int N = mObservers.size(); 372 if (N > 0) { 373 pw.println("Observers:"); 374 pw.increaseIndent(); 375 for (int userIdx = 0; userIdx < N; userIdx++) { 376 final int userId = mObservers.keyAt(userIdx); 377 ArrayMap<JobInfo.TriggerContentUri, ObserverInstance> observersOfUser = 378 mObservers.get(userId); 379 int numbOfObserversPerUser = observersOfUser.size(); 380 for (int observerIdx = 0 ; observerIdx < numbOfObserversPerUser; observerIdx++) { 381 ObserverInstance obs = observersOfUser.valueAt(observerIdx); 382 int M = obs.mJobs.size(); 383 boolean shouldDump = false; 384 for (int j = 0; j < M; j++) { 385 JobInstance inst = obs.mJobs.valueAt(j); 386 if (predicate.test(inst.mJobStatus)) { 387 shouldDump = true; 388 break; 389 } 390 } 391 if (!shouldDump) { 392 continue; 393 } 394 JobInfo.TriggerContentUri trigger = observersOfUser.keyAt(observerIdx); 395 pw.print(trigger.getUri()); 396 pw.print(" 0x"); 397 pw.print(Integer.toHexString(trigger.getFlags())); 398 pw.print(" ("); 399 pw.print(System.identityHashCode(obs)); 400 pw.println("):"); 401 pw.increaseIndent(); 402 pw.println("Jobs:"); 403 pw.increaseIndent(); 404 for (int j = 0; j < M; j++) { 405 JobInstance inst = obs.mJobs.valueAt(j); 406 pw.print("#"); 407 inst.mJobStatus.printUniqueId(pw); 408 pw.print(" from "); 409 UserHandle.formatUid(pw, inst.mJobStatus.getSourceUid()); 410 if (inst.mChangedAuthorities != null) { 411 pw.println(":"); 412 pw.increaseIndent(); 413 if (inst.mTriggerPending) { 414 pw.print("Trigger pending: update="); 415 TimeUtils.formatDuration( 416 inst.mJobStatus.getTriggerContentUpdateDelay(), pw); 417 pw.print(", max="); 418 TimeUtils.formatDuration( 419 inst.mJobStatus.getTriggerContentMaxDelay(), pw); 420 pw.println(); 421 } 422 pw.println("Changed Authorities:"); 423 for (int k = 0; k < inst.mChangedAuthorities.size(); k++) { 424 pw.println(inst.mChangedAuthorities.valueAt(k)); 425 } 426 if (inst.mChangedUris != null) { 427 pw.println(" Changed URIs:"); 428 for (int k = 0; k < inst.mChangedUris.size(); k++) { 429 pw.println(inst.mChangedUris.valueAt(k)); 430 } 431 } 432 pw.decreaseIndent(); 433 } else { 434 pw.println(); 435 } 436 } 437 pw.decreaseIndent(); 438 pw.decreaseIndent(); 439 } 440 } 441 pw.decreaseIndent(); 442 } 443 } 444 445 @Override 446 public void dumpControllerStateLocked(ProtoOutputStream proto, long fieldId, 447 Predicate<JobStatus> predicate) { 448 final long token = proto.start(fieldId); 449 final long mToken = proto.start(StateControllerProto.CONTENT_OBSERVER); 450 451 for (int i = 0; i < mTrackedTasks.size(); i++) { 452 JobStatus js = mTrackedTasks.valueAt(i); 453 if (!predicate.test(js)) { 454 continue; 455 } 456 final long jsToken = 457 proto.start(StateControllerProto.ContentObserverController.TRACKED_JOBS); 458 js.writeToShortProto(proto, 459 StateControllerProto.ContentObserverController.TrackedJob.INFO); 460 proto.write(StateControllerProto.ContentObserverController.TrackedJob.SOURCE_UID, 461 js.getSourceUid()); 462 proto.end(jsToken); 463 } 464 465 final int n = mObservers.size(); 466 for (int userIdx = 0; userIdx < n; userIdx++) { 467 final long oToken = 468 proto.start(StateControllerProto.ContentObserverController.OBSERVERS); 469 final int userId = mObservers.keyAt(userIdx); 470 471 proto.write(StateControllerProto.ContentObserverController.Observer.USER_ID, userId); 472 473 ArrayMap<JobInfo.TriggerContentUri, ObserverInstance> observersOfUser = 474 mObservers.get(userId); 475 int numbOfObserversPerUser = observersOfUser.size(); 476 for (int observerIdx = 0 ; observerIdx < numbOfObserversPerUser; observerIdx++) { 477 ObserverInstance obs = observersOfUser.valueAt(observerIdx); 478 int m = obs.mJobs.size(); 479 boolean shouldDump = false; 480 for (int j = 0; j < m; j++) { 481 JobInstance inst = obs.mJobs.valueAt(j); 482 if (predicate.test(inst.mJobStatus)) { 483 shouldDump = true; 484 break; 485 } 486 } 487 if (!shouldDump) { 488 continue; 489 } 490 final long tToken = proto.start( 491 StateControllerProto.ContentObserverController.Observer.TRIGGERS); 492 493 JobInfo.TriggerContentUri trigger = observersOfUser.keyAt(observerIdx); 494 Uri u = trigger.getUri(); 495 if (u != null) { 496 proto.write(TriggerContentData.URI, u.toString()); 497 } 498 proto.write(TriggerContentData.FLAGS, trigger.getFlags()); 499 500 for (int j = 0; j < m; j++) { 501 final long jToken = proto.start(TriggerContentData.JOBS); 502 JobInstance inst = obs.mJobs.valueAt(j); 503 504 inst.mJobStatus.writeToShortProto(proto, TriggerContentData.JobInstance.INFO); 505 proto.write(TriggerContentData.JobInstance.SOURCE_UID, 506 inst.mJobStatus.getSourceUid()); 507 508 if (inst.mChangedAuthorities == null) { 509 proto.end(jToken); 510 continue; 511 } 512 if (inst.mTriggerPending) { 513 proto.write(TriggerContentData.JobInstance.TRIGGER_CONTENT_UPDATE_DELAY_MS, 514 inst.mJobStatus.getTriggerContentUpdateDelay()); 515 proto.write(TriggerContentData.JobInstance.TRIGGER_CONTENT_MAX_DELAY_MS, 516 inst.mJobStatus.getTriggerContentMaxDelay()); 517 } 518 for (int k = 0; k < inst.mChangedAuthorities.size(); k++) { 519 proto.write(TriggerContentData.JobInstance.CHANGED_AUTHORITIES, 520 inst.mChangedAuthorities.valueAt(k)); 521 } 522 if (inst.mChangedUris != null) { 523 for (int k = 0; k < inst.mChangedUris.size(); k++) { 524 u = inst.mChangedUris.valueAt(k); 525 if (u != null) { 526 proto.write(TriggerContentData.JobInstance.CHANGED_URIS, 527 u.toString()); 528 } 529 } 530 } 531 532 proto.end(jToken); 533 } 534 535 proto.end(tToken); 536 } 537 538 proto.end(oToken); 539 } 540 541 proto.end(mToken); 542 proto.end(token); 543 } 544 } 545