diff options
4 files changed, 102 insertions, 0 deletions
diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml index 3be0d7ff06af..ec4d804587a4 100644 --- a/core/res/res/values/config.xml +++ b/core/res/res/values/config.xml @@ -2613,6 +2613,17 @@ assistant activities (ACTIVITY_TYPE_ASSISTANT) --> <bool name="config_dismissDreamOnActivityStart">false</bool> + <!-- Whether to send a user activity event to PowerManager when a dream quits unexpectedly so + that the screen won't immediately shut off. + + When a dream stops unexpectedly, such as due to an app update, if the device has been + inactive less than the user's screen timeout, the device goes to keyguard and times out + back to dreaming after a few seconds. If the device has been inactive longer, the screen + will immediately turn off. With this flag on, the device will go back to keyguard in all + scenarios rather than turning off, which gives the device a chance to start dreaming + again. --> + <bool name="config_resetScreenTimeoutOnUnexpectedDreamExit">false</bool> + <!-- The prefixes of dream component names that are loggable. Matched against ComponentName#flattenToString() for dream components. If empty, logs "other" for all. --> diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml index 85e9792fc99b..245473370d25 100644 --- a/core/res/res/values/symbols.xml +++ b/core/res/res/values/symbols.xml @@ -2211,6 +2211,7 @@ <java-symbol type="array" name="config_supportedDreamComplications" /> <java-symbol type="array" name="config_disabledDreamComponents" /> <java-symbol type="bool" name="config_dismissDreamOnActivityStart" /> + <java-symbol type="bool" name="config_resetScreenTimeoutOnUnexpectedDreamExit" /> <java-symbol type="integer" name="config_dreamOverlayReconnectTimeoutMs" /> <java-symbol type="integer" name="config_dreamOverlayMaxReconnectAttempts" /> <java-symbol type="integer" name="config_minDreamOverlayDurationMs" /> diff --git a/services/core/java/com/android/server/dreams/DreamController.java b/services/core/java/com/android/server/dreams/DreamController.java index 633bf73120e1..0e8a5fb050dd 100644 --- a/services/core/java/com/android/server/dreams/DreamController.java +++ b/services/core/java/com/android/server/dreams/DreamController.java @@ -17,6 +17,8 @@ package com.android.server.dreams; import static android.content.Intent.FLAG_RECEIVER_FOREGROUND; +import static android.os.PowerManager.USER_ACTIVITY_EVENT_OTHER; +import static android.os.PowerManager.USER_ACTIVITY_FLAG_NO_CHANGE_LIGHTS; import android.app.ActivityTaskManager; import android.app.BroadcastOptions; @@ -72,6 +74,7 @@ final class DreamController { private final Handler mHandler; private final Listener mListener; private final ActivityTaskManager mActivityTaskManager; + private final PowerManager mPowerManager; private final Intent mDreamingStartedIntent = new Intent(Intent.ACTION_DREAMING_STARTED) .addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY | FLAG_RECEIVER_FOREGROUND); @@ -84,6 +87,15 @@ final class DreamController { private final Intent mCloseNotificationShadeIntent; private final Bundle mCloseNotificationShadeOptions; + /** + * If this flag is on, we report user activity to {@link PowerManager} so that the screen + * doesn't shut off immediately when a dream quits unexpectedly. The device will instead go to + * keyguard and time out back to dreaming shortly. + * + * This allows the dream a second chance to relaunch in case of an app update or other crash. + */ + private final boolean mResetScreenTimeoutOnUnexpectedDreamExit; + private DreamRecord mCurrentDream; // Whether a dreaming started intent has been broadcast. @@ -101,6 +113,7 @@ final class DreamController { mHandler = handler; mListener = listener; mActivityTaskManager = mContext.getSystemService(ActivityTaskManager.class); + mPowerManager = mContext.getSystemService(PowerManager.class); mCloseNotificationShadeIntent = new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS); mCloseNotificationShadeIntent.putExtra(EXTRA_REASON_KEY, EXTRA_REASON_VALUE); mCloseNotificationShadeIntent.addFlags(Intent.FLAG_RECEIVER_FOREGROUND); @@ -110,6 +123,8 @@ final class DreamController { EXTRA_REASON_VALUE) .setDeferralPolicy(BroadcastOptions.DEFERRAL_POLICY_UNTIL_ACTIVE) .toBundle(); + mResetScreenTimeoutOnUnexpectedDreamExit = context.getResources().getBoolean( + com.android.internal.R.bool.config_resetScreenTimeoutOnUnexpectedDreamExit); } /** @@ -235,6 +250,17 @@ final class DreamController { } /** + * Sends a user activity signal to PowerManager to stop the screen from turning off immediately + * if there hasn't been any user interaction in a while. + */ + private void resetScreenTimeout() { + Slog.i(TAG, "Resetting screen timeout"); + long time = SystemClock.uptimeMillis(); + mPowerManager.userActivity(time, USER_ACTIVITY_EVENT_OTHER, + USER_ACTIVITY_FLAG_NO_CHANGE_LIGHTS); + } + + /** * Stops dreaming. * * The current dream, if any, and any unstopped previous dreams are stopped. The device stops @@ -448,6 +474,9 @@ final class DreamController { mHandler.post(() -> { mService = null; if (mCurrentDream == DreamRecord.this) { + if (mResetScreenTimeoutOnUnexpectedDreamExit) { + resetScreenTimeout(); + } stopDream(true /*immediate*/, "binder died"); } }); @@ -473,6 +502,9 @@ final class DreamController { mHandler.post(() -> { mService = null; if (mCurrentDream == DreamRecord.this) { + if (mResetScreenTimeoutOnUnexpectedDreamExit) { + resetScreenTimeout(); + } stopDream(true /*immediate*/, "service disconnected"); } }); diff --git a/services/tests/servicestests/src/com/android/server/dreams/DreamControllerTest.java b/services/tests/servicestests/src/com/android/server/dreams/DreamControllerTest.java index d5ad815d3cdb..b5bf1ea34a46 100644 --- a/services/tests/servicestests/src/com/android/server/dreams/DreamControllerTest.java +++ b/services/tests/servicestests/src/com/android/server/dreams/DreamControllerTest.java @@ -16,7 +16,11 @@ package com.android.server.dreams; +import static android.os.PowerManager.USER_ACTIVITY_EVENT_OTHER; +import static android.os.PowerManager.USER_ACTIVITY_FLAG_NO_CHANGE_LIGHTS; + import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.any; @@ -32,7 +36,9 @@ import android.content.ServiceConnection; import android.os.Binder; import android.os.Handler; import android.os.IBinder; +import android.os.IPowerManager; import android.os.IRemoteCallback; +import android.os.PowerManager; import android.os.RemoteException; import android.os.test.TestLooper; import android.service.dreams.IDreamService; @@ -58,6 +64,8 @@ public class DreamControllerTest { @Mock private ActivityTaskManager mActivityTaskManager; + @Mock + private IPowerManager mPowerManager; @Mock private IBinder mIBinder; @@ -67,6 +75,8 @@ public class DreamControllerTest { @Captor private ArgumentCaptor<ServiceConnection> mServiceConnectionACaptor; @Captor + private ArgumentCaptor<IBinder.DeathRecipient> mDeathRecipientCaptor; + @Captor private ArgumentCaptor<IRemoteCallback> mRemoteCallbackCaptor; private final TestLooper mLooper = new TestLooper(); @@ -90,6 +100,12 @@ public class DreamControllerTest { when(mContext.getSystemServiceName(ActivityTaskManager.class)) .thenReturn(Context.ACTIVITY_TASK_SERVICE); + final PowerManager powerManager = new PowerManager(mContext, mPowerManager, null, null); + when(mContext.getSystemService(Context.POWER_SERVICE)) + .thenReturn(powerManager); + when(mContext.getSystemServiceName(PowerManager.class)) + .thenReturn(Context.POWER_SERVICE); + mToken = new Binder(); mDreamName = ComponentName.unflattenFromString("dream"); mOverlayName = ComponentName.unflattenFromString("dream_overlay"); @@ -209,9 +225,51 @@ public class DreamControllerTest { verify(mIDreamService).detach(); } + @Test + public void serviceDisconnect_resetsScreenTimeout() throws RemoteException { + // Start dream. + mDreamController.startDream(mToken, mDreamName, false /*isPreview*/, false /*doze*/, + 0 /*userId*/, null /*wakeLock*/, mOverlayName, "test" /*reason*/); + ServiceConnection serviceConnection = captureServiceConnection(); + serviceConnection.onServiceConnected(mDreamName, mIBinder); + mLooper.dispatchAll(); + + // Dream disconnects unexpectedly. + serviceConnection.onServiceDisconnected(mDreamName); + mLooper.dispatchAll(); + + // Power manager receives user activity signal. + verify(mPowerManager).userActivity(/*displayId=*/ anyInt(), /*time=*/ anyLong(), + eq(USER_ACTIVITY_EVENT_OTHER), + eq(USER_ACTIVITY_FLAG_NO_CHANGE_LIGHTS)); + } + + @Test + public void binderDied_resetsScreenTimeout() throws RemoteException { + // Start dream. + mDreamController.startDream(mToken, mDreamName, false /*isPreview*/, false /*doze*/, + 0 /*userId*/, null /*wakeLock*/, mOverlayName, "test" /*reason*/); + captureServiceConnection().onServiceConnected(mDreamName, mIBinder); + mLooper.dispatchAll(); + + // Dream binder dies. + captureDeathRecipient().binderDied(); + mLooper.dispatchAll(); + + // Power manager receives user activity signal. + verify(mPowerManager).userActivity(/*displayId=*/ anyInt(), /*time=*/ anyLong(), + eq(USER_ACTIVITY_EVENT_OTHER), + eq(USER_ACTIVITY_FLAG_NO_CHANGE_LIGHTS)); + } + private ServiceConnection captureServiceConnection() { verify(mContext).bindServiceAsUser(any(), mServiceConnectionACaptor.capture(), anyInt(), any()); return mServiceConnectionACaptor.getValue(); } + + private IBinder.DeathRecipient captureDeathRecipient() throws RemoteException { + verify(mIBinder).linkToDeath(mDeathRecipientCaptor.capture(), anyInt()); + return mDeathRecipientCaptor.getValue(); + } } |