| /* |
| * 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.annotation.UserIdInt; |
| import android.app.job.JobInfo; |
| import android.database.ContentObserver; |
| import android.net.Uri; |
| import android.os.Handler; |
| import android.os.UserHandle; |
| import android.util.ArrayMap; |
| import android.util.ArraySet; |
| import android.util.Log; |
| import android.util.Slog; |
| import android.util.SparseArray; |
| import android.util.TimeUtils; |
| import android.util.proto.ProtoOutputStream; |
| |
| import com.android.internal.util.IndentingPrintWriter; |
| import com.android.server.job.JobSchedulerService; |
| import com.android.server.job.StateControllerProto; |
| import com.android.server.job.StateControllerProto.ContentObserverController.Observer.TriggerContentData; |
| |
| import java.util.ArrayList; |
| import java.util.function.Predicate; |
| |
| /** |
| * Controller for monitoring changes to content URIs through a ContentObserver. |
| */ |
| public final class ContentObserverController extends StateController { |
| private static final String TAG = "JobScheduler.ContentObserver"; |
| private static final boolean DEBUG = JobSchedulerService.DEBUG |
| || Log.isLoggable(TAG, Log.DEBUG); |
| |
| /** |
| * 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; |
| |
| final private ArraySet<JobStatus> mTrackedTasks = new ArraySet<>(); |
| /** |
| * Per-userid {@link JobInfo.TriggerContentUri} keyed ContentObserver cache. |
| */ |
| final SparseArray<ArrayMap<JobInfo.TriggerContentUri, ObserverInstance>> mObservers = |
| new SparseArray<>(); |
| final Handler mHandler; |
| |
| public ContentObserverController(JobSchedulerService service) { |
| super(service); |
| mHandler = new Handler(mContext.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); |
| taskStatus.setTrackingController(JobStatus.TRACKING_CONTENT); |
| 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.clearTrackingController(JobStatus.TRACKING_CONTENT)) { |
| mTrackedTasks.remove(taskStatus); |
| 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 happened 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); |
| } |
| } |
| } |
| |
| @Override |
| public void rescheduleForFailureLocked(JobStatus newJob, JobStatus failureToReschedule) { |
| if (failureToReschedule.hasContentTriggerConstraint() |
| && newJob.hasContentTriggerConstraint()) { |
| // 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 @UserIdInt int mUserId; |
| final ArraySet<JobInstance> mJobs = new ArraySet<>(); |
| |
| public ObserverInstance(Handler handler, JobInfo.TriggerContentUri uri, |
| @UserIdInt int userId) { |
| super(handler); |
| mUri = uri; |
| mUserId = userId; |
| } |
| |
| @Override |
| public void onChange(boolean selfChange, Uri uri) { |
| if (DEBUG) { |
| Slog.i(TAG, "onChange(self=" + selfChange + ") for " + uri |
| + " when mUri=" + mUri + " mUserId=" + mUserId); |
| } |
| 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; |
| |
| // This constructor must be called with the master job scheduler lock held. |
| JobInstance(JobStatus jobStatus) { |
| mJobStatus = jobStatus; |
| mExecuteRunner = new TriggerRunnable(this); |
| mTimeoutRunner = new TriggerRunnable(this); |
| final JobInfo.TriggerContentUri[] uris = jobStatus.getJob().getTriggerContentUris(); |
| final int sourceUserId = jobStatus.getSourceUserId(); |
| ArrayMap<JobInfo.TriggerContentUri, ObserverInstance> observersOfUser = |
| mObservers.get(sourceUserId); |
| if (observersOfUser == null) { |
| observersOfUser = new ArrayMap<>(); |
| mObservers.put(sourceUserId, observersOfUser); |
| } |
| if (uris != null) { |
| for (JobInfo.TriggerContentUri uri : uris) { |
| ObserverInstance obs = observersOfUser.get(uri); |
| if (obs == null) { |
| obs = new ObserverInstance(mHandler, uri, jobStatus.getSourceUserId()); |
| observersOfUser.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 |
| + " sourceUserId=" + sourceUserId); |
| } |
| mContext.getContentResolver().registerContentObserver( |
| uri.getUri(), |
| andDescendants, |
| obs, |
| sourceUserId |
| ); |
| } 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); |
| ArrayMap<JobInfo.TriggerContentUri, ObserverInstance> observerOfUser = |
| mObservers.get(obs.mUserId); |
| if (observerOfUser != null) { |
| observerOfUser.remove(obs.mUri); |
| } |
| } |
| } |
| } |
| } |
| |
| @Override |
| public void dumpControllerStateLocked(IndentingPrintWriter pw, |
| Predicate<JobStatus> predicate) { |
| for (int i = 0; i < mTrackedTasks.size(); i++) { |
| JobStatus js = mTrackedTasks.valueAt(i); |
| if (!predicate.test(js)) { |
| continue; |
| } |
| pw.print("#"); |
| js.printUniqueId(pw); |
| pw.print(" from "); |
| UserHandle.formatUid(pw, js.getSourceUid()); |
| pw.println(); |
| } |
| pw.println(); |
| |
| int N = mObservers.size(); |
| if (N > 0) { |
| pw.println("Observers:"); |
| pw.increaseIndent(); |
| for (int userIdx = 0; userIdx < N; userIdx++) { |
| final int userId = mObservers.keyAt(userIdx); |
| ArrayMap<JobInfo.TriggerContentUri, ObserverInstance> observersOfUser = |
| mObservers.get(userId); |
| int numbOfObserversPerUser = observersOfUser.size(); |
| for (int observerIdx = 0 ; observerIdx < numbOfObserversPerUser; observerIdx++) { |
| ObserverInstance obs = observersOfUser.valueAt(observerIdx); |
| int M = obs.mJobs.size(); |
| boolean shouldDump = false; |
| for (int j = 0; j < M; j++) { |
| JobInstance inst = obs.mJobs.valueAt(j); |
| if (predicate.test(inst.mJobStatus)) { |
| shouldDump = true; |
| break; |
| } |
| } |
| if (!shouldDump) { |
| continue; |
| } |
| JobInfo.TriggerContentUri trigger = observersOfUser.keyAt(observerIdx); |
| pw.print(trigger.getUri()); |
| pw.print(" 0x"); |
| pw.print(Integer.toHexString(trigger.getFlags())); |
| pw.print(" ("); |
| pw.print(System.identityHashCode(obs)); |
| pw.println("):"); |
| pw.increaseIndent(); |
| pw.println("Jobs:"); |
| pw.increaseIndent(); |
| 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(":"); |
| pw.increaseIndent(); |
| 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.println(inst.mChangedAuthorities.valueAt(k)); |
| } |
| if (inst.mChangedUris != null) { |
| pw.println(" Changed URIs:"); |
| for (int k = 0; k < inst.mChangedUris.size(); k++) { |
| pw.println(inst.mChangedUris.valueAt(k)); |
| } |
| } |
| pw.decreaseIndent(); |
| } else { |
| pw.println(); |
| } |
| } |
| pw.decreaseIndent(); |
| pw.decreaseIndent(); |
| } |
| } |
| pw.decreaseIndent(); |
| } |
| } |
| |
| @Override |
| public void dumpControllerStateLocked(ProtoOutputStream proto, long fieldId, |
| Predicate<JobStatus> predicate) { |
| final long token = proto.start(fieldId); |
| final long mToken = proto.start(StateControllerProto.CONTENT_OBSERVER); |
| |
| for (int i = 0; i < mTrackedTasks.size(); i++) { |
| JobStatus js = mTrackedTasks.valueAt(i); |
| if (!predicate.test(js)) { |
| continue; |
| } |
| final long jsToken = |
| proto.start(StateControllerProto.ContentObserverController.TRACKED_JOBS); |
| js.writeToShortProto(proto, |
| StateControllerProto.ContentObserverController.TrackedJob.INFO); |
| proto.write(StateControllerProto.ContentObserverController.TrackedJob.SOURCE_UID, |
| js.getSourceUid()); |
| proto.end(jsToken); |
| } |
| |
| final int n = mObservers.size(); |
| for (int userIdx = 0; userIdx < n; userIdx++) { |
| final long oToken = |
| proto.start(StateControllerProto.ContentObserverController.OBSERVERS); |
| final int userId = mObservers.keyAt(userIdx); |
| |
| proto.write(StateControllerProto.ContentObserverController.Observer.USER_ID, userId); |
| |
| ArrayMap<JobInfo.TriggerContentUri, ObserverInstance> observersOfUser = |
| mObservers.get(userId); |
| int numbOfObserversPerUser = observersOfUser.size(); |
| for (int observerIdx = 0 ; observerIdx < numbOfObserversPerUser; observerIdx++) { |
| ObserverInstance obs = observersOfUser.valueAt(observerIdx); |
| int m = obs.mJobs.size(); |
| boolean shouldDump = false; |
| for (int j = 0; j < m; j++) { |
| JobInstance inst = obs.mJobs.valueAt(j); |
| if (predicate.test(inst.mJobStatus)) { |
| shouldDump = true; |
| break; |
| } |
| } |
| if (!shouldDump) { |
| continue; |
| } |
| final long tToken = proto.start( |
| StateControllerProto.ContentObserverController.Observer.TRIGGERS); |
| |
| JobInfo.TriggerContentUri trigger = observersOfUser.keyAt(observerIdx); |
| Uri u = trigger.getUri(); |
| if (u != null) { |
| proto.write(TriggerContentData.URI, u.toString()); |
| } |
| proto.write(TriggerContentData.FLAGS, trigger.getFlags()); |
| |
| for (int j = 0; j < m; j++) { |
| final long jToken = proto.start(TriggerContentData.JOBS); |
| JobInstance inst = obs.mJobs.valueAt(j); |
| |
| inst.mJobStatus.writeToShortProto(proto, TriggerContentData.JobInstance.INFO); |
| proto.write(TriggerContentData.JobInstance.SOURCE_UID, |
| inst.mJobStatus.getSourceUid()); |
| |
| if (inst.mChangedAuthorities == null) { |
| proto.end(jToken); |
| continue; |
| } |
| if (inst.mTriggerPending) { |
| proto.write(TriggerContentData.JobInstance.TRIGGER_CONTENT_UPDATE_DELAY_MS, |
| inst.mJobStatus.getTriggerContentUpdateDelay()); |
| proto.write(TriggerContentData.JobInstance.TRIGGER_CONTENT_MAX_DELAY_MS, |
| inst.mJobStatus.getTriggerContentMaxDelay()); |
| } |
| for (int k = 0; k < inst.mChangedAuthorities.size(); k++) { |
| proto.write(TriggerContentData.JobInstance.CHANGED_AUTHORITIES, |
| inst.mChangedAuthorities.valueAt(k)); |
| } |
| if (inst.mChangedUris != null) { |
| for (int k = 0; k < inst.mChangedUris.size(); k++) { |
| u = inst.mChangedUris.valueAt(k); |
| if (u != null) { |
| proto.write(TriggerContentData.JobInstance.CHANGED_URIS, |
| u.toString()); |
| } |
| } |
| } |
| |
| proto.end(jToken); |
| } |
| |
| proto.end(tToken); |
| } |
| |
| proto.end(oToken); |
| } |
| |
| proto.end(mToken); |
| proto.end(token); |
| } |
| } |