ContentObserverController.java revision e9a988caca733d2f292991a52a0047685a69812f
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 17package com.android.server.job.controllers; 18 19import android.app.job.JobInfo; 20import android.content.Context; 21import android.database.ContentObserver; 22import android.net.Uri; 23import android.os.Handler; 24import android.os.UserHandle; 25import android.util.TimeUtils; 26import android.util.ArrayMap; 27import android.util.ArraySet; 28 29import com.android.internal.annotations.VisibleForTesting; 30import com.android.server.job.JobSchedulerService; 31import com.android.server.job.StateChangedListener; 32 33import java.io.PrintWriter; 34import java.util.ArrayList; 35import java.util.Iterator; 36import java.util.List; 37 38/** 39 * Controller for monitoring changes to content URIs through a ContentObserver. 40 */ 41public class ContentObserverController extends StateController { 42 private static final String TAG = "JobScheduler.Content"; 43 44 /** 45 * Maximum number of changing URIs we will batch together to report. 46 * XXX Should be smarter about this, restricting it by the maximum number 47 * of characters we will retain. 48 */ 49 private static final int MAX_URIS_REPORTED = 50; 50 51 /** 52 * At this point we consider it urgent to schedule the job ASAP. 53 */ 54 private static final int URIS_URGENT_THRESHOLD = 40; 55 56 private static final Object sCreationLock = new Object(); 57 private static volatile ContentObserverController sController; 58 59 final private List<JobStatus> mTrackedTasks = new ArrayList<JobStatus>(); 60 ArrayMap<Uri, ObserverInstance> mObservers = new ArrayMap<>(); 61 final Handler mHandler; 62 63 public static ContentObserverController get(JobSchedulerService taskManagerService) { 64 synchronized (sCreationLock) { 65 if (sController == null) { 66 sController = new ContentObserverController(taskManagerService, 67 taskManagerService.getContext(), taskManagerService.getLock()); 68 } 69 } 70 return sController; 71 } 72 73 @VisibleForTesting 74 public static ContentObserverController getForTesting(StateChangedListener stateChangedListener, 75 Context context) { 76 return new ContentObserverController(stateChangedListener, context, new Object()); 77 } 78 79 private ContentObserverController(StateChangedListener stateChangedListener, Context context, 80 Object lock) { 81 super(stateChangedListener, context, lock); 82 mHandler = new Handler(context.getMainLooper()); 83 } 84 85 @Override 86 public void maybeStartTrackingJobLocked(JobStatus taskStatus, JobStatus lastJob) { 87 if (taskStatus.hasContentTriggerConstraint()) { 88 if (taskStatus.contentObserverJobInstance == null) { 89 taskStatus.contentObserverJobInstance = new JobInstance(taskStatus); 90 } 91 mTrackedTasks.add(taskStatus); 92 boolean havePendingUris = false; 93 // If there is a previous job associated with the new job, propagate over 94 // any pending content URI trigger reports. 95 if (taskStatus.contentObserverJobInstance.mChangedAuthorities != null) { 96 havePendingUris = true; 97 } 98 // If we have previously reported changed authorities/uris, then we failed 99 // to complete the job with them so will re-record them to report again. 100 if (taskStatus.changedAuthorities != null) { 101 havePendingUris = true; 102 if (taskStatus.contentObserverJobInstance.mChangedAuthorities == null) { 103 taskStatus.contentObserverJobInstance.mChangedAuthorities 104 = new ArraySet<>(); 105 } 106 for (String auth : taskStatus.changedAuthorities) { 107 taskStatus.contentObserverJobInstance.mChangedAuthorities.add(auth); 108 } 109 if (taskStatus.changedUris != null) { 110 if (taskStatus.contentObserverJobInstance.mChangedUris == null) { 111 taskStatus.contentObserverJobInstance.mChangedUris = new ArraySet<>(); 112 } 113 for (Uri uri : taskStatus.changedUris) { 114 taskStatus.contentObserverJobInstance.mChangedUris.add(uri); 115 } 116 } 117 taskStatus.changedAuthorities = null; 118 taskStatus.changedUris = null; 119 } 120 taskStatus.changedAuthorities = null; 121 taskStatus.changedUris = null; 122 taskStatus.setContentTriggerConstraintSatisfied(havePendingUris); 123 } 124 if (lastJob != null && lastJob.contentObserverJobInstance != null) { 125 // And now we can detach the instance state from the last job. 126 lastJob.contentObserverJobInstance.detachLocked(); 127 lastJob.contentObserverJobInstance = null; 128 } 129 } 130 131 @Override 132 public void prepareForExecutionLocked(JobStatus taskStatus) { 133 if (taskStatus.hasContentTriggerConstraint()) { 134 if (taskStatus.contentObserverJobInstance != null) { 135 taskStatus.changedUris = taskStatus.contentObserverJobInstance.mChangedUris; 136 taskStatus.changedAuthorities 137 = taskStatus.contentObserverJobInstance.mChangedAuthorities; 138 taskStatus.contentObserverJobInstance.mChangedUris = null; 139 taskStatus.contentObserverJobInstance.mChangedAuthorities = null; 140 } 141 } 142 } 143 144 @Override 145 public void maybeStopTrackingJobLocked(JobStatus taskStatus, JobStatus incomingJob, 146 boolean forUpdate) { 147 if (taskStatus.hasContentTriggerConstraint()) { 148 if (taskStatus.contentObserverJobInstance != null) { 149 taskStatus.contentObserverJobInstance.unscheduleLocked(); 150 if (incomingJob != null) { 151 if (taskStatus.contentObserverJobInstance != null 152 && taskStatus.contentObserverJobInstance.mChangedAuthorities != null) { 153 // We are stopping this job, but it is going to be replaced by this given 154 // incoming job. We want to propagate our state over to it, so we don't 155 // lose any content changes that had happend since the last one started. 156 // If there is a previous job associated with the new job, propagate over 157 // any pending content URI trigger reports. 158 if (incomingJob.contentObserverJobInstance == null) { 159 incomingJob.contentObserverJobInstance = new JobInstance(incomingJob); 160 } 161 incomingJob.contentObserverJobInstance.mChangedAuthorities 162 = taskStatus.contentObserverJobInstance.mChangedAuthorities; 163 incomingJob.contentObserverJobInstance.mChangedUris 164 = taskStatus.contentObserverJobInstance.mChangedUris; 165 taskStatus.contentObserverJobInstance.mChangedAuthorities = null; 166 taskStatus.contentObserverJobInstance.mChangedUris = null; 167 } 168 // We won't detach the content observers here, because we want to 169 // allow them to continue monitoring so we don't miss anything... and 170 // since we are giving an incomingJob here, we know this will be 171 // immediately followed by a start tracking of that job. 172 } else { 173 // But here there is no incomingJob, so nothing coming up, so time to detach. 174 taskStatus.contentObserverJobInstance.detachLocked(); 175 taskStatus.contentObserverJobInstance = null; 176 } 177 } 178 mTrackedTasks.remove(taskStatus); 179 } 180 } 181 182 @Override 183 public void rescheduleForFailure(JobStatus newJob, JobStatus failureToReschedule) { 184 if (failureToReschedule.hasContentTriggerConstraint() 185 && newJob.hasContentTriggerConstraint()) { 186 synchronized (mLock) { 187 // Our job has failed, and we are scheduling a new job for it. 188 // Copy the last reported content changes in to the new job, so when 189 // we schedule the new one we will pick them up and report them again. 190 newJob.changedAuthorities = failureToReschedule.changedAuthorities; 191 newJob.changedUris = failureToReschedule.changedUris; 192 } 193 } 194 } 195 196 final class ObserverInstance extends ContentObserver { 197 final Uri mUri; 198 final ArraySet<JobInstance> mJobs = new ArraySet<>(); 199 200 public ObserverInstance(Handler handler, Uri uri) { 201 super(handler); 202 mUri = uri; 203 } 204 205 @Override 206 public void onChange(boolean selfChange, Uri uri) { 207 synchronized (mLock) { 208 final int N = mJobs.size(); 209 for (int i=0; i<N; i++) { 210 JobInstance inst = mJobs.valueAt(i); 211 if (inst.mChangedUris == null) { 212 inst.mChangedUris = new ArraySet<>(); 213 } 214 if (inst.mChangedUris.size() < MAX_URIS_REPORTED) { 215 inst.mChangedUris.add(uri); 216 } 217 if (inst.mChangedAuthorities == null) { 218 inst.mChangedAuthorities = new ArraySet<>(); 219 } 220 inst.mChangedAuthorities.add(uri.getAuthority()); 221 inst.scheduleLocked(); 222 } 223 } 224 } 225 } 226 227 static final class TriggerRunnable implements Runnable { 228 final JobInstance mInstance; 229 230 TriggerRunnable(JobInstance instance) { 231 mInstance = instance; 232 } 233 234 @Override public void run() { 235 mInstance.trigger(); 236 } 237 } 238 239 final class JobInstance { 240 final ArrayList<ObserverInstance> mMyObservers = new ArrayList<>(); 241 final JobStatus mJobStatus; 242 final Runnable mExecuteRunner; 243 final Runnable mTimeoutRunner; 244 ArraySet<Uri> mChangedUris; 245 ArraySet<String> mChangedAuthorities; 246 247 boolean mTriggerPending; 248 249 JobInstance(JobStatus jobStatus) { 250 mJobStatus = jobStatus; 251 mExecuteRunner = new TriggerRunnable(this); 252 mTimeoutRunner = new TriggerRunnable(this); 253 final JobInfo.TriggerContentUri[] uris = jobStatus.getJob().getTriggerContentUris(); 254 if (uris != null) { 255 for (JobInfo.TriggerContentUri uri : uris) { 256 ObserverInstance obs = mObservers.get(uri.getUri()); 257 if (obs == null) { 258 obs = new ObserverInstance(mHandler, uri.getUri()); 259 mObservers.put(uri.getUri(), obs); 260 mContext.getContentResolver().registerContentObserver( 261 uri.getUri(), 262 (uri.getFlags() & 263 JobInfo.TriggerContentUri.FLAG_NOTIFY_FOR_DESCENDANTS) 264 != 0, 265 obs); 266 } 267 obs.mJobs.add(this); 268 mMyObservers.add(obs); 269 } 270 } 271 } 272 273 void trigger() { 274 boolean reportChange = false; 275 synchronized (mLock) { 276 if (mTriggerPending) { 277 if (mJobStatus.setContentTriggerConstraintSatisfied(true)) { 278 reportChange = true; 279 } 280 unscheduleLocked(); 281 } 282 } 283 // Let the scheduler know that state has changed. This may or may not result in an 284 // execution. 285 if (reportChange) { 286 mStateChangedListener.onControllerStateChanged(); 287 } 288 } 289 290 void scheduleLocked() { 291 if (!mTriggerPending) { 292 mTriggerPending = true; 293 mHandler.postDelayed(mTimeoutRunner, mJobStatus.getTriggerContentMaxDelay()); 294 } 295 mHandler.removeCallbacks(mExecuteRunner); 296 if (mChangedUris.size() >= URIS_URGENT_THRESHOLD) { 297 // If we start getting near the limit, GO NOW! 298 mHandler.post(mExecuteRunner); 299 } else { 300 mHandler.postDelayed(mExecuteRunner, mJobStatus.getTriggerContentUpdateDelay()); 301 } 302 } 303 304 void unscheduleLocked() { 305 if (mTriggerPending) { 306 mHandler.removeCallbacks(mExecuteRunner); 307 mHandler.removeCallbacks(mTimeoutRunner); 308 mTriggerPending = false; 309 } 310 } 311 312 void detachLocked() { 313 final int N = mMyObservers.size(); 314 for (int i=0; i<N; i++) { 315 final ObserverInstance obs = mMyObservers.get(i); 316 obs.mJobs.remove(this); 317 if (obs.mJobs.size() == 0) { 318 mContext.getContentResolver().unregisterContentObserver(obs); 319 mObservers.remove(obs.mUri); 320 } 321 } 322 } 323 } 324 325 @Override 326 public void dumpControllerStateLocked(PrintWriter pw, int filterUid) { 327 pw.println("Content:"); 328 Iterator<JobStatus> it = mTrackedTasks.iterator(); 329 while (it.hasNext()) { 330 JobStatus js = it.next(); 331 if (!js.shouldDump(filterUid)) { 332 continue; 333 } 334 pw.print(" #"); 335 js.printUniqueId(pw); 336 pw.print(" from "); 337 UserHandle.formatUid(pw, js.getSourceUid()); 338 pw.println(); 339 } 340 int N = mObservers.size(); 341 if (N > 0) { 342 pw.println(" Observers:"); 343 for (int i = 0; i < N; i++) { 344 ObserverInstance obs = mObservers.valueAt(i); 345 int M = obs.mJobs.size(); 346 boolean shouldDump = false; 347 for (int j=0; j<M; j++) { 348 JobInstance inst = obs.mJobs.valueAt(j); 349 if (inst.mJobStatus.shouldDump(filterUid)) { 350 shouldDump = true; 351 break; 352 } 353 } 354 if (!shouldDump) { 355 continue; 356 } 357 pw.print(" "); 358 pw.print(mObservers.keyAt(i)); 359 pw.print(" ("); 360 pw.print(System.identityHashCode(obs)); 361 pw.println("):"); 362 pw.println(" Jobs:"); 363 for (int j=0; j<M; j++) { 364 JobInstance inst = obs.mJobs.valueAt(j); 365 pw.print(" #"); 366 inst.mJobStatus.printUniqueId(pw); 367 pw.print(" from "); 368 UserHandle.formatUid(pw, inst.mJobStatus.getSourceUid()); 369 if (inst.mChangedAuthorities != null) { 370 pw.println(":"); 371 if (inst.mTriggerPending) { 372 pw.print(" Trigger pending: update="); 373 TimeUtils.formatDuration( 374 inst.mJobStatus.getTriggerContentUpdateDelay(), pw); 375 pw.print(", max="); 376 TimeUtils.formatDuration( 377 inst.mJobStatus.getTriggerContentMaxDelay(), pw); 378 pw.println(); 379 } 380 pw.println(" Changed Authorities:"); 381 for (int k=0; k<inst.mChangedAuthorities.size(); k++) { 382 pw.print(" "); 383 pw.println(inst.mChangedAuthorities.valueAt(k)); 384 } 385 if (inst.mChangedUris != null) { 386 pw.println(" Changed URIs:"); 387 for (int k = 0; k<inst.mChangedUris.size(); k++) { 388 pw.print(" "); 389 pw.println(inst.mChangedUris.valueAt(k)); 390 } 391 } 392 } else { 393 pw.println(); 394 } 395 } 396 } 397 } 398 } 399} 400