Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 26fa1a2b authored by William Xiao's avatar William Xiao Committed by Android Build Coastguard Worker
Browse files

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)
(cherry picked from https://googleplex-android-review.googlesource.com/q/commit:f4e8a1c448faff095a89d1d5fa8b156d4324bb54)
Merged-In: I47c9561756ac27370b597a69a948892641ff47bb
Change-Id: I47c9561756ac27370b597a69a948892641ff47bb
parent c9c16cd7
Loading
Loading
Loading
Loading
+11 −0
Original line number Diff line number Diff line
@@ -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. -->
+1 −0
Original line number Diff line number Diff line
@@ -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" />
+32 −0
Original line number Diff line number Diff line
@@ -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");
                }
            });
+58 −0
Original line number Diff line number Diff line
@@ -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();
    }
}