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