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

Commit 7d2d5aab authored by Darrell Shi's avatar Darrell Shi Committed by Android (Google) Code Review
Browse files

Merge "Smooth transition when switching dreaming."

parents ca8dd2f3 07723e01
Loading
Loading
Loading
Loading
+1 −1
Original line number Diff line number Diff line
@@ -1047,7 +1047,7 @@ public class DreamService extends Service implements Window.Callback {
        }

        if (mDreamToken == null) {
            Slog.w(mTag, "Finish was called before the dream was attached.");
            if (mDebug) Slog.v(mTag, "finish() called when not attached.");
            stopSelf();
            return;
        }
+126 −69
Original line number Diff line number Diff line
@@ -42,6 +42,8 @@ import com.android.internal.logging.MetricsLogger;
import com.android.internal.logging.nano.MetricsProto.MetricsEvent;

import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.NoSuchElementException;

/**
@@ -62,8 +64,6 @@ final class DreamController {
    private final Handler mHandler;
    private final Listener mListener;
    private final ActivityTaskManager mActivityTaskManager;
    private long mDreamStartTime;
    private String mSavedStopReason;

    private final Intent mDreamingStartedIntent = new Intent(Intent.ACTION_DREAMING_STARTED)
            .addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY);
@@ -74,21 +74,15 @@ final class DreamController {

    private DreamRecord mCurrentDream;

    private final Runnable mStopUnconnectedDreamRunnable = new Runnable() {
        @Override
        public void run() {
            if (mCurrentDream != null && mCurrentDream.mBound && !mCurrentDream.mConnected) {
                Slog.w(TAG, "Bound dream did not connect in the time allotted");
                stopDream(true /*immediate*/, "slow to connect");
            }
        }
    };
    // Whether a dreaming started intent has been broadcast.
    private boolean mSentStartBroadcast = false;

    private final Runnable mStopStubbornDreamRunnable = () -> {
        Slog.w(TAG, "Stubborn dream did not finish itself in the time allotted");
        stopDream(true /*immediate*/, "slow to finish");
        mSavedStopReason = null;
    };
    // When a new dream is started and there is an existing dream, the existing dream is allowed to
    // live a little longer until the new dream is started, for a smoother transition. This dream is
    // stopped as soon as the new dream is started, and this list is cleared. Usually there should
    // only be one previous dream while waiting for a new dream to start, but we store a list to
    // proof the edge case of multiple previous dreams.
    private final ArrayList<DreamRecord> mPreviousDreams = new ArrayList<>();

    public DreamController(Context context, Handler handler, Listener listener) {
        mContext = context;
@@ -110,18 +104,17 @@ final class DreamController {
            pw.println("    mUserId=" + mCurrentDream.mUserId);
            pw.println("    mBound=" + mCurrentDream.mBound);
            pw.println("    mService=" + mCurrentDream.mService);
            pw.println("    mSentStartBroadcast=" + mCurrentDream.mSentStartBroadcast);
            pw.println("    mWakingGently=" + mCurrentDream.mWakingGently);
        } else {
            pw.println("  mCurrentDream: null");
        }

        pw.println("  mSentStartBroadcast=" + mSentStartBroadcast);
    }

    public void startDream(Binder token, ComponentName name,
            boolean isPreviewMode, boolean canDoze, int userId, PowerManager.WakeLock wakeLock,
            ComponentName overlayComponentName, String reason) {
        stopDream(true /*immediate*/, "starting new dream");

        Trace.traceBegin(Trace.TRACE_TAG_POWER, "startDream");
        try {
            // Close the notification shade. No need to send to all, but better to be explicit.
@@ -131,9 +124,12 @@ final class DreamController {
                    + ", isPreviewMode=" + isPreviewMode + ", canDoze=" + canDoze
                    + ", userId=" + userId + ", reason='" + reason + "'");

            if (mCurrentDream != null) {
                mPreviousDreams.add(mCurrentDream);
            }
            mCurrentDream = new DreamRecord(token, name, isPreviewMode, canDoze, userId, wakeLock);

            mDreamStartTime = SystemClock.elapsedRealtime();
            mCurrentDream.mDreamStartTime = SystemClock.elapsedRealtime();
            MetricsLogger.visible(mContext,
                    mCurrentDream.mCanDoze ? MetricsEvent.DOZING : MetricsEvent.DREAMING);

@@ -156,31 +152,49 @@ final class DreamController {
            }

            mCurrentDream.mBound = true;
            mHandler.postDelayed(mStopUnconnectedDreamRunnable, DREAM_CONNECTION_TIMEOUT);
            mHandler.postDelayed(mCurrentDream.mStopUnconnectedDreamRunnable,
                    DREAM_CONNECTION_TIMEOUT);
        } finally {
            Trace.traceEnd(Trace.TRACE_TAG_POWER);
        }
    }

    /**
     * Stops dreaming.
     *
     * The current dream, if any, and any unstopped previous dreams are stopped. The device stops
     * dreaming.
     */
    public void stopDream(boolean immediate, String reason) {
        if (mCurrentDream == null) {
        stopPreviousDreams();
        stopDreamInstance(immediate, reason, mCurrentDream);
    }

    /**
     * Stops the given dream instance.
     *
     * The device may still be dreaming afterwards if there are other dreams running.
     */
    private void stopDreamInstance(boolean immediate, String reason, DreamRecord dream) {
        if (dream == null) {
            return;
        }

        Trace.traceBegin(Trace.TRACE_TAG_POWER, "stopDream");
        try {
            if (!immediate) {
                if (mCurrentDream.mWakingGently) {
                if (dream.mWakingGently) {
                    return; // already waking gently
                }

                if (mCurrentDream.mService != null) {
                if (dream.mService != null) {
                    // Give the dream a moment to wake up and finish itself gently.
                    mCurrentDream.mWakingGently = true;
                    dream.mWakingGently = true;
                    try {
                        mSavedStopReason = reason;
                        mCurrentDream.mService.wakeUp();
                        mHandler.postDelayed(mStopStubbornDreamRunnable, DREAM_FINISH_TIMEOUT);
                        dream.mStopReason = reason;
                        dream.mService.wakeUp();
                        mHandler.postDelayed(dream.mStopStubbornDreamRunnable,
                                DREAM_FINISH_TIMEOUT);
                        return;
                    } catch (RemoteException ex) {
                        // oh well, we tried, finish immediately instead
@@ -188,56 +202,76 @@ final class DreamController {
                }
            }

            final DreamRecord oldDream = mCurrentDream;
            mCurrentDream = null;
            Slog.i(TAG, "Stopping dream: name=" + oldDream.mName
                    + ", isPreviewMode=" + oldDream.mIsPreviewMode
                    + ", canDoze=" + oldDream.mCanDoze
                    + ", userId=" + oldDream.mUserId
            Slog.i(TAG, "Stopping dream: name=" + dream.mName
                    + ", isPreviewMode=" + dream.mIsPreviewMode
                    + ", canDoze=" + dream.mCanDoze
                    + ", userId=" + dream.mUserId
                    + ", reason='" + reason + "'"
                    + (mSavedStopReason == null ? "" : "(from '" + mSavedStopReason + "')"));
                    + (dream.mStopReason == null ? "" : "(from '"
                    + dream.mStopReason + "')"));
            MetricsLogger.hidden(mContext,
                    oldDream.mCanDoze ? MetricsEvent.DOZING : MetricsEvent.DREAMING);
                    dream.mCanDoze ? MetricsEvent.DOZING : MetricsEvent.DREAMING);
            MetricsLogger.histogram(mContext,
                    oldDream.mCanDoze ? "dozing_minutes" : "dreaming_minutes" ,
                    (int) ((SystemClock.elapsedRealtime() - mDreamStartTime) / (1000L * 60L)));
                    dream.mCanDoze ? "dozing_minutes" : "dreaming_minutes",
                    (int) ((SystemClock.elapsedRealtime() - dream.mDreamStartTime) / (1000L
                            * 60L)));

            mHandler.removeCallbacks(mStopUnconnectedDreamRunnable);
            mHandler.removeCallbacks(mStopStubbornDreamRunnable);
            mSavedStopReason = null;
            mHandler.removeCallbacks(dream.mStopUnconnectedDreamRunnable);
            mHandler.removeCallbacks(dream.mStopStubbornDreamRunnable);

            if (oldDream.mSentStartBroadcast) {
                mContext.sendBroadcastAsUser(mDreamingStoppedIntent, UserHandle.ALL);
            }

            if (oldDream.mService != null) {
            if (dream.mService != null) {
                try {
                    oldDream.mService.detach();
                    dream.mService.detach();
                } catch (RemoteException ex) {
                    // we don't care; this thing is on the way out
                }

                try {
                    oldDream.mService.asBinder().unlinkToDeath(oldDream, 0);
                    dream.mService.asBinder().unlinkToDeath(dream, 0);
                } catch (NoSuchElementException ex) {
                    // don't care
                }
                oldDream.mService = null;
                dream.mService = null;
            }

            if (oldDream.mBound) {
                mContext.unbindService(oldDream);
            if (dream.mBound) {
                mContext.unbindService(dream);
            }
            oldDream.releaseWakeLockIfNeeded();
            dream.releaseWakeLockIfNeeded();

            mActivityTaskManager.removeRootTasksWithActivityTypes(new int[] {ACTIVITY_TYPE_DREAM});
            // Current dream stopped, device no longer dreaming.
            if (dream == mCurrentDream) {
                mCurrentDream = null;

            mHandler.post(() -> mListener.onDreamStopped(oldDream.mToken));
                if (mSentStartBroadcast) {
                    mContext.sendBroadcastAsUser(mDreamingStoppedIntent, UserHandle.ALL);
                }

                mActivityTaskManager.removeRootTasksWithActivityTypes(
                        new int[] {ACTIVITY_TYPE_DREAM});

                mListener.onDreamStopped(dream.mToken);
            }
        } finally {
            Trace.traceEnd(Trace.TRACE_TAG_POWER);
        }
    }

    /**
     * Stops all previous dreams, if any.
     */
    private void stopPreviousDreams() {
        if (mPreviousDreams.isEmpty()) {
            return;
        }

        // Using an iterator because mPreviousDreams is modified while the iteration is in process.
        for (final Iterator<DreamRecord> it = mPreviousDreams.iterator(); it.hasNext(); ) {
            stopDreamInstance(true /*immediate*/, "stop previous dream", it.next());
            it.remove();
        }
    }

    private void attach(IDreamService service) {
        try {
            service.asBinder().linkToDeath(mCurrentDream, 0);
@@ -251,9 +285,9 @@ final class DreamController {

        mCurrentDream.mService = service;

        if (!mCurrentDream.mIsPreviewMode) {
        if (!mCurrentDream.mIsPreviewMode && !mSentStartBroadcast) {
            mContext.sendBroadcastAsUser(mDreamingStartedIntent, UserHandle.ALL);
            mCurrentDream.mSentStartBroadcast = true;
            mSentStartBroadcast = true;
        }
    }

@@ -275,10 +309,35 @@ final class DreamController {
        public boolean mBound;
        public boolean mConnected;
        public IDreamService mService;
        public boolean mSentStartBroadcast;

        private String mStopReason;
        private long mDreamStartTime;
        public boolean mWakingGently;

        private final Runnable mStopPreviousDreamsIfNeeded = this::stopPreviousDreamsIfNeeded;
        private final Runnable mReleaseWakeLockIfNeeded = this::releaseWakeLockIfNeeded;

        private final Runnable mStopUnconnectedDreamRunnable = () -> {
            if (mBound && !mConnected) {
                Slog.w(TAG, "Bound dream did not connect in the time allotted");
                stopDream(true /*immediate*/, "slow to connect" /*reason*/);
            }
        };

        private final Runnable mStopStubbornDreamRunnable = () -> {
            Slog.w(TAG, "Stubborn dream did not finish itself in the time allotted");
            stopDream(true /*immediate*/, "slow to finish" /*reason*/);
            mStopReason = null;
        };

        private final IRemoteCallback mDreamingStartedCallback = new IRemoteCallback.Stub() {
            // May be called on any thread.
            @Override
            public void sendResult(Bundle data) {
                mHandler.post(mStopPreviousDreamsIfNeeded);
                mHandler.post(mReleaseWakeLockIfNeeded);
            }
        };

        DreamRecord(Binder token, ComponentName name, boolean isPreviewMode,
                boolean canDoze, int userId, PowerManager.WakeLock wakeLock) {
            mToken = token;
@@ -289,7 +348,9 @@ final class DreamController {
            mWakeLock = wakeLock;
            // Hold the lock while we're waiting for the service to connect and start dreaming.
            // Released after the service has started dreaming, we stop dreaming, or it timed out.
            if (mWakeLock != null) {
                mWakeLock.acquire();
            }
            mHandler.postDelayed(mReleaseWakeLockIfNeeded, 10000);
        }

@@ -329,6 +390,12 @@ final class DreamController {
            });
        }

        void stopPreviousDreamsIfNeeded() {
            if (mCurrentDream == DreamRecord.this) {
                stopPreviousDreams();
            }
        }

        void releaseWakeLockIfNeeded() {
            if (mWakeLock != null) {
                mWakeLock.release();
@@ -336,15 +403,5 @@ final class DreamController {
                mHandler.removeCallbacks(mReleaseWakeLockIfNeeded);
            }
        }

        final Runnable mReleaseWakeLockIfNeeded = this::releaseWakeLockIfNeeded;

        final IRemoteCallback mDreamingStartedCallback = new IRemoteCallback.Stub() {
            // May be called on any thread.
            @Override
            public void sendResult(Bundle data) throws RemoteException {
                mHandler.post(mReleaseWakeLockIfNeeded);
            }
        };
    }
}
+0 −2
Original line number Diff line number Diff line
@@ -493,8 +493,6 @@ public final class DreamManagerService extends SystemService {
            return;
        }

        stopDreamLocked(true /*immediate*/, "starting new dream");

        Slog.i(TAG, "Entering dreamland.");

        mCurrentDream = new DreamRecord(name, userId, isPreviewMode, canDoze);
+2 −5
Original line number Diff line number Diff line
@@ -22,7 +22,6 @@ import static android.app.ActivityTaskManager.RESIZE_MODE_FORCED;
import static android.app.ActivityTaskManager.RESIZE_MODE_SYSTEM_SCREEN_ROTATION;
import static android.app.ITaskStackListener.FORCED_RESIZEABLE_REASON_SPLIT_SCREEN;
import static android.app.WindowConfiguration.ACTIVITY_TYPE_ASSISTANT;
import static android.app.WindowConfiguration.ACTIVITY_TYPE_DREAM;
import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME;
import static android.app.WindowConfiguration.ACTIVITY_TYPE_RECENTS;
import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD;
@@ -5787,12 +5786,10 @@ class Task extends TaskFragment {
            return false;
        }

        // Existing Tasks can be reused if a new root task will be created anyway, or for the
        // Dream - because there can only ever be one DreamActivity.
        // Existing Tasks can be reused if a new root task will be created anyway.
        final int windowingMode = getWindowingMode();
        final int activityType = getActivityType();
        return DisplayContent.alwaysCreateRootTask(windowingMode, activityType)
                || activityType == ACTIVITY_TYPE_DREAM;
        return DisplayContent.alwaysCreateRootTask(windowingMode, activityType);
    }

    void addChild(WindowContainer child, final boolean toTop, boolean showForAllUsers) {
+160 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2022 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.server.dreams;

import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.any;
import static org.mockito.Mockito.clearInvocations;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import android.content.ComponentName;
import android.content.Context;
import android.content.ServiceConnection;
import android.os.Binder;
import android.os.Handler;
import android.os.IBinder;
import android.os.IRemoteCallback;
import android.os.RemoteException;
import android.os.test.TestLooper;
import android.service.dreams.IDreamService;

import androidx.test.filters.SmallTest;
import androidx.test.runner.AndroidJUnit4;

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;

@SmallTest
@RunWith(AndroidJUnit4.class)
public class DreamControllerTest {
    @Mock
    private DreamController.Listener mListener;
    @Mock
    private Context mContext;
    @Mock
    private IBinder mIBinder;
    @Mock
    private IDreamService mIDreamService;

    @Captor
    private ArgumentCaptor<ServiceConnection> mServiceConnectionACaptor;
    @Captor
    private ArgumentCaptor<IRemoteCallback> mRemoteCallbackCaptor;

    private final TestLooper mLooper = new TestLooper();
    private final Handler mHandler = new Handler(mLooper.getLooper());

    private DreamController mDreamController;

    private Binder mToken;
    private ComponentName mDreamName;
    private ComponentName mOverlayName;

    @Before
    public void setup() {
        MockitoAnnotations.initMocks(this);

        when(mIDreamService.asBinder()).thenReturn(mIBinder);
        when(mIBinder.queryLocalInterface(anyString())).thenReturn(mIDreamService);
        when(mContext.bindServiceAsUser(any(), any(), anyInt(), any())).thenReturn(true);

        mToken = new Binder();
        mDreamName = ComponentName.unflattenFromString("dream");
        mOverlayName = ComponentName.unflattenFromString("dream_overlay");
        mDreamController = new DreamController(mContext, mHandler, mListener);
    }

    @Test
    public void startDream_attachOnServiceConnected() throws RemoteException {
        // Call dream controller to start dreaming.
        mDreamController.startDream(mToken, mDreamName, false /*isPreview*/, false /*doze*/,
                0 /*userId*/, null /*wakeLock*/, mOverlayName, "test" /*reason*/);

        // Mock service connected.
        final ServiceConnection serviceConnection = captureServiceConnection();
        serviceConnection.onServiceConnected(mDreamName, mIBinder);
        mLooper.dispatchAll();

        // Verify that dream service is called to attach.
        verify(mIDreamService).attach(eq(mToken), eq(false) /*doze*/, any());
    }

    @Test
    public void startDream_startASecondDream_detachOldDreamOnceNewDreamIsStarted()
            throws RemoteException {
        // Start first dream.
        mDreamController.startDream(mToken, mDreamName, false /*isPreview*/, false /*doze*/,
                0 /*userId*/, null /*wakeLock*/, mOverlayName, "test" /*reason*/);
        captureServiceConnection().onServiceConnected(mDreamName, mIBinder);
        mLooper.dispatchAll();
        clearInvocations(mContext);

        // Set up second dream.
        final Binder newToken = new Binder();
        final ComponentName newDreamName = ComponentName.unflattenFromString("new_dream");
        final ComponentName newOverlayName = ComponentName.unflattenFromString("new_dream_overlay");
        final IDreamService newDreamService = mock(IDreamService.class);
        final IBinder newBinder = mock(IBinder.class);
        when(newDreamService.asBinder()).thenReturn(newBinder);
        when(newBinder.queryLocalInterface(anyString())).thenReturn(newDreamService);

        // Start second dream.
        mDreamController.startDream(newToken, newDreamName, false /*isPreview*/, false /*doze*/,
                0 /*userId*/, null /*wakeLock*/, newOverlayName, "test" /*reason*/);
        captureServiceConnection().onServiceConnected(newDreamName, newBinder);
        mLooper.dispatchAll();

        // Mock second dream started.
        verify(newDreamService).attach(eq(newToken), eq(false) /*doze*/,
                mRemoteCallbackCaptor.capture());
        mRemoteCallbackCaptor.getValue().sendResult(null /*data*/);
        mLooper.dispatchAll();

        // Verify that the first dream is called to detach.
        verify(mIDreamService).detach();
    }

    @Test
    public void stopDream_detachFromService() 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();

        // Stop dream.
        mDreamController.stopDream(true /*immediate*/, "test stop dream" /*reason*/);

        // Verify that dream service is called to detach.
        verify(mIDreamService).detach();
    }

    private ServiceConnection captureServiceConnection() {
        verify(mContext).bindServiceAsUser(any(), mServiceConnectionACaptor.capture(), anyInt(),
                any());
        return mServiceConnectionACaptor.getValue();
    }
}