summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author TreeHugger Robot <treehugger-gerrit@google.com> 2022-10-22 06:24:15 +0000
committer Android (Google) Code Review <android-gerrit@google.com> 2022-10-22 06:24:15 +0000
commitf932a2974efc2139d2ffdf41baf093df74b4b8b0 (patch)
tree919714fb41c8193ef22fe9bab96ad618c34fa914
parent922e19de8d6015315fda29644c0dd728fdaf19d5 (diff)
parent01fbf6b763d821c9fd94ff4ea38777e5213281a3 (diff)
Merge "Smooth transition when switching dreaming." into tm-qpr-dev
-rw-r--r--core/java/android/service/dreams/DreamService.java2
-rw-r--r--services/core/java/com/android/server/dreams/DreamController.java196
-rw-r--r--services/core/java/com/android/server/dreams/DreamManagerService.java2
-rw-r--r--services/core/java/com/android/server/wm/Task.java7
-rw-r--r--services/tests/servicestests/src/com/android/server/dreams/DreamControllerTest.java160
5 files changed, 287 insertions, 80 deletions
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<DreamRecord> 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<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);
@@ -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<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();
+ }
+}