Merge "Doze: Refactor v1"
diff --git a/packages/SystemUI/src/com/android/systemui/doze/DozeFactory.java b/packages/SystemUI/src/com/android/systemui/doze/DozeFactory.java
new file mode 100644
index 0000000..4cfc811
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/doze/DozeFactory.java
@@ -0,0 +1,83 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.doze;
+
+import android.app.Application;
+import android.content.Context;
+import android.hardware.SensorManager;
+import android.os.Handler;
+import android.os.PowerManager;
+
+import com.android.internal.hardware.AmbientDisplayConfiguration;
+import com.android.systemui.SystemUIApplication;
+import com.android.systemui.statusbar.phone.DozeParameters;
+
+public class DozeFactory {
+
+    /** Creates a DozeMachine with its parts for {@code dozeService}. */
+    public static DozeMachine assembleMachine(DozeService dozeService) {
+        Context context = dozeService;
+        SensorManager sensorManager = context.getSystemService(SensorManager.class);
+        PowerManager powerManager = context.getSystemService(PowerManager.class);
+
+        DozeHost host = getHost(dozeService);
+        AmbientDisplayConfiguration config = new AmbientDisplayConfiguration(context);
+        DozeParameters params = new DozeParameters(context);
+        Handler handler = new Handler();
+        DozeFactory.WakeLock wakeLock = new DozeFactory.WakeLock(powerManager.newWakeLock(
+                PowerManager.PARTIAL_WAKE_LOCK, "Doze"));
+
+        DozeMachine machine = new DozeMachine(dozeService, params, wakeLock);
+        machine.setParts(new DozeMachine.Part[]{
+                new DozeTriggers(context, machine, host, config, params,
+                        sensorManager, handler, wakeLock),
+                new DozeUi(context, machine, wakeLock, host),
+        });
+
+        return machine;
+    }
+
+    private static DozeHost getHost(DozeService service) {
+        Application appCandidate = service.getApplication();
+        final SystemUIApplication app = (SystemUIApplication) appCandidate;
+        return app.getComponent(DozeHost.class);
+    }
+
+    /** A wrapper around {@link PowerManager.WakeLock} for testability. */
+    public static class WakeLock {
+        private final PowerManager.WakeLock mInner;
+
+        public WakeLock(PowerManager.WakeLock inner) {
+            mInner = inner;
+        }
+
+        /** @see PowerManager.WakeLock#acquire() */
+        public void acquire() {
+            mInner.acquire();
+        }
+
+        /** @see PowerManager.WakeLock#release() */
+        public void release() {
+            mInner.release();
+        }
+
+        /** @see PowerManager.WakeLock#wrap(Runnable) */
+        public Runnable wrap(Runnable runnable) {
+            return mInner.wrap(runnable);
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/doze/DozeHost.java b/packages/SystemUI/src/com/android/systemui/doze/DozeHost.java
index 02a98b0..fb940b5 100644
--- a/packages/SystemUI/src/com/android/systemui/doze/DozeHost.java
+++ b/packages/SystemUI/src/com/android/systemui/doze/DozeHost.java
@@ -24,7 +24,7 @@
 public interface DozeHost {
     void addCallback(@NonNull Callback callback);
     void removeCallback(@NonNull Callback callback);
-    void startDozing(@NonNull Runnable ready);
+    void startDozing();
     void pulseWhileDozing(@NonNull PulseCallback callback, int reason);
     void stopDozing();
     void dozeTimeTick();
diff --git a/packages/SystemUI/src/com/android/systemui/doze/DozeMachine.java b/packages/SystemUI/src/com/android/systemui/doze/DozeMachine.java
new file mode 100644
index 0000000..c633aa1
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/doze/DozeMachine.java
@@ -0,0 +1,283 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.doze;
+
+import android.annotation.MainThread;
+import android.util.Log;
+import android.view.Display;
+
+import com.android.internal.util.Preconditions;
+import com.android.systemui.statusbar.phone.DozeParameters;
+import com.android.systemui.util.Assert;
+
+import java.io.PrintWriter;
+import java.util.ArrayList;
+
+/**
+ * Orchestrates all things doze.
+ *
+ * DozeMachine implements a state machine that orchestrates how the UI and triggers work and
+ * interfaces with the power and screen states.
+ *
+ * During state transitions and in certain states, DozeMachine holds a wake lock.
+ */
+public class DozeMachine {
+
+    static final String TAG = "DozeMachine";
+    static final boolean DEBUG = DozeService.DEBUG;
+
+    enum State {
+        /** Default state. Transition to INITIALIZED to get Doze going. */
+        UNINITIALIZED,
+        /** Doze components are set up. Followed by transition to DOZE or DOZE_AOD. */
+        INITIALIZED,
+        /** Regular doze. Device is asleep and listening for pulse triggers. */
+        DOZE,
+        /** Always-on doze. Device is asleep, showing UI and listening for pulse triggers. */
+        DOZE_AOD,
+        /** Pulse has been requested. Device is awake and preparing UI */
+        DOZE_REQUEST_PULSE,
+        /** Pulse is showing. Device is awake and showing UI. */
+        DOZE_PULSING,
+        /** Pulse is done showing. Followed by transition to DOZE or DOZE_AOD. */
+        DOZE_PULSE_DONE,
+        /** Doze is done. DozeService is finished. */
+        FINISH,
+    }
+
+    private final Service mDozeService;
+    private final DozeFactory.WakeLock mWakeLock;
+    private final DozeParameters mParams;
+    private Part[] mParts;
+
+    private final ArrayList<State> mQueuedRequests = new ArrayList<>();
+    private State mState = State.UNINITIALIZED;
+    private boolean mWakeLockHeldForCurrentState = false;
+
+    public DozeMachine(Service service, DozeParameters params, DozeFactory.WakeLock wakeLock) {
+        mDozeService = service;
+        mParams = params;
+        mWakeLock = wakeLock;
+    }
+
+    /** Initializes the set of {@link Part}s. Must be called exactly once after construction. */
+    public void setParts(Part[] parts) {
+        Preconditions.checkState(mParts == null);
+        mParts = parts;
+    }
+
+    /**
+     * Requests transitioning to {@code requestedState}.
+     *
+     * This can be called during a state transition, in which case it will be queued until all
+     * queued state transitions are done.
+     *
+     * A wake lock is held while the transition is happening.
+     *
+     * Note that {@link #transitionPolicy} can modify what state will be transitioned to.
+     */
+    @MainThread
+    public void requestState(State requestedState) {
+        Assert.isMainThread();
+        if (DEBUG) {
+            Log.i(TAG, "request: current=" + mState + " req=" + requestedState,
+                    new Throwable("here"));
+        }
+
+        boolean runNow = !isExecutingTransition();
+        mQueuedRequests.add(requestedState);
+        if (runNow) {
+            mWakeLock.acquire();
+            for (int i = 0; i < mQueuedRequests.size(); i++) {
+                // Transitions in Parts can call back into requestState, which will
+                // cause mQueuedRequests to grow.
+                transitionTo(mQueuedRequests.get(i));
+            }
+            mQueuedRequests.clear();
+            mWakeLock.release();
+        }
+    }
+
+    /**
+     * @return the current state.
+     *
+     * This must not be called during a transition.
+     */
+    @MainThread
+    public State getState() {
+        Assert.isMainThread();
+        Preconditions.checkState(!isExecutingTransition());
+        return mState;
+    }
+
+    private boolean isExecutingTransition() {
+        return !mQueuedRequests.isEmpty();
+    }
+
+    private void transitionTo(State requestedState) {
+        State newState = transitionPolicy(requestedState);
+
+        if (DEBUG) {
+            Log.i(TAG, "transition: old=" + mState + " req=" + requestedState + " new=" + newState);
+        }
+
+        if (newState == mState) {
+            return;
+        }
+
+        validateTransition(newState);
+
+        State oldState = mState;
+        mState = newState;
+
+        performTransitionOnComponents(oldState, newState);
+        updateScreenState(newState);
+        updateWakeLockState(newState);
+
+        resolveIntermediateState(newState);
+    }
+
+    private void performTransitionOnComponents(State oldState, State newState) {
+        for (Part p : mParts) {
+            p.transitionTo(oldState, newState);
+        }
+
+        switch (newState) {
+            case FINISH:
+                mDozeService.finish();
+                break;
+            default:
+        }
+    }
+
+    private void validateTransition(State newState) {
+        switch (mState) {
+            case FINISH:
+                Preconditions.checkState(newState == State.FINISH);
+                break;
+            case UNINITIALIZED:
+                Preconditions.checkState(newState == State.INITIALIZED);
+                break;
+        }
+        switch (newState) {
+            case UNINITIALIZED:
+                throw new IllegalArgumentException("can't go to UNINITIALIZED");
+            case INITIALIZED:
+                Preconditions.checkState(mState == State.UNINITIALIZED);
+                break;
+            case DOZE_PULSING:
+                Preconditions.checkState(mState == State.DOZE_REQUEST_PULSE);
+                break;
+            case DOZE_PULSE_DONE:
+                Preconditions.checkState(mState == State.DOZE_PULSING);
+                break;
+            default:
+                break;
+        }
+    }
+
+    private int screenPolicy(State newState) {
+        switch (newState) {
+            case UNINITIALIZED:
+            case INITIALIZED:
+            case DOZE:
+                return Display.STATE_OFF;
+            case DOZE_PULSING:
+            case DOZE_AOD:
+                return Display.STATE_DOZE; // TODO: use STATE_ON if appropriate.
+            default:
+                return Display.STATE_UNKNOWN;
+        }
+    }
+
+    private boolean wakeLockPolicy(State newState) {
+        switch (newState) {
+            case DOZE_REQUEST_PULSE:
+            case DOZE_PULSING:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    private State transitionPolicy(State requestedState) {
+        if (mState == State.FINISH) {
+            return State.FINISH;
+        }
+        return requestedState;
+    }
+
+    private void updateWakeLockState(State newState) {
+        boolean newPolicy = wakeLockPolicy(newState);
+        if (mWakeLockHeldForCurrentState && !newPolicy) {
+            mWakeLock.release();
+        } else if (!mWakeLockHeldForCurrentState && newPolicy) {
+            mWakeLock.acquire();
+        }
+    }
+
+    private void updateScreenState(State newState) {
+        int state = screenPolicy(newState);
+        if (state != Display.STATE_UNKNOWN) {
+            mDozeService.setDozeScreenState(state);
+        }
+    }
+
+    private void resolveIntermediateState(State state) {
+        switch (state) {
+            case INITIALIZED:
+            case DOZE_PULSE_DONE:
+                transitionTo(mParams.getAlwaysOn()
+                        ? DozeMachine.State.DOZE_AOD : DozeMachine.State.DOZE);
+                break;
+            default:
+                break;
+        }
+    }
+
+    /** Dumps the current state */
+    public void dump(PrintWriter pw) {
+        pw.print(" state="); pw.println(mState);
+        pw.print(" wakeLockHeldForCurrentState="); pw.println(mWakeLockHeldForCurrentState);
+        pw.println("Parts:");
+        for (Part p : mParts) {
+            p.dump(pw);
+        }
+    }
+
+    /** A part of the DozeMachine that needs to be notified about state changes. */
+    public interface Part {
+        /**
+         * Transition from {@code oldState} to {@code newState}.
+         *
+         * This method is guaranteed to only be called while a wake lock is held.
+         */
+        void transitionTo(State oldState, State newState);
+
+        /** Dump current state. For debugging only. */
+        default void dump(PrintWriter pw) {}
+    }
+
+    /** A wrapper interface for {@link android.service.dreams.DreamService} */
+    public interface Service {
+        /** Finish dreaming. */
+        void finish();
+
+        /** Request a display state. See {@link android.view.Display#STATE_DOZE}. */
+        void setDozeScreenState(int state);
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/doze/DozeSensors.java b/packages/SystemUI/src/com/android/systemui/doze/DozeSensors.java
index 81dfafc..bb4ea2d 100644
--- a/packages/SystemUI/src/com/android/systemui/doze/DozeSensors.java
+++ b/packages/SystemUI/src/com/android/systemui/doze/DozeSensors.java
@@ -11,16 +11,11 @@
  * distributed under the License is distributed on an "AS IS" BASIS,
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  * See the License for the specific language governing permissions and
- * limitations under the License
+ * limitations under the License.
  */
 
 package com.android.systemui.doze;
 
-import com.android.internal.hardware.AmbientDisplayConfiguration;
-import com.android.internal.logging.MetricsLogger;
-import com.android.internal.logging.MetricsProto;
-import com.android.systemui.statusbar.phone.DozeParameters;
-
 import android.annotation.AnyThread;
 import android.app.ActivityManager;
 import android.content.ContentResolver;
@@ -32,12 +27,17 @@
 import android.hardware.TriggerEventListener;
 import android.net.Uri;
 import android.os.Handler;
-import android.os.PowerManager;
 import android.os.UserHandle;
 import android.provider.Settings;
 import android.text.TextUtils;
 import android.util.Log;
 
+import com.android.internal.hardware.AmbientDisplayConfiguration;
+import com.android.internal.logging.MetricsLogger;
+import com.android.internal.logging.MetricsProto;
+import com.android.systemui.statusbar.phone.DozeParameters;
+
+import java.io.PrintWriter;
 import java.util.List;
 
 public class DozeSensors {
@@ -53,14 +53,14 @@
     private final TriggerSensor mPickupSensor;
     private final DozeParameters mDozeParameters;
     private final AmbientDisplayConfiguration mConfig;
-    private final PowerManager.WakeLock mWakeLock;
+    private final DozeFactory.WakeLock mWakeLock;
     private final Callback mCallback;
 
     private final Handler mHandler = new Handler();
 
 
     public DozeSensors(Context context, SensorManager sensorManager, DozeParameters dozeParameters,
-            AmbientDisplayConfiguration config, PowerManager.WakeLock wakeLock, Callback callback) {
+            AmbientDisplayConfiguration config, DozeFactory.WakeLock wakeLock, Callback callback) {
         mContext = context;
         mSensorManager = sensorManager;
         mDozeParameters = dozeParameters;
@@ -144,6 +144,13 @@
         mPickupSensor.setDisabled(disable);
     }
 
+    /** Dump current state */
+    public void dump(PrintWriter pw) {
+        for (TriggerSensor s : mSensors) {
+            pw.print("Sensor: "); pw.println(s.toString());
+        }
+    }
+
     private class TriggerSensor extends TriggerEventListener {
         final Sensor mSensor;
         final boolean mConfigured;
diff --git a/packages/SystemUI/src/com/android/systemui/doze/DozeService.java b/packages/SystemUI/src/com/android/systemui/doze/DozeService.java
index 2bb1d6a..94cbdd4 100644
--- a/packages/SystemUI/src/com/android/systemui/doze/DozeService.java
+++ b/packages/SystemUI/src/com/android/systemui/doze/DozeService.java
@@ -16,468 +16,46 @@
 
 package com.android.systemui.doze;
 
-import android.app.AlarmManager;
-import android.app.UiModeManager;
-import android.content.BroadcastReceiver;
-import android.content.Context;
-import android.content.Intent;
-import android.content.IntentFilter;
-import android.content.res.Configuration;
-import android.hardware.Sensor;
-import android.hardware.SensorEvent;
-import android.hardware.SensorEventListener;
-import android.hardware.SensorManager;
-import android.os.Handler;
-import android.os.PowerManager;
-import android.os.SystemClock;
-import android.os.UserHandle;
 import android.service.dreams.DreamService;
 import android.util.Log;
-import android.view.Display;
-
-import com.android.internal.hardware.AmbientDisplayConfiguration;
-import com.android.systemui.SystemUIApplication;
-import com.android.systemui.statusbar.phone.DozeParameters;
-import com.android.systemui.util.Assert;
 
 import java.io.FileDescriptor;
 import java.io.PrintWriter;
-import java.util.Calendar;
-import java.util.Date;
-import java.util.GregorianCalendar;
 
-public class DozeService extends DreamService implements DozeSensors.Callback {
+public class DozeService extends DreamService implements DozeMachine.Service {
     private static final String TAG = "DozeService";
     static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
 
-    private static final String ACTION_BASE = "com.android.systemui.doze";
-    private static final String PULSE_ACTION = ACTION_BASE + ".pulse";
-
-    /**
-     * If true, reregisters all trigger sensors when the screen turns off.
-     */
-    private static final boolean REREGISTER_ALL_SENSORS_ON_SCREEN_OFF = true;
-
-    private final String mTag = String.format(TAG + ".%08x", hashCode());
-    private final Context mContext = this;
-    private final DozeParameters mDozeParameters = new DozeParameters(mContext);
-    private final Handler mHandler = new Handler();
-
-    private DozeHost mHost;
-    private DozeSensors mDozeSensors;
-    private SensorManager mSensorManager;
-    private PowerManager mPowerManager;
-    private PowerManager.WakeLock mWakeLock;
-    private UiModeManager mUiModeManager;
-    private boolean mDreaming;
-    private boolean mPulsing;
-    private boolean mBroadcastReceiverRegistered;
-    private boolean mDisplayStateSupported;
-    private boolean mPowerSaveActive;
-    private boolean mCarMode;
-    private long mNotificationPulseTime;
-
-    private AmbientDisplayConfiguration mConfig;
-    private AlarmManager mAlarmManager;
+    private DozeMachine mDozeMachine;
 
     public DozeService() {
-        if (DEBUG) Log.d(mTag, "new DozeService()");
         setDebug(DEBUG);
     }
 
     @Override
-    protected void dumpOnHandler(FileDescriptor fd, PrintWriter pw, String[] args) {
-        super.dumpOnHandler(fd, pw, args);
-        pw.print("  mDreaming: "); pw.println(mDreaming);
-        pw.print("  mPulsing: "); pw.println(mPulsing);
-        pw.print("  mWakeLock: held="); pw.println(mWakeLock.isHeld());
-        pw.print("  mHost: "); pw.println(mHost);
-        pw.print("  mBroadcastReceiverRegistered: "); pw.println(mBroadcastReceiverRegistered);
-        pw.print("  mDisplayStateSupported: "); pw.println(mDisplayStateSupported);
-        pw.print("  mPowerSaveActive: "); pw.println(mPowerSaveActive);
-        pw.print("  mCarMode: "); pw.println(mCarMode);
-        pw.print("  mNotificationPulseTime: "); pw.println(
-                DozeLog.FORMAT.format(new Date(mNotificationPulseTime
-                        - SystemClock.elapsedRealtime() + System.currentTimeMillis())));
-        mDozeParameters.dump(pw);
-    }
-
-    @Override
     public void onCreate() {
-        if (DEBUG) Log.d(mTag, "onCreate");
         super.onCreate();
 
-        if (getApplication() instanceof SystemUIApplication) {
-            final SystemUIApplication app = (SystemUIApplication) getApplication();
-            mHost = app.getComponent(DozeHost.class);
-        }
-        if (mHost == null) Log.w(TAG, "No doze service host found.");
-
         setWindowless(true);
 
-        mSensorManager = (SensorManager) mContext.getSystemService(Context.SENSOR_SERVICE);
-        mAlarmManager = (AlarmManager) mContext.getSystemService(AlarmManager.class);
-        mConfig = new AmbientDisplayConfiguration(mContext);
-        mPowerManager = (PowerManager) mContext.getSystemService(Context.POWER_SERVICE);
-        mWakeLock = mPowerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, TAG);
-        mWakeLock.setReferenceCounted(true);
-        mDisplayStateSupported = mDozeParameters.getDisplayStateSupported();
-        mUiModeManager = (UiModeManager) mContext.getSystemService(Context.UI_MODE_SERVICE);
-        turnDisplayOff();
-        mDozeSensors = new DozeSensors(mContext, mSensorManager, mDozeParameters,
-                mConfig, mWakeLock, this);
-    }
-
-    @Override
-    public void onAttachedToWindow() {
-        if (DEBUG) Log.d(mTag, "onAttachedToWindow");
-        super.onAttachedToWindow();
+        mDozeMachine = DozeFactory.assembleMachine(this);
     }
 
     @Override
     public void onDreamingStarted() {
         super.onDreamingStarted();
-
-        if (mHost == null) {
-            finish();
-            return;
-        }
-
-        mPowerSaveActive = mHost.isPowerSaveActive();
-        mCarMode = mUiModeManager.getCurrentModeType() == Configuration.UI_MODE_TYPE_CAR;
-        if (DEBUG) Log.d(mTag, "onDreamingStarted canDoze=" + canDoze() + " mPowerSaveActive="
-                + mPowerSaveActive + " mCarMode=" + mCarMode);
-        if (mPowerSaveActive) {
-            finishToSavePower();
-            return;
-        }
-        if (mCarMode) {
-            finishForCarMode();
-            return;
-        }
-
-        mDreaming = true;
-        listenForPulseSignals(true);
-
-        // Ask the host to get things ready to start dozing.
-        // Once ready, we call startDozing() at which point the CPU may suspend
-        // and we will need to acquire a wakelock to do work.
-        mHost.startDozing(mWakeLock.wrap(() -> {
-            if (mDreaming) {
-                startDozing();
-
-                // From this point until onDreamingStopped we will need to hold a
-                // wakelock whenever we are doing work.  Note that we never call
-                // stopDozing because can we just keep dozing until the bitter end.
-            }
-        }));
-
-        if (mDozeParameters.getAlwaysOn()) {
-            mTimeTick.onAlarm();
-        }
+        mDozeMachine.requestState(DozeMachine.State.INITIALIZED);
+        startDozing();
     }
 
     @Override
     public void onDreamingStopped() {
-        if (DEBUG) Log.d(mTag, "onDreamingStopped isDozing=" + isDozing());
         super.onDreamingStopped();
-
-        if (mHost == null) {
-            return;
-        }
-
-        mDreaming = false;
-        listenForPulseSignals(false);
-
-        // Tell the host that it's over.
-        mHost.stopDozing();
-        mAlarmManager.cancel(mTimeTick);
-    }
-
-    private void requestPulse(final int reason) {
-        requestPulse(reason, false /* performedProxCheck */);
-    }
-
-    private void requestPulse(final int reason, boolean performedProxCheck) {
-        Assert.isMainThread();
-        if (mHost != null && mDreaming && !mPulsing) {
-            // Let the host know we want to pulse.  Wait for it to be ready, then
-            // turn the screen on.  When finished, turn the screen off again.
-            // Here we need a wakelock to stay awake until the pulse is finished.
-            mWakeLock.acquire();
-            mPulsing = true;
-            if (!mDozeParameters.getProxCheckBeforePulse()) {
-                // skip proximity check
-                continuePulsing(reason);
-                return;
-            }
-            final long start = SystemClock.uptimeMillis();
-            if (performedProxCheck) {
-                // the caller already performed a successful proximity check; we'll only do one to
-                // capture statistics, continue pulsing immediately.
-                continuePulsing(reason);
-            }
-            // perform a proximity check
-            new ProximityCheck() {
-                @Override
-                public void onProximityResult(int result) {
-                    final boolean isNear = result == RESULT_NEAR;
-                    final long end = SystemClock.uptimeMillis();
-                    DozeLog.traceProximityResult(mContext, isNear, end - start, reason);
-                    if (performedProxCheck) {
-                        // we already continued
-                        return;
-                    }
-                    // avoid pulsing in pockets
-                    if (isNear) {
-                        mPulsing = false;
-                        mWakeLock.release();
-                        return;
-                    }
-
-                    // not in-pocket, continue pulsing
-                    continuePulsing(reason);
-                }
-            }.check();
-        }
-    }
-
-    private void continuePulsing(int reason) {
-        if (mHost.isPulsingBlocked()) {
-            mPulsing = false;
-            mWakeLock.release();
-            return;
-        }
-        mHost.pulseWhileDozing(new DozeHost.PulseCallback() {
-            @Override
-            public void onPulseStarted() {
-                if (mPulsing && mDreaming) {
-                    turnDisplayOn();
-                }
-            }
-
-            @Override
-            public void onPulseFinished() {
-                if (mPulsing && mDreaming) {
-                    mPulsing = false;
-                    if (REREGISTER_ALL_SENSORS_ON_SCREEN_OFF) {
-                        mDozeSensors.reregisterAllSensors();
-                    }
-                    turnDisplayOff();
-                }
-                mWakeLock.release(); // needs to be unconditional to balance acquire
-            }
-        }, reason);
-    }
-
-    private void turnDisplayOff() {
-        if (DEBUG) Log.d(mTag, "Display off");
-        if (mDozeParameters.getAlwaysOn()) {
-            turnDisplayOn();
-        } else {
-            setDozeScreenState(Display.STATE_OFF);
-        }
-    }
-
-    private void turnDisplayOn() {
-        if (DEBUG) Log.d(mTag, "Display on");
-        setDozeScreenState(mDisplayStateSupported ? Display.STATE_DOZE : Display.STATE_ON);
-    }
-
-    private void finishToSavePower() {
-        Log.w(mTag, "Exiting ambient mode due to low power battery saver");
-        finish();
-    }
-
-    private void finishForCarMode() {
-        Log.w(mTag, "Exiting ambient mode, not allowed in car mode");
-        finish();
-    }
-
-    private void listenForPulseSignals(boolean listen) {
-        if (DEBUG) Log.d(mTag, "listenForPulseSignals: " + listen);
-        mDozeSensors.setListening(listen);
-        listenForBroadcasts(listen);
-        listenForNotifications(listen);
-    }
-
-    private void listenForBroadcasts(boolean listen) {
-        if (listen) {
-            final IntentFilter filter = new IntentFilter(PULSE_ACTION);
-            filter.addAction(UiModeManager.ACTION_ENTER_CAR_MODE);
-            filter.addAction(Intent.ACTION_USER_SWITCHED);
-            mContext.registerReceiver(mBroadcastReceiver, filter);
-
-            mBroadcastReceiverRegistered = true;
-        } else {
-            if (mBroadcastReceiverRegistered) {
-                mContext.unregisterReceiver(mBroadcastReceiver);
-            }
-            mBroadcastReceiverRegistered = false;
-        }
-    }
-
-    private void listenForNotifications(boolean listen) {
-        if (listen) {
-            mHost.addCallback(mHostCallback);
-        } else {
-            mHost.removeCallback(mHostCallback);
-        }
-    }
-
-    private void requestNotificationPulse() {
-        if (DEBUG) Log.d(mTag, "requestNotificationPulse");
-        if (!mConfig.pulseOnNotificationEnabled(UserHandle.USER_CURRENT)) return;
-        mNotificationPulseTime = SystemClock.elapsedRealtime();
-        requestPulse(DozeLog.PULSE_REASON_NOTIFICATION);
+        mDozeMachine.requestState(DozeMachine.State.FINISH);
     }
 
     @Override
-    public void onSensorPulse(int pulseReason, boolean sensorPerformedProxCheck) {
-        requestPulse(pulseReason, sensorPerformedProxCheck);
-
-        if (pulseReason == DozeLog.PULSE_REASON_SENSOR_PICKUP) {
-            final long timeSinceNotification =
-                    SystemClock.elapsedRealtime() - mNotificationPulseTime;
-            final boolean withinVibrationThreshold =
-                    timeSinceNotification < mDozeParameters.getPickupVibrationThreshold();
-            DozeLog.tracePickupPulse(mContext, withinVibrationThreshold);
-        }
-
-    }
-
-    private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
-        @Override
-        public void onReceive(Context context, Intent intent) {
-            if (PULSE_ACTION.equals(intent.getAction())) {
-                if (DEBUG) Log.d(mTag, "Received pulse intent");
-                requestPulse(DozeLog.PULSE_REASON_INTENT);
-            }
-            if (UiModeManager.ACTION_ENTER_CAR_MODE.equals(intent.getAction())) {
-                mCarMode = true;
-                if (mCarMode && mDreaming) {
-                    finishForCarMode();
-                }
-            }
-            if (Intent.ACTION_USER_SWITCHED.equals(intent.getAction())) {
-                mDozeSensors.onUserSwitched();
-            }
-        }
-    };
-
-    private AlarmManager.OnAlarmListener mTimeTick = new AlarmManager.OnAlarmListener() {
-        @Override
-        public void onAlarm() {
-            mHost.dozeTimeTick();
-
-            // Keep wakelock until a frame has been pushed.
-            mHandler.post(mWakeLock.wrap(()->{}));
-
-            Calendar calendar = GregorianCalendar.getInstance();
-            calendar.setTimeInMillis(System.currentTimeMillis());
-            calendar.set(Calendar.MILLISECOND, 0);
-            calendar.set(Calendar.SECOND, 0);
-            calendar.add(Calendar.MINUTE, 1);
-
-            long delta = calendar.getTimeInMillis() - System.currentTimeMillis();
-            mAlarmManager.setExact(AlarmManager.ELAPSED_REALTIME_WAKEUP,
-                    SystemClock.elapsedRealtime() + delta, "doze_time_tick", mTimeTick, mHandler);
-        }
-    };
-
-    private final DozeHost.Callback mHostCallback = new DozeHost.Callback() {
-        @Override
-        public void onNewNotifications() {
-            if (DEBUG) Log.d(mTag, "onNewNotifications (noop)");
-            // noop for now
-        }
-
-        @Override
-        public void onBuzzBeepBlinked() {
-            if (DEBUG) Log.d(mTag, "onBuzzBeepBlinked");
-            requestNotificationPulse();
-        }
-
-        @Override
-        public void onNotificationLight(boolean on) {
-            if (DEBUG) Log.d(mTag, "onNotificationLight (noop) on=" + on);
-            // noop for now
-        }
-
-        @Override
-        public void onPowerSaveChanged(boolean active) {
-            mPowerSaveActive = active;
-            if (mPowerSaveActive && mDreaming) {
-                finishToSavePower();
-            }
-        }
-    };
-
-    private abstract class ProximityCheck implements SensorEventListener, Runnable {
-        private static final int TIMEOUT_DELAY_MS = 500;
-
-        protected static final int RESULT_UNKNOWN = 0;
-        protected static final int RESULT_NEAR = 1;
-        protected static final int RESULT_FAR = 2;
-
-        private final String mTag = DozeService.this.mTag + ".ProximityCheck";
-
-        private boolean mRegistered;
-        private boolean mFinished;
-        private float mMaxRange;
-
-        abstract public void onProximityResult(int result);
-
-        public void check() {
-            if (mFinished || mRegistered) return;
-            final Sensor sensor = mSensorManager.getDefaultSensor(Sensor.TYPE_PROXIMITY);
-            if (sensor == null) {
-                if (DEBUG) Log.d(mTag, "No sensor found");
-                finishWithResult(RESULT_UNKNOWN);
-                return;
-            }
-            mDozeSensors.setDisableSensorsInterferingWithProximity(true);
-
-            mMaxRange = sensor.getMaximumRange();
-            mSensorManager.registerListener(this, sensor, SensorManager.SENSOR_DELAY_NORMAL, 0,
-                    mHandler);
-            mHandler.postDelayed(this, TIMEOUT_DELAY_MS);
-            mRegistered = true;
-        }
-
-        @Override
-        public void onSensorChanged(SensorEvent event) {
-            if (event.values.length == 0) {
-                if (DEBUG) Log.d(mTag, "Event has no values!");
-                finishWithResult(RESULT_UNKNOWN);
-            } else {
-                if (DEBUG) Log.d(mTag, "Event: value=" + event.values[0] + " max=" + mMaxRange);
-                final boolean isNear = event.values[0] < mMaxRange;
-                finishWithResult(isNear ? RESULT_NEAR : RESULT_FAR);
-            }
-        }
-
-        @Override
-        public void run() {
-            if (DEBUG) Log.d(mTag, "No event received before timeout");
-            finishWithResult(RESULT_UNKNOWN);
-        }
-
-        private void finishWithResult(int result) {
-            if (mFinished) return;
-            if (mRegistered) {
-                mHandler.removeCallbacks(this);
-                mSensorManager.unregisterListener(this);
-                mDozeSensors.setDisableSensorsInterferingWithProximity(false);
-                mRegistered = false;
-            }
-            onProximityResult(result);
-            mFinished = true;
-        }
-
-        @Override
-        public void onAccuracyChanged(Sensor sensor, int accuracy) {
-            // noop
-        }
+    protected void dumpOnHandler(FileDescriptor fd, PrintWriter pw, String[] args) {
+        mDozeMachine.dump(pw);
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/doze/DozeTriggers.java b/packages/SystemUI/src/com/android/systemui/doze/DozeTriggers.java
new file mode 100644
index 0000000..9df8113
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/doze/DozeTriggers.java
@@ -0,0 +1,329 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.doze;
+
+import android.app.UiModeManager;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.res.Configuration;
+import android.hardware.Sensor;
+import android.hardware.SensorEvent;
+import android.hardware.SensorEventListener;
+import android.hardware.SensorManager;
+import android.os.Handler;
+import android.os.SystemClock;
+import android.os.UserHandle;
+import android.text.format.Formatter;
+import android.util.Log;
+
+import com.android.internal.hardware.AmbientDisplayConfiguration;
+import com.android.internal.util.Preconditions;
+import com.android.systemui.statusbar.phone.DozeParameters;
+import com.android.systemui.util.Assert;
+
+import java.io.PrintWriter;
+
+/**
+ * Handles triggers for ambient state changes.
+ */
+public class DozeTriggers implements DozeMachine.Part {
+
+    private static final String TAG = "DozeTriggers";
+
+    /** adb shell am broadcast -a com.android.systemui.doze.pulse com.android.systemui */
+    private static final String PULSE_ACTION = "com.android.systemui.doze.pulse";
+
+    private final Context mContext;
+    private final DozeMachine mMachine;
+    private final DozeSensors mDozeSensors;
+    private final DozeHost mDozeHost;
+    private final AmbientDisplayConfiguration mConfig;
+    private final DozeParameters mDozeParameters;
+    private final SensorManager mSensorManager;
+    private final Handler mHandler;
+    private final DozeFactory.WakeLock mWakeLock;
+    private final UiModeManager mUiModeManager;
+    private final TriggerReceiver mBroadcastReceiver = new TriggerReceiver();
+
+    private long mNotificationPulseTime;
+    private boolean mPulsePending;
+
+
+    public DozeTriggers(Context context, DozeMachine machine, DozeHost dozeHost,
+            AmbientDisplayConfiguration config,
+            DozeParameters dozeParameters, SensorManager sensorManager, Handler handler,
+            DozeFactory.WakeLock wakeLock) {
+        mContext = context;
+        mMachine = machine;
+        mDozeHost = dozeHost;
+        mConfig = config;
+        mDozeParameters = dozeParameters;
+        mSensorManager = sensorManager;
+        mHandler = handler;
+        mWakeLock = wakeLock;
+        mDozeSensors = new DozeSensors(context, mSensorManager, dozeParameters, config,
+                wakeLock, this::onSensor);
+        mUiModeManager = mContext.getSystemService(UiModeManager.class);
+    }
+
+    private void onNotification() {
+        if (DozeMachine.DEBUG) Log.d(TAG, "requestNotificationPulse");
+        mNotificationPulseTime = SystemClock.elapsedRealtime();
+        if (!mConfig.pulseOnNotificationEnabled(UserHandle.USER_CURRENT)) return;
+        requestPulse(DozeLog.PULSE_REASON_NOTIFICATION, false /* performedProxCheck */);
+    }
+
+    private void onWhisper() {
+        requestPulse(DozeLog.PULSE_REASON_NOTIFICATION, false /* performedProxCheck */);
+    }
+
+    private void onSensor(int pulseReason, boolean sensorPerformedProxCheck) {
+        requestPulse(pulseReason, sensorPerformedProxCheck);
+
+        if (pulseReason == DozeLog.PULSE_REASON_SENSOR_PICKUP) {
+            final long timeSinceNotification =
+                    SystemClock.elapsedRealtime() - mNotificationPulseTime;
+            final boolean withinVibrationThreshold =
+                    timeSinceNotification < mDozeParameters.getPickupVibrationThreshold();
+            DozeLog.tracePickupPulse(mContext, withinVibrationThreshold);
+        }
+    }
+
+    private void onCarMode() {
+        mMachine.requestState(DozeMachine.State.FINISH);
+    }
+
+    private void onPowerSave() {
+        mMachine.requestState(DozeMachine.State.FINISH);
+    }
+
+    @Override
+    public void transitionTo(DozeMachine.State oldState, DozeMachine.State newState) {
+        switch (newState) {
+            case INITIALIZED:
+                mBroadcastReceiver.register(mContext);
+                mDozeHost.addCallback(mHostCallback);
+                checkTriggersAtInit();
+                break;
+            case DOZE:
+            case DOZE_AOD:
+                mDozeSensors.setListening(true);
+                if (oldState != DozeMachine.State.INITIALIZED) {
+                    mDozeSensors.reregisterAllSensors();
+                }
+                break;
+            case FINISH:
+                mBroadcastReceiver.unregister(mContext);
+                mDozeHost.removeCallback(mHostCallback);
+                mDozeSensors.setListening(false);
+                break;
+            default:
+        }
+    }
+
+    private void checkTriggersAtInit() {
+        if (mUiModeManager.getCurrentModeType() == Configuration.UI_MODE_TYPE_CAR) {
+            onCarMode();
+        }
+        if (mDozeHost.isPowerSaveActive()) {
+            onPowerSave();
+        }
+    }
+
+    private void requestPulse(final int reason, boolean performedProxCheck) {
+        Assert.isMainThread();
+        if (mPulsePending || !canPulse()) {
+            return;
+        }
+
+        mPulsePending = true;
+        if (!mDozeParameters.getProxCheckBeforePulse() || performedProxCheck) {
+            // skip proximity check
+            continuePulseRequest(reason);
+            return;
+        }
+
+        final long start = SystemClock.uptimeMillis();
+        new ProximityCheck() {
+            @Override
+            public void onProximityResult(int result) {
+                final long end = SystemClock.uptimeMillis();
+                DozeLog.traceProximityResult(mContext, result == RESULT_NEAR,
+                        end - start, reason);
+                if (performedProxCheck) {
+                    // we already continued
+                    return;
+                }
+                // avoid pulsing in pockets
+                if (result == RESULT_NEAR) {
+                    return;
+                }
+
+                // not in-pocket, continue pulsing
+                continuePulseRequest(reason);
+            }
+        }.check();
+    }
+
+    private boolean canPulse() {
+        return mMachine.getState() == DozeMachine.State.DOZE
+                || mMachine.getState() == DozeMachine.State.DOZE_AOD;
+    }
+
+    private void continuePulseRequest(int reason) {
+        mPulsePending = false;
+        if (mDozeHost.isPulsingBlocked() || !canPulse()) {
+            return;
+        }
+        mMachine.requestState(DozeMachine.State.DOZE_REQUEST_PULSE);
+    }
+
+    @Override
+    public void dump(PrintWriter pw) {
+        pw.print(" notificationPulseTime=");
+        pw.println(Formatter.formatShortElapsedTime(mContext, mNotificationPulseTime));
+
+        pw.print(" pulsePending="); pw.println(mPulsePending);
+        pw.println("DozeSensors:");
+        mDozeSensors.dump(pw);
+    }
+
+    private abstract class ProximityCheck implements SensorEventListener, Runnable {
+        private static final int TIMEOUT_DELAY_MS = 500;
+
+        protected static final int RESULT_UNKNOWN = 0;
+        protected static final int RESULT_NEAR = 1;
+        protected static final int RESULT_FAR = 2;
+
+        private boolean mRegistered;
+        private boolean mFinished;
+        private float mMaxRange;
+
+        protected abstract void onProximityResult(int result);
+
+        public void check() {
+            Preconditions.checkState(!mFinished && !mRegistered);
+            final Sensor sensor = mSensorManager.getDefaultSensor(Sensor.TYPE_PROXIMITY);
+            if (sensor == null) {
+                if (DozeMachine.DEBUG) Log.d(TAG, "ProxCheck: No sensor found");
+                finishWithResult(RESULT_UNKNOWN);
+                return;
+            }
+            mDozeSensors.setDisableSensorsInterferingWithProximity(true);
+
+            mMaxRange = sensor.getMaximumRange();
+            mSensorManager.registerListener(this, sensor, SensorManager.SENSOR_DELAY_NORMAL, 0,
+                    mHandler);
+            mHandler.postDelayed(this, TIMEOUT_DELAY_MS);
+            mWakeLock.acquire();
+            mRegistered = true;
+        }
+
+        @Override
+        public void onSensorChanged(SensorEvent event) {
+            if (event.values.length == 0) {
+                if (DozeMachine.DEBUG) Log.d(TAG, "ProxCheck: Event has no values!");
+                finishWithResult(RESULT_UNKNOWN);
+            } else {
+                if (DozeMachine.DEBUG) {
+                    Log.d(TAG, "ProxCheck: Event: value=" + event.values[0] + " max=" + mMaxRange);
+                }
+                final boolean isNear = event.values[0] < mMaxRange;
+                finishWithResult(isNear ? RESULT_NEAR : RESULT_FAR);
+            }
+        }
+
+        @Override
+        public void run() {
+            if (DozeMachine.DEBUG) Log.d(TAG, "ProxCheck: No event received before timeout");
+            finishWithResult(RESULT_UNKNOWN);
+        }
+
+        private void finishWithResult(int result) {
+            if (mFinished) return;
+            boolean wasRegistered = mRegistered;
+            if (mRegistered) {
+                mHandler.removeCallbacks(this);
+                mSensorManager.unregisterListener(this);
+                mDozeSensors.setDisableSensorsInterferingWithProximity(false);
+                mRegistered = false;
+            }
+            onProximityResult(result);
+            if (wasRegistered) {
+                mWakeLock.release();
+            }
+            mFinished = true;
+        }
+
+        @Override
+        public void onAccuracyChanged(Sensor sensor, int accuracy) {
+            // noop
+        }
+    }
+
+    private class TriggerReceiver extends BroadcastReceiver {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            if (PULSE_ACTION.equals(intent.getAction())) {
+                if (DozeMachine.DEBUG) Log.d(TAG, "Received pulse intent");
+                requestPulse(DozeLog.PULSE_REASON_INTENT, false /* performedProxCheck */);
+            }
+            if (UiModeManager.ACTION_ENTER_CAR_MODE.equals(intent.getAction())) {
+                onCarMode();
+            }
+            if (Intent.ACTION_USER_SWITCHED.equals(intent.getAction())) {
+                mDozeSensors.onUserSwitched();
+            }
+        }
+
+        public void register(Context context) {
+            IntentFilter filter = new IntentFilter(PULSE_ACTION);
+            filter.addAction(UiModeManager.ACTION_ENTER_CAR_MODE);
+            filter.addAction(Intent.ACTION_USER_SWITCHED);
+            context.registerReceiver(this, filter);
+        }
+
+        public void unregister(Context context) {
+            context.unregisterReceiver(this);
+        }
+    }
+
+    private DozeHost.Callback mHostCallback = new DozeHost.Callback() {
+        @Override
+        public void onNewNotifications() {
+        }
+
+        @Override
+        public void onBuzzBeepBlinked() {
+            onNotification();
+        }
+
+        @Override
+        public void onNotificationLight(boolean on) {
+
+        }
+
+        @Override
+        public void onPowerSaveChanged(boolean active) {
+            if (active) {
+                onPowerSave();
+            }
+        }
+    };
+}
diff --git a/packages/SystemUI/src/com/android/systemui/doze/DozeUi.java b/packages/SystemUI/src/com/android/systemui/doze/DozeUi.java
new file mode 100644
index 0000000..95e49ce
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/doze/DozeUi.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.doze;
+
+import android.content.Context;
+
+/**
+ * The policy controlling doze.
+ */
+public class DozeUi implements DozeMachine.Part {
+
+    private final Context mContext;
+    private final DozeHost mHost;
+    private DozeFactory.WakeLock mWakeLock;
+    private DozeMachine mMachine;
+
+    public DozeUi(Context context, DozeMachine machine, DozeFactory.WakeLock wakeLock,
+            DozeHost host) {
+        mContext = context;
+        mMachine = machine;
+        mWakeLock = wakeLock;
+        mHost = host;
+    }
+
+    private void pulseWhileDozing(int reason) {
+        mHost.pulseWhileDozing(
+                new DozeHost.PulseCallback() {
+                    @Override
+                    public void onPulseStarted() {
+                        mMachine.requestState(DozeMachine.State.DOZE_PULSING);
+                    }
+
+                    @Override
+                    public void onPulseFinished() {
+                        mMachine.requestState(DozeMachine.State.DOZE_PULSE_DONE);
+                    }
+                }, reason);
+    }
+
+    @Override
+    public void transitionTo(DozeMachine.State oldState, DozeMachine.State newState) {
+        switch (newState) {
+            case DOZE_REQUEST_PULSE:
+                pulseWhileDozing(DozeLog.PULSE_REASON_NOTIFICATION /* TODO */);
+                break;
+            case INITIALIZED:
+                mHost.startDozing();
+                break;
+            case FINISH:
+                mHost.stopDozing();
+                break;
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBar.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBar.java
index 669a512..c33d91a 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBar.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBar.java
@@ -5059,7 +5059,6 @@
         private static final long PROCESSING_TIME = 500;
 
         private final ArrayList<Callback> mCallbacks = new ArrayList<Callback>();
-        private final H mHandler = new H();
 
         // Keeps the last reported state by fireNotificationLight.
         private boolean mNotificationLightOn;
@@ -5105,18 +5104,39 @@
         }
 
         @Override
-        public void startDozing(@NonNull Runnable ready) {
-            mHandler.obtainMessage(H.MSG_START_DOZING, ready).sendToTarget();
+        public void startDozing() {
+            if (!mDozingRequested) {
+                mDozingRequested = true;
+                DozeLog.traceDozing(mContext, mDozing);
+                updateDozing();
+            }
         }
 
         @Override
         public void pulseWhileDozing(@NonNull PulseCallback callback, int reason) {
-            mHandler.obtainMessage(H.MSG_PULSE_WHILE_DOZING, reason, 0, callback).sendToTarget();
+            mDozeScrimController.pulse(new PulseCallback() {
+
+                @Override
+                public void onPulseStarted() {
+                    callback.onPulseStarted();
+                    mStackScroller.setPulsing(true);
+                }
+
+                @Override
+                public void onPulseFinished() {
+                    callback.onPulseFinished();
+                    mStackScroller.setPulsing(false);
+                }
+            }, reason);
         }
 
         @Override
         public void stopDozing() {
-            mHandler.obtainMessage(H.MSG_STOP_DOZING).sendToTarget();
+            if (mDozingRequested) {
+                mDozingRequested = false;
+                DozeLog.traceDozing(mContext, mDozing);
+                updateDozing();
+            }
         }
 
         @Override
@@ -5140,59 +5160,5 @@
             return mNotificationLightOn;
         }
 
-        private void handleStartDozing(@NonNull Runnable ready) {
-            if (!mDozingRequested) {
-                mDozingRequested = true;
-                DozeLog.traceDozing(mContext, mDozing);
-                updateDozing();
-            }
-            ready.run();
-        }
-
-        private void handlePulseWhileDozing(@NonNull PulseCallback callback, int reason) {
-            mDozeScrimController.pulse(new PulseCallback() {
-
-                @Override
-                public void onPulseStarted() {
-                    callback.onPulseStarted();
-                    mStackScroller.setPulsing(true);
-                }
-
-                @Override
-                public void onPulseFinished() {
-                    callback.onPulseFinished();
-                    mStackScroller.setPulsing(false);
-                }
-            }, reason);
-        }
-
-        private void handleStopDozing() {
-            if (mDozingRequested) {
-                mDozingRequested = false;
-                DozeLog.traceDozing(mContext, mDozing);
-                updateDozing();
-            }
-        }
-
-        private final class H extends Handler {
-            private static final int MSG_START_DOZING = 1;
-            private static final int MSG_PULSE_WHILE_DOZING = 2;
-            private static final int MSG_STOP_DOZING = 3;
-
-            @Override
-            public void handleMessage(Message msg) {
-                switch (msg.what) {
-                    case MSG_START_DOZING:
-                        handleStartDozing((Runnable) msg.obj);
-                        break;
-                    case MSG_PULSE_WHILE_DOZING:
-                        handlePulseWhileDozing((PulseCallback) msg.obj, msg.arg1);
-                        break;
-                    case MSG_STOP_DOZING:
-                        handleStopDozing();
-                        break;
-                }
-            }
-        }
     }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/doze/DozeMachineTest.java b/packages/SystemUI/tests/src/com/android/systemui/doze/DozeMachineTest.java
new file mode 100644
index 0000000..ba7c923
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/doze/DozeMachineTest.java
@@ -0,0 +1,257 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.doze;
+
+import static com.android.systemui.doze.DozeMachine.State.DOZE;
+import static com.android.systemui.doze.DozeMachine.State.DOZE_AOD;
+import static com.android.systemui.doze.DozeMachine.State.DOZE_PULSE_DONE;
+import static com.android.systemui.doze.DozeMachine.State.DOZE_PULSING;
+import static com.android.systemui.doze.DozeMachine.State.DOZE_REQUEST_PULSE;
+import static com.android.systemui.doze.DozeMachine.State.FINISH;
+import static com.android.systemui.doze.DozeMachine.State.INITIALIZED;
+import static com.android.systemui.doze.DozeMachine.State.UNINITIALIZED;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.support.test.annotation.UiThreadTest;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+import android.view.Display;
+
+import com.android.systemui.statusbar.phone.DozeParameters;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class DozeMachineTest {
+
+    DozeMachine mMachine;
+
+    private DozeServiceFake mServiceFake;
+    private WakeLockFake mWakeLockFake;
+    private DozeParameters mParamsMock;
+    private DozeMachine.Part mPartMock;
+
+    @Before
+    public void setUp() {
+        mServiceFake = new DozeServiceFake();
+        mWakeLockFake = new WakeLockFake();
+        mParamsMock = mock(DozeParameters.class);
+        mPartMock = mock(DozeMachine.Part.class);
+
+        mMachine = new DozeMachine(mServiceFake, mParamsMock, mWakeLockFake);
+
+        mMachine.setParts(new DozeMachine.Part[]{mPartMock});
+    }
+
+    @Test
+    @UiThreadTest
+    public void testInitialize_initializesParts() {
+        mMachine.requestState(INITIALIZED);
+
+        verify(mPartMock).transitionTo(UNINITIALIZED, INITIALIZED);
+    }
+
+    @Test
+    @UiThreadTest
+    public void testInitialize_goesToDoze() {
+        when(mParamsMock.getAlwaysOn()).thenReturn(false);
+
+        mMachine.requestState(INITIALIZED);
+
+        verify(mPartMock).transitionTo(INITIALIZED, DOZE);
+        assertEquals(DOZE, mMachine.getState());
+    }
+
+    @Test
+    @UiThreadTest
+    public void testInitialize_goesToAod() {
+        when(mParamsMock.getAlwaysOn()).thenReturn(true);
+
+        mMachine.requestState(INITIALIZED);
+
+        verify(mPartMock).transitionTo(INITIALIZED, DOZE_AOD);
+        assertEquals(DOZE_AOD, mMachine.getState());
+    }
+
+    @Test
+    @UiThreadTest
+    public void testPulseDone_goesToDoze() {
+        when(mParamsMock.getAlwaysOn()).thenReturn(false);
+        mMachine.requestState(INITIALIZED);
+        mMachine.requestState(DOZE_REQUEST_PULSE);
+        mMachine.requestState(DOZE_PULSING);
+
+        mMachine.requestState(DOZE_PULSE_DONE);
+
+        verify(mPartMock).transitionTo(DOZE_PULSE_DONE, DOZE);
+        assertEquals(DOZE, mMachine.getState());
+    }
+
+    @Test
+    @UiThreadTest
+    public void testPulseDone_goesToAoD() {
+        when(mParamsMock.getAlwaysOn()).thenReturn(true);
+        mMachine.requestState(INITIALIZED);
+        mMachine.requestState(DOZE_REQUEST_PULSE);
+        mMachine.requestState(DOZE_PULSING);
+
+        mMachine.requestState(DOZE_PULSE_DONE);
+
+        verify(mPartMock).transitionTo(DOZE_PULSE_DONE, DOZE_AOD);
+        assertEquals(DOZE_AOD, mMachine.getState());
+    }
+
+    @Test
+    @UiThreadTest
+    public void testFinished_staysFinished() {
+        mMachine.requestState(INITIALIZED);
+        mMachine.requestState(FINISH);
+        reset(mPartMock);
+
+        mMachine.requestState(DOZE);
+
+        verify(mPartMock, never()).transitionTo(any(), any());
+        assertEquals(FINISH, mMachine.getState());
+    }
+
+    @Test
+    @UiThreadTest
+    public void testFinish_finishesService() {
+        mMachine.requestState(INITIALIZED);
+
+        mMachine.requestState(FINISH);
+
+        assertTrue(mServiceFake.finished);
+    }
+
+    @Test
+    @UiThreadTest
+    public void testWakeLock_heldInTransition() {
+        doAnswer((inv) -> {
+            assertTrue(mWakeLockFake.isHeld());
+            return null;
+        }).when(mPartMock).transitionTo(any(), any());
+
+        mMachine.requestState(INITIALIZED);
+    }
+
+    @Test
+    @UiThreadTest
+    public void testWakeLock_heldInPulseStates() {
+        mMachine.requestState(INITIALIZED);
+
+        mMachine.requestState(DOZE_REQUEST_PULSE);
+        assertTrue(mWakeLockFake.isHeld());
+
+        mMachine.requestState(DOZE_PULSING);
+        assertTrue(mWakeLockFake.isHeld());
+    }
+
+    @Test
+    @UiThreadTest
+    public void testWakeLock_notHeldInDozeStates() {
+        mMachine.requestState(INITIALIZED);
+
+        mMachine.requestState(DOZE);
+        assertFalse(mWakeLockFake.isHeld());
+
+        mMachine.requestState(DOZE_AOD);
+        assertFalse(mWakeLockFake.isHeld());
+    }
+
+    @Test
+    @UiThreadTest
+    public void testScreen_offInDoze() {
+        mMachine.requestState(INITIALIZED);
+
+        mMachine.requestState(DOZE);
+
+        assertEquals(Display.STATE_OFF, mServiceFake.screenState);
+    }
+
+    @Test
+    @UiThreadTest
+    public void testScreen_onInAod() {
+        mMachine.requestState(INITIALIZED);
+
+        mMachine.requestState(DOZE_AOD);
+
+        assertEquals(Display.STATE_DOZE, mServiceFake.screenState);
+    }
+
+    @Test
+    @UiThreadTest
+    public void testScreen_onInPulse() {
+        mMachine.requestState(INITIALIZED);
+
+        mMachine.requestState(DOZE_REQUEST_PULSE);
+        mMachine.requestState(DOZE_PULSING);
+
+        assertEquals(Display.STATE_DOZE, mServiceFake.screenState);
+    }
+
+    @Test
+    @UiThreadTest
+    public void testScreen_offInRequestPulseWithoutAoD() {
+        mMachine.requestState(INITIALIZED);
+
+        mMachine.requestState(DOZE);
+        mMachine.requestState(DOZE_REQUEST_PULSE);
+
+        assertEquals(Display.STATE_OFF, mServiceFake.screenState);
+    }
+
+    @Test
+    @UiThreadTest
+    public void testScreen_onInRequestPulseWithoutAoD() {
+        mMachine.requestState(INITIALIZED);
+
+        mMachine.requestState(DOZE_AOD);
+        mMachine.requestState(DOZE_REQUEST_PULSE);
+
+        assertEquals(Display.STATE_DOZE, mServiceFake.screenState);
+    }
+
+    @Test
+    @UiThreadTest
+    public void testTransitions_canRequestTransitions() {
+        mMachine.requestState(INITIALIZED);
+        mMachine.requestState(DOZE);
+        doAnswer(inv -> {
+            mMachine.requestState(DOZE_PULSING);
+            return null;
+        }).when(mPartMock).transitionTo(any(), eq(DOZE_REQUEST_PULSE));
+
+        mMachine.requestState(DOZE_REQUEST_PULSE);
+
+        assertEquals(DOZE_PULSING, mMachine.getState());
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/doze/DozeServiceFake.java b/packages/SystemUI/tests/src/com/android/systemui/doze/DozeServiceFake.java
new file mode 100644
index 0000000..d12fc2c
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/doze/DozeServiceFake.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.doze;
+
+import android.view.Display;
+
+public class DozeServiceFake implements DozeMachine.Service {
+
+    public boolean finished;
+    public int screenState;
+
+    public DozeServiceFake() {
+        reset();
+    }
+
+    @Override
+    public void finish() {
+        finished = true;
+    }
+
+    @Override
+    public void setDozeScreenState(int state) {
+        screenState = state;
+    }
+
+    public void reset() {
+        finished = false;
+        screenState = Display.STATE_UNKNOWN;
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/doze/WakeLockFake.java b/packages/SystemUI/tests/src/com/android/systemui/doze/WakeLockFake.java
new file mode 100644
index 0000000..7c04fe2
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/doze/WakeLockFake.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.doze;
+
+import com.android.internal.util.Preconditions;
+
+public class WakeLockFake extends DozeFactory.WakeLock {
+
+    private int mAcquired = 0;
+
+    public WakeLockFake() {
+        super(null);
+    }
+
+    @Override
+    public void acquire() {
+        mAcquired++;
+    }
+
+    @Override
+    public void release() {
+        Preconditions.checkState(mAcquired > 0);
+        mAcquired--;
+    }
+
+    @Override
+    public Runnable wrap(Runnable runnable) {
+        acquire();
+        return () -> {
+            try {
+                runnable.run();
+            } finally {
+                release();
+            }
+        };
+    }
+
+    public boolean isHeld() {
+        return mAcquired > 0;
+    }
+}