From 01fbf6b763d821c9fd94ff4ea38777e5213281a3 Mon Sep 17 00:00:00 2001 From: Darrell Shi Date: Sat, 24 Sep 2022 02:47:44 +0000 Subject: 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 Fix: 244315094 Test: atest DreamControllerTest Test: atest SystemDreamTest Test: manually on device, see video in linked bug Change-Id: I9576470ae0f54d553a0db3aee6fb720edaa5d733 Merged-In: I9576470ae0f54d553a0db3aee6fb720edaa5d733 --- core/java/android/service/dreams/DreamService.java | 2 +- .../com/android/server/dreams/DreamController.java | 196 +++++++++++++-------- .../android/server/dreams/DreamManagerService.java | 2 - services/core/java/com/android/server/wm/Task.java | 7 +- .../android/server/dreams/DreamControllerTest.java | 160 +++++++++++++++++ 5 files changed, 287 insertions(+), 80 deletions(-) create mode 100644 services/tests/servicestests/src/com/android/server/dreams/DreamControllerTest.java diff --git a/core/java/android/service/dreams/DreamService.java b/core/java/android/service/dreams/DreamService.java index cb0dce91589e..32bdf7962273 100644 --- a/core/java/android/service/dreams/DreamService.java +++ b/core/java/android/service/dreams/DreamService.java @@ -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; } diff --git a/services/core/java/com/android/server/dreams/DreamController.java b/services/core/java/com/android/server/dreams/DreamController.java index b8af1bfcc254..b11a06eda025 100644 --- a/services/core/java/com/android/server/dreams/DreamController.java +++ b/services/core/java/com/android/server/dreams/DreamController.java @@ -34,13 +34,13 @@ import android.os.UserHandle; import android.service.dreams.DreamService; import android.service.dreams.IDreamService; import android.util.Slog; -import android.view.IWindowManager; -import android.view.WindowManagerGlobal; 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; /** @@ -60,9 +60,6 @@ final class DreamController { private final Context mContext; private final Handler mHandler; private final Listener mListener; - private final IWindowManager mIWindowManager; - private long mDreamStartTime; - private String mSavedStopReason; private final Intent mDreamingStartedIntent = new Intent(Intent.ACTION_DREAMING_STARTED) .addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY); @@ -73,27 +70,20 @@ 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 mPreviousDreams = new ArrayList<>(); public DreamController(Context context, Handler handler, Listener listener) { mContext = context; mHandler = handler; mListener = listener; - mIWindowManager = WindowManagerGlobal.getWindowManagerService(); mCloseNotificationShadeIntent = new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS); mCloseNotificationShadeIntent.putExtra("reason", "dream"); } @@ -109,18 +99,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. @@ -130,9 +119,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); @@ -155,31 +147,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 @@ -187,54 +197,73 @@ 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(); + + // Current dream stopped, device no longer dreaming. + if (dream == mCurrentDream) { + mCurrentDream = null; + + if (mSentStartBroadcast) { + mContext.sendBroadcastAsUser(mDreamingStoppedIntent, UserHandle.ALL); + } - mHandler.post(() -> mListener.onDreamStopped(oldDream.mToken)); + 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 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); @@ -248,9 +277,9 @@ final class DreamController { mCurrentDream.mService = service; - if (!mCurrentDream.mIsPreviewMode) { + if (!mCurrentDream.mIsPreviewMode && !mSentStartBroadcast) { mContext.sendBroadcastAsUser(mDreamingStartedIntent, UserHandle.ALL); - mCurrentDream.mSentStartBroadcast = true; + mSentStartBroadcast = true; } } @@ -272,10 +301,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; @@ -286,7 +340,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); } @@ -326,6 +382,12 @@ final class DreamController { }); } + void stopPreviousDreamsIfNeeded() { + if (mCurrentDream == DreamRecord.this) { + stopPreviousDreams(); + } + } + void releaseWakeLockIfNeeded() { if (mWakeLock != null) { mWakeLock.release(); @@ -333,15 +395,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); - } - }; } } diff --git a/services/core/java/com/android/server/dreams/DreamManagerService.java b/services/core/java/com/android/server/dreams/DreamManagerService.java index c9557d60c8b7..5589673973c3 100644 --- a/services/core/java/com/android/server/dreams/DreamManagerService.java +++ b/services/core/java/com/android/server/dreams/DreamManagerService.java @@ -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); diff --git a/services/core/java/com/android/server/wm/Task.java b/services/core/java/com/android/server/wm/Task.java index 731754e1f0cb..eba49bbc7301 100644 --- a/services/core/java/com/android/server/wm/Task.java +++ b/services/core/java/com/android/server/wm/Task.java @@ -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.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; @@ -5855,12 +5854,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) { diff --git a/services/tests/servicestests/src/com/android/server/dreams/DreamControllerTest.java b/services/tests/servicestests/src/com/android/server/dreams/DreamControllerTest.java new file mode 100644 index 000000000000..303a370b0ba9 --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/dreams/DreamControllerTest.java @@ -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 mServiceConnectionACaptor; + @Captor + private ArgumentCaptor 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(); + } +} -- cgit v1.2.3-59-g8ed1b