From f4e8a1c448faff095a89d1d5fa8b156d4324bb54 Mon Sep 17 00:00:00 2001 From: William Xiao Date: Fri, 14 Jul 2023 16:48:02 -0700 Subject: Send user activity when dream quits unexpectedly If a dream quits unexpectedly after the device has been idle for longer than the user's screen timeout, the device will go straight to a black screen. This happens routinely when GMSCore or the dream app itself are updated and is jarring for the user experience. This CL sends a user activity signal to PowerManager when we see a dream quit unexpectedly so that instead the device goes to keyguard and will timeout back to dreaming after a short wait. This is the same behavior as if a dream quit unexpectedly before the user activity timeout expired and allows for a graceful recovery. Bug: 286777009 Test: atest DreamControllerTest Test: manually verified that crashing the dreams app goes to keyguard instead of a black screen (cherry picked from https://googleplex-android-review.googlesource.com/q/commit:fc3cacb063077fcde131917d1a2bde919e3e70d9) Merged-In: I47c9561756ac27370b597a69a948892641ff47bb Change-Id: I47c9561756ac27370b597a69a948892641ff47bb --- core/res/res/values/config.xml | 11 ++++ core/res/res/values/symbols.xml | 1 + .../com/android/server/dreams/DreamController.java | 32 ++++++++++++ .../android/server/dreams/DreamControllerTest.java | 58 ++++++++++++++++++++++ 4 files changed, 102 insertions(+) diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml index 06be5fa5fb18..2de48fdac6df 100644 --- a/core/res/res/values/config.xml +++ b/core/res/res/values/config.xml @@ -2560,6 +2560,17 @@ assistant activities (ACTIVITY_TYPE_ASSISTANT) --> false + + false + diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml index dc4eafd2e00e..2d040bb66d83 100644 --- a/core/res/res/values/symbols.xml +++ b/core/res/res/values/symbols.xml @@ -2207,6 +2207,7 @@ + diff --git a/services/core/java/com/android/server/dreams/DreamController.java b/services/core/java/com/android/server/dreams/DreamController.java index 6d70d21e3b84..da93d0b0fc35 100644 --- a/services/core/java/com/android/server/dreams/DreamController.java +++ b/services/core/java/com/android/server/dreams/DreamController.java @@ -18,6 +18,8 @@ package com.android.server.dreams; import static android.app.WindowConfiguration.ACTIVITY_TYPE_DREAM; 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); } /** @@ -213,6 +228,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. * @@ -420,6 +446,9 @@ final class DreamController { mHandler.post(() -> { mService = null; if (mCurrentDream == DreamRecord.this) { + if (mResetScreenTimeoutOnUnexpectedDreamExit) { + resetScreenTimeout(); + } stopDream(true /*immediate*/, "binder died"); } }); @@ -445,6 +474,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 mServiceConnectionACaptor; @Captor + private ArgumentCaptor mDeathRecipientCaptor; + @Captor private ArgumentCaptor 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(); + } } -- cgit v1.2.3-59-g8ed1b