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