blob: 212cc949364d1a6503a5c76223fb10759f134b72 [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.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";
/**
* 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;
private static final Object sCreationLock = new Object();
private static volatile ContentObserverController sController;
final private List<JobStatus> mTrackedTasks = new ArrayList<JobStatus>();
ArrayMap<Uri, ObserverInstance> mObservers = new ArrayMap<>();
final Handler mHandler = new Handler();
public static ContentObserverController get(JobSchedulerService taskManagerService) {
synchronized (sCreationLock) {
if (sController == null) {
sController = new ContentObserverController(taskManagerService,
taskManagerService.getContext());
}
}
return sController;
}
@VisibleForTesting
public static ContentObserverController getForTesting(StateChangedListener stateChangedListener,
Context context) {
return new ContentObserverController(stateChangedListener, context);
}
private ContentObserverController(StateChangedListener stateChangedListener, Context context) {
super(stateChangedListener, context);
}
@Override
public void maybeStartTrackingJob(JobStatus taskStatus, JobStatus lastJob) {
if (taskStatus.hasContentTriggerConstraint()) {
synchronized (mTrackedTasks) {
if (taskStatus.contentObserverJobInstance == null) {
taskStatus.contentObserverJobInstance = new JobInstance(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 (lastJob != null && lastJob.contentObserverJobInstance != null
&& lastJob.contentObserverJobInstance
!= taskStatus.contentObserverJobInstance
&& lastJob.contentObserverJobInstance.mChangedAuthorities != null) {
havePendingUris = true;
taskStatus.contentObserverJobInstance.mChangedAuthorities
= lastJob.contentObserverJobInstance.mChangedAuthorities;
taskStatus.contentObserverJobInstance.mChangedUris
= lastJob.contentObserverJobInstance.mChangedUris;
lastJob.contentObserverJobInstance.mChangedAuthorities = null;
lastJob.contentObserverJobInstance.mChangedUris = null;
}
// 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.contentTriggerConstraintSatisfied.set(havePendingUris);
}
}
}
@Override
public void prepareForExecution(JobStatus taskStatus) {
if (taskStatus.hasContentTriggerConstraint()) {
synchronized (mTrackedTasks) {
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 maybeStopTrackingJob(JobStatus taskStatus, boolean forUpdate) {
if (taskStatus.hasContentTriggerConstraint()) {
synchronized (mTrackedTasks) {
if (!forUpdate) {
// We won't do this reset if being called for an update, because
// we know it will be immediately followed by maybeStartTrackingJob...
// and we don't want to lose any content changes in-between.
if (taskStatus.contentObserverJobInstance != null) {
taskStatus.contentObserverJobInstance.detach();
taskStatus.contentObserverJobInstance = null;
}
}
mTrackedTasks.remove(taskStatus);
}
}
}
@Override
public void rescheduleForFailure(JobStatus newJob, JobStatus failureToReschedule) {
if (failureToReschedule.hasContentTriggerConstraint()
&& newJob.hasContentTriggerConstraint()) {
synchronized (mTrackedTasks) {
// 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;
}
}
}
class ObserverInstance extends ContentObserver {
final Uri mUri;
final ArrayList<JobInstance> mJobs = new ArrayList<>();
public ObserverInstance(Handler handler, Uri uri) {
super(handler);
mUri = uri;
}
@Override
public void onChange(boolean selfChange, Uri uri) {
boolean reportChange = false;
synchronized (mTrackedTasks) {
final int N = mJobs.size();
for (int i=0; i<N; i++) {
JobInstance inst = mJobs.get(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());
boolean previous
= inst.mJobStatus.contentTriggerConstraintSatisfied.getAndSet(true);
if (!previous) {
reportChange = true;
}
}
}
// Let the scheduler know that state has changed. This may or may not result in an
// execution.
if (reportChange) {
mStateChangedListener.onControllerStateChanged();
}
}
}
class JobInstance extends ArrayList<ObserverInstance> {
private final JobStatus mJobStatus;
private ArraySet<Uri> mChangedUris;
private ArraySet<String> mChangedAuthorities;
JobInstance(JobStatus jobStatus) {
mJobStatus = jobStatus;
final JobInfo.TriggerContentUri[] uris = jobStatus.getJob().getTriggerContentUris();
if (uris != null) {
for (JobInfo.TriggerContentUri uri : uris) {
ObserverInstance obs = mObservers.get(uri.getUri());
if (obs == null) {
obs = new ObserverInstance(mHandler, uri.getUri());
mObservers.put(uri.getUri(), obs);
mContext.getContentResolver().registerContentObserver(
uri.getUri(),
(uri.getFlags() &
JobInfo.TriggerContentUri.FLAG_NOTIFY_FOR_DESCENDANTS)
!= 0,
obs);
}
obs.mJobs.add(this);
add(obs);
}
}
}
void detach() {
final int N = size();
for (int i=0; i<N; i++) {
final ObserverInstance obs = get(i);
obs.mJobs.remove(this);
if (obs.mJobs.size() == 0) {
mContext.getContentResolver().unregisterContentObserver(obs);
mObservers.remove(obs.mUri);
}
}
}
}
@Override
public void dumpControllerState(PrintWriter pw) {
pw.println("Content.");
synchronized (mTrackedTasks) {
Iterator<JobStatus> it = mTrackedTasks.iterator();
if (it.hasNext()) {
pw.print(String.valueOf(it.next().hashCode()));
}
while (it.hasNext()) {
pw.print("," + String.valueOf(it.next().hashCode()));
}
pw.println();
int N = mObservers.size();
if (N > 0) {
pw.println("URIs:");
for (int i = 0; i < N; i++) {
ObserverInstance obs = mObservers.valueAt(i);
pw.print(" ");
pw.print(mObservers.keyAt(i));
pw.println(":");
pw.print(" ");
pw.println(obs);
pw.println(" Jobs:");
int M = obs.mJobs.size();
for (int j=0; j<M; j++) {
JobInstance inst = obs.mJobs.get(j);
pw.print(" ");
pw.print(inst.hashCode());
if (inst.mChangedAuthorities != null) {
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();
}
}
}
}
}
}
}