ContentObserverController.java revision 33d31c5b70c7d056e799e34bb6eccbe6939714ea
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 maybeStartTrackingJob(JobStatus taskStatus, JobStatus lastJob) {
79        if (taskStatus.hasContentTriggerConstraint()) {
80            synchronized (mLock) {
81                if (taskStatus.contentObserverJobInstance == null) {
82                    taskStatus.contentObserverJobInstance = new JobInstance(taskStatus);
83                }
84                mTrackedTasks.add(taskStatus);
85                boolean havePendingUris = false;
86                // If there is a previous job associated with the new job, propagate over
87                // any pending content URI trigger reports.
88                if (lastJob != null && lastJob.contentObserverJobInstance != null
89                        && lastJob.contentObserverJobInstance
90                        != taskStatus.contentObserverJobInstance
91                        && lastJob.contentObserverJobInstance.mChangedAuthorities != null) {
92                    havePendingUris = true;
93                    taskStatus.contentObserverJobInstance.mChangedAuthorities
94                            = lastJob.contentObserverJobInstance.mChangedAuthorities;
95                    taskStatus.contentObserverJobInstance.mChangedUris
96                            = lastJob.contentObserverJobInstance.mChangedUris;
97                    lastJob.contentObserverJobInstance.mChangedAuthorities = null;
98                    lastJob.contentObserverJobInstance.mChangedUris = null;
99                }
100                // If we have previously reported changed authorities/uris, then we failed
101                // to complete the job with them so will re-record them to report again.
102                if (taskStatus.changedAuthorities != null) {
103                    havePendingUris = true;
104                    if (taskStatus.contentObserverJobInstance.mChangedAuthorities == null) {
105                        taskStatus.contentObserverJobInstance.mChangedAuthorities
106                                = new ArraySet<>();
107                    }
108                    for (String auth : taskStatus.changedAuthorities) {
109                        taskStatus.contentObserverJobInstance.mChangedAuthorities.add(auth);
110                    }
111                    if (taskStatus.changedUris != null) {
112                        if (taskStatus.contentObserverJobInstance.mChangedUris == null) {
113                            taskStatus.contentObserverJobInstance.mChangedUris = new ArraySet<>();
114                        }
115                        for (Uri uri : taskStatus.changedUris) {
116                            taskStatus.contentObserverJobInstance.mChangedUris.add(uri);
117                        }
118                    }
119                    taskStatus.changedAuthorities = null;
120                    taskStatus.changedUris = null;
121                }
122                taskStatus.changedAuthorities = null;
123                taskStatus.changedUris = null;
124                taskStatus.contentTriggerConstraintSatisfied.set(havePendingUris);
125            }
126        }
127    }
128
129    @Override
130    public void prepareForExecution(JobStatus taskStatus) {
131        if (taskStatus.hasContentTriggerConstraint()) {
132            synchronized (mLock) {
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
144    @Override
145    public void maybeStopTrackingJob(JobStatus taskStatus, boolean forUpdate) {
146        if (taskStatus.hasContentTriggerConstraint()) {
147            synchronized (mLock) {
148                if (!forUpdate) {
149                    // We won't do this reset if being called for an update, because
150                    // we know it will be immediately followed by maybeStartTrackingJob...
151                    // and we don't want to lose any content changes in-between.
152                    if (taskStatus.contentObserverJobInstance != null) {
153                        taskStatus.contentObserverJobInstance.detach();
154                        taskStatus.contentObserverJobInstance = null;
155                    }
156                }
157                mTrackedTasks.remove(taskStatus);
158            }
159        }
160    }
161
162    @Override
163    public void rescheduleForFailure(JobStatus newJob, JobStatus failureToReschedule) {
164        if (failureToReschedule.hasContentTriggerConstraint()
165                && newJob.hasContentTriggerConstraint()) {
166            synchronized (mLock) {
167                // Our job has failed, and we are scheduling a new job for it.
168                // Copy the last reported content changes in to the new job, so when
169                // we schedule the new one we will pick them up and report them again.
170                newJob.changedAuthorities = failureToReschedule.changedAuthorities;
171                newJob.changedUris = failureToReschedule.changedUris;
172            }
173        }
174    }
175
176    class ObserverInstance extends ContentObserver {
177        final Uri mUri;
178        final ArrayList<JobInstance> mJobs = new ArrayList<>();
179
180        public ObserverInstance(Handler handler, Uri uri) {
181            super(handler);
182            mUri = uri;
183        }
184
185        @Override
186        public void onChange(boolean selfChange, Uri uri) {
187            boolean reportChange = false;
188            synchronized (mLock) {
189                final int N = mJobs.size();
190                for (int i=0; i<N; i++) {
191                    JobInstance inst = mJobs.get(i);
192                    if (inst.mChangedUris == null) {
193                        inst.mChangedUris = new ArraySet<>();
194                    }
195                    if (inst.mChangedUris.size() < MAX_URIS_REPORTED) {
196                        inst.mChangedUris.add(uri);
197                    }
198                    if (inst.mChangedAuthorities == null) {
199                        inst.mChangedAuthorities = new ArraySet<>();
200                    }
201                    inst.mChangedAuthorities.add(uri.getAuthority());
202                    boolean previous
203                            = inst.mJobStatus.contentTriggerConstraintSatisfied.getAndSet(true);
204                    if (!previous) {
205                        reportChange = true;
206                    }
207                }
208            }
209            // Let the scheduler know that state has changed. This may or may not result in an
210            // execution.
211            if (reportChange) {
212                mStateChangedListener.onControllerStateChanged();
213            }
214        }
215    }
216
217    class JobInstance extends ArrayList<ObserverInstance> {
218        private final JobStatus mJobStatus;
219        private ArraySet<Uri> mChangedUris;
220        private ArraySet<String> mChangedAuthorities;
221
222        JobInstance(JobStatus jobStatus) {
223            mJobStatus = jobStatus;
224            final JobInfo.TriggerContentUri[] uris = jobStatus.getJob().getTriggerContentUris();
225            if (uris != null) {
226                for (JobInfo.TriggerContentUri uri : uris) {
227                    ObserverInstance obs = mObservers.get(uri.getUri());
228                    if (obs == null) {
229                        obs = new ObserverInstance(mHandler, uri.getUri());
230                        mObservers.put(uri.getUri(), obs);
231                        mContext.getContentResolver().registerContentObserver(
232                                uri.getUri(),
233                                (uri.getFlags() &
234                                        JobInfo.TriggerContentUri.FLAG_NOTIFY_FOR_DESCENDANTS)
235                                    != 0,
236                                obs);
237                    }
238                    obs.mJobs.add(this);
239                    add(obs);
240                }
241            }
242        }
243
244        void detach() {
245            final int N = size();
246            for (int i=0; i<N; i++) {
247                final ObserverInstance obs = get(i);
248                obs.mJobs.remove(this);
249                if (obs.mJobs.size() == 0) {
250                    mContext.getContentResolver().unregisterContentObserver(obs);
251                    mObservers.remove(obs.mUri);
252                }
253            }
254        }
255    }
256
257    @Override
258    public void dumpControllerState(PrintWriter pw) {
259        pw.println("Content.");
260        synchronized (mLock) {
261            Iterator<JobStatus> it = mTrackedTasks.iterator();
262            if (it.hasNext()) {
263                pw.print(String.valueOf(it.next().hashCode()));
264            }
265            while (it.hasNext()) {
266                pw.print("," + String.valueOf(it.next().hashCode()));
267            }
268            pw.println();
269            int N = mObservers.size();
270            if (N > 0) {
271                pw.println("URIs:");
272                for (int i = 0; i < N; i++) {
273                    ObserverInstance obs = mObservers.valueAt(i);
274                    pw.print("  ");
275                    pw.print(mObservers.keyAt(i));
276                    pw.println(":");
277                    pw.print("    ");
278                    pw.println(obs);
279                    pw.println("    Jobs:");
280                    int M = obs.mJobs.size();
281                    for (int j=0; j<M; j++) {
282                        JobInstance inst = obs.mJobs.get(j);
283                        pw.print("      ");
284                        pw.print(inst.hashCode());
285                        if (inst.mChangedAuthorities != null) {
286                            pw.println(":");
287                            pw.println("        Changed Authorities:");
288                            for (int k=0; k<inst.mChangedAuthorities.size(); k++) {
289                                pw.print("          ");
290                                pw.println(inst.mChangedAuthorities.valueAt(k));
291                            }
292                            if (inst.mChangedUris != null) {
293                                pw.println("        Changed URIs:");
294                                for (int k = 0; k<inst.mChangedUris.size(); k++) {
295                                    pw.print("          ");
296                                    pw.println(inst.mChangedUris.valueAt(k));
297                                }
298                            }
299                        } else {
300                            pw.println();
301                        }
302                    }
303                }
304            }
305        }
306    }
307}
308