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

Commit f932a297 authored by TreeHugger Robot's avatar TreeHugger Robot Committed by Android (Google) Code Review
Browse files

Merge "Smooth transition when switching dreaming." into tm-qpr-dev

parents 922e19de 01fbf6b7
Loading
Loading
Loading
Loading
+1 −1
Original line number Original line Diff line number Diff line
@@ -1047,7 +1047,7 @@ public class DreamService extends Service implements Window.Callback {
        }
        }


        if (mDreamToken == null) {
        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();
            stopSelf();
            return;
            return;
        }
        }
+124 −72
Original line number Original line Diff line number Diff line
@@ -34,13 +34,13 @@ import android.os.UserHandle;
import android.service.dreams.DreamService;
import android.service.dreams.DreamService;
import android.service.dreams.IDreamService;
import android.service.dreams.IDreamService;
import android.util.Slog;
import android.util.Slog;
import android.view.IWindowManager;
import android.view.WindowManagerGlobal;


import com.android.internal.logging.MetricsLogger;
import com.android.internal.logging.MetricsLogger;
import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
import com.android.internal.logging.nano.MetricsProto.MetricsEvent;


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


/**
/**
@@ -60,9 +60,6 @@ final class DreamController {
    private final Context mContext;
    private final Context mContext;
    private final Handler mHandler;
    private final Handler mHandler;
    private final Listener mListener;
    private final Listener mListener;
    private final IWindowManager mIWindowManager;
    private long mDreamStartTime;
    private String mSavedStopReason;


    private final Intent mDreamingStartedIntent = new Intent(Intent.ACTION_DREAMING_STARTED)
    private final Intent mDreamingStartedIntent = new Intent(Intent.ACTION_DREAMING_STARTED)
            .addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY);
            .addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY);
@@ -73,27 +70,20 @@ final class DreamController {


    private DreamRecord mCurrentDream;
    private DreamRecord mCurrentDream;


    private final Runnable mStopUnconnectedDreamRunnable = new Runnable() {
    // Whether a dreaming started intent has been broadcast.
        @Override
    private boolean mSentStartBroadcast = false;
        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");
            }
        }
    };


    private final Runnable mStopStubbornDreamRunnable = () -> {
    // When a new dream is started and there is an existing dream, the existing dream is allowed to
        Slog.w(TAG, "Stubborn dream did not finish itself in the time allotted");
    // live a little longer until the new dream is started, for a smoother transition. This dream is
        stopDream(true /*immediate*/, "slow to finish");
    // stopped as soon as the new dream is started, and this list is cleared. Usually there should
        mSavedStopReason = null;
    // 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) {
    public DreamController(Context context, Handler handler, Listener listener) {
        mContext = context;
        mContext = context;
        mHandler = handler;
        mHandler = handler;
        mListener = listener;
        mListener = listener;
        mIWindowManager = WindowManagerGlobal.getWindowManagerService();
        mCloseNotificationShadeIntent = new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS);
        mCloseNotificationShadeIntent = new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS);
        mCloseNotificationShadeIntent.putExtra("reason", "dream");
        mCloseNotificationShadeIntent.putExtra("reason", "dream");
    }
    }
@@ -109,18 +99,17 @@ final class DreamController {
            pw.println("    mUserId=" + mCurrentDream.mUserId);
            pw.println("    mUserId=" + mCurrentDream.mUserId);
            pw.println("    mBound=" + mCurrentDream.mBound);
            pw.println("    mBound=" + mCurrentDream.mBound);
            pw.println("    mService=" + mCurrentDream.mService);
            pw.println("    mService=" + mCurrentDream.mService);
            pw.println("    mSentStartBroadcast=" + mCurrentDream.mSentStartBroadcast);
            pw.println("    mWakingGently=" + mCurrentDream.mWakingGently);
            pw.println("    mWakingGently=" + mCurrentDream.mWakingGently);
        } else {
        } else {
            pw.println("  mCurrentDream: null");
            pw.println("  mCurrentDream: null");
        }
        }

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


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

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


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


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


@@ -155,31 +147,49 @@ final class DreamController {
            }
            }


            mCurrentDream.mBound = true;
            mCurrentDream.mBound = true;
            mHandler.postDelayed(mStopUnconnectedDreamRunnable, DREAM_CONNECTION_TIMEOUT);
            mHandler.postDelayed(mCurrentDream.mStopUnconnectedDreamRunnable,
                    DREAM_CONNECTION_TIMEOUT);
        } finally {
        } finally {
            Trace.traceEnd(Trace.TRACE_TAG_POWER);
            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) {
    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;
            return;
        }
        }


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


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


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


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


            if (oldDream.mSentStartBroadcast) {
            if (dream.mService != null) {
                mContext.sendBroadcastAsUser(mDreamingStoppedIntent, UserHandle.ALL);
            }

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


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


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

            // 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);
                }

                mListener.onDreamStopped(dream.mToken);
            }
        } finally {
        } finally {
            Trace.traceEnd(Trace.TRACE_TAG_POWER);
            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) {
    private void attach(IDreamService service) {
        try {
        try {
            service.asBinder().linkToDeath(mCurrentDream, 0);
            service.asBinder().linkToDeath(mCurrentDream, 0);
@@ -248,9 +277,9 @@ final class DreamController {


        mCurrentDream.mService = service;
        mCurrentDream.mService = service;


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


@@ -272,10 +301,35 @@ final class DreamController {
        public boolean mBound;
        public boolean mBound;
        public boolean mConnected;
        public boolean mConnected;
        public IDreamService mService;
        public IDreamService mService;
        public boolean mSentStartBroadcast;
        private String mStopReason;

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


@@ -326,6 +382,12 @@ final class DreamController {
            });
            });
        }
        }


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

        void releaseWakeLockIfNeeded() {
        void releaseWakeLockIfNeeded() {
            if (mWakeLock != null) {
            if (mWakeLock != null) {
                mWakeLock.release();
                mWakeLock.release();
@@ -333,15 +395,5 @@ final class DreamController {
                mHandler.removeCallbacks(mReleaseWakeLockIfNeeded);
                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 Original line Diff line number Diff line
@@ -493,8 +493,6 @@ public final class DreamManagerService extends SystemService {
            return;
            return;
        }
        }


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

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


        mCurrentDream = new DreamRecord(name, userId, isPreviewMode, canDoze);
        mCurrentDream = new DreamRecord(name, userId, isPreviewMode, canDoze);
+2 −5
Original line number Original line Diff line number Diff line
@@ -21,7 +21,6 @@ import static android.app.ActivityTaskManager.RESIZE_MODE_FORCED;
import static android.app.ActivityTaskManager.RESIZE_MODE_SYSTEM_SCREEN_ROTATION;
import static android.app.ActivityTaskManager.RESIZE_MODE_SYSTEM_SCREEN_ROTATION;
import static android.app.ITaskStackListener.FORCED_RESIZEABLE_REASON_SPLIT_SCREEN;
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_ASSISTANT;
import static android.app.WindowConfiguration.ACTIVITY_TYPE_DREAM;
import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME;
import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME;
import static android.app.WindowConfiguration.ACTIVITY_TYPE_RECENTS;
import static android.app.WindowConfiguration.ACTIVITY_TYPE_RECENTS;
import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD;
import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD;
@@ -5855,12 +5854,10 @@ class Task extends TaskFragment {
            return false;
            return false;
        }
        }


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


    void addChild(WindowContainer child, final boolean toTop, boolean showForAllUsers) {
    void addChild(WindowContainer child, final boolean toTop, boolean showForAllUsers) {
+160 −0
Original line number Original line 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();
    }
}