Smooth transition when switching dreaming.
This CL makes the following changes - instead of stopping any existing dream before starting a new one, now stop *after* the new dream has been started - do not reuse the same task for multiple dream activities This allows a smooth inter-task transition when swiching between dreams, e.g. between user configured dream and system dream. Bug: 244315094 Test: atest DreamControllerTest Test: atest SystemDreamTest Test: manually on device, see video in linked bug Change-Id: I9576470ae0f54d553a0db3aee6fb720edaa5d733
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
mWakeLock.acquire();
|
||||
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);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -0,0 +1,160 @@
|
||||
/*
|
||||
* 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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user