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
This commit is contained in:
William Xiao
2023-07-14 16:48:02 -07:00
parent 321ed901a3
commit f4e8a1c448
4 changed files with 102 additions and 0 deletions

View File

@@ -2560,6 +2560,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. -->

View File

@@ -2207,6 +2207,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" />

View File

@@ -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");
}
});

View File

@@ -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();
}
}