From 1e16b3c2175f55c35eb48d0c797a056ebe27bf9d Mon Sep 17 00:00:00 2001 From: Lucas Silva Date: Mon, 29 Aug 2022 15:45:28 -0400 Subject: [PATCH] Don't start dreams immediately if keyguard is occluded when docked. The keyguard must be showing and unoccluded, or the device is not interactive (eg screen off) in order for dreams to be started immediately when docked. This prevents dreams from starting if there is an activity currently showing over the lockscreen. Also includes some refactoring to be able to unit test this change, since dock state changes were not previously tested. Fixes: 233412496 Test: atest UiModeManagerServiceTest Test: manually on device with keyguard occluded, unoccluded, and screen off while docked Change-Id: I12f1a4cf3ae4784e391e3c5a3fd1d50b4d4f7ecd --- .../service/dreams/DreamManagerInternal.java | 6 + core/java/android/service/dreams/Sandman.java | 48 +++-- .../android/server/UiModeManagerService.java | 8 +- .../server/dreams/DreamManagerService.java | 5 + services/tests/uiservicestests/Android.bp | 2 + .../server/UiModeManagerServiceTest.java | 165 +++++++++++++++++- 6 files changed, 201 insertions(+), 33 deletions(-) diff --git a/core/java/android/service/dreams/DreamManagerInternal.java b/core/java/android/service/dreams/DreamManagerInternal.java index 7bf5c38b7fd49..6956cd4cae6b8 100644 --- a/core/java/android/service/dreams/DreamManagerInternal.java +++ b/core/java/android/service/dreams/DreamManagerInternal.java @@ -45,6 +45,12 @@ public abstract class DreamManagerInternal { */ public abstract boolean isDreaming(); + /** + * Ask the power manager to nap. It will eventually call back into startDream() if/when it is + * appropriate to start dreaming. + */ + public abstract void requestDream(); + /** * Called by the ActivityTaskManagerService to verify that the startDreamActivity * request comes from the current active dream component. diff --git a/core/java/android/service/dreams/Sandman.java b/core/java/android/service/dreams/Sandman.java index ced2a01cb1d0d..fae72a2e91a89 100644 --- a/core/java/android/service/dreams/Sandman.java +++ b/core/java/android/service/dreams/Sandman.java @@ -20,13 +20,13 @@ import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.os.PowerManager; -import android.os.RemoteException; -import android.os.ServiceManager; import android.os.SystemClock; import android.os.UserHandle; import android.provider.Settings; import android.util.Slog; +import com.android.server.LocalServices; + /** * Internal helper for launching dreams to ensure consistency between the * UiModeManagerService system service and the Somnambulator activity. @@ -75,32 +75,28 @@ public final class Sandman { } private static void startDream(Context context, boolean docked) { - try { - IDreamManager dreamManagerService = IDreamManager.Stub.asInterface( - ServiceManager.getService(DreamService.DREAM_SERVICE)); - if (dreamManagerService != null && !dreamManagerService.isDreaming()) { - if (docked) { - Slog.i(TAG, "Activating dream while docked."); + DreamManagerInternal dreamManagerService = + LocalServices.getService(DreamManagerInternal.class); + if (dreamManagerService != null && !dreamManagerService.isDreaming()) { + if (docked) { + Slog.i(TAG, "Activating dream while docked."); - // Wake up. - // The power manager will wake up the system automatically when it starts - // receiving power from a dock but there is a race between that happening - // and the UI mode manager starting a dream. We want the system to already - // be awake by the time this happens. Otherwise the dream may not start. - PowerManager powerManager = - context.getSystemService(PowerManager.class); - powerManager.wakeUp(SystemClock.uptimeMillis(), - PowerManager.WAKE_REASON_PLUGGED_IN, - "android.service.dreams:DREAM"); - } else { - Slog.i(TAG, "Activating dream by user request."); - } - - // Dream. - dreamManagerService.dream(); + // Wake up. + // The power manager will wake up the system automatically when it starts + // receiving power from a dock but there is a race between that happening + // and the UI mode manager starting a dream. We want the system to already + // be awake by the time this happens. Otherwise the dream may not start. + PowerManager powerManager = + context.getSystemService(PowerManager.class); + powerManager.wakeUp(SystemClock.uptimeMillis(), + PowerManager.WAKE_REASON_PLUGGED_IN, + "android.service.dreams:DREAM"); + } else { + Slog.i(TAG, "Activating dream by user request."); } - } catch (RemoteException ex) { - Slog.e(TAG, "Could not start dream when docked.", ex); + + // Dream. + dreamManagerService.requestDream(); } } diff --git a/services/core/java/com/android/server/UiModeManagerService.java b/services/core/java/com/android/server/UiModeManagerService.java index c853ba93f4ab3..e9dd836e527af 100644 --- a/services/core/java/com/android/server/UiModeManagerService.java +++ b/services/core/java/com/android/server/UiModeManagerService.java @@ -359,6 +359,11 @@ final class UiModeManagerService extends SystemService { SystemProperties.set(SYSTEM_PROPERTY_DEVICE_THEME, Integer.toString(mode)); } + @VisibleForTesting + void setStartDreamImmediatelyOnDock(boolean startDreamImmediatelyOnDock) { + mStartDreamImmediatelyOnDock = startDreamImmediatelyOnDock; + } + @Override public void onUserSwitching(@Nullable TargetUser from, @NonNull TargetUser to) { mCurrentUser = to.getUserIdentifier(); @@ -1824,7 +1829,8 @@ final class UiModeManagerService extends SystemService { // If we did not start a dock app, then start dreaming if appropriate. if (category != null && !dockAppStarted && (mStartDreamImmediatelyOnDock - || mKeyguardManager.isKeyguardLocked())) { + || mWindowManager.isKeyguardShowingAndNotOccluded() + || !mPowerManager.isInteractive())) { Sandman.startDreamWhenDockedIfAppropriate(getContext()); } } diff --git a/services/core/java/com/android/server/dreams/DreamManagerService.java b/services/core/java/com/android/server/dreams/DreamManagerService.java index 4e0489a5c4bc3..fb01ce5c94404 100644 --- a/services/core/java/com/android/server/dreams/DreamManagerService.java +++ b/services/core/java/com/android/server/dreams/DreamManagerService.java @@ -842,6 +842,11 @@ public final class DreamManagerService extends SystemService { public ComponentName getActiveDreamComponent(boolean doze) { return getActiveDreamComponentInternal(doze); } + + @Override + public void requestDream() { + requestDreamInternal(); + } } private final Runnable mSystemPropertiesChanged = new Runnable() { diff --git a/services/tests/uiservicestests/Android.bp b/services/tests/uiservicestests/Android.bp index 2128a08d53d20..3d6736e33cacb 100644 --- a/services/tests/uiservicestests/Android.bp +++ b/services/tests/uiservicestests/Android.bp @@ -20,6 +20,7 @@ android_test { ], static_libs: [ + "frameworks-base-testutils", "services.accessibility", "services.core", "services.devicepolicy", @@ -32,6 +33,7 @@ android_test { "platformprotosnano", "statsdprotolite", "hamcrest-library", + "servicestests-utils", "testables", "truth-prebuilt", // TODO: remove once Android migrates to JUnit 4.12, diff --git a/services/tests/uiservicestests/src/com/android/server/UiModeManagerServiceTest.java b/services/tests/uiservicestests/src/com/android/server/UiModeManagerServiceTest.java index aee27554b023b..617a34f44bff4 100644 --- a/services/tests/uiservicestests/src/com/android/server/UiModeManagerServiceTest.java +++ b/services/tests/uiservicestests/src/com/android/server/UiModeManagerServiceTest.java @@ -45,6 +45,7 @@ import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.notNull; +import static org.mockito.ArgumentMatchers.nullable; import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.atLeast; import static org.mockito.Mockito.atLeastOnce; @@ -58,31 +59,36 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoMoreInteractions; import static org.mockito.Mockito.verifyZeroInteractions; import static org.mockito.Mockito.when; -import static org.mockito.MockitoAnnotations.initMocks; import static org.testng.Assert.assertThrows; import android.Manifest; +import android.app.Activity; import android.app.AlarmManager; import android.app.IOnProjectionStateChangedListener; import android.app.IUiModeManager; import android.content.BroadcastReceiver; -import android.content.ContentResolver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.pm.PackageManager; import android.content.res.Configuration; import android.content.res.Resources; +import android.os.Bundle; import android.os.Handler; import android.os.IBinder; -import android.os.Process; import android.os.PowerManager; import android.os.PowerManagerInternal; import android.os.PowerSaveState; +import android.os.Process; import android.os.RemoteException; +import android.os.UserHandle; +import android.provider.Settings; +import android.service.dreams.DreamManagerInternal; +import android.test.mock.MockContentResolver; import android.testing.AndroidTestingRunner; import android.testing.TestableLooper; +import com.android.internal.util.test.FakeSettingsProvider; import com.android.server.twilight.TwilightListener; import com.android.server.twilight.TwilightManager; import com.android.server.twilight.TwilightState; @@ -93,6 +99,7 @@ import org.junit.Ignore; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; +import org.mockito.Captor; import org.mockito.Mock; import java.time.LocalDateTime; @@ -107,8 +114,7 @@ public class UiModeManagerServiceTest extends UiServiceTestCase { private static final String PACKAGE_NAME = "Diane Coffee"; private UiModeManagerService mUiManagerService; private IUiModeManager mService; - @Mock - private ContentResolver mContentResolver; + private MockContentResolver mContentResolver; @Mock private WindowManagerInternal mWindowManager; @Mock @@ -131,16 +137,22 @@ public class UiModeManagerServiceTest extends UiServiceTestCase { private PackageManager mPackageManager; @Mock private IBinder mBinder; + @Mock + private DreamManagerInternal mDreamManager; + @Captor + private ArgumentCaptor mOrderedBroadcastIntent; + @Captor + private ArgumentCaptor mOrderedBroadcastReceiver; private BroadcastReceiver mScreenOffCallback; private BroadcastReceiver mTimeChangedCallback; + private BroadcastReceiver mDockStateChangedCallback; private AlarmManager.OnAlarmListener mCustomListener; private Consumer mPowerSaveConsumer; private TwilightListener mTwilightListener; @Before public void setUp() { - initMocks(this); when(mContext.checkCallingOrSelfPermission(anyString())) .thenReturn(PackageManager.PERMISSION_GRANTED); doAnswer(inv -> { @@ -154,6 +166,10 @@ public class UiModeManagerServiceTest extends UiServiceTestCase { when(mLocalPowerManager.getLowPowerState(anyInt())) .thenReturn(new PowerSaveState.Builder().setBatterySaverEnabled(false).build()); when(mContext.getResources()).thenReturn(mResources); + when(mResources.getString(com.android.internal.R.string.config_somnambulatorComponent)) + .thenReturn("somnambulator"); + mContentResolver = new MockContentResolver(); + mContentResolver.addProvider(Settings.AUTHORITY, new FakeSettingsProvider()); when(mContext.getContentResolver()).thenReturn(mContentResolver); when(mContext.getPackageManager()).thenReturn(mPackageManager); when(mPowerManager.isInteractive()).thenReturn(true); @@ -168,6 +184,9 @@ public class UiModeManagerServiceTest extends UiServiceTestCase { if (filter.hasAction(Intent.ACTION_SCREEN_OFF)) { mScreenOffCallback = inv.getArgument(0); } + if (filter.hasAction(Intent.ACTION_DOCK_EVENT)) { + mDockStateChangedCallback = inv.getArgument(0); + } return null; }); doAnswer(inv -> { @@ -182,11 +201,13 @@ public class UiModeManagerServiceTest extends UiServiceTestCase { }).when(mAlarmManager).cancel(eq(mCustomListener)); when(mContext.getSystemService(eq(Context.POWER_SERVICE))) .thenReturn(mPowerManager); + when(mContext.getSystemService(PowerManager.class)).thenReturn(mPowerManager); when(mContext.getSystemService(eq(Context.ALARM_SERVICE))) .thenReturn(mAlarmManager); addLocalService(WindowManagerInternal.class, mWindowManager); addLocalService(PowerManagerInternal.class, mLocalPowerManager); addLocalService(TwilightManager.class, mTwilightManager); + addLocalService(DreamManagerInternal.class, mDreamManager); mUiManagerService = new UiModeManagerService(mContext, /* setupWizardComplete= */ true, mTwilightManager, new TestInjector()); @@ -1298,6 +1319,138 @@ public class UiModeManagerServiceTest extends UiServiceTestCase { assertThat(mService.getCurrentModeType()).isNotEqualTo(Configuration.UI_MODE_TYPE_CAR); } + @Test + public void dreamWhenDocked() { + setScreensaverActivateOnDock(true); + setScreensaverEnabled(true); + + triggerDockIntent(); + verifyAndSendResultBroadcast(); + verify(mDreamManager).requestDream(); + } + + @Test + public void noDreamWhenDocked_dreamsDisabled() { + setScreensaverActivateOnDock(true); + setScreensaverEnabled(false); + + triggerDockIntent(); + verifyAndSendResultBroadcast(); + verify(mDreamManager, never()).requestDream(); + } + + @Test + public void noDreamWhenDocked_dreamsWhenDockedDisabled() { + setScreensaverActivateOnDock(false); + setScreensaverEnabled(true); + + triggerDockIntent(); + verifyAndSendResultBroadcast(); + verify(mDreamManager, never()).requestDream(); + } + + @Test + public void noDreamWhenDocked_keyguardNotShowing_interactive() { + setScreensaverActivateOnDock(true); + setScreensaverEnabled(true); + mUiManagerService.setStartDreamImmediatelyOnDock(false); + when(mWindowManager.isKeyguardShowingAndNotOccluded()).thenReturn(false); + when(mPowerManager.isInteractive()).thenReturn(true); + + triggerDockIntent(); + verifyAndSendResultBroadcast(); + verify(mDreamManager, never()).requestDream(); + } + + @Test + public void dreamWhenDocked_keyguardShowing_interactive() { + setScreensaverActivateOnDock(true); + setScreensaverEnabled(true); + mUiManagerService.setStartDreamImmediatelyOnDock(false); + when(mWindowManager.isKeyguardShowingAndNotOccluded()).thenReturn(true); + when(mPowerManager.isInteractive()).thenReturn(false); + + triggerDockIntent(); + verifyAndSendResultBroadcast(); + verify(mDreamManager).requestDream(); + } + + @Test + public void dreamWhenDocked_keyguardNotShowing_notInteractive() { + setScreensaverActivateOnDock(true); + setScreensaverEnabled(true); + mUiManagerService.setStartDreamImmediatelyOnDock(false); + when(mWindowManager.isKeyguardShowingAndNotOccluded()).thenReturn(false); + when(mPowerManager.isInteractive()).thenReturn(false); + + triggerDockIntent(); + verifyAndSendResultBroadcast(); + verify(mDreamManager).requestDream(); + } + + @Test + public void dreamWhenDocked_keyguardShowing_notInteractive() { + setScreensaverActivateOnDock(true); + setScreensaverEnabled(true); + mUiManagerService.setStartDreamImmediatelyOnDock(false); + when(mWindowManager.isKeyguardShowingAndNotOccluded()).thenReturn(true); + when(mPowerManager.isInteractive()).thenReturn(false); + + triggerDockIntent(); + verifyAndSendResultBroadcast(); + verify(mDreamManager).requestDream(); + } + + private void triggerDockIntent() { + final Intent dockedIntent = + new Intent(Intent.ACTION_DOCK_EVENT) + .putExtra(Intent.EXTRA_DOCK_STATE, Intent.EXTRA_DOCK_STATE_DESK); + mDockStateChangedCallback.onReceive(mContext, dockedIntent); + } + + private void verifyAndSendResultBroadcast() { + verify(mContext).sendOrderedBroadcastAsUser( + mOrderedBroadcastIntent.capture(), + any(UserHandle.class), + nullable(String.class), + mOrderedBroadcastReceiver.capture(), + nullable(Handler.class), + anyInt(), + nullable(String.class), + nullable(Bundle.class)); + + mOrderedBroadcastReceiver.getValue().setPendingResult( + new BroadcastReceiver.PendingResult( + Activity.RESULT_OK, + /* resultData= */ "", + /* resultExtras= */ null, + /* type= */ 0, + /* ordered= */ true, + /* sticky= */ false, + /* token= */ null, + /* userId= */ 0, + /* flags= */ 0)); + mOrderedBroadcastReceiver.getValue().onReceive( + mContext, + mOrderedBroadcastIntent.getValue()); + } + + private void setScreensaverEnabled(boolean enable) { + Settings.Secure.putIntForUser( + mContentResolver, + Settings.Secure.SCREENSAVER_ENABLED, + enable ? 1 : 0, + UserHandle.USER_CURRENT); + } + + private void setScreensaverActivateOnDock(boolean enable) { + Settings.Secure.putIntForUser( + mContentResolver, + Settings.Secure.SCREENSAVER_ACTIVATE_ON_DOCK, + enable ? 1 : 0, + UserHandle.USER_CURRENT); + } + private void requestAllPossibleProjectionTypes() throws RemoteException { for (int i = 0; i < Integer.SIZE; ++i) { mService.requestProjection(mBinder, 1 << i, PACKAGE_NAME);