diff --git a/apex/jobscheduler/service/java/com/android/server/DeviceIdleController.java b/apex/jobscheduler/service/java/com/android/server/DeviceIdleController.java index c706a3a54c382..823738347bac5 100644 --- a/apex/jobscheduler/service/java/com/android/server/DeviceIdleController.java +++ b/apex/jobscheduler/service/java/com/android/server/DeviceIdleController.java @@ -25,6 +25,7 @@ import static android.os.Process.INVALID_UID; import android.Manifest; import android.annotation.NonNull; import android.annotation.Nullable; +import android.annotation.SuppressLint; import android.app.ActivityManager; import android.app.ActivityManagerInternal; import android.app.AlarmManager; @@ -127,28 +128,47 @@ import java.util.stream.Collectors;
 
    digraph {
+     subgraph cluster_legend {
+       label="Legend"
+
+       wakeup_alarm [label="Entering this state requires a wakeup alarm",color=red,shape=box]
+       nonwakeup_alarm [
+         label="This state can be entered from a non-wakeup alarm",color=blue,shape=oval
+       ]
+       no_alarm [label="This state doesn't require an alarm",color=black,shape=diamond]
+     }
+
      subgraph deep {
        label="deep";
 
-       STATE_ACTIVE [label="STATE_ACTIVE\nScreen on OR Charging OR Alarm going off soon"]
-       STATE_INACTIVE [label="STATE_INACTIVE\nScreen off AND Not charging"]
+       STATE_ACTIVE [
+         label="STATE_ACTIVE\nScreen on OR Charging OR Alarm going off soon",
+         color=black,shape=diamond
+       ]
+       STATE_INACTIVE [
+         label="STATE_INACTIVE\nScreen off AND Not charging",color=black,shape=diamond
+       ]
        STATE_QUICK_DOZE_DELAY [
          label="STATE_QUICK_DOZE_DELAY\n"
              + "Screen off AND Not charging\n"
-             + "Location, motion detection, and significant motion monitoring turned off"
+             + "Location, motion detection, and significant motion monitoring turned off",
+         color=black,shape=diamond
        ]
        STATE_IDLE_PENDING [
-         label="STATE_IDLE_PENDING\nSignificant motion monitoring turned on"
+         label="STATE_IDLE_PENDING\nSignificant motion monitoring turned on",
+         color=red,shape=box
        ]
-       STATE_SENSING [label="STATE_SENSING\nMonitoring for ANY motion"]
+       STATE_SENSING [label="STATE_SENSING\nMonitoring for ANY motion",color=red,shape=box]
        STATE_LOCATING [
-         label="STATE_LOCATING\nRequesting location, motion monitoring still on"
+         label="STATE_LOCATING\nRequesting location, motion monitoring still on",
+         color=red,shape=box
        ]
        STATE_IDLE [
          label="STATE_IDLE\nLocation and motion detection turned off\n"
-             + "Significant motion monitoring state unchanged"
+             + "Significant motion monitoring state unchanged",
+         color=red,shape=box
        ]
-       STATE_IDLE_MAINTENANCE [label="STATE_IDLE_MAINTENANCE\n"]
+       STATE_IDLE_MAINTENANCE [label="STATE_IDLE_MAINTENANCE\n",color=red,shape=box]
 
        STATE_ACTIVE -> STATE_INACTIVE [
          label="becomeInactiveIfAppropriateLocked() AND Quick Doze not enabled"
@@ -213,19 +233,22 @@ import java.util.stream.Collectors;
        label="light"
 
        LIGHT_STATE_ACTIVE [
-         label="LIGHT_STATE_ACTIVE\nScreen on OR Charging OR Alarm going off soon"
+         label="LIGHT_STATE_ACTIVE\nScreen on OR Charging OR Alarm going off soon",
+         color=black,shape=diamond
        ]
-       LIGHT_STATE_INACTIVE [label="LIGHT_STATE_INACTIVE\nScreen off AND Not charging"]
-       LIGHT_STATE_PRE_IDLE [
-         label="LIGHT_STATE_PRE_IDLE\n"
-             + "Delay going into LIGHT_STATE_IDLE due to some running jobs or alarms"
+       LIGHT_STATE_INACTIVE [
+         label="LIGHT_STATE_INACTIVE\nScreen off AND Not charging",
+         color=black,shape=diamond
        ]
-       LIGHT_STATE_IDLE [label="LIGHT_STATE_IDLE\n"]
+       LIGHT_STATE_IDLE [label="LIGHT_STATE_IDLE\n",color=blue,shape=oval]
        LIGHT_STATE_WAITING_FOR_NETWORK [
          label="LIGHT_STATE_WAITING_FOR_NETWORK\n"
-             + "Coming out of LIGHT_STATE_IDLE, waiting for network"
+             + "Coming out of LIGHT_STATE_IDLE, waiting for network",
+         color=black,shape=diamond
+       ]
+       LIGHT_STATE_IDLE_MAINTENANCE [
+         label="LIGHT_STATE_IDLE_MAINTENANCE\n",color=red,shape=box
        ]
-       LIGHT_STATE_IDLE_MAINTENANCE [label="LIGHT_STATE_IDLE_MAINTENANCE\n"]
        LIGHT_STATE_OVERRIDE [
          label="LIGHT_STATE_OVERRIDE\nDevice in deep doze, light no longer changing states"
        ]
@@ -236,16 +259,9 @@ import java.util.stream.Collectors;
        LIGHT_STATE_ACTIVE -> LIGHT_STATE_OVERRIDE [label="deep goes to STATE_IDLE"]
 
        LIGHT_STATE_INACTIVE -> LIGHT_STATE_ACTIVE [label="becomeActiveLocked()"]
-       LIGHT_STATE_INACTIVE -> LIGHT_STATE_PRE_IDLE [label="active jobs"]
-       LIGHT_STATE_INACTIVE -> LIGHT_STATE_IDLE [label="no active jobs"]
+       LIGHT_STATE_INACTIVE -> LIGHT_STATE_IDLE [label="some time transpires"]
        LIGHT_STATE_INACTIVE -> LIGHT_STATE_OVERRIDE [label="deep goes to STATE_IDLE"]
 
-       LIGHT_STATE_PRE_IDLE -> LIGHT_STATE_ACTIVE [label="becomeActiveLocked()"]
-       LIGHT_STATE_PRE_IDLE -> LIGHT_STATE_IDLE [
-         label="stepLightIdleStateLocked(), exitMaintenanceEarlyIfNeededLocked()"
-       ]
-       LIGHT_STATE_PRE_IDLE -> LIGHT_STATE_OVERRIDE [label="deep goes to STATE_IDLE"]
-
        LIGHT_STATE_IDLE -> LIGHT_STATE_ACTIVE [label="becomeActiveLocked()"]
        LIGHT_STATE_IDLE -> LIGHT_STATE_WAITING_FOR_NETWORK [label="no network"]
        LIGHT_STATE_IDLE -> LIGHT_STATE_IDLE_MAINTENANCE
@@ -421,9 +437,6 @@ public class DeviceIdleController extends SystemService
     /** Device is inactive (screen off) and we are waiting to for the first light idle. */
     @VisibleForTesting
     static final int LIGHT_STATE_INACTIVE = 1;
-    /** Device is about to go idle for the first time, wait for current work to complete. */
-    @VisibleForTesting
-    static final int LIGHT_STATE_PRE_IDLE = 3;
     /** Device is in the light idle state, trying to stay asleep as much as possible. */
     @VisibleForTesting
     static final int LIGHT_STATE_IDLE = 4;
@@ -434,7 +447,7 @@ public class DeviceIdleController extends SystemService
     /** Device is in the light idle state, but temporarily out of idle to do regular maintenance. */
     @VisibleForTesting
     static final int LIGHT_STATE_IDLE_MAINTENANCE = 6;
-    /** Device light idle state is overriden, now applying deep doze state. */
+    /** Device light idle state is overridden, now applying deep doze state. */
     @VisibleForTesting
     static final int LIGHT_STATE_OVERRIDE = 7;
 
@@ -443,7 +456,6 @@ public class DeviceIdleController extends SystemService
         switch (state) {
             case LIGHT_STATE_ACTIVE: return "ACTIVE";
             case LIGHT_STATE_INACTIVE: return "INACTIVE";
-            case LIGHT_STATE_PRE_IDLE: return "PRE_IDLE";
             case LIGHT_STATE_IDLE: return "IDLE";
             case LIGHT_STATE_WAITING_FOR_NETWORK: return "WAITING_FOR_NETWORK";
             case LIGHT_STATE_IDLE_MAINTENANCE: return "IDLE_MAINTENANCE";
@@ -468,10 +480,10 @@ public class DeviceIdleController extends SystemService
     @GuardedBy("this")
     private long mNextLightIdleDelay;
     @GuardedBy("this")
-    private long mNextLightIdleDelayFlex;
-    @GuardedBy("this")
     private long mNextLightAlarmTime;
     @GuardedBy("this")
+    private long mNextLightMaintenanceAlarmTime;
+    @GuardedBy("this")
     private long mNextSensingTimeoutAlarmTime;
 
     /** How long a light idle maintenance window should last. */
@@ -658,13 +670,21 @@ public class DeviceIdleController extends SystemService
         }
     };
 
-    private final AlarmManager.OnAlarmListener mLightAlarmListener
-            = new AlarmManager.OnAlarmListener() {
-        @Override
-        public void onAlarm() {
-            synchronized (DeviceIdleController.this) {
-                stepLightIdleStateLocked("s:alarm");
-            }
+    private final AlarmManager.OnAlarmListener mLightAlarmListener = () -> {
+        if (DEBUG) {
+            Slog.d(TAG, "Light progression alarm fired");
+        }
+        synchronized (DeviceIdleController.this) {
+            stepLightIdleStateLocked("s:alarm");
+        }
+    };
+
+    private final AlarmManager.OnAlarmListener mLightMaintenanceAlarmListener = () -> {
+        if (DEBUG) {
+            Slog.d(TAG, "Light maintenance alarm fired");
+        }
+        synchronized (DeviceIdleController.this) {
+            stepLightIdleStateLocked("s:alarm");
         }
     };
 
@@ -928,11 +948,7 @@ public class DeviceIdleController extends SystemService
         private static final String KEY_FLEX_TIME_SHORT = "flex_time_short";
         private static final String KEY_LIGHT_IDLE_AFTER_INACTIVE_TIMEOUT =
                 "light_after_inactive_to";
-        private static final String KEY_LIGHT_PRE_IDLE_TIMEOUT = "light_pre_idle_to";
         private static final String KEY_LIGHT_IDLE_TIMEOUT = "light_idle_to";
-        private static final String KEY_LIGHT_IDLE_TIMEOUT_INITIAL_FLEX =
-                "light_idle_to_initial_flex";
-        private static final String KEY_LIGHT_MAX_IDLE_TIMEOUT_FLEX = "light_max_idle_to_flex";
         private static final String KEY_LIGHT_IDLE_FACTOR = "light_idle_factor";
         private static final String KEY_LIGHT_MAX_IDLE_TIMEOUT = "light_max_idle_to";
         private static final String KEY_LIGHT_IDLE_MAINTENANCE_MIN_BUDGET =
@@ -978,15 +994,9 @@ public class DeviceIdleController extends SystemService
         private static final long DEFAULT_FLEX_TIME_SHORT =
                 !COMPRESS_TIME ? 60 * 1000L : 5 * 1000L;
         private static final long DEFAULT_LIGHT_IDLE_AFTER_INACTIVE_TIMEOUT =
-                !COMPRESS_TIME ? 60 * 1000L : 15 * 1000L;
-        private static final long DEFAULT_LIGHT_PRE_IDLE_TIMEOUT =
-                !COMPRESS_TIME ? 3 * 60 * 1000L : 30 * 1000L;
+                !COMPRESS_TIME ? 4 * 60 * 1000L : 30 * 1000L;
         private static final long DEFAULT_LIGHT_IDLE_TIMEOUT =
                 !COMPRESS_TIME ? 5 * 60 * 1000L : 15 * 1000L;
-        private static final long DEFAULT_LIGHT_IDLE_TIMEOUT_INITIAL_FLEX =
-                !COMPRESS_TIME ? 60 * 1000L : 5 * 1000L;
-        private static final long DEFAULT_LIGHT_MAX_IDLE_TIMEOUT_FLEX =
-                !COMPRESS_TIME ? 15 * 60 * 1000L : 60 * 1000L;
         private static final float DEFAULT_LIGHT_IDLE_FACTOR = 2f;
         private static final long DEFAULT_LIGHT_MAX_IDLE_TIMEOUT =
                 !COMPRESS_TIME ? 15 * 60 * 1000L : 60 * 1000L;
@@ -1053,15 +1063,6 @@ public class DeviceIdleController extends SystemService
          */
         public long LIGHT_IDLE_AFTER_INACTIVE_TIMEOUT = DEFAULT_LIGHT_IDLE_AFTER_INACTIVE_TIMEOUT;
 
-        /**
-         * This is amount of time we will wait from the point where we decide we would
-         * like to go idle until we actually do, while waiting for jobs and other current
-         * activity to finish.
-         *
-         * @see #KEY_LIGHT_PRE_IDLE_TIMEOUT
-         */
-        public long LIGHT_PRE_IDLE_TIMEOUT = DEFAULT_LIGHT_PRE_IDLE_TIMEOUT;
-
         /**
          * This is the initial time that we will run in light idle maintenance mode.
          *
@@ -1069,21 +1070,6 @@ public class DeviceIdleController extends SystemService
          */
         public long LIGHT_IDLE_TIMEOUT = DEFAULT_LIGHT_IDLE_TIMEOUT;
 
-        /**
-         * This is the initial alarm window size that we will tolerate for light idle maintenance
-         * timing.
-         *
-         * @see #KEY_LIGHT_IDLE_TIMEOUT_INITIAL_FLEX
-         */
-        public long LIGHT_IDLE_TIMEOUT_INITIAL_FLEX = DEFAULT_LIGHT_IDLE_TIMEOUT_INITIAL_FLEX;
-
-        /**
-         * This is the maximum value that {@link #LIGHT_IDLE_TIMEOUT_INITIAL_FLEX} should take.
-         *
-         * @see #KEY_LIGHT_MAX_IDLE_TIMEOUT_FLEX
-         */
-        public long LIGHT_MAX_IDLE_TIMEOUT_FLEX = DEFAULT_LIGHT_MAX_IDLE_TIMEOUT_FLEX;
-
         /**
          * Scaling factor to apply to the light idle mode time each time we complete a cycle.
          *
@@ -1327,24 +1313,10 @@ public class DeviceIdleController extends SystemService
                                     KEY_LIGHT_IDLE_AFTER_INACTIVE_TIMEOUT,
                                     DEFAULT_LIGHT_IDLE_AFTER_INACTIVE_TIMEOUT);
                             break;
-                        case KEY_LIGHT_PRE_IDLE_TIMEOUT:
-                            LIGHT_PRE_IDLE_TIMEOUT = properties.getLong(
-                                    KEY_LIGHT_PRE_IDLE_TIMEOUT, DEFAULT_LIGHT_PRE_IDLE_TIMEOUT);
-                            break;
                         case KEY_LIGHT_IDLE_TIMEOUT:
                             LIGHT_IDLE_TIMEOUT = properties.getLong(
                                     KEY_LIGHT_IDLE_TIMEOUT, DEFAULT_LIGHT_IDLE_TIMEOUT);
                             break;
-                        case KEY_LIGHT_IDLE_TIMEOUT_INITIAL_FLEX:
-                            LIGHT_IDLE_TIMEOUT_INITIAL_FLEX = properties.getLong(
-                                    KEY_LIGHT_IDLE_TIMEOUT_INITIAL_FLEX,
-                                    DEFAULT_LIGHT_IDLE_TIMEOUT_INITIAL_FLEX);
-                            break;
-                        case KEY_LIGHT_MAX_IDLE_TIMEOUT_FLEX:
-                            LIGHT_MAX_IDLE_TIMEOUT_FLEX = properties.getLong(
-                                    KEY_LIGHT_MAX_IDLE_TIMEOUT_FLEX,
-                                    DEFAULT_LIGHT_MAX_IDLE_TIMEOUT_FLEX);
-                            break;
                         case KEY_LIGHT_IDLE_FACTOR:
                             LIGHT_IDLE_FACTOR = Math.max(1, properties.getFloat(
                                     KEY_LIGHT_IDLE_FACTOR, DEFAULT_LIGHT_IDLE_FACTOR));
@@ -1497,22 +1469,10 @@ public class DeviceIdleController extends SystemService
             TimeUtils.formatDuration(LIGHT_IDLE_AFTER_INACTIVE_TIMEOUT, pw);
             pw.println();
 
-            pw.print("    "); pw.print(KEY_LIGHT_PRE_IDLE_TIMEOUT); pw.print("=");
-            TimeUtils.formatDuration(LIGHT_PRE_IDLE_TIMEOUT, pw);
-            pw.println();
-
             pw.print("    "); pw.print(KEY_LIGHT_IDLE_TIMEOUT); pw.print("=");
             TimeUtils.formatDuration(LIGHT_IDLE_TIMEOUT, pw);
             pw.println();
 
-            pw.print("    "); pw.print(KEY_LIGHT_IDLE_TIMEOUT_INITIAL_FLEX); pw.print("=");
-            TimeUtils.formatDuration(LIGHT_IDLE_TIMEOUT_INITIAL_FLEX, pw);
-            pw.println();
-
-            pw.print("    "); pw.print(KEY_LIGHT_MAX_IDLE_TIMEOUT_FLEX); pw.print("=");
-            TimeUtils.formatDuration(LIGHT_MAX_IDLE_TIMEOUT_FLEX, pw);
-            pw.println();
-
             pw.print("    "); pw.print(KEY_LIGHT_IDLE_FACTOR); pw.print("=");
             pw.print(LIGHT_IDLE_FACTOR);
             pw.println();
@@ -3088,7 +3048,7 @@ public class DeviceIdleController extends SystemService
             if (conn != mNetworkConnected) {
                 mNetworkConnected = conn;
                 if (conn && mLightState == LIGHT_STATE_WAITING_FOR_NETWORK) {
-                    stepLightIdleStateLocked("network");
+                    stepLightIdleStateLocked("network", /* forceProgression */ true);
                 }
             }
         }
@@ -3343,7 +3303,11 @@ public class DeviceIdleController extends SystemService
             if (DEBUG) Slog.d(TAG, "Moved from LIGHT_STATE_ACTIVE to LIGHT_STATE_INACTIVE");
             resetLightIdleManagementLocked();
             scheduleLightAlarmLocked(mConstants.LIGHT_IDLE_AFTER_INACTIVE_TIMEOUT,
-                    mConstants.FLEX_TIME_SHORT, true);
+                    mConstants.FLEX_TIME_SHORT);
+            // After moving in INACTIVE, the maintenance window should start the time inactive
+            // timeout and a single light idle period.
+            scheduleLightMaintenanceAlarmLocked(
+                    mConstants.LIGHT_IDLE_AFTER_INACTIVE_TIMEOUT + mConstants.LIGHT_IDLE_TIMEOUT);
             EventLogTags.writeDeviceIdleLight(mLightState, "no activity");
         }
     }
@@ -3364,9 +3328,9 @@ public class DeviceIdleController extends SystemService
 
     @GuardedBy("this")
     private void resetLightIdleManagementLocked() {
-        mNextLightIdleDelay = 0;
-        mNextLightIdleDelayFlex = 0;
-        mCurLightIdleBudget = 0;
+        mNextLightIdleDelay = mConstants.LIGHT_IDLE_TIMEOUT;
+        mMaintenanceStartTime = 0;
+        mCurLightIdleBudget = mConstants.LIGHT_IDLE_MAINTENANCE_MIN_BUDGET;
         cancelLightAlarmLocked();
     }
 
@@ -3401,90 +3365,115 @@ public class DeviceIdleController extends SystemService
     }
 
     @GuardedBy("this")
-    void stepLightIdleStateLocked(String reason) {
-        if (mLightState == LIGHT_STATE_OVERRIDE) {
+    private void stepLightIdleStateLocked(String reason) {
+        stepLightIdleStateLocked(reason, false);
+    }
+
+    @GuardedBy("this")
+    @VisibleForTesting
+    @SuppressLint("WakelockTimeout")
+    void stepLightIdleStateLocked(String reason, boolean forceProgression) {
+        if (mLightState == LIGHT_STATE_ACTIVE || mLightState == LIGHT_STATE_OVERRIDE) {
             // If we are already in deep device idle mode, then
             // there is nothing left to do for light mode.
             return;
         }
 
-        if (DEBUG) Slog.d(TAG, "stepLightIdleStateLocked: mLightState=" + mLightState);
+        if (DEBUG) {
+            Slog.d(TAG, "stepLightIdleStateLocked: mLightState=" + lightStateToString(mLightState)
+                    + " force=" + forceProgression);
+        }
         EventLogTags.writeDeviceIdleLightStep();
 
-        switch (mLightState) {
-            case LIGHT_STATE_INACTIVE:
-                mCurLightIdleBudget = mConstants.LIGHT_IDLE_MAINTENANCE_MIN_BUDGET;
-                // Reset the upcoming idle delays.
-                mNextLightIdleDelay = mConstants.LIGHT_IDLE_TIMEOUT;
-                mNextLightIdleDelayFlex = mConstants.LIGHT_IDLE_TIMEOUT_INITIAL_FLEX;
-                mMaintenanceStartTime = 0;
-                if (!isOpsInactiveLocked()) {
-                    // We have some active ops going on...  give them a chance to finish
-                    // before going in to our first idle.
-                    mLightState = LIGHT_STATE_PRE_IDLE;
-                    EventLogTags.writeDeviceIdleLight(mLightState, reason);
-                    scheduleLightAlarmLocked(mConstants.LIGHT_PRE_IDLE_TIMEOUT,
-                            mConstants.FLEX_TIME_SHORT, true);
-                    break;
+        final long nowElapsed = mInjector.getElapsedRealtime();
+        final boolean crossedMaintenanceTime =
+                mNextLightMaintenanceAlarmTime > 0 && nowElapsed >= mNextLightMaintenanceAlarmTime;
+        final boolean crossedProgressionTime =
+                mNextLightAlarmTime > 0 && nowElapsed >= mNextLightAlarmTime;
+        final boolean enterMaintenance;
+        if (crossedMaintenanceTime) {
+            if (crossedProgressionTime) {
+                enterMaintenance = (mNextLightAlarmTime <= mNextLightMaintenanceAlarmTime);
+            } else {
+                enterMaintenance = true;
+            }
+        } else if (crossedProgressionTime) {
+            enterMaintenance = false;
+        } else if (forceProgression) {
+            // This will happen for adb commands, unit tests,
+            // and when we're in WAITING_FOR_NETWORK and the network connects.
+            enterMaintenance =
+                    mLightState == LIGHT_STATE_IDLE
+                            || mLightState == LIGHT_STATE_WAITING_FOR_NETWORK;
+        } else {
+            Slog.wtfStack(TAG, "stepLightIdleStateLocked called in invalid state");
+            return;
+        }
+
+        if (enterMaintenance) {
+            if (mNetworkConnected || mLightState == LIGHT_STATE_WAITING_FOR_NETWORK) {
+                // We have been idling long enough, now it is time to do some work.
+                mActiveIdleOpCount = 1;
+                mActiveIdleWakeLock.acquire();
+                mMaintenanceStartTime = SystemClock.elapsedRealtime();
+                if (mCurLightIdleBudget < mConstants.LIGHT_IDLE_MAINTENANCE_MIN_BUDGET) {
+                    mCurLightIdleBudget = mConstants.LIGHT_IDLE_MAINTENANCE_MIN_BUDGET;
+                } else if (mCurLightIdleBudget > mConstants.LIGHT_IDLE_MAINTENANCE_MAX_BUDGET) {
+                    mCurLightIdleBudget = mConstants.LIGHT_IDLE_MAINTENANCE_MAX_BUDGET;
                 }
-                // Nothing active, fall through to immediately idle.
-            case LIGHT_STATE_PRE_IDLE:
-            case LIGHT_STATE_IDLE_MAINTENANCE:
-                if (mMaintenanceStartTime != 0) {
-                    long duration = SystemClock.elapsedRealtime() - mMaintenanceStartTime;
-                    if (duration < mConstants.LIGHT_IDLE_MAINTENANCE_MIN_BUDGET) {
-                        // We didn't use up all of our minimum budget; add this to the reserve.
-                        mCurLightIdleBudget +=
-                                (mConstants.LIGHT_IDLE_MAINTENANCE_MIN_BUDGET - duration);
-                    } else {
-                        // We used more than our minimum budget; this comes out of the reserve.
-                        mCurLightIdleBudget -=
-                                (duration - mConstants.LIGHT_IDLE_MAINTENANCE_MIN_BUDGET);
-                    }
-                }
-                mMaintenanceStartTime = 0;
-                scheduleLightAlarmLocked(mNextLightIdleDelay, mNextLightIdleDelayFlex, false);
                 mNextLightIdleDelay = Math.min(mConstants.LIGHT_MAX_IDLE_TIMEOUT,
                         (long) (mNextLightIdleDelay * mConstants.LIGHT_IDLE_FACTOR));
-                mNextLightIdleDelayFlex = Math.min(mConstants.LIGHT_MAX_IDLE_TIMEOUT_FLEX,
-                        (long) (mNextLightIdleDelayFlex * mConstants.LIGHT_IDLE_FACTOR));
-                if (DEBUG) Slog.d(TAG, "Moved to LIGHT_STATE_IDLE.");
-                mLightState = LIGHT_STATE_IDLE;
-                EventLogTags.writeDeviceIdleLight(mLightState, reason);
-                addEvent(EVENT_LIGHT_IDLE, null);
-                mGoingIdleWakeLock.acquire();
-                mHandler.sendEmptyMessage(MSG_REPORT_IDLE_ON_LIGHT);
-                break;
-            case LIGHT_STATE_IDLE:
-            case LIGHT_STATE_WAITING_FOR_NETWORK:
-                if (mNetworkConnected || mLightState == LIGHT_STATE_WAITING_FOR_NETWORK) {
-                    // We have been idling long enough, now it is time to do some work.
-                    mActiveIdleOpCount = 1;
-                    mActiveIdleWakeLock.acquire();
-                    mMaintenanceStartTime = SystemClock.elapsedRealtime();
-                    if (mCurLightIdleBudget < mConstants.LIGHT_IDLE_MAINTENANCE_MIN_BUDGET) {
-                        mCurLightIdleBudget = mConstants.LIGHT_IDLE_MAINTENANCE_MIN_BUDGET;
-                    } else if (mCurLightIdleBudget > mConstants.LIGHT_IDLE_MAINTENANCE_MAX_BUDGET) {
-                        mCurLightIdleBudget = mConstants.LIGHT_IDLE_MAINTENANCE_MAX_BUDGET;
-                    }
-                    scheduleLightAlarmLocked(mCurLightIdleBudget, mConstants.FLEX_TIME_SHORT, true);
-                    if (DEBUG) Slog.d(TAG,
-                            "Moved from LIGHT_STATE_IDLE to LIGHT_STATE_IDLE_MAINTENANCE.");
-                    mLightState = LIGHT_STATE_IDLE_MAINTENANCE;
-                    EventLogTags.writeDeviceIdleLight(mLightState, reason);
-                    addEvent(EVENT_LIGHT_MAINTENANCE, null);
-                    mHandler.sendEmptyMessage(MSG_REPORT_IDLE_OFF);
-                } else {
-                    // We'd like to do maintenance, but currently don't have network
-                    // connectivity...  let's try to wait until the network comes back.
-                    // We'll only wait for another full idle period, however, and then give up.
-                    scheduleLightAlarmLocked(mNextLightIdleDelay,
-                            mNextLightIdleDelayFlex / 2, true);
-                    if (DEBUG) Slog.d(TAG, "Moved to LIGHT_WAITING_FOR_NETWORK.");
-                    mLightState = LIGHT_STATE_WAITING_FOR_NETWORK;
-                    EventLogTags.writeDeviceIdleLight(mLightState, reason);
+                // We're entering MAINTENANCE. It should end curLightIdleBudget time from now.
+                // The next maintenance window should be curLightIdleBudget + nextLightIdleDelay
+                // time from now.
+                scheduleLightAlarmLocked(mCurLightIdleBudget, mConstants.FLEX_TIME_SHORT);
+                scheduleLightMaintenanceAlarmLocked(mCurLightIdleBudget + mNextLightIdleDelay);
+                if (DEBUG) {
+                    Slog.d(TAG, "Moved from " + lightStateToString(mLightState)
+                            + " to LIGHT_STATE_IDLE_MAINTENANCE");
                 }
-                break;
+                mLightState = LIGHT_STATE_IDLE_MAINTENANCE;
+                EventLogTags.writeDeviceIdleLight(mLightState, reason);
+                addEvent(EVENT_LIGHT_MAINTENANCE, null);
+                mHandler.sendEmptyMessage(MSG_REPORT_IDLE_OFF);
+            } else {
+                // We'd like to do maintenance, but currently don't have network
+                // connectivity...  let's try to wait until the network comes back.
+                // We'll only wait for another full idle period, however, and then give up.
+                scheduleLightMaintenanceAlarmLocked(mNextLightIdleDelay);
+                mNextLightAlarmTime = 0;
+                if (DEBUG) Slog.d(TAG, "Moved to LIGHT_WAITING_FOR_NETWORK.");
+                mLightState = LIGHT_STATE_WAITING_FOR_NETWORK;
+                EventLogTags.writeDeviceIdleLight(mLightState, reason);
+            }
+        } else {
+            if (mMaintenanceStartTime != 0) {
+                // Cap duration at budget since the non-wakeup alarm to exit maintenance may
+                // not fire at the exact intended time, but once the system is up, we will stop
+                // more ongoing work.
+                long duration = Math.min(mCurLightIdleBudget,
+                        SystemClock.elapsedRealtime() - mMaintenanceStartTime);
+                if (duration < mConstants.LIGHT_IDLE_MAINTENANCE_MIN_BUDGET) {
+                    // We didn't use up all of our minimum budget; add this to the reserve.
+                    mCurLightIdleBudget +=
+                            (mConstants.LIGHT_IDLE_MAINTENANCE_MIN_BUDGET - duration);
+                } else {
+                    // We used more than our minimum budget; this comes out of the reserve.
+                    mCurLightIdleBudget -=
+                            (duration - mConstants.LIGHT_IDLE_MAINTENANCE_MIN_BUDGET);
+                }
+            }
+            mMaintenanceStartTime = 0;
+            // We're entering IDLE. We may have used less than curLightIdleBudget for the
+            // maintenance window, so reschedule the alarm starting from now.
+            scheduleLightMaintenanceAlarmLocked(mNextLightIdleDelay);
+            mNextLightAlarmTime = 0;
+            if (DEBUG) Slog.d(TAG, "Moved to LIGHT_STATE_IDLE.");
+            mLightState = LIGHT_STATE_IDLE;
+            EventLogTags.writeDeviceIdleLight(mLightState, reason);
+            addEvent(EVENT_LIGHT_IDLE, null);
+            mGoingIdleWakeLock.acquire();
+            mHandler.sendEmptyMessage(MSG_REPORT_IDLE_ON_LIGHT);
         }
     }
 
@@ -3839,8 +3828,7 @@ public class DeviceIdleController extends SystemService
 
     @GuardedBy("this")
     void exitMaintenanceEarlyIfNeededLocked() {
-        if (mState == STATE_IDLE_MAINTENANCE || mLightState == LIGHT_STATE_IDLE_MAINTENANCE
-                || mLightState == LIGHT_STATE_PRE_IDLE) {
+        if (mState == STATE_IDLE_MAINTENANCE || mLightState == LIGHT_STATE_IDLE_MAINTENANCE) {
             if (isOpsInactiveLocked()) {
                 final long now = SystemClock.elapsedRealtime();
                 if (DEBUG) {
@@ -3853,10 +3841,8 @@ public class DeviceIdleController extends SystemService
                 }
                 if (mState == STATE_IDLE_MAINTENANCE) {
                     stepIdleStateLocked("s:early");
-                } else if (mLightState == LIGHT_STATE_PRE_IDLE) {
-                    stepLightIdleStateLocked("s:predone");
                 } else {
-                    stepLightIdleStateLocked("s:early");
+                    stepLightIdleStateLocked("s:early", /* forceProgression */ true);
                 }
             }
         }
@@ -3969,6 +3955,10 @@ public class DeviceIdleController extends SystemService
             mNextLightAlarmTime = 0;
             mAlarmManager.cancel(mLightAlarmListener);
         }
+        if (mNextLightMaintenanceAlarmTime != 0) {
+            mNextLightMaintenanceAlarmTime = 0;
+            mAlarmManager.cancel(mLightMaintenanceAlarmListener);
+        }
     }
 
     @GuardedBy("this")
@@ -4035,26 +4025,54 @@ public class DeviceIdleController extends SystemService
     }
 
     @GuardedBy("this")
-    void scheduleLightAlarmLocked(long delay, long flex, boolean wakeup) {
+    @VisibleForTesting
+    void scheduleLightAlarmLocked(long delay, long flex) {
         if (DEBUG) {
             Slog.d(TAG, "scheduleLightAlarmLocked(" + delay
                     + (mConstants.USE_WINDOW_ALARMS ? "/" + flex : "")
-                    + ", wakeup=" + wakeup + ")");
+                    + ")");
         }
-        mNextLightAlarmTime = SystemClock.elapsedRealtime() + delay;
+        mNextLightAlarmTime = mInjector.getElapsedRealtime() + delay;
         if (mConstants.USE_WINDOW_ALARMS) {
             mAlarmManager.setWindow(
-                    wakeup ? AlarmManager.ELAPSED_REALTIME_WAKEUP : AlarmManager.ELAPSED_REALTIME,
+                    AlarmManager.ELAPSED_REALTIME,
                     mNextLightAlarmTime, flex,
                     "DeviceIdleController.light", mLightAlarmListener, mHandler);
         } else {
             mAlarmManager.set(
-                    wakeup ? AlarmManager.ELAPSED_REALTIME_WAKEUP : AlarmManager.ELAPSED_REALTIME,
+                    AlarmManager.ELAPSED_REALTIME,
                     mNextLightAlarmTime,
                     "DeviceIdleController.light", mLightAlarmListener, mHandler);
         }
     }
 
+    @GuardedBy("this")
+    @VisibleForTesting
+    void scheduleLightMaintenanceAlarmLocked(long delay) {
+        if (DEBUG) {
+            Slog.d(TAG, "scheduleLightMaintenanceAlarmLocked(" + delay + ")");
+        }
+        mNextLightMaintenanceAlarmTime = mInjector.getElapsedRealtime() + delay;
+        mAlarmManager.setWindow(
+                AlarmManager.ELAPSED_REALTIME_WAKEUP,
+                mNextLightMaintenanceAlarmTime, mConstants.FLEX_TIME_SHORT,
+                "DeviceIdleController.light", mLightMaintenanceAlarmListener, mHandler);
+    }
+
+    @VisibleForTesting
+    long getNextLightAlarmTimeForTesting() {
+        synchronized (this) {
+            return mNextLightAlarmTime;
+        }
+    }
+
+    @VisibleForTesting
+    long getNextLightMaintenanceAlarmTimeForTesting() {
+        synchronized (this) {
+            return mNextLightMaintenanceAlarmTime;
+        }
+    }
+
     private void scheduleMotionRegistrationAlarmLocked() {
         if (DEBUG) Slog.d(TAG, "scheduleMotionRegistrationAlarmLocked");
         long nextMotionRegistrationAlarmTime =
@@ -4424,7 +4442,7 @@ public class DeviceIdleController extends SystemService
                         pw.print("Stepped to deep: ");
                         pw.println(stateToString(mState));
                     } else if ("light".equals(arg)) {
-                        stepLightIdleStateLocked("s:shell");
+                        stepLightIdleStateLocked("s:shell", /* forceProgression */ true);
                         pw.print("Stepped to light: "); pw.println(lightStateToString(mLightState));
                     } else {
                         pw.println("Unknown idle mode: " + arg);
@@ -4464,7 +4482,7 @@ public class DeviceIdleController extends SystemService
                         becomeInactiveIfAppropriateLocked();
                         int curLightState = mLightState;
                         while (curLightState != LIGHT_STATE_IDLE) {
-                            stepLightIdleStateLocked("s:shell");
+                            stepLightIdleStateLocked("s:shell", /* forceProgression */ true);
                             if (curLightState == mLightState) {
                                 pw.print("Unable to go light idle; stopped at ");
                                 pw.println(lightStateToString(mLightState));
@@ -5076,19 +5094,19 @@ public class DeviceIdleController extends SystemService
             if (mNextLightIdleDelay != 0) {
                 pw.print("  mNextLightIdleDelay=");
                 TimeUtils.formatDuration(mNextLightIdleDelay, pw);
-                if (mConstants.USE_WINDOW_ALARMS) {
-                    pw.print(" (flex=");
-                    TimeUtils.formatDuration(mNextLightIdleDelayFlex, pw);
-                    pw.println(")");
-                } else {
-                    pw.println();
-                }
+                pw.println();
             }
             if (mNextLightAlarmTime != 0) {
                 pw.print("  mNextLightAlarmTime=");
                 TimeUtils.formatDuration(mNextLightAlarmTime, SystemClock.elapsedRealtime(), pw);
                 pw.println();
             }
+            if (mNextLightMaintenanceAlarmTime != 0) {
+                pw.print("  mNextLightMaintenanceAlarmTime=");
+                TimeUtils.formatDuration(
+                        mNextLightMaintenanceAlarmTime, SystemClock.elapsedRealtime(), pw);
+                pw.println();
+            }
             if (mCurLightIdleBudget != 0) {
                 pw.print("  mCurLightIdleBudget=");
                 TimeUtils.formatDuration(mCurLightIdleBudget, pw);
diff --git a/services/tests/mockingservicestests/src/com/android/server/DeviceIdleControllerTest.java b/services/tests/mockingservicestests/src/com/android/server/DeviceIdleControllerTest.java
index d71030802c2b9..ac115a26f9e47 100644
--- a/services/tests/mockingservicestests/src/com/android/server/DeviceIdleControllerTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/DeviceIdleControllerTest.java
@@ -30,7 +30,6 @@ import static com.android.server.DeviceIdleController.LIGHT_STATE_IDLE;
 import static com.android.server.DeviceIdleController.LIGHT_STATE_IDLE_MAINTENANCE;
 import static com.android.server.DeviceIdleController.LIGHT_STATE_INACTIVE;
 import static com.android.server.DeviceIdleController.LIGHT_STATE_OVERRIDE;
-import static com.android.server.DeviceIdleController.LIGHT_STATE_PRE_IDLE;
 import static com.android.server.DeviceIdleController.LIGHT_STATE_WAITING_FOR_NETWORK;
 import static com.android.server.DeviceIdleController.MSG_REPORT_STATIONARY_STATUS;
 import static com.android.server.DeviceIdleController.MSG_RESET_PRE_IDLE_TIMEOUT_FACTOR;
@@ -112,6 +111,7 @@ import java.util.concurrent.Executor;
 /**
  * Tests for {@link com.android.server.DeviceIdleController}.
  */
+@SuppressWarnings("GuardedBy")
 @RunWith(AndroidJUnit4.class)
 public class DeviceIdleControllerTest {
     private DeviceIdleController mDeviceIdleController;
@@ -875,7 +875,7 @@ public class DeviceIdleControllerTest {
     @Test
     public void testLightStepIdleStateLocked_InvalidStates() {
         mDeviceIdleController.becomeActiveLocked("testing", 0);
-        mDeviceIdleController.stepLightIdleStateLocked("testing");
+        mDeviceIdleController.stepLightIdleStateLocked("testing", true);
         // stepLightIdleStateLocked doesn't handle the ACTIVE case, so the state
         // should stay as ACTIVE.
         verifyLightStateConditions(LIGHT_STATE_ACTIVE);
@@ -888,7 +888,7 @@ public class DeviceIdleControllerTest {
     @Test
     public void testLightStepIdleStateLocked_Overriden() {
         enterLightState(LIGHT_STATE_OVERRIDE);
-        mDeviceIdleController.stepLightIdleStateLocked("testing");
+        mDeviceIdleController.stepLightIdleStateLocked("testing", true);
         verifyLightStateConditions(LIGHT_STATE_OVERRIDE);
     }
 
@@ -906,18 +906,18 @@ public class DeviceIdleControllerTest {
         verifyLightStateConditions(LIGHT_STATE_INACTIVE);
 
         // No active ops means INACTIVE should go straight to IDLE.
-        mDeviceIdleController.stepLightIdleStateLocked("testing");
+        mDeviceIdleController.stepLightIdleStateLocked("testing", true);
         verifyLightStateConditions(LIGHT_STATE_IDLE);
 
         // Should just alternate between IDLE and IDLE_MAINTENANCE now.
 
-        mDeviceIdleController.stepLightIdleStateLocked("testing");
+        mDeviceIdleController.stepLightIdleStateLocked("testing", true);
         verifyLightStateConditions(LIGHT_STATE_IDLE_MAINTENANCE);
 
-        mDeviceIdleController.stepLightIdleStateLocked("testing");
+        mDeviceIdleController.stepLightIdleStateLocked("testing", true);
         verifyLightStateConditions(LIGHT_STATE_IDLE);
 
-        mDeviceIdleController.stepLightIdleStateLocked("testing");
+        mDeviceIdleController.stepLightIdleStateLocked("testing", true);
         verifyLightStateConditions(LIGHT_STATE_IDLE_MAINTENANCE);
     }
 
@@ -930,26 +930,22 @@ public class DeviceIdleControllerTest {
         setScreenOn(false);
         verifyLightStateConditions(LIGHT_STATE_INACTIVE);
 
-        // Active ops means INACTIVE should go to PRE_IDLE to wait.
+        // After enough time, INACTIVE should go to IDLE regardless of any active ops.
         mDeviceIdleController.setJobsActive(true);
         mDeviceIdleController.setAlarmsActive(true);
         mDeviceIdleController.setActiveIdleOpsForTest(1);
-        mDeviceIdleController.stepLightIdleStateLocked("testing");
-        verifyLightStateConditions(LIGHT_STATE_PRE_IDLE);
-
-        // Even with active ops, PRE_IDLE should go to IDLE.
-        mDeviceIdleController.stepLightIdleStateLocked("testing");
+        mDeviceIdleController.stepLightIdleStateLocked("testing", true);
         verifyLightStateConditions(LIGHT_STATE_IDLE);
 
         // Should just alternate between IDLE and IDLE_MAINTENANCE now.
 
-        mDeviceIdleController.stepLightIdleStateLocked("testing");
+        mDeviceIdleController.stepLightIdleStateLocked("testing", true);
         verifyLightStateConditions(LIGHT_STATE_IDLE_MAINTENANCE);
 
-        mDeviceIdleController.stepLightIdleStateLocked("testing");
+        mDeviceIdleController.stepLightIdleStateLocked("testing", true);
         verifyLightStateConditions(LIGHT_STATE_IDLE);
 
-        mDeviceIdleController.stepLightIdleStateLocked("testing");
+        mDeviceIdleController.stepLightIdleStateLocked("testing", true);
         verifyLightStateConditions(LIGHT_STATE_IDLE_MAINTENANCE);
     }
 
@@ -967,24 +963,24 @@ public class DeviceIdleControllerTest {
         verifyLightStateConditions(LIGHT_STATE_INACTIVE);
 
         // No active ops means INACTIVE should go straight to IDLE.
-        mDeviceIdleController.stepLightIdleStateLocked("testing");
+        mDeviceIdleController.stepLightIdleStateLocked("testing", true);
         verifyLightStateConditions(LIGHT_STATE_IDLE);
 
         // Should cycle between IDLE, WAITING_FOR_NETWORK, and IDLE_MAINTENANCE now.
 
-        mDeviceIdleController.stepLightIdleStateLocked("testing");
+        mDeviceIdleController.stepLightIdleStateLocked("testing", true);
         verifyLightStateConditions(LIGHT_STATE_WAITING_FOR_NETWORK);
 
-        mDeviceIdleController.stepLightIdleStateLocked("testing");
+        mDeviceIdleController.stepLightIdleStateLocked("testing", true);
         verifyLightStateConditions(LIGHT_STATE_IDLE_MAINTENANCE);
 
-        mDeviceIdleController.stepLightIdleStateLocked("testing");
+        mDeviceIdleController.stepLightIdleStateLocked("testing", true);
         verifyLightStateConditions(LIGHT_STATE_IDLE);
 
-        mDeviceIdleController.stepLightIdleStateLocked("testing");
+        mDeviceIdleController.stepLightIdleStateLocked("testing", true);
         verifyLightStateConditions(LIGHT_STATE_WAITING_FOR_NETWORK);
 
-        mDeviceIdleController.stepLightIdleStateLocked("testing");
+        mDeviceIdleController.stepLightIdleStateLocked("testing", true);
         verifyLightStateConditions(LIGHT_STATE_IDLE_MAINTENANCE);
     }
 
@@ -997,35 +993,176 @@ public class DeviceIdleControllerTest {
         setScreenOn(false);
         verifyLightStateConditions(LIGHT_STATE_INACTIVE);
 
-        // Active ops means INACTIVE should go to PRE_IDLE to wait.
+        // After enough time, INACTIVE should go to IDLE regardless of any active ops.
         mDeviceIdleController.setJobsActive(true);
         mDeviceIdleController.setAlarmsActive(true);
         mDeviceIdleController.setActiveIdleOpsForTest(1);
-        mDeviceIdleController.stepLightIdleStateLocked("testing");
-        verifyLightStateConditions(LIGHT_STATE_PRE_IDLE);
-
-        // Even with active ops, PRE_IDLE should go to IDLE.
-        mDeviceIdleController.stepLightIdleStateLocked("testing");
+        mDeviceIdleController.stepLightIdleStateLocked("testing", true);
         verifyLightStateConditions(LIGHT_STATE_IDLE);
 
         // Should cycle between IDLE, WAITING_FOR_NETWORK, and IDLE_MAINTENANCE now.
 
-        mDeviceIdleController.stepLightIdleStateLocked("testing");
+        mDeviceIdleController.stepLightIdleStateLocked("testing", true);
         verifyLightStateConditions(LIGHT_STATE_WAITING_FOR_NETWORK);
 
-        mDeviceIdleController.stepLightIdleStateLocked("testing");
+        mDeviceIdleController.stepLightIdleStateLocked("testing", true);
         verifyLightStateConditions(LIGHT_STATE_IDLE_MAINTENANCE);
 
-        mDeviceIdleController.stepLightIdleStateLocked("testing");
+        mDeviceIdleController.stepLightIdleStateLocked("testing", true);
         verifyLightStateConditions(LIGHT_STATE_IDLE);
 
-        mDeviceIdleController.stepLightIdleStateLocked("testing");
+        mDeviceIdleController.stepLightIdleStateLocked("testing", true);
         verifyLightStateConditions(LIGHT_STATE_WAITING_FOR_NETWORK);
 
-        mDeviceIdleController.stepLightIdleStateLocked("testing");
+        mDeviceIdleController.stepLightIdleStateLocked("testing", true);
         verifyLightStateConditions(LIGHT_STATE_IDLE_MAINTENANCE);
     }
 
+    @Test
+    public void testLightStepIdleStateSkippedAlarms() {
+        setNetworkConnected(true);
+        mDeviceIdleController.setJobsActive(false);
+        mDeviceIdleController.setAlarmsActive(false);
+        mDeviceIdleController.setActiveIdleOpsForTest(0);
+
+        final ArgumentCaptor alarmListenerCaptor = ArgumentCaptor
+                .forClass(AlarmManager.OnAlarmListener.class);
+        doNothing().when(mAlarmManager).setWindow(anyInt(), anyLong(), anyLong(),
+                eq("DeviceIdleController.light"), alarmListenerCaptor.capture(), any());
+        doNothing().when(mAlarmManager).setWindow(anyInt(), anyLong(), anyLong(),
+                eq("DeviceIdleController.light"), alarmListenerCaptor.capture(), any());
+
+        // Set state to INACTIVE.
+        mDeviceIdleController.becomeActiveLocked("testing", 0);
+        setChargingOn(false);
+        setScreenOn(false);
+        verifyLightStateConditions(LIGHT_STATE_INACTIVE);
+
+        final AlarmManager.OnAlarmListener progressionListener =
+                alarmListenerCaptor.getAllValues().get(0);
+        final AlarmManager.OnAlarmListener maintenanceListener =
+                alarmListenerCaptor.getAllValues().get(1);
+
+        // Set things to make it look like the INACTIVE -> IDLE alarm didn't fire and the
+        // MAINTENANCE alarm just fired.
+        mInjector.nowElapsed = mDeviceIdleController.getNextLightMaintenanceAlarmTimeForTesting();
+        // If the non-wakeup alarm doesn't fire in a timely manner, we would see both fire at the
+        // same time.
+        progressionListener.onAlarm();
+        verifyLightStateConditions(LIGHT_STATE_IDLE_MAINTENANCE);
+        maintenanceListener.onAlarm();
+        verifyLightStateConditions(LIGHT_STATE_IDLE_MAINTENANCE);
+
+        assertTrue(mInjector.nowElapsed < mDeviceIdleController.getNextLightAlarmTimeForTesting());
+
+        // MAINTENANCE->IDLE alarm goes off at correct time.
+        mInjector.nowElapsed = mDeviceIdleController.getNextLightAlarmTimeForTesting();
+        progressionListener.onAlarm();
+        verifyLightStateConditions(LIGHT_STATE_IDLE);
+
+        // Go back to MAINTENANCE
+        mInjector.nowElapsed = mDeviceIdleController.getNextLightMaintenanceAlarmTimeForTesting();
+        maintenanceListener.onAlarm();
+        verifyLightStateConditions(LIGHT_STATE_IDLE_MAINTENANCE);
+
+        assertTrue(mInjector.nowElapsed < mDeviceIdleController.getNextLightAlarmTimeForTesting());
+        assertTrue(mInjector.nowElapsed
+                < mDeviceIdleController.getNextLightMaintenanceAlarmTimeForTesting());
+
+        // MAINTENANCE->IDLE alarm is delayed until IDLE->MAINTENANCE alarm goes off.
+        mInjector.nowElapsed = mDeviceIdleController.getNextLightMaintenanceAlarmTimeForTesting();
+        progressionListener.onAlarm();
+        verifyLightStateConditions(LIGHT_STATE_IDLE_MAINTENANCE);
+        maintenanceListener.onAlarm();
+        verifyLightStateConditions(LIGHT_STATE_IDLE_MAINTENANCE);
+    }
+
+    @Test
+    public void testLightStepIdleStateIdlingTimeIncreases() {
+        final long maintenanceTimeMs = 60_000L;
+        mConstants.LIGHT_IDLE_MAINTENANCE_MIN_BUDGET = maintenanceTimeMs;
+        mConstants.LIGHT_IDLE_MAINTENANCE_MAX_BUDGET = maintenanceTimeMs;
+        mConstants.LIGHT_IDLE_TIMEOUT = 5 * 60_000L;
+        mConstants.LIGHT_MAX_IDLE_TIMEOUT = 20 * 60_000L;
+        mConstants.LIGHT_IDLE_FACTOR = 2f;
+
+        setNetworkConnected(true);
+        mDeviceIdleController.setJobsActive(false);
+        mDeviceIdleController.setAlarmsActive(false);
+        mDeviceIdleController.setActiveIdleOpsForTest(0);
+
+        InOrder alarmManagerInOrder = inOrder(mAlarmManager);
+
+        final ArgumentCaptor alarmListenerCaptor = ArgumentCaptor
+                .forClass(AlarmManager.OnAlarmListener.class);
+        doNothing().when(mAlarmManager).setWindow(anyInt(), anyLong(), anyLong(),
+                eq("DeviceIdleController.light"), alarmListenerCaptor.capture(), any());
+        doNothing().when(mAlarmManager).setWindow(anyInt(), anyLong(), anyLong(),
+                eq("DeviceIdleController.light"), alarmListenerCaptor.capture(), any());
+
+        // Set state to INACTIVE.
+        mDeviceIdleController.becomeActiveLocked("testing", 0);
+        setChargingOn(false);
+        setScreenOn(false);
+        verifyLightStateConditions(LIGHT_STATE_INACTIVE);
+        long idlingTimeMs = mConstants.LIGHT_IDLE_TIMEOUT;
+        final long idleAfterInactiveExpiryTime =
+                mInjector.nowElapsed + mConstants.LIGHT_IDLE_AFTER_INACTIVE_TIMEOUT;
+        alarmManagerInOrder.verify(mAlarmManager).setWindow(
+                eq(AlarmManager.ELAPSED_REALTIME),
+                eq(idleAfterInactiveExpiryTime),
+                anyLong(), anyString(), any(), any());
+        // Maintenance alarm
+        alarmManagerInOrder.verify(mAlarmManager).setWindow(
+                eq(AlarmManager.ELAPSED_REALTIME_WAKEUP),
+                eq(idleAfterInactiveExpiryTime + idlingTimeMs),
+                anyLong(), anyString(), any(), any());
+
+        final AlarmManager.OnAlarmListener progressionListener =
+                alarmListenerCaptor.getAllValues().get(0);
+        final AlarmManager.OnAlarmListener maintenanceListener =
+                alarmListenerCaptor.getAllValues().get(1);
+
+        // INACTIVE -> IDLE alarm
+        mInjector.nowElapsed = mDeviceIdleController.getNextLightAlarmTimeForTesting();
+        progressionListener.onAlarm();
+        verifyLightStateConditions(LIGHT_STATE_IDLE);
+        alarmManagerInOrder.verify(mAlarmManager).setWindow(
+                eq(AlarmManager.ELAPSED_REALTIME_WAKEUP),
+                eq(mInjector.nowElapsed + idlingTimeMs),
+                anyLong(), anyString(), any(), any());
+
+        for (int i = 0; i < 2; ++i) {
+            // IDLE->MAINTENANCE alarm
+            mInjector.nowElapsed =
+                    mDeviceIdleController.getNextLightMaintenanceAlarmTimeForTesting();
+            maintenanceListener.onAlarm();
+            verifyLightStateConditions(LIGHT_STATE_IDLE_MAINTENANCE);
+            long maintenanceExpiryTime = mInjector.nowElapsed + maintenanceTimeMs;
+            idlingTimeMs *= mConstants.LIGHT_IDLE_FACTOR;
+            // Set MAINTENANCE->IDLE
+            alarmManagerInOrder.verify(mAlarmManager).setWindow(
+                    eq(AlarmManager.ELAPSED_REALTIME),
+                    eq(maintenanceExpiryTime),
+                    anyLong(), anyString(), any(), any());
+            // Set IDLE->MAINTENANCE
+            alarmManagerInOrder.verify(mAlarmManager).setWindow(
+                    eq(AlarmManager.ELAPSED_REALTIME_WAKEUP),
+                    eq(maintenanceExpiryTime + idlingTimeMs),
+                    anyLong(), anyString(), any(), any());
+
+            // MAINTENANCE->IDLE alarm
+            mInjector.nowElapsed = mDeviceIdleController.getNextLightAlarmTimeForTesting();
+            progressionListener.onAlarm();
+            verifyLightStateConditions(LIGHT_STATE_IDLE);
+            // Set IDLE->MAINTENANCE again
+            alarmManagerInOrder.verify(mAlarmManager).setWindow(
+                    eq(AlarmManager.ELAPSED_REALTIME_WAKEUP),
+                    eq(mInjector.nowElapsed + idlingTimeMs),
+                    anyLong(), anyString(), any(), any());
+        }
+    }
+
     @Test
     public void testLightIdleAlarmUnaffectedByMotion() {
         setNetworkConnected(true);
@@ -1043,45 +1180,37 @@ public class DeviceIdleControllerTest {
         verifyLightStateConditions(LIGHT_STATE_INACTIVE);
 
         // No active ops means INACTIVE should go straight to IDLE.
-        mDeviceIdleController.stepLightIdleStateLocked("testing");
+        mDeviceIdleController.stepLightIdleStateLocked("testing", true);
         verifyLightStateConditions(LIGHT_STATE_IDLE);
-        inOrder.verify(mDeviceIdleController).scheduleLightAlarmLocked(
-                longThat(l -> l == mConstants.LIGHT_IDLE_TIMEOUT),
-                longThat(l -> l == mConstants.LIGHT_IDLE_TIMEOUT_INITIAL_FLEX),
-                eq(false));
+        inOrder.verify(mDeviceIdleController).scheduleLightMaintenanceAlarmLocked(
+                longThat(l -> l == mConstants.LIGHT_IDLE_TIMEOUT));
 
         // Should just alternate between IDLE and IDLE_MAINTENANCE now.
 
-        mDeviceIdleController.stepLightIdleStateLocked("testing");
+        mDeviceIdleController.stepLightIdleStateLocked("testing", true);
         verifyLightStateConditions(LIGHT_STATE_IDLE_MAINTENANCE);
         inOrder.verify(mDeviceIdleController).scheduleLightAlarmLocked(
                 longThat(l -> l >= mConstants.LIGHT_IDLE_MAINTENANCE_MIN_BUDGET),
-                longThat(l -> l == mConstants.FLEX_TIME_SHORT),
-                eq(true));
+                longThat(l -> l == mConstants.FLEX_TIME_SHORT));
 
-        mDeviceIdleController.stepLightIdleStateLocked("testing");
+        mDeviceIdleController.stepLightIdleStateLocked("testing", true);
         verifyLightStateConditions(LIGHT_STATE_IDLE);
-        inOrder.verify(mDeviceIdleController).scheduleLightAlarmLocked(
-                longThat(l -> l > mConstants.LIGHT_IDLE_TIMEOUT),
-                longThat(l -> l > mConstants.LIGHT_IDLE_TIMEOUT_INITIAL_FLEX),
-                eq(false));
+        inOrder.verify(mDeviceIdleController).scheduleLightMaintenanceAlarmLocked(
+                longThat(l -> l > mConstants.LIGHT_IDLE_TIMEOUT));
 
-        mDeviceIdleController.stepLightIdleStateLocked("testing");
+        mDeviceIdleController.stepLightIdleStateLocked("testing", true);
         verifyLightStateConditions(LIGHT_STATE_IDLE_MAINTENANCE);
         inOrder.verify(mDeviceIdleController).scheduleLightAlarmLocked(
                 longThat(l -> l >= mConstants.LIGHT_IDLE_MAINTENANCE_MIN_BUDGET),
-                longThat(l -> l == mConstants.FLEX_TIME_SHORT),
-                eq(true));
+                longThat(l -> l == mConstants.FLEX_TIME_SHORT));
 
         // Test that motion doesn't reset the idle timeout.
         mDeviceIdleController.handleMotionDetectedLocked(50, "test");
 
-        mDeviceIdleController.stepLightIdleStateLocked("testing");
+        mDeviceIdleController.stepLightIdleStateLocked("testing", true);
         verifyLightStateConditions(LIGHT_STATE_IDLE);
-        inOrder.verify(mDeviceIdleController).scheduleLightAlarmLocked(
-                longThat(l -> l > mConstants.LIGHT_IDLE_TIMEOUT),
-                longThat(l -> l > mConstants.LIGHT_IDLE_TIMEOUT_INITIAL_FLEX),
-                eq(false));
+        inOrder.verify(mDeviceIdleController).scheduleLightMaintenanceAlarmLocked(
+                longThat(l -> l > mConstants.LIGHT_IDLE_TIMEOUT));
     }
 
     ///////////////// EXIT conditions ///////////////////
@@ -1268,10 +1397,6 @@ public class DeviceIdleControllerTest {
         mDeviceIdleController.exitMaintenanceEarlyIfNeededLocked();
         verifyLightStateConditions(LIGHT_STATE_INACTIVE);
 
-        enterLightState(LIGHT_STATE_PRE_IDLE);
-        mDeviceIdleController.exitMaintenanceEarlyIfNeededLocked();
-        verifyLightStateConditions(LIGHT_STATE_IDLE);
-
         enterLightState(LIGHT_STATE_IDLE);
         mDeviceIdleController.exitMaintenanceEarlyIfNeededLocked();
         verifyLightStateConditions(LIGHT_STATE_IDLE);
@@ -1307,10 +1432,6 @@ public class DeviceIdleControllerTest {
         mDeviceIdleController.exitMaintenanceEarlyIfNeededLocked();
         verifyLightStateConditions(LIGHT_STATE_INACTIVE);
 
-        enterLightState(LIGHT_STATE_PRE_IDLE);
-        mDeviceIdleController.exitMaintenanceEarlyIfNeededLocked();
-        verifyLightStateConditions(LIGHT_STATE_PRE_IDLE);
-
         enterLightState(LIGHT_STATE_IDLE);
         mDeviceIdleController.exitMaintenanceEarlyIfNeededLocked();
         verifyLightStateConditions(LIGHT_STATE_IDLE);
@@ -1344,10 +1465,6 @@ public class DeviceIdleControllerTest {
         mDeviceIdleController.exitMaintenanceEarlyIfNeededLocked();
         verifyLightStateConditions(LIGHT_STATE_INACTIVE);
 
-        enterLightState(LIGHT_STATE_PRE_IDLE);
-        mDeviceIdleController.exitMaintenanceEarlyIfNeededLocked();
-        verifyLightStateConditions(LIGHT_STATE_PRE_IDLE);
-
         enterLightState(LIGHT_STATE_IDLE);
         mDeviceIdleController.exitMaintenanceEarlyIfNeededLocked();
         verifyLightStateConditions(LIGHT_STATE_IDLE);
@@ -1381,10 +1498,6 @@ public class DeviceIdleControllerTest {
         mDeviceIdleController.exitMaintenanceEarlyIfNeededLocked();
         verifyLightStateConditions(LIGHT_STATE_INACTIVE);
 
-        enterLightState(LIGHT_STATE_PRE_IDLE);
-        mDeviceIdleController.exitMaintenanceEarlyIfNeededLocked();
-        verifyLightStateConditions(LIGHT_STATE_PRE_IDLE);
-
         enterLightState(LIGHT_STATE_IDLE);
         mDeviceIdleController.exitMaintenanceEarlyIfNeededLocked();
         verifyLightStateConditions(LIGHT_STATE_IDLE);
@@ -1510,10 +1623,6 @@ public class DeviceIdleControllerTest {
         mDeviceIdleController.handleMotionDetectedLocked(50, "test");
         verifyLightStateConditions(LIGHT_STATE_INACTIVE);
 
-        enterLightState(LIGHT_STATE_PRE_IDLE);
-        mDeviceIdleController.handleMotionDetectedLocked(50, "test");
-        verifyLightStateConditions(LIGHT_STATE_PRE_IDLE);
-
         enterLightState(LIGHT_STATE_IDLE);
         mDeviceIdleController.handleMotionDetectedLocked(50, "test");
         verifyLightStateConditions(LIGHT_STATE_IDLE);
@@ -1580,10 +1689,6 @@ public class DeviceIdleControllerTest {
         mDeviceIdleController.becomeActiveLocked("test", 1000);
         verifyLightStateConditions(LIGHT_STATE_ACTIVE);
 
-        enterLightState(LIGHT_STATE_PRE_IDLE);
-        mDeviceIdleController.becomeActiveLocked("test", 1000);
-        verifyLightStateConditions(LIGHT_STATE_ACTIVE);
-
         enterLightState(LIGHT_STATE_IDLE);
         mDeviceIdleController.becomeActiveLocked("test", 1000);
         verifyLightStateConditions(LIGHT_STATE_ACTIVE);
@@ -2059,7 +2164,7 @@ public class DeviceIdleControllerTest {
                 while (mDeviceIdleController.getLightState() != lightState) {
                     // Stepping through each state ensures that the proper features are turned
                     // on/off.
-                    mDeviceIdleController.stepLightIdleStateLocked("testing");
+                    mDeviceIdleController.stepLightIdleStateLocked("testing", true);
 
                     count++;
                     if (count > 10) {
@@ -2068,7 +2173,6 @@ public class DeviceIdleControllerTest {
                     }
                 }
                 break;
-            case LIGHT_STATE_PRE_IDLE:
             case LIGHT_STATE_WAITING_FOR_NETWORK:
             case LIGHT_STATE_OVERRIDE:
                 setScreenOn(false);
@@ -2213,7 +2317,6 @@ public class DeviceIdleControllerTest {
                                 > mAlarmManager.getNextWakeFromIdleTime());
                 break;
             case LIGHT_STATE_INACTIVE:
-            case LIGHT_STATE_PRE_IDLE:
             case LIGHT_STATE_IDLE:
             case LIGHT_STATE_WAITING_FOR_NETWORK:
             case LIGHT_STATE_IDLE_MAINTENANCE: