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