/* * 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 mTrackedTasks = new ArrayList(); ArrayMap 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 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(); } 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 mMyObservers = new ArrayList<>(); final JobStatus mJobStatus; final Runnable mExecuteRunner; final Runnable mTimeoutRunner; ArraySet mChangedUris; ArraySet 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 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