Home | History | Annotate | Download | only in controllers
      1 /*
      2  * Copyright (C) 2014 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 static com.android.server.job.JobSchedulerService.sElapsedRealtimeClock;
     20 
     21 import android.annotation.Nullable;
     22 import android.app.AlarmManager;
     23 import android.app.AlarmManager.OnAlarmListener;
     24 import android.content.Context;
     25 import android.os.Process;
     26 import android.os.UserHandle;
     27 import android.os.WorkSource;
     28 import android.util.Log;
     29 import android.util.Slog;
     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 
     37 import java.util.Iterator;
     38 import java.util.LinkedList;
     39 import java.util.List;
     40 import java.util.ListIterator;
     41 import java.util.function.Predicate;
     42 
     43 /**
     44  * This class sets an alarm for the next expiring job, and determines whether a job's minimum
     45  * delay has been satisfied.
     46  */
     47 public final class TimeController extends StateController {
     48     private static final String TAG = "JobScheduler.Time";
     49     private static final boolean DEBUG = JobSchedulerService.DEBUG
     50             || Log.isLoggable(TAG, Log.DEBUG);
     51 
     52     /** Deadline alarm tag for logging purposes */
     53     private final String DEADLINE_TAG = "*job.deadline*";
     54     /** Delay alarm tag for logging purposes */
     55     private final String DELAY_TAG = "*job.delay*";
     56 
     57     private long mNextJobExpiredElapsedMillis;
     58     private long mNextDelayExpiredElapsedMillis;
     59 
     60     private final boolean mChainedAttributionEnabled;
     61 
     62     private AlarmManager mAlarmService = null;
     63     /** List of tracked jobs, sorted asc. by deadline */
     64     private final List<JobStatus> mTrackedJobs = new LinkedList<>();
     65 
     66     public TimeController(JobSchedulerService service) {
     67         super(service);
     68 
     69         mNextJobExpiredElapsedMillis = Long.MAX_VALUE;
     70         mNextDelayExpiredElapsedMillis = Long.MAX_VALUE;
     71         mChainedAttributionEnabled = WorkSource.isChainedBatteryAttributionEnabled(mContext);
     72     }
     73 
     74     /**
     75      * Check if the job has a timing constraint, and if so determine where to insert it in our
     76      * list.
     77      */
     78     @Override
     79     public void maybeStartTrackingJobLocked(JobStatus job, JobStatus lastJob) {
     80         if (job.hasTimingDelayConstraint() || job.hasDeadlineConstraint()) {
     81             maybeStopTrackingJobLocked(job, null, false);
     82 
     83             // First: check the constraints now, because if they are already satisfied
     84             // then there is no need to track it.  This gives us a fast path for a common
     85             // pattern of having a job with a 0 deadline constraint ("run immediately").
     86             // Unlike most controllers, once one of our constraints has been satisfied, it
     87             // will never be unsatisfied (our time base can not go backwards).
     88             final long nowElapsedMillis = sElapsedRealtimeClock.millis();
     89             if (job.hasDeadlineConstraint() && evaluateDeadlineConstraint(job, nowElapsedMillis)) {
     90                 return;
     91             } else if (job.hasTimingDelayConstraint() && evaluateTimingDelayConstraint(job,
     92                     nowElapsedMillis)) {
     93                 if (!job.hasDeadlineConstraint()) {
     94                     // If it doesn't have a deadline, we'll never have to touch it again.
     95                     return;
     96                 }
     97             }
     98 
     99             boolean isInsert = false;
    100             ListIterator<JobStatus> it = mTrackedJobs.listIterator(mTrackedJobs.size());
    101             while (it.hasPrevious()) {
    102                 JobStatus ts = it.previous();
    103                 if (ts.getLatestRunTimeElapsed() < job.getLatestRunTimeElapsed()) {
    104                     // Insert
    105                     isInsert = true;
    106                     break;
    107                 }
    108             }
    109             if (isInsert) {
    110                 it.next();
    111             }
    112             it.add(job);
    113             job.setTrackingController(JobStatus.TRACKING_TIME);
    114             maybeUpdateAlarmsLocked(
    115                     job.hasTimingDelayConstraint() ? job.getEarliestRunTime() : Long.MAX_VALUE,
    116                     job.hasDeadlineConstraint() ? job.getLatestRunTimeElapsed() : Long.MAX_VALUE,
    117                     deriveWorkSource(job.getSourceUid(), job.getSourcePackageName()));
    118         }
    119     }
    120 
    121     /**
    122      * When we stop tracking a job, we only need to update our alarms if the job we're no longer
    123      * tracking was the one our alarms were based off of.
    124      */
    125     @Override
    126     public void maybeStopTrackingJobLocked(JobStatus job, JobStatus incomingJob,
    127             boolean forUpdate) {
    128         if (job.clearTrackingController(JobStatus.TRACKING_TIME)) {
    129             if (mTrackedJobs.remove(job)) {
    130                 checkExpiredDelaysAndResetAlarm();
    131                 checkExpiredDeadlinesAndResetAlarm();
    132             }
    133         }
    134     }
    135 
    136     /**
    137      * Determines whether this controller can stop tracking the given job.
    138      * The controller is no longer interested in a job once its time constraint is satisfied, and
    139      * the job's deadline is fulfilled - unlike other controllers a time constraint can't toggle
    140      * back and forth.
    141      */
    142     private boolean canStopTrackingJobLocked(JobStatus job) {
    143         return (!job.hasTimingDelayConstraint() ||
    144                 (job.satisfiedConstraints&JobStatus.CONSTRAINT_TIMING_DELAY) != 0) &&
    145                 (!job.hasDeadlineConstraint() ||
    146                         (job.satisfiedConstraints&JobStatus.CONSTRAINT_DEADLINE) != 0);
    147     }
    148 
    149     private void ensureAlarmServiceLocked() {
    150         if (mAlarmService == null) {
    151             mAlarmService = (AlarmManager) mContext.getSystemService(Context.ALARM_SERVICE);
    152         }
    153     }
    154 
    155     /**
    156      * Checks list of jobs for ones that have an expired deadline, sending them to the JobScheduler
    157      * if so, removing them from this list, and updating the alarm for the next expiry time.
    158      */
    159     private void checkExpiredDeadlinesAndResetAlarm() {
    160         synchronized (mLock) {
    161             long nextExpiryTime = Long.MAX_VALUE;
    162             int nextExpiryUid = 0;
    163             String nextExpiryPackageName = null;
    164             final long nowElapsedMillis = sElapsedRealtimeClock.millis();
    165 
    166             Iterator<JobStatus> it = mTrackedJobs.iterator();
    167             while (it.hasNext()) {
    168                 JobStatus job = it.next();
    169                 if (!job.hasDeadlineConstraint()) {
    170                     continue;
    171                 }
    172 
    173                 if (evaluateDeadlineConstraint(job, nowElapsedMillis)) {
    174                     mStateChangedListener.onRunJobNow(job);
    175                     it.remove();
    176                 } else {  // Sorted by expiry time, so take the next one and stop.
    177                     nextExpiryTime = job.getLatestRunTimeElapsed();
    178                     nextExpiryUid = job.getSourceUid();
    179                     nextExpiryPackageName = job.getSourcePackageName();
    180                     break;
    181                 }
    182             }
    183             setDeadlineExpiredAlarmLocked(nextExpiryTime,
    184                     deriveWorkSource(nextExpiryUid, nextExpiryPackageName));
    185         }
    186     }
    187 
    188     private boolean evaluateDeadlineConstraint(JobStatus job, long nowElapsedMillis) {
    189         final long jobDeadline = job.getLatestRunTimeElapsed();
    190 
    191         if (jobDeadline <= nowElapsedMillis) {
    192             if (job.hasTimingDelayConstraint()) {
    193                 job.setTimingDelayConstraintSatisfied(true);
    194             }
    195             job.setDeadlineConstraintSatisfied(true);
    196             return true;
    197         }
    198         return false;
    199     }
    200 
    201     /**
    202      * Handles alarm that notifies us that a job's delay has expired. Iterates through the list of
    203      * tracked jobs and marks them as ready as appropriate.
    204      */
    205     private void checkExpiredDelaysAndResetAlarm() {
    206         synchronized (mLock) {
    207             final long nowElapsedMillis = sElapsedRealtimeClock.millis();
    208             long nextDelayTime = Long.MAX_VALUE;
    209             int nextDelayUid = 0;
    210             String nextDelayPackageName = null;
    211             boolean ready = false;
    212             Iterator<JobStatus> it = mTrackedJobs.iterator();
    213             while (it.hasNext()) {
    214                 final JobStatus job = it.next();
    215                 if (!job.hasTimingDelayConstraint()) {
    216                     continue;
    217                 }
    218                 if (evaluateTimingDelayConstraint(job, nowElapsedMillis)) {
    219                     if (canStopTrackingJobLocked(job)) {
    220                         it.remove();
    221                     }
    222                     if (job.isReady()) {
    223                         ready = true;
    224                     }
    225                 } else if (!job.isConstraintSatisfied(JobStatus.CONSTRAINT_TIMING_DELAY)) {
    226                     // If this job still doesn't have its delay constraint satisfied,
    227                     // then see if it is the next upcoming delay time for the alarm.
    228                     final long jobDelayTime = job.getEarliestRunTime();
    229                     if (nextDelayTime > jobDelayTime) {
    230                         nextDelayTime = jobDelayTime;
    231                         nextDelayUid = job.getSourceUid();
    232                         nextDelayPackageName = job.getSourcePackageName();
    233                     }
    234                 }
    235             }
    236             if (ready) {
    237                 mStateChangedListener.onControllerStateChanged();
    238             }
    239             setDelayExpiredAlarmLocked(nextDelayTime,
    240                     deriveWorkSource(nextDelayUid, nextDelayPackageName));
    241         }
    242     }
    243 
    244     private WorkSource deriveWorkSource(int uid, @Nullable String packageName) {
    245         if (mChainedAttributionEnabled) {
    246             WorkSource ws = new WorkSource();
    247             ws.createWorkChain()
    248                     .addNode(uid, packageName)
    249                     .addNode(Process.SYSTEM_UID, "JobScheduler");
    250             return ws;
    251         } else {
    252             return packageName == null ? new WorkSource(uid) : new WorkSource(uid, packageName);
    253         }
    254     }
    255 
    256     private boolean evaluateTimingDelayConstraint(JobStatus job, long nowElapsedMillis) {
    257         final long jobDelayTime = job.getEarliestRunTime();
    258         if (jobDelayTime <= nowElapsedMillis) {
    259             job.setTimingDelayConstraintSatisfied(true);
    260             return true;
    261         }
    262         return false;
    263     }
    264 
    265     private void maybeUpdateAlarmsLocked(long delayExpiredElapsed, long deadlineExpiredElapsed,
    266             WorkSource ws) {
    267         if (delayExpiredElapsed < mNextDelayExpiredElapsedMillis) {
    268             setDelayExpiredAlarmLocked(delayExpiredElapsed, ws);
    269         }
    270         if (deadlineExpiredElapsed < mNextJobExpiredElapsedMillis) {
    271             setDeadlineExpiredAlarmLocked(deadlineExpiredElapsed, ws);
    272         }
    273     }
    274 
    275     /**
    276      * Set an alarm with the {@link android.app.AlarmManager} for the next time at which a job's
    277      * delay will expire.
    278      * This alarm <b>will</b> wake up the phone.
    279      */
    280     private void setDelayExpiredAlarmLocked(long alarmTimeElapsedMillis, WorkSource ws) {
    281         alarmTimeElapsedMillis = maybeAdjustAlarmTime(alarmTimeElapsedMillis);
    282         mNextDelayExpiredElapsedMillis = alarmTimeElapsedMillis;
    283         updateAlarmWithListenerLocked(DELAY_TAG, mNextDelayExpiredListener,
    284                 mNextDelayExpiredElapsedMillis, ws);
    285     }
    286 
    287     /**
    288      * Set an alarm with the {@link android.app.AlarmManager} for the next time at which a job's
    289      * deadline will expire.
    290      * This alarm <b>will</b> wake up the phone.
    291      */
    292     private void setDeadlineExpiredAlarmLocked(long alarmTimeElapsedMillis, WorkSource ws) {
    293         alarmTimeElapsedMillis = maybeAdjustAlarmTime(alarmTimeElapsedMillis);
    294         mNextJobExpiredElapsedMillis = alarmTimeElapsedMillis;
    295         updateAlarmWithListenerLocked(DEADLINE_TAG, mDeadlineExpiredListener,
    296                 mNextJobExpiredElapsedMillis, ws);
    297     }
    298 
    299     private long maybeAdjustAlarmTime(long proposedAlarmTimeElapsedMillis) {
    300         final long earliestWakeupTimeElapsed = sElapsedRealtimeClock.millis();
    301         if (proposedAlarmTimeElapsedMillis < earliestWakeupTimeElapsed) {
    302             return earliestWakeupTimeElapsed;
    303         }
    304         return proposedAlarmTimeElapsedMillis;
    305     }
    306 
    307     private void updateAlarmWithListenerLocked(String tag, OnAlarmListener listener,
    308             long alarmTimeElapsed, WorkSource ws) {
    309         ensureAlarmServiceLocked();
    310         if (alarmTimeElapsed == Long.MAX_VALUE) {
    311             mAlarmService.cancel(listener);
    312         } else {
    313             if (DEBUG) {
    314                 Slog.d(TAG, "Setting " + tag + " for: " + alarmTimeElapsed);
    315             }
    316             mAlarmService.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, alarmTimeElapsed,
    317                     AlarmManager.WINDOW_HEURISTIC, 0, tag, listener, null, ws);
    318         }
    319     }
    320 
    321     // Job/delay expiration alarm handling
    322 
    323     private final OnAlarmListener mDeadlineExpiredListener = new OnAlarmListener() {
    324         @Override
    325         public void onAlarm() {
    326             if (DEBUG) {
    327                 Slog.d(TAG, "Deadline-expired alarm fired");
    328             }
    329             checkExpiredDeadlinesAndResetAlarm();
    330         }
    331     };
    332 
    333     private final OnAlarmListener mNextDelayExpiredListener = new OnAlarmListener() {
    334         @Override
    335         public void onAlarm() {
    336             if (DEBUG) {
    337                 Slog.d(TAG, "Delay-expired alarm fired");
    338             }
    339             checkExpiredDelaysAndResetAlarm();
    340         }
    341     };
    342 
    343     @Override
    344     public void dumpControllerStateLocked(IndentingPrintWriter pw,
    345             Predicate<JobStatus> predicate) {
    346         final long nowElapsed = sElapsedRealtimeClock.millis();
    347         pw.println("Elapsed clock: " + nowElapsed);
    348 
    349         pw.print("Next delay alarm in ");
    350         TimeUtils.formatDuration(mNextDelayExpiredElapsedMillis, nowElapsed, pw);
    351         pw.println();
    352         pw.print("Next deadline alarm in ");
    353         TimeUtils.formatDuration(mNextJobExpiredElapsedMillis, nowElapsed, pw);
    354         pw.println();
    355         pw.println();
    356 
    357         for (JobStatus ts : mTrackedJobs) {
    358             if (!predicate.test(ts)) {
    359                 continue;
    360             }
    361             pw.print("#");
    362             ts.printUniqueId(pw);
    363             pw.print(" from ");
    364             UserHandle.formatUid(pw, ts.getSourceUid());
    365             pw.print(": Delay=");
    366             if (ts.hasTimingDelayConstraint()) {
    367                 TimeUtils.formatDuration(ts.getEarliestRunTime(), nowElapsed, pw);
    368             } else {
    369                 pw.print("N/A");
    370             }
    371             pw.print(", Deadline=");
    372             if (ts.hasDeadlineConstraint()) {
    373                 TimeUtils.formatDuration(ts.getLatestRunTimeElapsed(), nowElapsed, pw);
    374             } else {
    375                 pw.print("N/A");
    376             }
    377             pw.println();
    378         }
    379     }
    380 
    381     @Override
    382     public void dumpControllerStateLocked(ProtoOutputStream proto, long fieldId,
    383             Predicate<JobStatus> predicate) {
    384         final long token = proto.start(fieldId);
    385         final long mToken = proto.start(StateControllerProto.TIME);
    386 
    387         final long nowElapsed = sElapsedRealtimeClock.millis();
    388         proto.write(StateControllerProto.TimeController.NOW_ELAPSED_REALTIME, nowElapsed);
    389         proto.write(StateControllerProto.TimeController.TIME_UNTIL_NEXT_DELAY_ALARM_MS,
    390                 mNextDelayExpiredElapsedMillis - nowElapsed);
    391         proto.write(StateControllerProto.TimeController.TIME_UNTIL_NEXT_DEADLINE_ALARM_MS,
    392                 mNextJobExpiredElapsedMillis - nowElapsed);
    393 
    394         for (JobStatus ts : mTrackedJobs) {
    395             if (!predicate.test(ts)) {
    396                 continue;
    397             }
    398             final long tsToken = proto.start(StateControllerProto.TimeController.TRACKED_JOBS);
    399             ts.writeToShortProto(proto, StateControllerProto.TimeController.TrackedJob.INFO);
    400 
    401             proto.write(StateControllerProto.TimeController.TrackedJob.HAS_TIMING_DELAY_CONSTRAINT,
    402                     ts.hasTimingDelayConstraint());
    403             proto.write(StateControllerProto.TimeController.TrackedJob.DELAY_TIME_REMAINING_MS,
    404                     ts.getEarliestRunTime() - nowElapsed);
    405 
    406             proto.write(StateControllerProto.TimeController.TrackedJob.HAS_DEADLINE_CONSTRAINT,
    407                     ts.hasDeadlineConstraint());
    408             proto.write(StateControllerProto.TimeController.TrackedJob.TIME_REMAINING_UNTIL_DEADLINE_MS,
    409                     ts.getLatestRunTimeElapsed() - nowElapsed);
    410 
    411             proto.end(tsToken);
    412         }
    413 
    414         proto.end(mToken);
    415         proto.end(token);
    416     }
    417 }
    418