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 android.app.AlarmManager;
     20 import android.app.PendingIntent;
     21 import android.content.BroadcastReceiver;
     22 import android.content.Context;
     23 import android.content.Intent;
     24 import android.content.IntentFilter;
     25 import android.os.SystemClock;
     26 import android.util.Slog;
     27 
     28 import com.android.server.job.JobSchedulerService;
     29 import com.android.server.job.StateChangedListener;
     30 
     31 import java.io.PrintWriter;
     32 import java.util.Iterator;
     33 import java.util.LinkedList;
     34 import java.util.List;
     35 import java.util.ListIterator;
     36 
     37 /**
     38  * This class sets an alarm for the next expiring job, and determines whether a job's minimum
     39  * delay has been satisfied.
     40  */
     41 public class TimeController extends StateController {
     42     private static final String TAG = "JobScheduler.Time";
     43     private static final String ACTION_JOB_EXPIRED =
     44             "android.content.jobscheduler.JOB_DEADLINE_EXPIRED";
     45     private static final String ACTION_JOB_DELAY_EXPIRED =
     46             "android.content.jobscheduler.JOB_DELAY_EXPIRED";
     47 
     48     /** Set an alarm for the next job expiry. */
     49     private final PendingIntent mDeadlineExpiredAlarmIntent;
     50     /** Set an alarm for the next job delay expiry. This*/
     51     private final PendingIntent mNextDelayExpiredAlarmIntent;
     52 
     53     private long mNextJobExpiredElapsedMillis;
     54     private long mNextDelayExpiredElapsedMillis;
     55 
     56     private AlarmManager mAlarmService = null;
     57     /** List of tracked jobs, sorted asc. by deadline */
     58     private final List<JobStatus> mTrackedJobs = new LinkedList<JobStatus>();
     59     /** Singleton. */
     60     private static TimeController mSingleton;
     61 
     62     public static synchronized TimeController get(JobSchedulerService jms) {
     63         if (mSingleton == null) {
     64             mSingleton = new TimeController(jms, jms.getContext());
     65         }
     66         return mSingleton;
     67     }
     68 
     69     private TimeController(StateChangedListener stateChangedListener, Context context) {
     70         super(stateChangedListener, context);
     71         mDeadlineExpiredAlarmIntent =
     72                 PendingIntent.getBroadcast(mContext, 0 /* ignored */,
     73                         new Intent(ACTION_JOB_EXPIRED), 0);
     74         mNextDelayExpiredAlarmIntent =
     75                 PendingIntent.getBroadcast(mContext, 0 /* ignored */,
     76                         new Intent(ACTION_JOB_DELAY_EXPIRED), 0);
     77         mNextJobExpiredElapsedMillis = Long.MAX_VALUE;
     78         mNextDelayExpiredElapsedMillis = Long.MAX_VALUE;
     79 
     80         // Register BR for these intents.
     81         IntentFilter intentFilter = new IntentFilter(ACTION_JOB_EXPIRED);
     82         intentFilter.addAction(ACTION_JOB_DELAY_EXPIRED);
     83         mContext.registerReceiver(mAlarmExpiredReceiver, intentFilter);
     84     }
     85 
     86     /**
     87      * Check if the job has a timing constraint, and if so determine where to insert it in our
     88      * list.
     89      */
     90     @Override
     91     public synchronized void maybeStartTrackingJob(JobStatus job) {
     92         if (job.hasTimingDelayConstraint() || job.hasDeadlineConstraint()) {
     93             maybeStopTrackingJob(job);
     94             boolean isInsert = false;
     95             ListIterator<JobStatus> it = mTrackedJobs.listIterator(mTrackedJobs.size());
     96             while (it.hasPrevious()) {
     97                 JobStatus ts = it.previous();
     98                 if (ts.getLatestRunTimeElapsed() < job.getLatestRunTimeElapsed()) {
     99                     // Insert
    100                     isInsert = true;
    101                     break;
    102                 }
    103             }
    104             if(isInsert)
    105             {
    106                 it.next();
    107             }
    108             it.add(job);
    109             maybeUpdateAlarms(
    110                     job.hasTimingDelayConstraint() ? job.getEarliestRunTime() : Long.MAX_VALUE,
    111                     job.hasDeadlineConstraint() ? job.getLatestRunTimeElapsed() : Long.MAX_VALUE);
    112         }
    113     }
    114 
    115     /**
    116      * When we stop tracking a job, we only need to update our alarms if the job we're no longer
    117      * tracking was the one our alarms were based off of.
    118      * Really an == comparison should be enough, but why play with fate? We'll do <=.
    119      */
    120     @Override
    121     public synchronized void maybeStopTrackingJob(JobStatus job) {
    122         if (mTrackedJobs.remove(job)) {
    123             checkExpiredDelaysAndResetAlarm();
    124             checkExpiredDeadlinesAndResetAlarm();
    125         }
    126     }
    127 
    128     /**
    129      * Determines whether this controller can stop tracking the given job.
    130      * The controller is no longer interested in a job once its time constraint is satisfied, and
    131      * the job's deadline is fulfilled - unlike other controllers a time constraint can't toggle
    132      * back and forth.
    133      */
    134     private boolean canStopTrackingJob(JobStatus job) {
    135         return (!job.hasTimingDelayConstraint() ||
    136                 job.timeDelayConstraintSatisfied.get()) &&
    137                 (!job.hasDeadlineConstraint() ||
    138                         job.deadlineConstraintSatisfied.get());
    139     }
    140 
    141     private void ensureAlarmService() {
    142         if (mAlarmService == null) {
    143             mAlarmService = (AlarmManager) mContext.getSystemService(Context.ALARM_SERVICE);
    144         }
    145     }
    146 
    147     /**
    148      * Checks list of jobs for ones that have an expired deadline, sending them to the JobScheduler
    149      * if so, removing them from this list, and updating the alarm for the next expiry time.
    150      */
    151     private synchronized void checkExpiredDeadlinesAndResetAlarm() {
    152         long nextExpiryTime = Long.MAX_VALUE;
    153         final long nowElapsedMillis = SystemClock.elapsedRealtime();
    154 
    155         Iterator<JobStatus> it = mTrackedJobs.iterator();
    156         while (it.hasNext()) {
    157             JobStatus job = it.next();
    158             if (!job.hasDeadlineConstraint()) {
    159                 continue;
    160             }
    161             final long jobDeadline = job.getLatestRunTimeElapsed();
    162 
    163             if (jobDeadline <= nowElapsedMillis) {
    164                 job.deadlineConstraintSatisfied.set(true);
    165                 mStateChangedListener.onRunJobNow(job);
    166                 it.remove();
    167             } else {  // Sorted by expiry time, so take the next one and stop.
    168                 nextExpiryTime = jobDeadline;
    169                 break;
    170             }
    171         }
    172         setDeadlineExpiredAlarm(nextExpiryTime);
    173     }
    174 
    175     /**
    176      * Handles alarm that notifies us that a job's delay has expired. Iterates through the list of
    177      * tracked jobs and marks them as ready as appropriate.
    178      */
    179     private synchronized void checkExpiredDelaysAndResetAlarm() {
    180         final long nowElapsedMillis = SystemClock.elapsedRealtime();
    181         long nextDelayTime = Long.MAX_VALUE;
    182         boolean ready = false;
    183         Iterator<JobStatus> it = mTrackedJobs.iterator();
    184         while (it.hasNext()) {
    185             final JobStatus job = it.next();
    186             if (!job.hasTimingDelayConstraint()) {
    187                 continue;
    188             }
    189             final long jobDelayTime = job.getEarliestRunTime();
    190             if (jobDelayTime <= nowElapsedMillis) {
    191                 job.timeDelayConstraintSatisfied.set(true);
    192                 if (canStopTrackingJob(job)) {
    193                     it.remove();
    194                 }
    195                 if (job.isReady()) {
    196                     ready = true;
    197                 }
    198             } else {  // Keep going through list to get next delay time.
    199                 if (nextDelayTime > jobDelayTime) {
    200                     nextDelayTime = jobDelayTime;
    201                 }
    202             }
    203         }
    204         if (ready) {
    205             mStateChangedListener.onControllerStateChanged();
    206         }
    207         setDelayExpiredAlarm(nextDelayTime);
    208     }
    209 
    210     private void maybeUpdateAlarms(long delayExpiredElapsed, long deadlineExpiredElapsed) {
    211         if (delayExpiredElapsed < mNextDelayExpiredElapsedMillis) {
    212             setDelayExpiredAlarm(delayExpiredElapsed);
    213         }
    214         if (deadlineExpiredElapsed < mNextJobExpiredElapsedMillis) {
    215             setDeadlineExpiredAlarm(deadlineExpiredElapsed);
    216         }
    217     }
    218 
    219     /**
    220      * Set an alarm with the {@link android.app.AlarmManager} for the next time at which a job's
    221      * delay will expire.
    222      * This alarm <b>will not</b> wake up the phone.
    223      */
    224     private void setDelayExpiredAlarm(long alarmTimeElapsedMillis) {
    225         alarmTimeElapsedMillis = maybeAdjustAlarmTime(alarmTimeElapsedMillis);
    226         mNextDelayExpiredElapsedMillis = alarmTimeElapsedMillis;
    227         updateAlarmWithPendingIntent(mNextDelayExpiredAlarmIntent, mNextDelayExpiredElapsedMillis);
    228     }
    229 
    230     /**
    231      * Set an alarm with the {@link android.app.AlarmManager} for the next time at which a job's
    232      * deadline will expire.
    233      * This alarm <b>will</b> wake up the phone.
    234      */
    235     private void setDeadlineExpiredAlarm(long alarmTimeElapsedMillis) {
    236         alarmTimeElapsedMillis = maybeAdjustAlarmTime(alarmTimeElapsedMillis);
    237         mNextJobExpiredElapsedMillis = alarmTimeElapsedMillis;
    238         updateAlarmWithPendingIntent(mDeadlineExpiredAlarmIntent, mNextJobExpiredElapsedMillis);
    239     }
    240 
    241     private long maybeAdjustAlarmTime(long proposedAlarmTimeElapsedMillis) {
    242         final long earliestWakeupTimeElapsed = SystemClock.elapsedRealtime();
    243         if (proposedAlarmTimeElapsedMillis < earliestWakeupTimeElapsed) {
    244             return earliestWakeupTimeElapsed;
    245         }
    246         return proposedAlarmTimeElapsedMillis;
    247     }
    248 
    249     private void updateAlarmWithPendingIntent(PendingIntent pi, long alarmTimeElapsed) {
    250         ensureAlarmService();
    251         if (alarmTimeElapsed == Long.MAX_VALUE) {
    252             mAlarmService.cancel(pi);
    253         } else {
    254             if (DEBUG) {
    255                 Slog.d(TAG, "Setting " + pi.getIntent().getAction() + " for: " + alarmTimeElapsed);
    256             }
    257             mAlarmService.set(AlarmManager.ELAPSED_REALTIME, alarmTimeElapsed, pi);
    258         }
    259     }
    260 
    261     private final BroadcastReceiver mAlarmExpiredReceiver = new BroadcastReceiver() {
    262         @Override
    263         public void onReceive(Context context, Intent intent) {
    264             if (DEBUG) {
    265                 Slog.d(TAG, "Just received alarm: " + intent.getAction());
    266             }
    267             // A job has just expired, so we run through the list of jobs that we have and
    268             // notify our StateChangedListener.
    269             if (ACTION_JOB_EXPIRED.equals(intent.getAction())) {
    270                 checkExpiredDeadlinesAndResetAlarm();
    271             } else if (ACTION_JOB_DELAY_EXPIRED.equals(intent.getAction())) {
    272                 checkExpiredDelaysAndResetAlarm();
    273             }
    274         }
    275     };
    276 
    277     @Override
    278     public void dumpControllerState(PrintWriter pw) {
    279         final long nowElapsed = SystemClock.elapsedRealtime();
    280         pw.println("Alarms (" + SystemClock.elapsedRealtime() + ")");
    281         pw.println(
    282                 "Next delay alarm in " + (mNextDelayExpiredElapsedMillis - nowElapsed)/1000 + "s");
    283         pw.println("Next deadline alarm in " + (mNextJobExpiredElapsedMillis - nowElapsed)/1000
    284                 + "s");
    285         pw.println("Tracking:");
    286         for (JobStatus ts : mTrackedJobs) {
    287             pw.println(String.valueOf(ts.hashCode()).substring(0, 3) + ".."
    288                     + ": (" + (ts.hasTimingDelayConstraint() ? ts.getEarliestRunTime() : "N/A")
    289                     + ", " + (ts.hasDeadlineConstraint() ?ts.getLatestRunTimeElapsed() : "N/A")
    290                     + ")");
    291         }
    292     }
    293 }