ContentObserverController.java revision ef3aa6ee53c5e4f1c50dd5a9b5821c54e449d4b3
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, int filterUid) {
326        pw.println("Content.");
327        boolean printed = false;
328        Iterator<JobStatus> it = mTrackedTasks.iterator();
329        while (it.hasNext()) {
330            JobStatus js = it.next();
331            if (!js.shouldDump(filterUid)) {
332                continue;
333            }
334            if (!printed) {
335                pw.print("  ");
336                printed = true;
337            } else {
338                pw.print(",");
339            }
340            pw.print(System.identityHashCode(it.next()));
341        }
342        if (printed) {
343            pw.println();
344        }
345        int N = mObservers.size();
346        if (N > 0) {
347            pw.println("  Observers:");
348            for (int i = 0; i < N; i++) {
349                ObserverInstance obs = mObservers.valueAt(i);
350                int M = obs.mJobs.size();
351                boolean shouldDump = false;
352                for (int j=0; j<M; j++) {
353                    JobInstance inst = obs.mJobs.valueAt(j);
354                    if (inst.mJobStatus.shouldDump(filterUid)) {
355                        shouldDump = true;
356                        break;
357                    }
358                }
359                if (!shouldDump) {
360                    continue;
361                }
362                pw.print("    ");
363                pw.print(mObservers.keyAt(i));
364                pw.print(" (");
365                pw.print(System.identityHashCode(obs));
366                pw.println("):");
367                pw.println("      Jobs:");
368                for (int j=0; j<M; j++) {
369                    JobInstance inst = obs.mJobs.valueAt(j);
370                    pw.print("        ");
371                    pw.print(System.identityHashCode(inst.mJobStatus));
372                    if (inst.mChangedAuthorities != null) {
373                        pw.println(":");
374                        if (inst.mTriggerPending) {
375                            pw.print("          Trigger pending: update=");
376                            TimeUtils.formatDuration(
377                                    inst.mJobStatus.getTriggerContentUpdateDelay(), pw);
378                            pw.print(", max=");
379                            TimeUtils.formatDuration(
380                                    inst.mJobStatus.getTriggerContentMaxDelay(), pw);
381                            pw.println();
382                        }
383                        pw.println("          Changed Authorities:");
384                        for (int k=0; k<inst.mChangedAuthorities.size(); k++) {
385                            pw.print("          ");
386                            pw.println(inst.mChangedAuthorities.valueAt(k));
387                        }
388                        if (inst.mChangedUris != null) {
389                            pw.println("          Changed URIs:");
390                            for (int k = 0; k<inst.mChangedUris.size(); k++) {
391                                pw.print("          ");
392                                pw.println(inst.mChangedUris.valueAt(k));
393                            }
394                        }
395                    } else {
396                        pw.println();
397                    }
398                }
399            }
400        }
401    }
402}
403