ContentObserverController.java revision b0001f6fb1383d9824c2733896b0b348e7f77240
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.ArrayMap; 25import android.util.ArraySet; 26 27import com.android.internal.annotations.VisibleForTesting; 28import com.android.server.job.JobSchedulerService; 29import com.android.server.job.StateChangedListener; 30 31import java.io.PrintWriter; 32import java.util.ArrayList; 33import java.util.Iterator; 34import java.util.List; 35 36/** 37 * Controller for monitoring changes to content URIs through a ContentObserver. 38 */ 39public class ContentObserverController extends StateController { 40 private static final String TAG = "JobScheduler.Content"; 41 42 /** 43 * Maximum number of changing URIs we will batch together to report. 44 * XXX Should be smarter about this, restricting it by the maximum number 45 * of characters we will retain. 46 */ 47 private static final int MAX_URIS_REPORTED = 50; 48 49 private static final Object sCreationLock = new Object(); 50 private static volatile ContentObserverController sController; 51 52 final private List<JobStatus> mTrackedTasks = new ArrayList<JobStatus>(); 53 ArrayMap<Uri, ObserverInstance> mObservers = new ArrayMap<>(); 54 final Handler mHandler = new Handler(); 55 56 public static ContentObserverController get(JobSchedulerService taskManagerService) { 57 synchronized (sCreationLock) { 58 if (sController == null) { 59 sController = new ContentObserverController(taskManagerService, 60 taskManagerService.getContext(), taskManagerService.getLock()); 61 } 62 } 63 return sController; 64 } 65 66 @VisibleForTesting 67 public static ContentObserverController getForTesting(StateChangedListener stateChangedListener, 68 Context context) { 69 return new ContentObserverController(stateChangedListener, context, new Object()); 70 } 71 72 private ContentObserverController(StateChangedListener stateChangedListener, Context context, 73 Object lock) { 74 super(stateChangedListener, context, lock); 75 } 76 77 @Override 78 public void maybeStartTrackingJobLocked(JobStatus taskStatus, JobStatus lastJob) { 79 if (taskStatus.hasContentTriggerConstraint()) { 80 if (taskStatus.contentObserverJobInstance == null) { 81 taskStatus.contentObserverJobInstance = new JobInstance(taskStatus); 82 } 83 mTrackedTasks.add(taskStatus); 84 boolean havePendingUris = false; 85 // If there is a previous job associated with the new job, propagate over 86 // any pending content URI trigger reports. 87 if (lastJob != null && lastJob.contentObserverJobInstance != null 88 && lastJob.contentObserverJobInstance 89 != taskStatus.contentObserverJobInstance 90 && lastJob.contentObserverJobInstance.mChangedAuthorities != null) { 91 havePendingUris = true; 92 taskStatus.contentObserverJobInstance.mChangedAuthorities 93 = lastJob.contentObserverJobInstance.mChangedAuthorities; 94 taskStatus.contentObserverJobInstance.mChangedUris 95 = lastJob.contentObserverJobInstance.mChangedUris; 96 lastJob.contentObserverJobInstance.mChangedAuthorities = null; 97 lastJob.contentObserverJobInstance.mChangedUris = null; 98 } 99 // If we have previously reported changed authorities/uris, then we failed 100 // to complete the job with them so will re-record them to report again. 101 if (taskStatus.changedAuthorities != null) { 102 havePendingUris = true; 103 if (taskStatus.contentObserverJobInstance.mChangedAuthorities == null) { 104 taskStatus.contentObserverJobInstance.mChangedAuthorities 105 = new ArraySet<>(); 106 } 107 for (String auth : taskStatus.changedAuthorities) { 108 taskStatus.contentObserverJobInstance.mChangedAuthorities.add(auth); 109 } 110 if (taskStatus.changedUris != null) { 111 if (taskStatus.contentObserverJobInstance.mChangedUris == null) { 112 taskStatus.contentObserverJobInstance.mChangedUris = new ArraySet<>(); 113 } 114 for (Uri uri : taskStatus.changedUris) { 115 taskStatus.contentObserverJobInstance.mChangedUris.add(uri); 116 } 117 } 118 taskStatus.changedAuthorities = null; 119 taskStatus.changedUris = null; 120 } 121 taskStatus.changedAuthorities = null; 122 taskStatus.changedUris = null; 123 taskStatus.setContentTriggerConstraintSatisfied(havePendingUris); 124 } 125 } 126 127 @Override 128 public void prepareForExecutionLocked(JobStatus taskStatus) { 129 if (taskStatus.hasContentTriggerConstraint()) { 130 if (taskStatus.contentObserverJobInstance != null) { 131 taskStatus.changedUris = taskStatus.contentObserverJobInstance.mChangedUris; 132 taskStatus.changedAuthorities 133 = taskStatus.contentObserverJobInstance.mChangedAuthorities; 134 taskStatus.contentObserverJobInstance.mChangedUris = null; 135 taskStatus.contentObserverJobInstance.mChangedAuthorities = null; 136 } 137 } 138 } 139 140 @Override 141 public void maybeStopTrackingJobLocked(JobStatus taskStatus, boolean forUpdate) { 142 if (taskStatus.hasContentTriggerConstraint()) { 143 if (!forUpdate) { 144 // We won't do this reset if being called for an update, because 145 // we know it will be immediately followed by maybeStartTrackingJobLocked... 146 // and we don't want to lose any content changes in-between. 147 if (taskStatus.contentObserverJobInstance != null) { 148 taskStatus.contentObserverJobInstance.detach(); 149 taskStatus.contentObserverJobInstance = null; 150 } 151 } 152 mTrackedTasks.remove(taskStatus); 153 } 154 } 155 156 @Override 157 public void rescheduleForFailure(JobStatus newJob, JobStatus failureToReschedule) { 158 if (failureToReschedule.hasContentTriggerConstraint() 159 && newJob.hasContentTriggerConstraint()) { 160 synchronized (mLock) { 161 // Our job has failed, and we are scheduling a new job for it. 162 // Copy the last reported content changes in to the new job, so when 163 // we schedule the new one we will pick them up and report them again. 164 newJob.changedAuthorities = failureToReschedule.changedAuthorities; 165 newJob.changedUris = failureToReschedule.changedUris; 166 } 167 } 168 } 169 170 class ObserverInstance extends ContentObserver { 171 final Uri mUri; 172 final ArrayList<JobInstance> mJobs = new ArrayList<>(); 173 174 public ObserverInstance(Handler handler, Uri uri) { 175 super(handler); 176 mUri = uri; 177 } 178 179 @Override 180 public void onChange(boolean selfChange, Uri uri) { 181 boolean reportChange = false; 182 synchronized (mLock) { 183 final int N = mJobs.size(); 184 for (int i=0; i<N; i++) { 185 JobInstance inst = mJobs.get(i); 186 if (inst.mChangedUris == null) { 187 inst.mChangedUris = new ArraySet<>(); 188 } 189 if (inst.mChangedUris.size() < MAX_URIS_REPORTED) { 190 inst.mChangedUris.add(uri); 191 } 192 if (inst.mChangedAuthorities == null) { 193 inst.mChangedAuthorities = new ArraySet<>(); 194 } 195 inst.mChangedAuthorities.add(uri.getAuthority()); 196 if (inst.mJobStatus.setContentTriggerConstraintSatisfied(true)) { 197 reportChange = true; 198 } 199 } 200 } 201 // Let the scheduler know that state has changed. This may or may not result in an 202 // execution. 203 if (reportChange) { 204 mStateChangedListener.onControllerStateChanged(); 205 } 206 } 207 } 208 209 class JobInstance extends ArrayList<ObserverInstance> { 210 private final JobStatus mJobStatus; 211 private ArraySet<Uri> mChangedUris; 212 private ArraySet<String> mChangedAuthorities; 213 214 JobInstance(JobStatus jobStatus) { 215 mJobStatus = jobStatus; 216 final JobInfo.TriggerContentUri[] uris = jobStatus.getJob().getTriggerContentUris(); 217 if (uris != null) { 218 for (JobInfo.TriggerContentUri uri : uris) { 219 ObserverInstance obs = mObservers.get(uri.getUri()); 220 if (obs == null) { 221 obs = new ObserverInstance(mHandler, uri.getUri()); 222 mObservers.put(uri.getUri(), obs); 223 mContext.getContentResolver().registerContentObserver( 224 uri.getUri(), 225 (uri.getFlags() & 226 JobInfo.TriggerContentUri.FLAG_NOTIFY_FOR_DESCENDANTS) 227 != 0, 228 obs); 229 } 230 obs.mJobs.add(this); 231 add(obs); 232 } 233 } 234 } 235 236 void detach() { 237 final int N = size(); 238 for (int i=0; i<N; i++) { 239 final ObserverInstance obs = get(i); 240 obs.mJobs.remove(this); 241 if (obs.mJobs.size() == 0) { 242 mContext.getContentResolver().unregisterContentObserver(obs); 243 mObservers.remove(obs.mUri); 244 } 245 } 246 } 247 } 248 249 @Override 250 public void dumpControllerStateLocked(PrintWriter pw) { 251 pw.println("Content."); 252 Iterator<JobStatus> it = mTrackedTasks.iterator(); 253 if (it.hasNext()) { 254 pw.print(String.valueOf(it.next().hashCode())); 255 } 256 while (it.hasNext()) { 257 pw.print("," + String.valueOf(it.next().hashCode())); 258 } 259 pw.println(); 260 int N = mObservers.size(); 261 if (N > 0) { 262 pw.println("URIs:"); 263 for (int i = 0; i < N; i++) { 264 ObserverInstance obs = mObservers.valueAt(i); 265 pw.print(" "); 266 pw.print(mObservers.keyAt(i)); 267 pw.println(":"); 268 pw.print(" "); 269 pw.println(obs); 270 pw.println(" Jobs:"); 271 int M = obs.mJobs.size(); 272 for (int j=0; j<M; j++) { 273 JobInstance inst = obs.mJobs.get(j); 274 pw.print(" "); 275 pw.print(inst.hashCode()); 276 if (inst.mChangedAuthorities != null) { 277 pw.println(":"); 278 pw.println(" Changed Authorities:"); 279 for (int k=0; k<inst.mChangedAuthorities.size(); k++) { 280 pw.print(" "); 281 pw.println(inst.mChangedAuthorities.valueAt(k)); 282 } 283 if (inst.mChangedUris != null) { 284 pw.println(" Changed URIs:"); 285 for (int k = 0; k<inst.mChangedUris.size(); k++) { 286 pw.print(" "); 287 pw.println(inst.mChangedUris.valueAt(k)); 288 } 289 } 290 } else { 291 pw.println(); 292 } 293 } 294 } 295 } 296 } 297} 298