Home | History | Annotate | Download | only in controllers
      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 
     17 package com.android.server.job.controllers;
     18 
     19 import android.annotation.UserIdInt;
     20 import android.app.job.JobInfo;
     21 import android.database.ContentObserver;
     22 import android.net.Uri;
     23 import android.os.Handler;
     24 import android.os.UserHandle;
     25 import android.util.ArrayMap;
     26 import android.util.ArraySet;
     27 import android.util.Log;
     28 import android.util.Slog;
     29 import android.util.SparseArray;
     30 import android.util.TimeUtils;
     31 import android.util.proto.ProtoOutputStream;
     32 
     33 import com.android.internal.util.IndentingPrintWriter;
     34 import com.android.server.job.JobSchedulerService;
     35 import com.android.server.job.StateControllerProto;
     36 import com.android.server.job.StateControllerProto.ContentObserverController.Observer.TriggerContentData;
     37 
     38 import java.util.ArrayList;
     39 import java.util.function.Predicate;
     40 
     41 /**
     42  * Controller for monitoring changes to content URIs through a ContentObserver.
     43  */
     44 public final class ContentObserverController extends StateController {
     45     private static final String TAG = "JobScheduler.ContentObserver";
     46     private static final boolean DEBUG = JobSchedulerService.DEBUG
     47             || Log.isLoggable(TAG, Log.DEBUG);
     48 
     49     /**
     50      * Maximum number of changing URIs we will batch together to report.
     51      * XXX Should be smarter about this, restricting it by the maximum number
     52      * of characters we will retain.
     53      */
     54     private static final int MAX_URIS_REPORTED = 50;
     55 
     56     /**
     57      * At this point we consider it urgent to schedule the job ASAP.
     58      */
     59     private static final int URIS_URGENT_THRESHOLD = 40;
     60 
     61     final private ArraySet<JobStatus> mTrackedTasks = new ArraySet<>();
     62     /**
     63      * Per-userid {@link JobInfo.TriggerContentUri} keyed ContentObserver cache.
     64      */
     65     final SparseArray<ArrayMap<JobInfo.TriggerContentUri, ObserverInstance>> mObservers =
     66             new SparseArray<>();
     67     final Handler mHandler;
     68 
     69     public ContentObserverController(JobSchedulerService service) {
     70         super(service);
     71         mHandler = new Handler(mContext.getMainLooper());
     72     }
     73 
     74     @Override
     75     public void maybeStartTrackingJobLocked(JobStatus taskStatus, JobStatus lastJob) {
     76         if (taskStatus.hasContentTriggerConstraint()) {
     77             if (taskStatus.contentObserverJobInstance == null) {
     78                 taskStatus.contentObserverJobInstance = new JobInstance(taskStatus);
     79             }
     80             if (DEBUG) {
     81                 Slog.i(TAG, "Tracking content-trigger job " + taskStatus);
     82             }
     83             mTrackedTasks.add(taskStatus);
     84             taskStatus.setTrackingController(JobStatus.TRACKING_CONTENT);
     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 (taskStatus.contentObserverJobInstance.mChangedAuthorities != null) {
     89                 havePendingUris = true;
     90             }
     91             // If we have previously reported changed authorities/uris, then we failed
     92             // to complete the job with them so will re-record them to report again.
     93             if (taskStatus.changedAuthorities != null) {
     94                 havePendingUris = true;
     95                 if (taskStatus.contentObserverJobInstance.mChangedAuthorities == null) {
     96                     taskStatus.contentObserverJobInstance.mChangedAuthorities
     97                             = new ArraySet<>();
     98                 }
     99                 for (String auth : taskStatus.changedAuthorities) {
    100                     taskStatus.contentObserverJobInstance.mChangedAuthorities.add(auth);
    101                 }
    102                 if (taskStatus.changedUris != null) {
    103                     if (taskStatus.contentObserverJobInstance.mChangedUris == null) {
    104                         taskStatus.contentObserverJobInstance.mChangedUris = new ArraySet<>();
    105                     }
    106                     for (Uri uri : taskStatus.changedUris) {
    107                         taskStatus.contentObserverJobInstance.mChangedUris.add(uri);
    108                     }
    109                 }
    110                 taskStatus.changedAuthorities = null;
    111                 taskStatus.changedUris = null;
    112             }
    113             taskStatus.changedAuthorities = null;
    114             taskStatus.changedUris = null;
    115             taskStatus.setContentTriggerConstraintSatisfied(havePendingUris);
    116         }
    117         if (lastJob != null && lastJob.contentObserverJobInstance != null) {
    118             // And now we can detach the instance state from the last job.
    119             lastJob.contentObserverJobInstance.detachLocked();
    120             lastJob.contentObserverJobInstance = null;
    121         }
    122     }
    123 
    124     @Override
    125     public void prepareForExecutionLocked(JobStatus taskStatus) {
    126         if (taskStatus.hasContentTriggerConstraint()) {
    127             if (taskStatus.contentObserverJobInstance != null) {
    128                 taskStatus.changedUris = taskStatus.contentObserverJobInstance.mChangedUris;
    129                 taskStatus.changedAuthorities
    130                         = taskStatus.contentObserverJobInstance.mChangedAuthorities;
    131                 taskStatus.contentObserverJobInstance.mChangedUris = null;
    132                 taskStatus.contentObserverJobInstance.mChangedAuthorities = null;
    133             }
    134         }
    135     }
    136 
    137     @Override
    138     public void maybeStopTrackingJobLocked(JobStatus taskStatus, JobStatus incomingJob,
    139             boolean forUpdate) {
    140         if (taskStatus.clearTrackingController(JobStatus.TRACKING_CONTENT)) {
    141             mTrackedTasks.remove(taskStatus);
    142             if (taskStatus.contentObserverJobInstance != null) {
    143                 taskStatus.contentObserverJobInstance.unscheduleLocked();
    144                 if (incomingJob != null) {
    145                     if (taskStatus.contentObserverJobInstance != null
    146                             && taskStatus.contentObserverJobInstance.mChangedAuthorities != null) {
    147                         // We are stopping this job, but it is going to be replaced by this given
    148                         // incoming job.  We want to propagate our state over to it, so we don't
    149                         // lose any content changes that had happened since the last one started.
    150                         // If there is a previous job associated with the new job, propagate over
    151                         // any pending content URI trigger reports.
    152                         if (incomingJob.contentObserverJobInstance == null) {
    153                             incomingJob.contentObserverJobInstance = new JobInstance(incomingJob);
    154                         }
    155                         incomingJob.contentObserverJobInstance.mChangedAuthorities
    156                                 = taskStatus.contentObserverJobInstance.mChangedAuthorities;
    157                         incomingJob.contentObserverJobInstance.mChangedUris
    158                                 = taskStatus.contentObserverJobInstance.mChangedUris;
    159                         taskStatus.contentObserverJobInstance.mChangedAuthorities = null;
    160                         taskStatus.contentObserverJobInstance.mChangedUris = null;
    161                     }
    162                     // We won't detach the content observers here, because we want to
    163                     // allow them to continue monitoring so we don't miss anything...  and
    164                     // since we are giving an incomingJob here, we know this will be
    165                     // immediately followed by a start tracking of that job.
    166                 } else {
    167                     // But here there is no incomingJob, so nothing coming up, so time to detach.
    168                     taskStatus.contentObserverJobInstance.detachLocked();
    169                     taskStatus.contentObserverJobInstance = null;
    170                 }
    171             }
    172             if (DEBUG) {
    173                 Slog.i(TAG, "No longer tracking job " + taskStatus);
    174             }
    175         }
    176     }
    177 
    178     @Override
    179     public void rescheduleForFailureLocked(JobStatus newJob, JobStatus failureToReschedule) {
    180         if (failureToReschedule.hasContentTriggerConstraint()
    181                 && newJob.hasContentTriggerConstraint()) {
    182             // Our job has failed, and we are scheduling a new job for it.
    183             // Copy the last reported content changes in to the new job, so when
    184             // we schedule the new one we will pick them up and report them again.
    185             newJob.changedAuthorities = failureToReschedule.changedAuthorities;
    186             newJob.changedUris = failureToReschedule.changedUris;
    187         }
    188     }
    189 
    190     final class ObserverInstance extends ContentObserver {
    191         final JobInfo.TriggerContentUri mUri;
    192         final @UserIdInt int mUserId;
    193         final ArraySet<JobInstance> mJobs = new ArraySet<>();
    194 
    195         public ObserverInstance(Handler handler, JobInfo.TriggerContentUri uri,
    196                 @UserIdInt int userId) {
    197             super(handler);
    198             mUri = uri;
    199             mUserId = userId;
    200         }
    201 
    202         @Override
    203         public void onChange(boolean selfChange, Uri uri) {
    204             if (DEBUG) {
    205                 Slog.i(TAG, "onChange(self=" + selfChange + ") for " + uri
    206                         + " when mUri=" + mUri + " mUserId=" + mUserId);
    207             }
    208             synchronized (mLock) {
    209                 final int N = mJobs.size();
    210                 for (int i=0; i<N; i++) {
    211                     JobInstance inst = mJobs.valueAt(i);
    212                     if (inst.mChangedUris == null) {
    213                         inst.mChangedUris = new ArraySet<>();
    214                     }
    215                     if (inst.mChangedUris.size() < MAX_URIS_REPORTED) {
    216                         inst.mChangedUris.add(uri);
    217                     }
    218                     if (inst.mChangedAuthorities == null) {
    219                         inst.mChangedAuthorities = new ArraySet<>();
    220                     }
    221                     inst.mChangedAuthorities.add(uri.getAuthority());
    222                     inst.scheduleLocked();
    223                 }
    224             }
    225         }
    226     }
    227 
    228     static final class TriggerRunnable implements Runnable {
    229         final JobInstance mInstance;
    230 
    231         TriggerRunnable(JobInstance instance) {
    232             mInstance = instance;
    233         }
    234 
    235         @Override public void run() {
    236             mInstance.trigger();
    237         }
    238     }
    239 
    240     final class JobInstance {
    241         final ArrayList<ObserverInstance> mMyObservers = new ArrayList<>();
    242         final JobStatus mJobStatus;
    243         final Runnable mExecuteRunner;
    244         final Runnable mTimeoutRunner;
    245         ArraySet<Uri> mChangedUris;
    246         ArraySet<String> mChangedAuthorities;
    247 
    248         boolean mTriggerPending;
    249 
    250         // This constructor must be called with the master job scheduler lock held.
    251         JobInstance(JobStatus jobStatus) {
    252             mJobStatus = jobStatus;
    253             mExecuteRunner = new TriggerRunnable(this);
    254             mTimeoutRunner = new TriggerRunnable(this);
    255             final JobInfo.TriggerContentUri[] uris = jobStatus.getJob().getTriggerContentUris();
    256             final int sourceUserId = jobStatus.getSourceUserId();
    257             ArrayMap<JobInfo.TriggerContentUri, ObserverInstance> observersOfUser =
    258                     mObservers.get(sourceUserId);
    259             if (observersOfUser == null) {
    260                 observersOfUser = new ArrayMap<>();
    261                 mObservers.put(sourceUserId, observersOfUser);
    262             }
    263             if (uris != null) {
    264                 for (JobInfo.TriggerContentUri uri : uris) {
    265                     ObserverInstance obs = observersOfUser.get(uri);
    266                     if (obs == null) {
    267                         obs = new ObserverInstance(mHandler, uri, jobStatus.getSourceUserId());
    268                         observersOfUser.put(uri, obs);
    269                         final boolean andDescendants = (uri.getFlags() &
    270                                 JobInfo.TriggerContentUri.FLAG_NOTIFY_FOR_DESCENDANTS) != 0;
    271                         if (DEBUG) {
    272                             Slog.v(TAG, "New observer " + obs + " for " + uri.getUri()
    273                                     + " andDescendants=" + andDescendants
    274                                     + " sourceUserId=" + sourceUserId);
    275                         }
    276                         mContext.getContentResolver().registerContentObserver(
    277                                 uri.getUri(),
    278                                 andDescendants,
    279                                 obs,
    280                                 sourceUserId
    281                         );
    282                     } else {
    283                         if (DEBUG) {
    284                             final boolean andDescendants = (uri.getFlags() &
    285                                     JobInfo.TriggerContentUri.FLAG_NOTIFY_FOR_DESCENDANTS) != 0;
    286                             Slog.v(TAG, "Reusing existing observer " + obs + " for " + uri.getUri()
    287                                     + " andDescendants=" + andDescendants);
    288                         }
    289                     }
    290                     obs.mJobs.add(this);
    291                     mMyObservers.add(obs);
    292                 }
    293             }
    294         }
    295 
    296         void trigger() {
    297             boolean reportChange = false;
    298             synchronized (mLock) {
    299                 if (mTriggerPending) {
    300                     if (mJobStatus.setContentTriggerConstraintSatisfied(true)) {
    301                         reportChange = true;
    302                     }
    303                     unscheduleLocked();
    304                 }
    305             }
    306             // Let the scheduler know that state has changed. This may or may not result in an
    307             // execution.
    308             if (reportChange) {
    309                 mStateChangedListener.onControllerStateChanged();
    310             }
    311         }
    312 
    313         void scheduleLocked() {
    314             if (!mTriggerPending) {
    315                 mTriggerPending = true;
    316                 mHandler.postDelayed(mTimeoutRunner, mJobStatus.getTriggerContentMaxDelay());
    317             }
    318             mHandler.removeCallbacks(mExecuteRunner);
    319             if (mChangedUris.size() >= URIS_URGENT_THRESHOLD) {
    320                 // If we start getting near the limit, GO NOW!
    321                 mHandler.post(mExecuteRunner);
    322             } else {
    323                 mHandler.postDelayed(mExecuteRunner, mJobStatus.getTriggerContentUpdateDelay());
    324             }
    325         }
    326 
    327         void unscheduleLocked() {
    328             if (mTriggerPending) {
    329                 mHandler.removeCallbacks(mExecuteRunner);
    330                 mHandler.removeCallbacks(mTimeoutRunner);
    331                 mTriggerPending = false;
    332             }
    333         }
    334 
    335         void detachLocked() {
    336             final int N = mMyObservers.size();
    337             for (int i=0; i<N; i++) {
    338                 final ObserverInstance obs = mMyObservers.get(i);
    339                 obs.mJobs.remove(this);
    340                 if (obs.mJobs.size() == 0) {
    341                     if (DEBUG) {
    342                         Slog.i(TAG, "Unregistering observer " + obs + " for " + obs.mUri.getUri());
    343                     }
    344                     mContext.getContentResolver().unregisterContentObserver(obs);
    345                     ArrayMap<JobInfo.TriggerContentUri, ObserverInstance> observerOfUser =
    346                             mObservers.get(obs.mUserId);
    347                     if (observerOfUser !=  null) {
    348                         observerOfUser.remove(obs.mUri);
    349                     }
    350                 }
    351             }
    352         }
    353     }
    354 
    355     @Override
    356     public void dumpControllerStateLocked(IndentingPrintWriter pw,
    357             Predicate<JobStatus> predicate) {
    358         for (int i = 0; i < mTrackedTasks.size(); i++) {
    359             JobStatus js = mTrackedTasks.valueAt(i);
    360             if (!predicate.test(js)) {
    361                 continue;
    362             }
    363             pw.print("#");
    364             js.printUniqueId(pw);
    365             pw.print(" from ");
    366             UserHandle.formatUid(pw, js.getSourceUid());
    367             pw.println();
    368         }
    369         pw.println();
    370 
    371         int N = mObservers.size();
    372         if (N > 0) {
    373             pw.println("Observers:");
    374             pw.increaseIndent();
    375             for (int userIdx = 0; userIdx < N; userIdx++) {
    376                 final int userId = mObservers.keyAt(userIdx);
    377                 ArrayMap<JobInfo.TriggerContentUri, ObserverInstance> observersOfUser =
    378                         mObservers.get(userId);
    379                 int numbOfObserversPerUser = observersOfUser.size();
    380                 for (int observerIdx = 0 ; observerIdx < numbOfObserversPerUser; observerIdx++) {
    381                     ObserverInstance obs = observersOfUser.valueAt(observerIdx);
    382                     int M = obs.mJobs.size();
    383                     boolean shouldDump = false;
    384                     for (int j = 0; j < M; j++) {
    385                         JobInstance inst = obs.mJobs.valueAt(j);
    386                         if (predicate.test(inst.mJobStatus)) {
    387                             shouldDump = true;
    388                             break;
    389                         }
    390                     }
    391                     if (!shouldDump) {
    392                         continue;
    393                     }
    394                     JobInfo.TriggerContentUri trigger = observersOfUser.keyAt(observerIdx);
    395                     pw.print(trigger.getUri());
    396                     pw.print(" 0x");
    397                     pw.print(Integer.toHexString(trigger.getFlags()));
    398                     pw.print(" (");
    399                     pw.print(System.identityHashCode(obs));
    400                     pw.println("):");
    401                     pw.increaseIndent();
    402                     pw.println("Jobs:");
    403                     pw.increaseIndent();
    404                     for (int j = 0; j < M; j++) {
    405                         JobInstance inst = obs.mJobs.valueAt(j);
    406                         pw.print("#");
    407                         inst.mJobStatus.printUniqueId(pw);
    408                         pw.print(" from ");
    409                         UserHandle.formatUid(pw, inst.mJobStatus.getSourceUid());
    410                         if (inst.mChangedAuthorities != null) {
    411                             pw.println(":");
    412                             pw.increaseIndent();
    413                             if (inst.mTriggerPending) {
    414                                 pw.print("Trigger pending: update=");
    415                                 TimeUtils.formatDuration(
    416                                         inst.mJobStatus.getTriggerContentUpdateDelay(), pw);
    417                                 pw.print(", max=");
    418                                 TimeUtils.formatDuration(
    419                                         inst.mJobStatus.getTriggerContentMaxDelay(), pw);
    420                                 pw.println();
    421                             }
    422                             pw.println("Changed Authorities:");
    423                             for (int k = 0; k < inst.mChangedAuthorities.size(); k++) {
    424                                 pw.println(inst.mChangedAuthorities.valueAt(k));
    425                             }
    426                             if (inst.mChangedUris != null) {
    427                                 pw.println("          Changed URIs:");
    428                                 for (int k = 0; k < inst.mChangedUris.size(); k++) {
    429                                     pw.println(inst.mChangedUris.valueAt(k));
    430                                 }
    431                             }
    432                             pw.decreaseIndent();
    433                         } else {
    434                             pw.println();
    435                         }
    436                     }
    437                     pw.decreaseIndent();
    438                     pw.decreaseIndent();
    439                 }
    440             }
    441             pw.decreaseIndent();
    442         }
    443     }
    444 
    445     @Override
    446     public void dumpControllerStateLocked(ProtoOutputStream proto, long fieldId,
    447             Predicate<JobStatus> predicate) {
    448         final long token = proto.start(fieldId);
    449         final long mToken = proto.start(StateControllerProto.CONTENT_OBSERVER);
    450 
    451         for (int i = 0; i < mTrackedTasks.size(); i++) {
    452             JobStatus js = mTrackedTasks.valueAt(i);
    453             if (!predicate.test(js)) {
    454                 continue;
    455             }
    456             final long jsToken =
    457                     proto.start(StateControllerProto.ContentObserverController.TRACKED_JOBS);
    458             js.writeToShortProto(proto,
    459                     StateControllerProto.ContentObserverController.TrackedJob.INFO);
    460             proto.write(StateControllerProto.ContentObserverController.TrackedJob.SOURCE_UID,
    461                     js.getSourceUid());
    462             proto.end(jsToken);
    463         }
    464 
    465         final int n = mObservers.size();
    466         for (int userIdx = 0; userIdx < n; userIdx++) {
    467             final long oToken =
    468                     proto.start(StateControllerProto.ContentObserverController.OBSERVERS);
    469             final int userId = mObservers.keyAt(userIdx);
    470 
    471             proto.write(StateControllerProto.ContentObserverController.Observer.USER_ID, userId);
    472 
    473             ArrayMap<JobInfo.TriggerContentUri, ObserverInstance> observersOfUser =
    474                     mObservers.get(userId);
    475             int numbOfObserversPerUser = observersOfUser.size();
    476             for (int observerIdx = 0 ; observerIdx < numbOfObserversPerUser; observerIdx++) {
    477                 ObserverInstance obs = observersOfUser.valueAt(observerIdx);
    478                 int m = obs.mJobs.size();
    479                 boolean shouldDump = false;
    480                 for (int j = 0; j < m; j++) {
    481                     JobInstance inst = obs.mJobs.valueAt(j);
    482                     if (predicate.test(inst.mJobStatus)) {
    483                         shouldDump = true;
    484                         break;
    485                     }
    486                 }
    487                 if (!shouldDump) {
    488                     continue;
    489                 }
    490                 final long tToken = proto.start(
    491                         StateControllerProto.ContentObserverController.Observer.TRIGGERS);
    492 
    493                 JobInfo.TriggerContentUri trigger = observersOfUser.keyAt(observerIdx);
    494                 Uri u = trigger.getUri();
    495                 if (u != null) {
    496                     proto.write(TriggerContentData.URI, u.toString());
    497                 }
    498                 proto.write(TriggerContentData.FLAGS, trigger.getFlags());
    499 
    500                 for (int j = 0; j < m; j++) {
    501                     final long jToken = proto.start(TriggerContentData.JOBS);
    502                     JobInstance inst = obs.mJobs.valueAt(j);
    503 
    504                     inst.mJobStatus.writeToShortProto(proto, TriggerContentData.JobInstance.INFO);
    505                     proto.write(TriggerContentData.JobInstance.SOURCE_UID,
    506                             inst.mJobStatus.getSourceUid());
    507 
    508                     if (inst.mChangedAuthorities == null) {
    509                         proto.end(jToken);
    510                         continue;
    511                     }
    512                     if (inst.mTriggerPending) {
    513                         proto.write(TriggerContentData.JobInstance.TRIGGER_CONTENT_UPDATE_DELAY_MS,
    514                                 inst.mJobStatus.getTriggerContentUpdateDelay());
    515                         proto.write(TriggerContentData.JobInstance.TRIGGER_CONTENT_MAX_DELAY_MS,
    516                                 inst.mJobStatus.getTriggerContentMaxDelay());
    517                     }
    518                     for (int k = 0; k < inst.mChangedAuthorities.size(); k++) {
    519                         proto.write(TriggerContentData.JobInstance.CHANGED_AUTHORITIES,
    520                                 inst.mChangedAuthorities.valueAt(k));
    521                     }
    522                     if (inst.mChangedUris != null) {
    523                         for (int k = 0; k < inst.mChangedUris.size(); k++) {
    524                             u = inst.mChangedUris.valueAt(k);
    525                             if (u != null) {
    526                                 proto.write(TriggerContentData.JobInstance.CHANGED_URIS,
    527                                         u.toString());
    528                             }
    529                         }
    530                     }
    531 
    532                     proto.end(jToken);
    533                 }
    534 
    535                 proto.end(tToken);
    536             }
    537 
    538             proto.end(oToken);
    539         }
    540 
    541         proto.end(mToken);
    542         proto.end(token);
    543     }
    544 }
    545