Suspend and resume DozeTriggers when entering and exiting car mode.

This change introduces a new DozeMachine state DOZE_SUSPEND_TRIGGERS
that is equivalent of DOZE but also suspends any triggers that awaken
the device. Entering car mode transitions the state machine to
DOZE_SUSPEND_TRIGGERS and leaving the car mode transitions to either
DOZE or DOZE_AOD depending on whether Always on Display is enabled.

Current behavior when entering car mode:
  - DozeService is finished.
  - Always on display is turned off
  - Tap on screen doesn’t wake up the device
  - Lifting device doesn’t wake up the device
  - Sending a notification doesn’t wake up the device.

New behavior when entering car mode:
  - DozeService is not finished.
  - Remaining behavior is the same as DozeTrigger stops listening to all
    triggers (sensors, intents, notifications)

Current behavior when exiting car mode:
  - DozeService is not running, normal triggers don't wake up the device

New behavior when exiting car mode:
  - DozeService is running, normal triggers wake up the device as
    expected.
  - DozeSuppressor transitions from DOZE_SUSPEND_TRIGGERS to DOZE/DOZE_AOD

Bug: 230968777
Fixes: 230968777
Test: manual
Test: atest com.android.systemui.doze
Change-Id: I5f49bb5700d7ed38763dccc8c5b6438d934c6476
diff --git a/packages/SystemUI/docs/device-entry/doze.md b/packages/SystemUI/docs/device-entry/doze.md
index 5ff8851..6b6dce5 100644
--- a/packages/SystemUI/docs/device-entry/doze.md
+++ b/packages/SystemUI/docs/device-entry/doze.md
@@ -17,6 +17,9 @@
 ### DOZE
 Device is asleep and listening for enabled pulsing and wake-up gesture triggers. In this state, no UI shows.
 
+### DOZE_SUSPEND_TRIGGERS
+Device is asleep and not listening for any triggers to wake up. This state is used only when CAR_MODE is active. In this state, no UI shows.
+
 ### DOZE_AOD
 Device is asleep, showing UI, and listening for enabled pulsing and wake-up triggers. In this state, screen brightness is handled by [DozeScreenBrightness][5] which uses the brightness sensor specified by `doze_brightness_sensor_type` in the [SystemUI config][6]. To save power, this should be a low-powered sensor that shouldn't trigger as often as the light sensor used for on-screen adaptive brightness.
 
diff --git a/packages/SystemUI/src/com/android/systemui/doze/DozeLog.java b/packages/SystemUI/src/com/android/systemui/doze/DozeLog.java
index 19287c1..4161cf6 100644
--- a/packages/SystemUI/src/com/android/systemui/doze/DozeLog.java
+++ b/packages/SystemUI/src/com/android/systemui/doze/DozeLog.java
@@ -332,6 +332,20 @@
     }
 
     /**
+     * Logs the car mode started event.
+     */
+    public void traceCarModeStarted() {
+        mLogger.logCarModeStarted();
+    }
+
+    /**
+     * Logs the car mode ended event.
+     */
+    public void traceCarModeEnded() {
+        mLogger.logCarModeEnded();
+    }
+
+    /**
      * Appends power save changes that may cause a new doze state
      * @param powerSaveActive true if power saving is active
      * @param nextState the state that we'll transition to
diff --git a/packages/SystemUI/src/com/android/systemui/doze/DozeLogger.kt b/packages/SystemUI/src/com/android/systemui/doze/DozeLogger.kt
index 4c81563..4b279ec 100644
--- a/packages/SystemUI/src/com/android/systemui/doze/DozeLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/doze/DozeLogger.kt
@@ -105,7 +105,7 @@
             bool4 = screenOnFromTouch
         }, {
             "Fling expand=$bool1 aboveThreshold=$bool2 thresholdNeeded=$bool3 " +
-                    "screenOnFromTouch=$bool4"
+                "screenOnFromTouch=$bool4"
         })
     }
 
@@ -151,7 +151,7 @@
             long2 = triggerAt
         }, {
             "Time tick scheduledAt=${DATE_FORMAT.format(Date(long1))} " +
-                    "triggerAt=${DATE_FORMAT.format(Date(long2))}"
+                "triggerAt=${DATE_FORMAT.format(Date(long2))}"
         })
     }
 
@@ -220,7 +220,7 @@
             str1 = partUpdated
         }, {
             "Posture changed, posture=${DevicePostureController.devicePostureToString(int1)}" +
-                    " partUpdated=$str1"
+                " partUpdated=$str1"
         })
     }
 
@@ -299,6 +299,18 @@
             "Doze aod dimming scrim opacity set, opacity=$long1"
         })
     }
+
+    fun logCarModeEnded() {
+        buffer.log(TAG, INFO, {}, {
+            "Doze car mode ended"
+        })
+    }
+
+    fun logCarModeStarted() {
+        buffer.log(TAG, INFO, {}, {
+            "Doze car mode started"
+        })
+    }
 }
 
 private const val TAG = "DozeLog"
diff --git a/packages/SystemUI/src/com/android/systemui/doze/DozeMachine.java b/packages/SystemUI/src/com/android/systemui/doze/DozeMachine.java
index 5779bb3..32bc9de 100644
--- a/packages/SystemUI/src/com/android/systemui/doze/DozeMachine.java
+++ b/packages/SystemUI/src/com/android/systemui/doze/DozeMachine.java
@@ -20,6 +20,8 @@
 import static com.android.systemui.keyguard.WakefulnessLifecycle.WAKEFULNESS_WAKING;
 
 import android.annotation.MainThread;
+import android.app.UiModeManager;
+import android.content.res.Configuration;
 import android.hardware.display.AmbientDisplayConfiguration;
 import android.os.Trace;
 import android.os.UserHandle;
@@ -33,7 +35,6 @@
 import com.android.systemui.keyguard.WakefulnessLifecycle;
 import com.android.systemui.keyguard.WakefulnessLifecycle.Wakefulness;
 import com.android.systemui.statusbar.phone.DozeParameters;
-import com.android.systemui.statusbar.policy.BatteryController;
 import com.android.systemui.util.Assert;
 import com.android.systemui.util.wakelock.WakeLock;
 
@@ -66,6 +67,8 @@
         INITIALIZED,
         /** Regular doze. Device is asleep and listening for pulse triggers. */
         DOZE,
+        /** Deep doze. Device is asleep and is not listening for pulse triggers. */
+        DOZE_SUSPEND_TRIGGERS,
         /** 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 */
@@ -125,6 +128,7 @@
                             : Display.STATE_ON;
                 case DOZE_AOD_PAUSED:
                 case DOZE:
+                case DOZE_SUSPEND_TRIGGERS:
                     return Display.STATE_OFF;
                 case DOZE_PULSING:
                 case DOZE_PULSING_BRIGHT:
@@ -143,26 +147,27 @@
     private final WakeLock mWakeLock;
     private final AmbientDisplayConfiguration mConfig;
     private final WakefulnessLifecycle mWakefulnessLifecycle;
-    private final BatteryController mBatteryController;
     private final DozeHost mDozeHost;
-    private Part[] mParts;
+    private final UiModeManager mUiModeManager;
+    private final DockManager mDockManager;
+    private final Part[] mParts;
 
     private final ArrayList<State> mQueuedRequests = new ArrayList<>();
     private State mState = State.UNINITIALIZED;
     private int mPulseReason;
     private boolean mWakeLockHeldForCurrentState = false;
-    private DockManager mDockManager;
 
     @Inject
     public DozeMachine(@WrappedService Service service, AmbientDisplayConfiguration config,
             WakeLock wakeLock, WakefulnessLifecycle wakefulnessLifecycle,
-            BatteryController batteryController, DozeLog dozeLog, DockManager dockManager,
+            UiModeManager uiModeManager,
+            DozeLog dozeLog, DockManager dockManager,
             DozeHost dozeHost, Part[] parts) {
         mDozeService = service;
         mConfig = config;
         mWakefulnessLifecycle = wakefulnessLifecycle;
         mWakeLock = wakeLock;
-        mBatteryController = batteryController;
+        mUiModeManager = uiModeManager;
         mDozeLog = dozeLog;
         mDockManager = dockManager;
         mDozeHost = dozeHost;
@@ -244,7 +249,7 @@
         Assert.isMainThread();
         if (isExecutingTransition()) {
             throw new IllegalStateException("Cannot get state because there were pending "
-                    + "transitions: " + mQueuedRequests.toString());
+                    + "transitions: " + mQueuedRequests);
         }
         return mState;
     }
@@ -313,11 +318,8 @@
         }
         mDozeLog.traceDozeStateSendComplete(newState);
 
-        switch (newState) {
-            case FINISH:
-                mDozeService.finish();
-                break;
-            default:
+        if (newState == State.FINISH) {
+            mDozeService.finish();
         }
     }
 
@@ -357,6 +359,12 @@
         if (mState == State.FINISH) {
             return State.FINISH;
         }
+        if (mUiModeManager.getCurrentModeType() == Configuration.UI_MODE_TYPE_CAR
+                && (requestedState.canPulse() || requestedState.staysAwake())) {
+            Log.i(TAG, "Doze is suppressed with all triggers disabled as car mode is active");
+            mDozeLog.traceCarModeStarted();
+            return State.DOZE_SUSPEND_TRIGGERS;
+        }
         if (mDozeHost.isAlwaysOnSuppressed() && requestedState.isAlwaysOn()) {
             Log.i(TAG, "Doze is suppressed by an app. Suppressing state: " + requestedState);
             mDozeLog.traceAlwaysOnSuppressed(requestedState, "app");
diff --git a/packages/SystemUI/src/com/android/systemui/doze/DozeScreenBrightness.java b/packages/SystemUI/src/com/android/systemui/doze/DozeScreenBrightness.java
index 83220ca..60227ee9 100644
--- a/packages/SystemUI/src/com/android/systemui/doze/DozeScreenBrightness.java
+++ b/packages/SystemUI/src/com/android/systemui/doze/DozeScreenBrightness.java
@@ -147,6 +147,7 @@
                 setLightSensorEnabled(true);
                 break;
             case DOZE:
+            case DOZE_SUSPEND_TRIGGERS:
                 setLightSensorEnabled(false);
                 resetBrightnessToDefault();
                 break;
diff --git a/packages/SystemUI/src/com/android/systemui/doze/DozeSuppressor.java b/packages/SystemUI/src/com/android/systemui/doze/DozeSuppressor.java
index 89f50ad..7ed4b35 100644
--- a/packages/SystemUI/src/com/android/systemui/doze/DozeSuppressor.java
+++ b/packages/SystemUI/src/com/android/systemui/doze/DozeSuppressor.java
@@ -17,6 +17,7 @@
 package com.android.systemui.doze;
 
 import static android.app.UiModeManager.ACTION_ENTER_CAR_MODE;
+import static android.app.UiModeManager.ACTION_EXIT_CAR_MODE;
 
 import android.app.UiModeManager;
 import android.content.BroadcastReceiver;
@@ -96,6 +97,7 @@
                 registerBroadcastReceiver();
                 mDozeHost.addCallback(mHostCallback);
                 checkShouldImmediatelyEndDoze();
+                checkShouldImmediatelySuspendDoze();
                 break;
             case FINISH:
                 destroy();
@@ -110,11 +112,16 @@
         mDozeHost.removeCallback(mHostCallback);
     }
 
+    private void checkShouldImmediatelySuspendDoze() {
+        if (mUiModeManager.getCurrentModeType() == Configuration.UI_MODE_TYPE_CAR) {
+            mDozeLog.traceCarModeStarted();
+            mMachine.requestState(DozeMachine.State.DOZE_SUSPEND_TRIGGERS);
+        }
+    }
+
     private void checkShouldImmediatelyEndDoze() {
         String reason = null;
-        if (mUiModeManager.getCurrentModeType() == Configuration.UI_MODE_TYPE_CAR) {
-            reason = "car_mode";
-        } else if (!mDozeHost.isProvisioned()) {
+        if (!mDozeHost.isProvisioned()) {
             reason = "device_unprovisioned";
         } else if (mBiometricUnlockControllerLazy.get().hasPendingAuthentication()) {
             reason = "has_pending_auth";
@@ -141,6 +148,7 @@
             return;
         }
         IntentFilter filter = new IntentFilter(ACTION_ENTER_CAR_MODE);
+        filter.addAction(ACTION_EXIT_CAR_MODE);
         mBroadcastDispatcher.registerReceiver(mBroadcastReceiver, filter);
         mBroadcastReceiverRegistered = true;
     }
@@ -156,9 +164,14 @@
     private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
         @Override
         public void onReceive(Context context, Intent intent) {
-            if (ACTION_ENTER_CAR_MODE.equals(intent.getAction())) {
-                mDozeLog.traceImmediatelyEndDoze("car_mode");
-                mMachine.requestState(DozeMachine.State.FINISH);
+            String action = intent.getAction();
+            if (ACTION_ENTER_CAR_MODE.equals(action)) {
+                mDozeLog.traceCarModeStarted();
+                mMachine.requestState(DozeMachine.State.DOZE_SUSPEND_TRIGGERS);
+            } else if (ACTION_EXIT_CAR_MODE.equals(action)) {
+                mDozeLog.traceCarModeEnded();
+                mMachine.requestState(mConfig.alwaysOnEnabled(UserHandle.USER_CURRENT)
+                        ? DozeMachine.State.DOZE_AOD : DozeMachine.State.DOZE);
             }
         }
     };
diff --git a/packages/SystemUI/src/com/android/systemui/doze/DozeTriggers.java b/packages/SystemUI/src/com/android/systemui/doze/DozeTriggers.java
index 1cc5df5..0014d6b 100644
--- a/packages/SystemUI/src/com/android/systemui/doze/DozeTriggers.java
+++ b/packages/SystemUI/src/com/android/systemui/doze/DozeTriggers.java
@@ -16,6 +16,10 @@
 
 package com.android.systemui.doze;
 
+import static com.android.systemui.doze.DozeMachine.State.DOZE_SUSPEND_TRIGGERS;
+import static com.android.systemui.doze.DozeMachine.State.FINISH;
+import static com.android.systemui.doze.DozeMachine.State.UNINITIALIZED;
+
 import android.annotation.Nullable;
 import android.content.BroadcastReceiver;
 import android.content.Context;
@@ -437,14 +441,18 @@
 
     @Override
     public void transitionTo(DozeMachine.State oldState, DozeMachine.State newState) {
+        if (oldState == DOZE_SUSPEND_TRIGGERS && (newState != FINISH
+                && newState != UNINITIALIZED)) {
+            // Register callbacks that were unregistered when we switched to
+            // DOZE_SUSPEND_TRIGGERS state.
+            registerCallbacks();
+        }
         switch (newState) {
             case INITIALIZED:
                 mAodInterruptRunnable = null;
                 sWakeDisplaySensorState = true;
-                mBroadcastReceiver.register(mBroadcastDispatcher);
-                mDockManager.addListener(mDockEventListener);
+                registerCallbacks();
                 mDozeSensors.requestTemporaryDisable();
-                mDozeHost.addCallback(mHostCallback);
                 break;
             case DOZE:
             case DOZE_AOD:
@@ -472,21 +480,36 @@
             case DOZE_PULSE_DONE:
                 mDozeSensors.requestTemporaryDisable();
                 break;
+            case DOZE_SUSPEND_TRIGGERS:
             case FINISH:
-                mBroadcastReceiver.unregister(mBroadcastDispatcher);
-                mDozeHost.removeCallback(mHostCallback);
-                mDockManager.removeListener(mDockEventListener);
-                mDozeSensors.setListening(false, false);
-                mDozeSensors.setProxListening(false);
-                mWantSensors = false;
-                mWantProxSensor = false;
-                mWantTouchScreenSensors = false;
+                stopListeningToAllTriggers();
                 break;
             default:
         }
         mDozeSensors.setListening(mWantSensors, mWantTouchScreenSensors);
     }
 
+    private void registerCallbacks() {
+        mBroadcastReceiver.register(mBroadcastDispatcher);
+        mDockManager.addListener(mDockEventListener);
+        mDozeHost.addCallback(mHostCallback);
+    }
+
+    private void unregisterCallbacks() {
+        mBroadcastReceiver.unregister(mBroadcastDispatcher);
+        mDozeHost.removeCallback(mHostCallback);
+        mDockManager.removeListener(mDockEventListener);
+    }
+
+    private void stopListeningToAllTriggers() {
+        unregisterCallbacks();
+        mDozeSensors.setListening(false, false);
+        mDozeSensors.setProxListening(false);
+        mWantSensors = false;
+        mWantProxSensor = false;
+        mWantTouchScreenSensors = false;
+    }
+
     @Override
     public void onScreenState(int state) {
         mDozeSensors.onScreenState(state);
diff --git a/packages/SystemUI/src/com/android/systemui/doze/DozeUi.java b/packages/SystemUI/src/com/android/systemui/doze/DozeUi.java
index e568b82..70e4fa3 100644
--- a/packages/SystemUI/src/com/android/systemui/doze/DozeUi.java
+++ b/packages/SystemUI/src/com/android/systemui/doze/DozeUi.java
@@ -139,6 +139,7 @@
                 break;
             case DOZE:
             case DOZE_AOD_PAUSED:
+            case DOZE_SUSPEND_TRIGGERS:
                 unscheduleTimeTick();
                 break;
             case DOZE_REQUEST_PULSE:
diff --git a/packages/SystemUI/tests/src/com/android/systemui/doze/DozeMachineTest.java b/packages/SystemUI/tests/src/com/android/systemui/doze/DozeMachineTest.java
index 3340f2f..abe7ae1 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/doze/DozeMachineTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/doze/DozeMachineTest.java
@@ -21,7 +21,9 @@
 import static com.android.systemui.doze.DozeMachine.State.DOZE_AOD_DOCKED;
 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_PULSING_BRIGHT;
 import static com.android.systemui.doze.DozeMachine.State.DOZE_REQUEST_PULSE;
+import static com.android.systemui.doze.DozeMachine.State.DOZE_SUSPEND_TRIGGERS;
 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;
@@ -39,6 +41,8 @@
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
+import android.app.UiModeManager;
+import android.content.res.Configuration;
 import android.hardware.display.AmbientDisplayConfiguration;
 import android.testing.AndroidTestingRunner;
 import android.testing.UiThreadTest;
@@ -50,7 +54,6 @@
 import com.android.systemui.dock.DockManager;
 import com.android.systemui.keyguard.WakefulnessLifecycle;
 import com.android.systemui.statusbar.phone.DozeParameters;
-import com.android.systemui.statusbar.policy.BatteryController;
 import com.android.systemui.util.wakelock.WakeLockFake;
 
 import org.junit.Before;
@@ -70,9 +73,12 @@
     private WakefulnessLifecycle mWakefulnessLifecycle;
     @Mock
     private DozeLog mDozeLog;
-    @Mock private DockManager mDockManager;
+    @Mock
+    private DockManager mDockManager;
     @Mock
     private DozeHost mHost;
+    @Mock
+    private UiModeManager mUiModeManager;
     private DozeServiceFake mServiceFake;
     private WakeLockFake mWakeLockFake;
     private AmbientDisplayConfiguration mConfigMock;
@@ -89,7 +95,7 @@
         when(mDockManager.isHidden()).thenReturn(false);
 
         mMachine = new DozeMachine(mServiceFake, mConfigMock, mWakeLockFake,
-                mWakefulnessLifecycle, mock(BatteryController.class), mDozeLog, mDockManager,
+                mWakefulnessLifecycle, mUiModeManager, mDozeLog, mDockManager,
                 mHost, new DozeMachine.Part[]{mPartMock});
     }
 
@@ -462,4 +468,64 @@
 
         assertEquals(Display.STATE_ON, DOZE_REQUEST_PULSE.screenState(dozeParameters));
     }
+
+    @Test
+    public void testTransitionToInitialized_carModeIsEnabled() {
+        when(mUiModeManager.getCurrentModeType()).thenReturn(Configuration.UI_MODE_TYPE_CAR);
+        mMachine.requestState(INITIALIZED);
+
+        verify(mPartMock).transitionTo(UNINITIALIZED, INITIALIZED);
+        verify(mPartMock).transitionTo(INITIALIZED, DOZE_SUSPEND_TRIGGERS);
+        assertEquals(DOZE_SUSPEND_TRIGGERS, mMachine.getState());
+    }
+
+    @Test
+    public void testTransitionToFinish_carModeIsEnabled() {
+        when(mUiModeManager.getCurrentModeType()).thenReturn(Configuration.UI_MODE_TYPE_CAR);
+        mMachine.requestState(INITIALIZED);
+        mMachine.requestState(FINISH);
+
+        assertEquals(FINISH, mMachine.getState());
+    }
+
+    @Test
+    public void testDozeToDozeSuspendTriggers_carModeIsEnabled() {
+        when(mUiModeManager.getCurrentModeType()).thenReturn(Configuration.UI_MODE_TYPE_CAR);
+        mMachine.requestState(INITIALIZED);
+        mMachine.requestState(DOZE);
+
+        assertEquals(DOZE_SUSPEND_TRIGGERS, mMachine.getState());
+    }
+
+    @Test
+    public void testDozeAoDToDozeSuspendTriggers_carModeIsEnabled() {
+        when(mUiModeManager.getCurrentModeType()).thenReturn(Configuration.UI_MODE_TYPE_CAR);
+        mMachine.requestState(INITIALIZED);
+        mMachine.requestState(DOZE_AOD);
+
+        assertEquals(DOZE_SUSPEND_TRIGGERS, mMachine.getState());
+    }
+
+    @Test
+    public void testDozePulsingBrightDozeSuspendTriggers_carModeIsEnabled() {
+        when(mUiModeManager.getCurrentModeType()).thenReturn(Configuration.UI_MODE_TYPE_CAR);
+        mMachine.requestState(INITIALIZED);
+        mMachine.requestState(DOZE_PULSING_BRIGHT);
+
+        assertEquals(DOZE_SUSPEND_TRIGGERS, mMachine.getState());
+    }
+
+    @Test
+    public void testDozeAodDockedDozeSuspendTriggers_carModeIsEnabled() {
+        when(mUiModeManager.getCurrentModeType()).thenReturn(Configuration.UI_MODE_TYPE_CAR);
+        mMachine.requestState(INITIALIZED);
+        mMachine.requestState(DOZE_AOD_DOCKED);
+
+        assertEquals(DOZE_SUSPEND_TRIGGERS, mMachine.getState());
+    }
+
+    @Test
+    public void testDozeSuppressTriggers_screenState() {
+        assertEquals(Display.STATE_OFF, DOZE_SUSPEND_TRIGGERS.screenState(null));
+    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/doze/DozeScreenBrightnessTest.java b/packages/SystemUI/tests/src/com/android/systemui/doze/DozeScreenBrightnessTest.java
index 2e7b88d..03827da 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/doze/DozeScreenBrightnessTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/doze/DozeScreenBrightnessTest.java
@@ -24,6 +24,7 @@
 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.DOZE_SUSPEND_TRIGGERS;
 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;
@@ -182,6 +183,21 @@
     }
 
     @Test
+    public void dozeSuspendTriggers_doesNotUseLightSensor() {
+        // GIVEN the device is DOZE and the display state changes to ON
+        mScreen.transitionTo(UNINITIALIZED, INITIALIZED);
+        mScreen.transitionTo(INITIALIZED, DOZE_SUSPEND_TRIGGERS);
+        waitForSensorManager();
+
+        // WHEN new sensor event sent
+        mSensor.sendSensorEvent(3);
+
+        // THEN brightness is NOT changed, it's set to the default brightness
+        assertNotSame(3, mServiceFake.screenBrightness);
+        assertEquals(DEFAULT_BRIGHTNESS, mServiceFake.screenBrightness);
+    }
+
+    @Test
     public void aod_usesLightSensor() {
         // GIVEN the device is DOZE_AOD and the display state changes to ON
         mScreen.transitionTo(UNINITIALIZED, INITIALIZED);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/doze/DozeSuppressorTest.java b/packages/SystemUI/tests/src/com/android/systemui/doze/DozeSuppressorTest.java
index aa0a909..0f29dcd 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/doze/DozeSuppressorTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/doze/DozeSuppressorTest.java
@@ -16,19 +16,26 @@
 
 package com.android.systemui.doze;
 
+import static android.app.UiModeManager.ACTION_ENTER_CAR_MODE;
+import static android.app.UiModeManager.ACTION_EXIT_CAR_MODE;
+
 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_SUSPEND_TRIGGERS;
 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.mockito.Matchers.anyObject;
+import static org.hamcrest.Matchers.containsInAnyOrder;
+import static org.junit.Assert.assertEquals;
 import static org.mockito.Mockito.anyInt;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
 import android.app.UiModeManager;
 import android.content.BroadcastReceiver;
+import android.content.Intent;
+import android.content.IntentFilter;
 import android.content.res.Configuration;
 import android.hardware.display.AmbientDisplayConfiguration;
 import android.testing.AndroidTestingRunner;
@@ -77,7 +84,10 @@
 
     @Captor
     private ArgumentCaptor<BroadcastReceiver> mBroadcastReceiverCaptor;
+    @Captor
+    private ArgumentCaptor<IntentFilter> mIntentFilterCaptor;
     private BroadcastReceiver mBroadcastReceiver;
+    private IntentFilter mIntentFilter;
 
     @Captor
     private ArgumentCaptor<DozeHost.Callback> mDozeHostCaptor;
@@ -122,15 +132,59 @@
     }
 
     @Test
-    public void testEndDoze_carMode() {
+    public void testSuspendTriggersDoze_carMode() {
         // GIVEN car mode
         when(mUiModeManager.getCurrentModeType()).thenReturn(Configuration.UI_MODE_TYPE_CAR);
 
         // WHEN dozing begins
         mDozeSuppressor.transitionTo(UNINITIALIZED, INITIALIZED);
 
-        // THEN doze immediately ends
-        verify(mDozeMachine).requestState(FINISH);
+        // THEN doze continues with all doze triggers disabled.
+        verify(mDozeMachine).requestState(DOZE_SUSPEND_TRIGGERS);
+    }
+
+    @Test
+    public void testSuspendTriggersDoze_enterCarMode() {
+        // GIVEN currently dozing
+        mDozeSuppressor.transitionTo(UNINITIALIZED, INITIALIZED);
+        captureBroadcastReceiver();
+        mDozeSuppressor.transitionTo(INITIALIZED, DOZE);
+
+        // WHEN car mode entered
+        mBroadcastReceiver.onReceive(null, new Intent(ACTION_ENTER_CAR_MODE));
+
+        // THEN doze continues with all doze triggers disabled.
+        verify(mDozeMachine).requestState(DOZE_SUSPEND_TRIGGERS);
+    }
+
+    @Test
+    public void testDozeResume_exitCarMode() {
+        // GIVEN currently suspended, with AOD not enabled
+        when(mConfig.alwaysOnEnabled(anyInt())).thenReturn(false);
+        mDozeSuppressor.transitionTo(UNINITIALIZED, INITIALIZED);
+        captureBroadcastReceiver();
+        mDozeSuppressor.transitionTo(INITIALIZED, DOZE_SUSPEND_TRIGGERS);
+
+        // WHEN exiting car mode
+        mBroadcastReceiver.onReceive(null, new Intent(ACTION_EXIT_CAR_MODE));
+
+        // THEN doze is resumed
+        verify(mDozeMachine).requestState(DOZE);
+    }
+
+    @Test
+    public void testDozeAoDResume_exitCarMode() {
+        // GIVEN currently suspended, with AOD not enabled
+        when(mConfig.alwaysOnEnabled(anyInt())).thenReturn(true);
+        mDozeSuppressor.transitionTo(UNINITIALIZED, INITIALIZED);
+        captureBroadcastReceiver();
+        mDozeSuppressor.transitionTo(INITIALIZED, DOZE_SUSPEND_TRIGGERS);
+
+        // WHEN exiting car mode
+        mBroadcastReceiver.onReceive(null, new Intent(ACTION_EXIT_CAR_MODE));
+
+        // THEN doze AOD is resumed
+        verify(mDozeMachine).requestState(DOZE_AOD);
     }
 
     @Test
@@ -225,7 +279,11 @@
 
     private void captureBroadcastReceiver() {
         verify(mBroadcastDispatcher).registerReceiver(mBroadcastReceiverCaptor.capture(),
-                anyObject());
+                mIntentFilterCaptor.capture());
         mBroadcastReceiver = mBroadcastReceiverCaptor.getValue();
+        mIntentFilter = mIntentFilterCaptor.getValue();
+        assertEquals(2, mIntentFilter.countActions());
+        org.hamcrest.MatcherAssert.assertThat(() -> mIntentFilter.actionsIterator(),
+                containsInAnyOrder(ACTION_ENTER_CAR_MODE, ACTION_EXIT_CAR_MODE));
     }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/doze/DozeTriggersTest.java b/packages/SystemUI/tests/src/com/android/systemui/doze/DozeTriggersTest.java
index 4eeb4ac..01a1a37 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/doze/DozeTriggersTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/doze/DozeTriggersTest.java
@@ -17,6 +17,8 @@
 package com.android.systemui.doze;
 
 import static com.android.systemui.doze.DozeMachine.State.DOZE_AOD;
+import static com.android.systemui.doze.DozeMachine.State.INITIALIZED;
+import static com.android.systemui.doze.DozeMachine.State.UNINITIALIZED;
 
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyFloat;
@@ -133,7 +135,7 @@
         ArgumentCaptor<DozeHost.Callback> captor = ArgumentCaptor.forClass(DozeHost.Callback.class);
         doAnswer(invocation -> null).when(mHost).addCallback(captor.capture());
 
-        mTriggers.transitionTo(DozeMachine.State.UNINITIALIZED, DozeMachine.State.INITIALIZED);
+        mTriggers.transitionTo(UNINITIALIZED, DozeMachine.State.INITIALIZED);
         mTriggers.transitionTo(DozeMachine.State.INITIALIZED, DozeMachine.State.DOZE);
         clearInvocations(mMachine);
 
@@ -192,8 +194,21 @@
     }
 
     @Test
+    public void transitionToDozeSuspendTriggers_disablesAllCallbacks() {
+        mTriggers.transitionTo(UNINITIALIZED, INITIALIZED);
+        when(mMachine.getState()).thenReturn(DozeMachine.State.DOZE_SUSPEND_TRIGGERS);
+
+        mTriggers.transitionTo(DozeMachine.State.INITIALIZED,
+                DozeMachine.State.DOZE_SUSPEND_TRIGGERS);
+
+        verify(mDockManager).removeListener(any());
+        verify(mBroadcastDispatcher).unregisterReceiver(any());
+        verify(mHost).removeCallback(any());
+    }
+
+    @Test
     public void testDockEventListener_registerAndUnregister() {
-        mTriggers.transitionTo(DozeMachine.State.UNINITIALIZED, DozeMachine.State.INITIALIZED);
+        mTriggers.transitionTo(UNINITIALIZED, DozeMachine.State.INITIALIZED);
         verify(mDockManager).addListener(any());
 
         mTriggers.transitionTo(DozeMachine.State.DOZE, DozeMachine.State.FINISH);