blob: a42d0cd4c831c7aa68020f13828a1eea9aaca331 [file] [log] [blame]
/*
* Copyright (C) 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License
*/
package com.android.server.job.controllers;
import android.app.job.JobInfo;
import android.content.Context;
import android.database.ContentObserver;
import android.net.Uri;
import android.os.Handler;
import android.os.UserHandle;
import android.util.Slog;
import android.util.TimeUtils;
import android.util.ArrayMap;
import android.util.ArraySet;
import com.android.internal.annotations.VisibleForTesting;
import com.android.server.job.JobSchedulerService;
import com.android.server.job.StateChangedListener;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
/**
* Controller for monitoring changes to content URIs through a ContentObserver.
*/
public class ContentObserverController extends StateController {
private static final String TAG = "JobScheduler.Content";
private static final boolean DEBUG = false;
/**
* Maximum number of changing URIs we will batch together to report.
* XXX Should be smarter about this, restricting it by the maximum number
* of characters we will retain.
*/
private static final int MAX_URIS_REPORTED = 50;
/**
* At this point we consider it urgent to schedule the job ASAP.
*/
private static final int URIS_URGENT_THRESHOLD = 40;
private static final Object sCreationLock = new Object();
private static volatile ContentObserverController sController;
final private List<JobStatus> mTrackedTasks = new ArrayList<JobStatus>();
ArrayMap<JobInfo.TriggerContentUri, ObserverInstance> mObservers = new ArrayMap<>();
final Handler mHandler;
public static ContentObserverController get(JobSchedulerService taskManagerService) {
synchronized (sCreationLock) {
if (sController == null) {
sController = new ContentObserverController(taskManagerService,
taskManagerService.getContext(), taskManagerService.getLock());
}
}
return sController;
}
@VisibleForTesting
public static ContentObserverController getForTesting(StateChangedListener stateChangedListener,
Context context) {
return new ContentObserverController(stateChangedListener, context, new Object());
}
private ContentObserverController(StateChangedListener stateChangedListener, Context context,
Object lock) {
super(stateChangedListener, context, lock);
mHandler = new Handler(context.getMainLooper());
}
@Override
public void maybeStartTrackingJobLocked(JobStatus taskStatus, JobStatus lastJob) {
if (taskStatus.hasContentTriggerConstraint()) {
if (taskStatus.contentObserverJobInstance == null) {
taskStatus.contentObserverJobInstance = new JobInstance(taskStatus);
}
if (DEBUG) {
Slog.i(TAG, "Tracking content-trigger job " + taskStatus);
}
mTrackedTasks.add(taskStatus);
boolean havePendingUris = false;
// If there is a previous job associated with the new job, propagate over
// any pending content URI trigger reports.
if (taskStatus.contentObserverJobInstance.mChangedAuthorities != null) {
havePendingUris = true;
}
// If we have previously reported changed authorities/uris, then we failed
// to complete the job with them so will re-record them to report again.
if (taskStatus.changedAuthorities != null) {
havePendingUris = true;
if (taskStatus.contentObserverJobInstance.mChangedAuthorities == null) {
taskStatus.contentObserverJobInstance.mChangedAuthorities
= new ArraySet<>();
}
for (String auth : taskStatus.changedAuthorities) {
taskStatus.contentObserverJobInstance.mChangedAuthorities.add(auth);
}
if (taskStatus.changedUris != null) {
if (taskStatus.contentObserverJobInstance.mChangedUris == null) {
taskStatus.contentObserverJobInstance.mChangedUris = new ArraySet<>();
}
for (Uri uri : taskStatus.changedUris) {
taskStatus.contentObserverJobInstance.mChangedUris.add(uri);
}
}
taskStatus.changedAuthorities = null;
taskStatus.changedUris = null;
}
taskStatus.changedAuthorities = null;
taskStatus.changedUris = null;
taskStatus.setContentTriggerConstraintSatisfied(havePendingUris);
}
if (lastJob != null && lastJob.contentObserverJobInstance != null) {
// And now we can detach the instance state from the last job.
lastJob.contentObserverJobInstance.detachLocked();
lastJob.contentObserverJobInstance = null;
}
}
@Override
public void prepareForExecutionLocked(JobStatus taskStatus) {
if (taskStatus.hasContentTriggerConstraint()) {
if (taskStatus.contentObserverJobInstance != null) {
taskStatus.changedUris = taskStatus.contentObserverJobInstance.mChangedUris;
taskStatus.changedAuthorities
= taskStatus.contentObserverJobInstance.mChangedAuthorities;
taskStatus.contentObserverJobInstance.mChangedUris = null;
taskStatus.contentObserverJobInstance.mChangedAuthorities = null;
}
}
}
@Override
public void maybeStopTrackingJobLocked(JobStatus taskStatus, JobStatus incomingJob,
boolean forUpdate) {
if (taskStatus.hasContentTriggerConstraint()) {
if (taskStatus.contentObserverJobInstance != null) {
taskStatus.contentObserverJobInstance.unscheduleLocked();
if (incomingJob != null) {
if (taskStatus.contentObserverJobInstance != null
&& taskStatus.contentObserverJobInstance.mChangedAuthorities != null) {
// We are stopping this job, but it is going to be replaced by this given
// incoming job. We want to propagate our state over to it, so we don't
// lose any content changes that had happend since the last one started.
// If there is a previous job associated with the new job, propagate over
// any pending content URI trigger reports.
if (incomingJob.contentObserverJobInstance == null) {
incomingJob.contentObserverJobInstance = new JobInstance(incomingJob);
}
incomingJob.contentObserverJobInstance.mChangedAuthorities
= taskStatus.contentObserverJobInstance.mChangedAuthorities;
incomingJob.contentObserverJobInstance.mChangedUris
= taskStatus.contentObserverJobInstance.mChangedUris;
taskStatus.contentObserverJobInstance.mChangedAuthorities = null;
taskStatus.contentObserverJobInstance.mChangedUris = null;
}
// We won't detach the content observers here, because we want to
// allow them to continue monitoring so we don't miss anything... and
// since we are giving an incomingJob here, we know this will be
// immediately followed by a start tracking of that job.
} else {
// But here there is no incomingJob, so nothing coming up, so time to detach.
taskStatus.contentObserverJobInstance.detachLocked();
taskStatus.contentObserverJobInstance = null;
}
}
if (DEBUG) {
Slog.i(TAG, "No longer tracking job " + taskStatus);
}
mTrackedTasks.remove(taskStatus);
}
}
@Override
public void rescheduleForFailure(JobStatus newJob, JobStatus failureToReschedule) {
if (failureToReschedule.hasContentTriggerConstraint()
&& newJob.hasContentTriggerConstraint()) {
synchronized (mLock) {
// Our job has failed, and we are scheduling a new job for it.
// Copy the last reported content changes in to the new job, so when
// we schedule the new one we will pick them up and report them again.
newJob.changedAuthorities = failureToReschedule.changedAuthorities;
newJob.changedUris = failureToReschedule.changedUris;
}
}
}
final class ObserverInstance extends ContentObserver {
final JobInfo.TriggerContentUri mUri;
final ArraySet<JobInstance> mJobs = new ArraySet<>();
public ObserverInstance(Handler handler, JobInfo.TriggerContentUri uri) {
super(handler);
mUri = uri;
}
@Override
public void onChange(boolean selfChange, Uri uri) {
if (DEBUG) {
Slog.i(TAG, "onChange(self=" + selfChange + ") for " + uri
+ " when mUri=" + mUri);
}
synchronized (mLock) {
final int N = mJobs.size();
for (int i=0; i<N; i++) {
JobInstance inst = mJobs.valueAt(i);
if (inst.mChangedUris == null) {
inst.mChangedUris = new ArraySet<>();
}
if (inst.mChangedUris.size() < MAX_URIS_REPORTED) {
inst.mChangedUris.add(uri);
}
if (inst.mChangedAuthorities == null) {
inst.mChangedAuthorities = new ArraySet<>();
}
inst.mChangedAuthorities.add(uri.getAuthority());
inst.scheduleLocked();
}
}
}
}
static final class TriggerRunnable implements Runnable {
final JobInstance mInstance;
TriggerRunnable(JobInstance instance) {
mInstance = instance;
}
@Override public void run() {
mInstance.trigger();
}
}
final class JobInstance {
final ArrayList<ObserverInstance> mMyObservers = new ArrayList<>();
final JobStatus mJobStatus;
final Runnable mExecuteRunner;
final Runnable mTimeoutRunner;
ArraySet<Uri> mChangedUris;
ArraySet<String> mChangedAuthorities;
boolean mTriggerPending;
JobInstance(JobStatus jobStatus) {
mJobStatus = jobStatus;
mExecuteRunner = new TriggerRunnable(this);
mTimeoutRunner = new TriggerRunnable(this);
final JobInfo.TriggerContentUri[] uris = jobStatus.getJob().getTriggerContentUris();
if (uris != null) {
for (JobInfo.TriggerContentUri uri : uris) {
ObserverInstance obs = mObservers.get(uri);
if (obs == null) {
obs = new ObserverInstance(mHandler, uri);
mObservers.put(uri, obs);
final boolean andDescendants = (uri.getFlags() &
JobInfo.TriggerContentUri.FLAG_NOTIFY_FOR_DESCENDANTS) != 0;
if (DEBUG) {
Slog.v(TAG, "New observer " + obs + " for " + uri.getUri()
+ " andDescendants=" + andDescendants);
}
mContext.getContentResolver().registerContentObserver(
uri.getUri(),
andDescendants,
obs);
} else {
if (DEBUG) {
final boolean andDescendants = (uri.getFlags() &
JobInfo.TriggerContentUri.FLAG_NOTIFY_FOR_DESCENDANTS) != 0;
Slog.v(TAG, "Reusing existing observer " + obs + " for " + uri.getUri()
+ " andDescendants=" + andDescendants);
}
}
obs.mJobs.add(this);
mMyObservers.add(obs);
}
}
}
void trigger() {
boolean reportChange = false;
synchronized (mLock) {
if (mTriggerPending) {
if (mJobStatus.setContentTriggerConstraintSatisfied(true)) {
reportChange = true;
}
unscheduleLocked();
}
}
// Let the scheduler know that state has changed. This may or may not result in an
// execution.
if (reportChange) {
mStateChangedListener.onControllerStateChanged();
}
}
void scheduleLocked() {
if (!mTriggerPending) {
mTriggerPending = true;
mHandler.postDelayed(mTimeoutRunner, mJobStatus.getTriggerContentMaxDelay());
}
mHandler.removeCallbacks(mExecuteRunner);
if (mChangedUris.size() >= URIS_URGENT_THRESHOLD) {
// If we start getting near the limit, GO NOW!
mHandler.post(mExecuteRunner);
} else {
mHandler.postDelayed(mExecuteRunner, mJobStatus.getTriggerContentUpdateDelay());
}
}
void unscheduleLocked() {
if (mTriggerPending) {
mHandler.removeCallbacks(mExecuteRunner);
mHandler.removeCallbacks(mTimeoutRunner);
mTriggerPending = false;
}
}
void detachLocked() {
final int N = mMyObservers.size();
for (int i=0; i<N; i++) {
final ObserverInstance obs = mMyObservers.get(i);
obs.mJobs.remove(this);
if (obs.mJobs.size() == 0) {
if (DEBUG) {
Slog.i(TAG, "Unregistering observer " + obs + " for " + obs.mUri.getUri());
}
mContext.getContentResolver().unregisterContentObserver(obs);
mObservers.remove(obs.mUri);
}
}
}
}
@Override
public void dumpControllerStateLocked(PrintWriter pw, int filterUid) {
pw.println("Content:");
Iterator<JobStatus> it = mTrackedTasks.iterator();
while (it.hasNext()) {
JobStatus js = it.next();
if (!js.shouldDump(filterUid)) {
continue;
}
pw.print(" #");
js.printUniqueId(pw);
pw.print(" from ");
UserHandle.formatUid(pw, js.getSourceUid());
pw.println();
}
int N = mObservers.size();
if (N > 0) {
pw.println(" Observers:");
for (int i = 0; i < N; i++) {
ObserverInstance obs = mObservers.valueAt(i);
int M = obs.mJobs.size();
boolean shouldDump = false;
for (int j=0; j<M; j++) {
JobInstance inst = obs.mJobs.valueAt(j);
if (inst.mJobStatus.shouldDump(filterUid)) {
shouldDump = true;
break;
}
}
if (!shouldDump) {
continue;
}
pw.print(" ");
JobInfo.TriggerContentUri trigger = mObservers.keyAt(i);
pw.print(trigger.getUri());
pw.print(" 0x");
pw.print(Integer.toHexString(trigger.getFlags()));
pw.print(" (");
pw.print(System.identityHashCode(obs));
pw.println("):");
pw.println(" Jobs:");
for (int j=0; j<M; j++) {
JobInstance inst = obs.mJobs.valueAt(j);
pw.print(" #");
inst.mJobStatus.printUniqueId(pw);
pw.print(" from ");
UserHandle.formatUid(pw, inst.mJobStatus.getSourceUid());
if (inst.mChangedAuthorities != null) {
pw.println(":");
if (inst.mTriggerPending) {
pw.print(" Trigger pending: update=");
TimeUtils.formatDuration(
inst.mJobStatus.getTriggerContentUpdateDelay(), pw);
pw.print(", max=");
TimeUtils.formatDuration(
inst.mJobStatus.getTriggerContentMaxDelay(), pw);
pw.println();
}
pw.println(" Changed Authorities:");
for (int k=0; k<inst.mChangedAuthorities.size(); k++) {
pw.print(" ");
pw.println(inst.mChangedAuthorities.valueAt(k));
}
if (inst.mChangedUris != null) {
pw.println(" Changed URIs:");
for (int k = 0; k<inst.mChangedUris.size(); k++) {
pw.print(" ");
pw.println(inst.mChangedUris.valueAt(k));
}
}
} else {
pw.println();
}
}
}
}
}
}