diff --git a/services/core/java/com/android/server/DeviceIdleController.java b/services/core/java/com/android/server/DeviceIdleController.java index 2ab46e65e77fa..4aaf0b7b9b364 100644 --- a/services/core/java/com/android/server/DeviceIdleController.java +++ b/services/core/java/com/android/server/DeviceIdleController.java @@ -291,6 +291,7 @@ public class DeviceIdleController extends SystemService private boolean mLightEnabled; private boolean mDeepEnabled; private boolean mQuickDozeActivated; + private boolean mQuickDozeActivatedWhileIdling; private boolean mForceIdle; private boolean mNetworkConnected; private boolean mScreenOn; @@ -302,6 +303,10 @@ public class DeviceIdleController extends SystemService private boolean mHasNetworkLocation; private Location mLastGenericLocation; private Location mLastGpsLocation; + + /** Time in the elapsed realtime timebase when this listener last received a motion event. */ + private long mLastMotionEventElapsed; + // Current locked state of the screen private boolean mScreenLocked; private int mNumBlockingConstraints = 0; @@ -547,6 +552,9 @@ public class DeviceIdleController extends SystemService */ private ArrayMap mRemovedFromSystemWhitelistApps = new ArrayMap<>(); + private final ArraySet mStationaryListeners = + new ArraySet<>(); + private static final int EVENT_NULL = 0; private static final int EVENT_NORMAL = 1; private static final int EVENT_LIGHT_IDLE = 2; @@ -605,6 +613,21 @@ public class DeviceIdleController extends SystemService } }; + private final AlarmManager.OnAlarmListener mMotionTimeoutAlarmListener = () -> { + synchronized (DeviceIdleController.this) { + if (!isStationaryLocked()) { + // If the device keeps registering motion, then the alarm should be + // rescheduled, so this shouldn't go off until the device is stationary. + // This case may happen in a race condition (alarm goes off right before + // motion is detected, but handleMotionDetectedLocked is called before + // we enter this block). + Slog.w(TAG, "motion timeout went off and device isn't stationary"); + return; + } + } + postStationaryStatusUpdated(); + }; + private final AlarmManager.OnAlarmListener mSensingTimeoutAlarmListener = new AlarmManager.OnAlarmListener() { @Override @@ -654,12 +677,70 @@ public class DeviceIdleController extends SystemService } }; + /** Post stationary status only to this listener. */ + private void postStationaryStatus(StationaryListener listener) { + mHandler.obtainMessage(MSG_REPORT_STATIONARY_STATUS, listener).sendToTarget(); + } + + /** Post stationary status to all registered listeners. */ + private void postStationaryStatusUpdated() { + mHandler.sendEmptyMessage(MSG_REPORT_STATIONARY_STATUS); + } + + private boolean isStationaryLocked() { + final long now = mInjector.getElapsedRealtime(); + return mMotionListener.active + // Listening for motion for long enough and last motion was long enough ago. + && now - Math.max(mMotionListener.activatedTimeElapsed, mLastMotionEventElapsed) + >= mConstants.MOTION_INACTIVE_TIMEOUT; + } + + @VisibleForTesting + void registerStationaryListener(StationaryListener listener) { + synchronized (this) { + if (!mStationaryListeners.add(listener)) { + // Listener already registered. + return; + } + postStationaryStatus(listener); + if (mMotionListener.active) { + if (!isStationaryLocked() && mStationaryListeners.size() == 1) { + // First listener to be registered and the device isn't stationary, so we + // need to register the alarm to report the device is stationary. + scheduleMotionTimeoutAlarmLocked(); + } + } else { + startMonitoringMotionLocked(); + scheduleMotionTimeoutAlarmLocked(); + } + } + } + + private void unregisterStationaryListener(StationaryListener listener) { + synchronized (this) { + if (mStationaryListeners.remove(listener) && mStationaryListeners.size() == 0 + // Motion detection is started when transitioning from INACTIVE to IDLE_PENDING + // and so doesn't need to be on for ACTIVE or INACTIVE states. + // Motion detection isn't needed when idling due to Quick Doze. + && (mState == STATE_ACTIVE || mState == STATE_INACTIVE + || mQuickDozeActivated)) { + maybeStopMonitoringMotionLocked(); + } + } + } + @VisibleForTesting final class MotionListener extends TriggerEventListener implements SensorEventListener { boolean active = false; + /** + * Time in the elapsed realtime timebase when this listener was activated. Only valid if + * {@link #active} is true. + */ + long activatedTimeElapsed; + public boolean isActive() { return active; } @@ -667,7 +748,6 @@ public class DeviceIdleController extends SystemService @Override public void onTrigger(TriggerEvent event) { synchronized (DeviceIdleController.this) { - active = false; motionLocked(); } } @@ -675,8 +755,6 @@ public class DeviceIdleController extends SystemService @Override public void onSensorChanged(SensorEvent event) { synchronized (DeviceIdleController.this) { - mSensorManager.unregisterListener(this, mMotionSensor); - active = false; motionLocked(); } } @@ -694,6 +772,7 @@ public class DeviceIdleController extends SystemService } if (success) { active = true; + activatedTimeElapsed = mInjector.getElapsedRealtime(); } else { Slog.e(TAG, "Unable to register for " + mMotionSensor); } @@ -1307,6 +1386,8 @@ public class DeviceIdleController extends SystemService private static final int MSG_SEND_CONSTRAINT_MONITORING = 10; private static final int MSG_UPDATE_PRE_IDLE_TIMEOUT_FACTOR = 11; private static final int MSG_RESET_PRE_IDLE_TIMEOUT_FACTOR = 12; + @VisibleForTesting + static final int MSG_REPORT_STATIONARY_STATUS = 13; final class MyHandler extends Handler { MyHandler(Looper looper) { @@ -1443,6 +1524,30 @@ public class DeviceIdleController extends SystemService updatePreIdleFactor(); maybeDoImmediateMaintenance(); } break; + case MSG_REPORT_STATIONARY_STATUS: { + final StationaryListener newListener = (StationaryListener) msg.obj; + final StationaryListener[] listeners; + final boolean isStationary; + synchronized (DeviceIdleController.this) { + isStationary = isStationaryLocked(); + if (newListener == null) { + // Only notify all listeners if we aren't directing to one listener. + listeners = mStationaryListeners.toArray( + new StationaryListener[mStationaryListeners.size()]); + } else { + listeners = null; + } + } + if (listeners != null) { + for (StationaryListener listener : listeners) { + listener.onDeviceStationaryChanged(isStationary); + } + } + if (newListener != null) { + newListener.onDeviceStationaryChanged(isStationary); + } + } + break; } } } @@ -1628,6 +1733,19 @@ public class DeviceIdleController extends SystemService } } + /** + * Listener to be notified when DeviceIdleController determines that the device has + * moved or is stationary. + */ + public interface StationaryListener { + /** + * Called when DeviceIdleController has determined that the device is stationary or moving. + * + * @param isStationary true if the device is stationary, false otherwise + */ + void onDeviceStationaryChanged(boolean isStationary); + } + public class LocalService { public void onConstraintStateChanged(IDeviceIdleConstraint constraint, boolean active) { synchronized (DeviceIdleController.this) { @@ -1693,6 +1811,24 @@ public class DeviceIdleController extends SystemService public int[] getPowerSaveTempWhitelistAppIds() { return DeviceIdleController.this.getAppIdTempWhitelistInternal(); } + + /** + * Registers a listener that will be notified when the system has detected that the device + * is + * stationary or in motion. + */ + public void registerStationaryListener(StationaryListener listener) { + DeviceIdleController.this.registerStationaryListener(listener); + } + + /** + * Unregisters a registered stationary listener from being notified when the system has + * detected + * that the device is stationary or in motion. + */ + public void unregisterStationaryListener(StationaryListener listener) { + DeviceIdleController.this.unregisterStationaryListener(listener); + } } static class Injector { @@ -1734,6 +1870,11 @@ public class DeviceIdleController extends SystemService return mConstants; } + /** Returns the current elapsed realtime in milliseconds. */ + long getElapsedRealtime() { + return SystemClock.elapsedRealtime(); + } + LocationManager getLocationManager() { if (mLocationManager == null) { mLocationManager = mContext.getSystemService(LocationManager.class); @@ -2601,6 +2742,8 @@ public class DeviceIdleController extends SystemService void updateQuickDozeFlagLocked(boolean enabled) { if (DEBUG) Slog.i(TAG, "updateQuickDozeFlagLocked: enabled=" + enabled); mQuickDozeActivated = enabled; + mQuickDozeActivatedWhileIdling = + mQuickDozeActivated && (mState == STATE_IDLE || mState == STATE_IDLE_MAINTENANCE); if (enabled) { // If Quick Doze is enabled, see if we should go straight into it. becomeInactiveIfAppropriateLocked(); @@ -2767,10 +2910,11 @@ public class DeviceIdleController extends SystemService mNextIdleDelay = 0; mNextLightIdleDelay = 0; mIdleStartTime = 0; + mQuickDozeActivatedWhileIdling = false; cancelAlarmLocked(); cancelSensingTimeoutAlarmLocked(); cancelLocatingLocked(); - stopMonitoringMotionLocked(); + maybeStopMonitoringMotionLocked(); mAnyMotionDetector.stop(); updateActiveConstraintsLocked(); } @@ -3270,11 +3414,23 @@ public class DeviceIdleController extends SystemService void motionLocked() { if (DEBUG) Slog.d(TAG, "motionLocked()"); - // The motion sensor will have been disabled at this point + mLastMotionEventElapsed = mInjector.getElapsedRealtime(); handleMotionDetectedLocked(mConstants.MOTION_INACTIVE_TIMEOUT, "motion"); } void handleMotionDetectedLocked(long timeout, String type) { + if (mStationaryListeners.size() > 0) { + postStationaryStatusUpdated(); + scheduleMotionTimeoutAlarmLocked(); + } + if (mQuickDozeActivated && !mQuickDozeActivatedWhileIdling) { + // Don't exit idle due to motion if quick doze is enabled. + // However, if the device started idling due to the normal progression (going through + // all the states) and then had quick doze activated, come out briefly on motion so the + // user can get slightly fresher content. + return; + } + maybeStopMonitoringMotionLocked(); // The device is not yet active, so we want to go back to the pending idle // state to wait again for no motion. Note that we only monitor for motion // after moving out of the inactive state, so no need to worry about that. @@ -3326,10 +3482,15 @@ public class DeviceIdleController extends SystemService } } - void stopMonitoringMotionLocked() { - if (DEBUG) Slog.d(TAG, "stopMonitoringMotionLocked()"); - if (mMotionSensor != null && mMotionListener.active) { + /** + * Stops motion monitoring. Will not stop monitoring if there are registered stationary + * listeners. + */ + private void maybeStopMonitoringMotionLocked() { + if (DEBUG) Slog.d(TAG, "maybeStopMonitoringMotionLocked()"); + if (mMotionSensor != null && mMotionListener.active && mStationaryListeners.size() == 0) { mMotionListener.unregisterLocked(); + cancelMotionTimeoutAlarmLocked(); } } @@ -3356,6 +3517,10 @@ public class DeviceIdleController extends SystemService } } + private void cancelMotionTimeoutAlarmLocked() { + mAlarmManager.cancel(mMotionTimeoutAlarmListener); + } + void cancelSensingTimeoutAlarmLocked() { if (mNextSensingTimeoutAlarmTime != 0) { mNextSensingTimeoutAlarmTime = 0; @@ -3402,6 +3567,14 @@ public class DeviceIdleController extends SystemService mNextLightAlarmTime, "DeviceIdleController.light", mLightAlarmListener, mHandler); } + private void scheduleMotionTimeoutAlarmLocked() { + if (DEBUG) Slog.d(TAG, "scheduleMotionAlarmLocked"); + long nextMotionTimeoutAlarmTime = + mInjector.getElapsedRealtime() + mConstants.MOTION_INACTIVE_TIMEOUT; + mAlarmManager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, nextMotionTimeoutAlarmTime, + "DeviceIdleController.motion", mMotionTimeoutAlarmListener, mHandler); + } + void scheduleSensingTimeoutAlarmLocked(long delay) { if (DEBUG) Slog.d(TAG, "scheduleSensingAlarmLocked(" + delay + ")"); mNextSensingTimeoutAlarmTime = SystemClock.elapsedRealtime() + delay; @@ -4322,9 +4495,14 @@ public class DeviceIdleController extends SystemService } pw.println(" }"); } - if (mUseMotionSensor) { + if (mUseMotionSensor || mStationaryListeners.size() > 0) { pw.print(" mMotionActive="); pw.println(mMotionListener.active); pw.print(" mNotMoving="); pw.println(mNotMoving); + pw.print(" mMotionListener.activatedTimeElapsed="); + pw.println(mMotionListener.activatedTimeElapsed); + pw.print(" mLastMotionEventElapsed="); pw.println(mLastMotionEventElapsed); + pw.print(" "); pw.print(mStationaryListeners.size()); + pw.println(" stationary listeners registered"); } pw.print(" mLocating="); pw.print(mLocating); pw.print(" mHasGps="); pw.print(mHasGps); pw.print(" mHasNetwork="); diff --git a/services/core/java/com/android/server/location/GnssLocationProvider.java b/services/core/java/com/android/server/location/GnssLocationProvider.java index 181a435493132..4b79677b475b5 100644 --- a/services/core/java/com/android/server/location/GnssLocationProvider.java +++ b/services/core/java/com/android/server/location/GnssLocationProvider.java @@ -75,6 +75,8 @@ import com.android.internal.location.ProviderProperties; import com.android.internal.location.ProviderRequest; import com.android.internal.location.gnssmetrics.GnssMetrics; import com.android.internal.telephony.TelephonyIntents; +import com.android.server.DeviceIdleController; +import com.android.server.LocalServices; import com.android.server.location.GnssSatelliteBlacklistHelper.GnssSatelliteBlacklistCallback; import com.android.server.location.NtpTimeHelper.InjectNtpTimeCallback; @@ -178,6 +180,7 @@ public class GnssLocationProvider extends AbstractLocationProvider implements private static final int AGPS_SUPL_MODE_MSA = 0x02; private static final int AGPS_SUPL_MODE_MSB = 0x01; + private static final int UPDATE_LOW_POWER_MODE = 1; private static final int SET_REQUEST = 3; private static final int INJECT_NTP_TIME = 5; // PSDS stands for Predicted Satellite Data Service @@ -370,6 +373,12 @@ public class GnssLocationProvider extends AbstractLocationProvider implements // True if gps should be disabled because of PowerManager controls private boolean mDisableGpsForPowerManager = false; + /** + * True if the device idle controller has determined that the device is stationary. This is only + * updated when the device enters idle mode. + */ + private volatile boolean mIsDeviceStationary = false; + /** * Properties loaded from PROPERTIES_FILE. * It must be accessed only inside {@link #mHandler}. @@ -462,6 +471,15 @@ public class GnssLocationProvider extends AbstractLocationProvider implements public GnssNavigationMessageProvider getGnssNavigationMessageProvider() { return mGnssNavigationMessageProvider; } + + private final DeviceIdleController.StationaryListener mDeviceIdleStationaryListener = + isStationary -> { + mIsDeviceStationary = isStationary; + // Call updateLowPowerMode on handler thread so it's always called from the same + // thread. + mHandler.sendEmptyMessage(UPDATE_LOW_POWER_MODE); + }; + private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { @@ -478,11 +496,22 @@ public class GnssLocationProvider extends AbstractLocationProvider implements case ALARM_TIMEOUT: hibernate(); break; - case PowerManager.ACTION_POWER_SAVE_MODE_CHANGED: case PowerManager.ACTION_DEVICE_IDLE_MODE_CHANGED: + DeviceIdleController.LocalService deviceIdleService = LocalServices.getService( + DeviceIdleController.LocalService.class); + if (mPowerManager.isDeviceIdleMode()) { + deviceIdleService.registerStationaryListener(mDeviceIdleStationaryListener); + } else { + deviceIdleService.unregisterStationaryListener( + mDeviceIdleStationaryListener); + } + // Intentional fall-through. + case PowerManager.ACTION_POWER_SAVE_MODE_CHANGED: case Intent.ACTION_SCREEN_OFF: case Intent.ACTION_SCREEN_ON: - updateLowPowerMode(); + // Call updateLowPowerMode on handler thread so it's always called from the + // same thread. + mHandler.sendEmptyMessage(UPDATE_LOW_POWER_MODE); break; case CarrierConfigManager.ACTION_CARRIER_CONFIG_CHANGED: case TelephonyIntents.ACTION_DEFAULT_DATA_SUBSCRIPTION_CHANGED: @@ -540,10 +569,9 @@ public class GnssLocationProvider extends AbstractLocationProvider implements } private void updateLowPowerMode() { - // Disable GPS if we are in device idle mode. - boolean disableGpsForPowerManager = mPowerManager.isDeviceIdleMode(); - final PowerSaveState result = - mPowerManager.getPowerSaveState(ServiceType.LOCATION); + // Disable GPS if we are in device idle mode and the device is stationary. + boolean disableGpsForPowerManager = mPowerManager.isDeviceIdleMode() && mIsDeviceStationary; + final PowerSaveState result = mPowerManager.getPowerSaveState(ServiceType.LOCATION); switch (result.locationMode) { case PowerManager.LOCATION_MODE_GPS_DISABLED_WHEN_SCREEN_OFF: case PowerManager.LOCATION_MODE_ALL_DISABLED_WHEN_SCREEN_OFF: @@ -2048,6 +2076,9 @@ public class GnssLocationProvider extends AbstractLocationProvider implements case REPORT_SV_STATUS: handleReportSvStatus((SvStatusInfo) msg.obj); break; + case UPDATE_LOW_POWER_MODE: + updateLowPowerMode(); + break; } if (msg.arg2 == 1) { // wakelock was taken for this message, release it diff --git a/services/tests/mockingservicestests/src/com/android/server/DeviceIdleControllerTest.java b/services/tests/mockingservicestests/src/com/android/server/DeviceIdleControllerTest.java index e51ee947cba1e..88273b7bc2cf4 100644 --- a/services/tests/mockingservicestests/src/com/android/server/DeviceIdleControllerTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/DeviceIdleControllerTest.java @@ -17,6 +17,7 @@ package com.android.server; import static androidx.test.InstrumentationRegistry.getContext; +import static com.android.dx.mockito.inline.extended.ExtendedMockito.doAnswer; import static com.android.dx.mockito.inline.extended.ExtendedMockito.doNothing; import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn; import static com.android.dx.mockito.inline.extended.ExtendedMockito.inOrder; @@ -31,6 +32,7 @@ import static com.android.server.DeviceIdleController.LIGHT_STATE_INACTIVE; import static com.android.server.DeviceIdleController.LIGHT_STATE_OVERRIDE; import static com.android.server.DeviceIdleController.LIGHT_STATE_PRE_IDLE; import static com.android.server.DeviceIdleController.LIGHT_STATE_WAITING_FOR_NETWORK; +import static com.android.server.DeviceIdleController.MSG_REPORT_STATIONARY_STATUS; import static com.android.server.DeviceIdleController.STATE_ACTIVE; import static com.android.server.DeviceIdleController.STATE_IDLE; import static com.android.server.DeviceIdleController.STATE_IDLE_MAINTENANCE; @@ -51,6 +53,7 @@ import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.argThat; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.never; import static org.mockito.Mockito.reset; @@ -70,6 +73,7 @@ import android.net.ConnectivityManager; import android.net.NetworkInfo; import android.os.Handler; import android.os.Looper; +import android.os.Message; import android.os.PowerManager; import android.os.PowerManagerInternal; import android.os.PowerSaveState; @@ -85,11 +89,13 @@ import org.junit.After; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; -import org.mockito.Answers; +import org.mockito.ArgumentCaptor; import org.mockito.InOrder; import org.mockito.Mock; import org.mockito.MockitoSession; +import org.mockito.invocation.InvocationOnMock; import org.mockito.quality.Strictness; +import org.mockito.stubbing.Answer; /** * Tests for {@link com.android.server.DeviceIdleController}. @@ -97,6 +103,7 @@ import org.mockito.quality.Strictness; @RunWith(AndroidJUnit4.class) public class DeviceIdleControllerTest { private DeviceIdleController mDeviceIdleController; + private DeviceIdleController.MyHandler mHandler; private AnyMotionDetectorForTest mAnyMotionDetector; private AppStateTrackerForTest mAppStateTracker; private DeviceIdleController.Constants mConstants; @@ -110,8 +117,6 @@ public class DeviceIdleControllerTest { @Mock private ContentResolver mContentResolver; @Mock - private DeviceIdleController.MyHandler mHandler; - @Mock private IActivityManager mIActivityManager; @Mock private LocationManager mLocationManager; @@ -128,6 +133,8 @@ public class DeviceIdleControllerTest { ConnectivityService connectivityService; LocationManager locationManager; ConstraintController constraintController; + // Freeze time for testing. + long nowElapsed; InjectorForTest(Context ctx) { super(ctx); @@ -154,6 +161,11 @@ public class DeviceIdleControllerTest { return connectivityService; } + @Override + long getElapsedRealtime() { + return nowElapsed; + } + @Override LocationManager getLocationManager() { return locationManager; @@ -161,6 +173,23 @@ public class DeviceIdleControllerTest { @Override DeviceIdleController.MyHandler getHandler(DeviceIdleController controller) { + if (mHandler == null) { + mHandler = controller.new MyHandler(getContext().getMainLooper()); + spyOn(mHandler); + doNothing().when(mHandler).handleMessage(argThat((message) -> + message.what != MSG_REPORT_STATIONARY_STATUS)); + doAnswer(new Answer() { + @Override + public Boolean answer(InvocationOnMock invocation) throws Throwable { + Message msg = invocation.getArgument(0); + mHandler.handleMessage(msg); + return true; + } + }).when(mHandler).sendMessageDelayed( + argThat((message) -> message.what == MSG_REPORT_STATIONARY_STATUS), + anyLong()); + } + return mHandler; } @@ -226,6 +255,19 @@ public class DeviceIdleControllerTest { } } + private class StationaryListenerForTest implements DeviceIdleController.StationaryListener { + boolean motionExpected = false; + boolean isStationary = false; + + @Override + public void onDeviceStationaryChanged(boolean isStationary) { + if (isStationary == motionExpected) { + fail("Unexpected device stationary status: " + isStationary); + } + this.isStationary = isStationary; + } + } + @Before public void setUp() { mMockingSession = mockitoSession() @@ -255,8 +297,6 @@ public class DeviceIdleControllerTest { doReturn(true).when(mSensorManager).registerListener(any(), any(), anyInt()); mAppStateTracker = new AppStateTrackerForTest(getContext(), Looper.getMainLooper()); mAnyMotionDetector = new AnyMotionDetectorForTest(); - mHandler = mock(DeviceIdleController.MyHandler.class, Answers.RETURNS_DEEP_STUBS); - doNothing().when(mHandler).handleMessage(any()); mInjector = new InjectorForTest(getContext()); doNothing().when(mContentResolver).registerContentObserver(any(), anyBoolean(), any()); @@ -1607,6 +1647,86 @@ public class DeviceIdleControllerTest { 1.0f, curfactor, delta); } + @Test + public void testStationaryDetection_QuickDozeOff() { + setQuickDozeEnabled(false); + enterDeepState(STATE_IDLE); + // Regular progression through states, so time should have increased appropriately. + mInjector.nowElapsed += mConstants.IDLE_AFTER_INACTIVE_TIMEOUT + mConstants.SENSING_TIMEOUT + + mConstants.LOCATING_TIMEOUT; + + StationaryListenerForTest stationaryListener = new StationaryListenerForTest(); + + mDeviceIdleController.registerStationaryListener(stationaryListener); + + // Go to IDLE_MAINTENANCE + mDeviceIdleController.stepIdleStateLocked("testing"); + + // Back to IDLE + mDeviceIdleController.stepIdleStateLocked("testing"); + assertTrue(stationaryListener.isStationary); + + // Test motion + stationaryListener.motionExpected = true; + mDeviceIdleController.mMotionListener.onTrigger(null); + assertFalse(stationaryListener.isStationary); + } + + @Test + public void testStationaryDetection_QuickDozeOn() { + setAlarmSoon(false); + enterDeepState(STATE_QUICK_DOZE_DELAY); + mDeviceIdleController.stepIdleStateLocked("testing"); + verifyStateConditions(STATE_IDLE); + // Quick doze progression through states, so time should have increased appropriately. + mInjector.nowElapsed += mConstants.QUICK_DOZE_DELAY_TIMEOUT; + final ArgumentCaptor alarmListener = ArgumentCaptor + .forClass(AlarmManager.OnAlarmListener.class); + doNothing().when(mAlarmManager).set(anyInt(), anyLong(), eq("DeviceIdleController.motion"), + alarmListener.capture(), any()); + + StationaryListenerForTest stationaryListener = new StationaryListenerForTest(); + + stationaryListener.motionExpected = true; + mDeviceIdleController.registerStationaryListener(stationaryListener); + assertFalse(stationaryListener.isStationary); + + // Go to IDLE_MAINTENANCE + mDeviceIdleController.stepIdleStateLocked("testing"); + + mInjector.nowElapsed += mConstants.MOTION_INACTIVE_TIMEOUT / 2; + + // Back to IDLE + mDeviceIdleController.stepIdleStateLocked("testing"); + + // Now enough time has passed. + mInjector.nowElapsed += mConstants.MOTION_INACTIVE_TIMEOUT / 2; + stationaryListener.motionExpected = false; + alarmListener.getValue().onAlarm(); + assertTrue(stationaryListener.isStationary); + + stationaryListener.motionExpected = true; + mDeviceIdleController.mMotionListener.onSensorChanged(null); + assertFalse(stationaryListener.isStationary); + + // Since we're in quick doze, the device shouldn't stop idling. + verifyStateConditions(STATE_IDLE); + + // Go to IDLE_MAINTENANCE + mDeviceIdleController.stepIdleStateLocked("testing"); + + mInjector.nowElapsed += mConstants.MOTION_INACTIVE_TIMEOUT / 2; + + // Back to IDLE + mDeviceIdleController.stepIdleStateLocked("testing"); + + // Now enough time has passed. + mInjector.nowElapsed += mConstants.MOTION_INACTIVE_TIMEOUT / 2; + stationaryListener.motionExpected = false; + alarmListener.getValue().onAlarm(); + assertTrue(stationaryListener.isStationary); + } + private void enterDeepState(int state) { switch (state) { case STATE_ACTIVE: