Loading core/java/android/service/dreams/DreamService.java +1 −1 Original line number Diff line number Diff line Loading @@ -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; } Loading services/core/java/com/android/server/dreams/DreamController.java +124 −72 Original line number Diff line number Diff line Loading @@ -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; /** Loading @@ -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); Loading @@ -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"); } Loading @@ -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. Loading @@ -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); Loading @@ -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 Loading @@ -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; mHandler.post(() -> mListener.onDreamStopped(oldDream.mToken)); if (mSentStartBroadcast) { mContext.sendBroadcastAsUser(mDreamingStoppedIntent, UserHandle.ALL); } 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); Loading @@ -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; } } Loading @@ -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; Loading @@ -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. if (mWakeLock != null) { mWakeLock.acquire(); } mHandler.postDelayed(mReleaseWakeLockIfNeeded, 10000); } Loading Loading @@ -326,6 +382,12 @@ final class DreamController { }); } void stopPreviousDreamsIfNeeded() { if (mCurrentDream == DreamRecord.this) { stopPreviousDreams(); } } void releaseWakeLockIfNeeded() { if (mWakeLock != null) { mWakeLock.release(); Loading @@ -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); } }; } } services/core/java/com/android/server/dreams/DreamManagerService.java +0 −2 Original line number Diff line number Diff line Loading @@ -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); Loading services/core/java/com/android/server/wm/Task.java +2 −5 Original line number Diff line number Diff line Loading @@ -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; Loading Loading @@ -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) { Loading services/tests/servicestests/src/com/android/server/dreams/DreamControllerTest.java 0 → 100644 +160 −0 Original line number Diff line number Diff line /* * Copyright (C) 2022 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.server.dreams; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.any; import static org.mockito.Mockito.clearInvocations; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import android.content.ComponentName; import android.content.Context; import android.content.ServiceConnection; import android.os.Binder; import android.os.Handler; import android.os.IBinder; import android.os.IRemoteCallback; import android.os.RemoteException; import android.os.test.TestLooper; import android.service.dreams.IDreamService; import androidx.test.filters.SmallTest; import androidx.test.runner.AndroidJUnit4; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; import org.mockito.Captor; import org.mockito.Mock; import org.mockito.MockitoAnnotations; @SmallTest @RunWith(AndroidJUnit4.class) public class DreamControllerTest { @Mock private DreamController.Listener mListener; @Mock private Context mContext; @Mock private IBinder mIBinder; @Mock private IDreamService mIDreamService; @Captor private ArgumentCaptor<ServiceConnection> mServiceConnectionACaptor; @Captor private ArgumentCaptor<IRemoteCallback> mRemoteCallbackCaptor; private final TestLooper mLooper = new TestLooper(); private final Handler mHandler = new Handler(mLooper.getLooper()); private DreamController mDreamController; private Binder mToken; private ComponentName mDreamName; private ComponentName mOverlayName; @Before public void setup() { MockitoAnnotations.initMocks(this); when(mIDreamService.asBinder()).thenReturn(mIBinder); when(mIBinder.queryLocalInterface(anyString())).thenReturn(mIDreamService); when(mContext.bindServiceAsUser(any(), any(), anyInt(), any())).thenReturn(true); mToken = new Binder(); mDreamName = ComponentName.unflattenFromString("dream"); mOverlayName = ComponentName.unflattenFromString("dream_overlay"); mDreamController = new DreamController(mContext, mHandler, mListener); } @Test public void startDream_attachOnServiceConnected() throws RemoteException { // Call dream controller to start dreaming. mDreamController.startDream(mToken, mDreamName, false /*isPreview*/, false /*doze*/, 0 /*userId*/, null /*wakeLock*/, mOverlayName, "test" /*reason*/); // Mock service connected. final ServiceConnection serviceConnection = captureServiceConnection(); serviceConnection.onServiceConnected(mDreamName, mIBinder); mLooper.dispatchAll(); // Verify that dream service is called to attach. verify(mIDreamService).attach(eq(mToken), eq(false) /*doze*/, any()); } @Test public void startDream_startASecondDream_detachOldDreamOnceNewDreamIsStarted() throws RemoteException { // Start first dream. mDreamController.startDream(mToken, mDreamName, false /*isPreview*/, false /*doze*/, 0 /*userId*/, null /*wakeLock*/, mOverlayName, "test" /*reason*/); captureServiceConnection().onServiceConnected(mDreamName, mIBinder); mLooper.dispatchAll(); clearInvocations(mContext); // Set up second dream. final Binder newToken = new Binder(); final ComponentName newDreamName = ComponentName.unflattenFromString("new_dream"); final ComponentName newOverlayName = ComponentName.unflattenFromString("new_dream_overlay"); final IDreamService newDreamService = mock(IDreamService.class); final IBinder newBinder = mock(IBinder.class); when(newDreamService.asBinder()).thenReturn(newBinder); when(newBinder.queryLocalInterface(anyString())).thenReturn(newDreamService); // Start second dream. mDreamController.startDream(newToken, newDreamName, false /*isPreview*/, false /*doze*/, 0 /*userId*/, null /*wakeLock*/, newOverlayName, "test" /*reason*/); captureServiceConnection().onServiceConnected(newDreamName, newBinder); mLooper.dispatchAll(); // Mock second dream started. verify(newDreamService).attach(eq(newToken), eq(false) /*doze*/, mRemoteCallbackCaptor.capture()); mRemoteCallbackCaptor.getValue().sendResult(null /*data*/); mLooper.dispatchAll(); // Verify that the first dream is called to detach. verify(mIDreamService).detach(); } @Test public void stopDream_detachFromService() throws RemoteException { // Start dream. mDreamController.startDream(mToken, mDreamName, false /*isPreview*/, false /*doze*/, 0 /*userId*/, null /*wakeLock*/, mOverlayName, "test" /*reason*/); captureServiceConnection().onServiceConnected(mDreamName, mIBinder); mLooper.dispatchAll(); // Stop dream. mDreamController.stopDream(true /*immediate*/, "test stop dream" /*reason*/); // Verify that dream service is called to detach. verify(mIDreamService).detach(); } private ServiceConnection captureServiceConnection() { verify(mContext).bindServiceAsUser(any(), mServiceConnectionACaptor.capture(), anyInt(), any()); return mServiceConnectionACaptor.getValue(); } } Loading
core/java/android/service/dreams/DreamService.java +1 −1 Original line number Diff line number Diff line Loading @@ -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; } Loading
services/core/java/com/android/server/dreams/DreamController.java +124 −72 Original line number Diff line number Diff line Loading @@ -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; /** Loading @@ -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); Loading @@ -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"); } Loading @@ -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. Loading @@ -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); Loading @@ -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 Loading @@ -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; mHandler.post(() -> mListener.onDreamStopped(oldDream.mToken)); if (mSentStartBroadcast) { mContext.sendBroadcastAsUser(mDreamingStoppedIntent, UserHandle.ALL); } 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); Loading @@ -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; } } Loading @@ -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; Loading @@ -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. if (mWakeLock != null) { mWakeLock.acquire(); } mHandler.postDelayed(mReleaseWakeLockIfNeeded, 10000); } Loading Loading @@ -326,6 +382,12 @@ final class DreamController { }); } void stopPreviousDreamsIfNeeded() { if (mCurrentDream == DreamRecord.this) { stopPreviousDreams(); } } void releaseWakeLockIfNeeded() { if (mWakeLock != null) { mWakeLock.release(); Loading @@ -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); } }; } }
services/core/java/com/android/server/dreams/DreamManagerService.java +0 −2 Original line number Diff line number Diff line Loading @@ -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); Loading
services/core/java/com/android/server/wm/Task.java +2 −5 Original line number Diff line number Diff line Loading @@ -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; Loading Loading @@ -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) { Loading
services/tests/servicestests/src/com/android/server/dreams/DreamControllerTest.java 0 → 100644 +160 −0 Original line number Diff line number Diff line /* * Copyright (C) 2022 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.server.dreams; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.any; import static org.mockito.Mockito.clearInvocations; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import android.content.ComponentName; import android.content.Context; import android.content.ServiceConnection; import android.os.Binder; import android.os.Handler; import android.os.IBinder; import android.os.IRemoteCallback; import android.os.RemoteException; import android.os.test.TestLooper; import android.service.dreams.IDreamService; import androidx.test.filters.SmallTest; import androidx.test.runner.AndroidJUnit4; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; import org.mockito.Captor; import org.mockito.Mock; import org.mockito.MockitoAnnotations; @SmallTest @RunWith(AndroidJUnit4.class) public class DreamControllerTest { @Mock private DreamController.Listener mListener; @Mock private Context mContext; @Mock private IBinder mIBinder; @Mock private IDreamService mIDreamService; @Captor private ArgumentCaptor<ServiceConnection> mServiceConnectionACaptor; @Captor private ArgumentCaptor<IRemoteCallback> mRemoteCallbackCaptor; private final TestLooper mLooper = new TestLooper(); private final Handler mHandler = new Handler(mLooper.getLooper()); private DreamController mDreamController; private Binder mToken; private ComponentName mDreamName; private ComponentName mOverlayName; @Before public void setup() { MockitoAnnotations.initMocks(this); when(mIDreamService.asBinder()).thenReturn(mIBinder); when(mIBinder.queryLocalInterface(anyString())).thenReturn(mIDreamService); when(mContext.bindServiceAsUser(any(), any(), anyInt(), any())).thenReturn(true); mToken = new Binder(); mDreamName = ComponentName.unflattenFromString("dream"); mOverlayName = ComponentName.unflattenFromString("dream_overlay"); mDreamController = new DreamController(mContext, mHandler, mListener); } @Test public void startDream_attachOnServiceConnected() throws RemoteException { // Call dream controller to start dreaming. mDreamController.startDream(mToken, mDreamName, false /*isPreview*/, false /*doze*/, 0 /*userId*/, null /*wakeLock*/, mOverlayName, "test" /*reason*/); // Mock service connected. final ServiceConnection serviceConnection = captureServiceConnection(); serviceConnection.onServiceConnected(mDreamName, mIBinder); mLooper.dispatchAll(); // Verify that dream service is called to attach. verify(mIDreamService).attach(eq(mToken), eq(false) /*doze*/, any()); } @Test public void startDream_startASecondDream_detachOldDreamOnceNewDreamIsStarted() throws RemoteException { // Start first dream. mDreamController.startDream(mToken, mDreamName, false /*isPreview*/, false /*doze*/, 0 /*userId*/, null /*wakeLock*/, mOverlayName, "test" /*reason*/); captureServiceConnection().onServiceConnected(mDreamName, mIBinder); mLooper.dispatchAll(); clearInvocations(mContext); // Set up second dream. final Binder newToken = new Binder(); final ComponentName newDreamName = ComponentName.unflattenFromString("new_dream"); final ComponentName newOverlayName = ComponentName.unflattenFromString("new_dream_overlay"); final IDreamService newDreamService = mock(IDreamService.class); final IBinder newBinder = mock(IBinder.class); when(newDreamService.asBinder()).thenReturn(newBinder); when(newBinder.queryLocalInterface(anyString())).thenReturn(newDreamService); // Start second dream. mDreamController.startDream(newToken, newDreamName, false /*isPreview*/, false /*doze*/, 0 /*userId*/, null /*wakeLock*/, newOverlayName, "test" /*reason*/); captureServiceConnection().onServiceConnected(newDreamName, newBinder); mLooper.dispatchAll(); // Mock second dream started. verify(newDreamService).attach(eq(newToken), eq(false) /*doze*/, mRemoteCallbackCaptor.capture()); mRemoteCallbackCaptor.getValue().sendResult(null /*data*/); mLooper.dispatchAll(); // Verify that the first dream is called to detach. verify(mIDreamService).detach(); } @Test public void stopDream_detachFromService() throws RemoteException { // Start dream. mDreamController.startDream(mToken, mDreamName, false /*isPreview*/, false /*doze*/, 0 /*userId*/, null /*wakeLock*/, mOverlayName, "test" /*reason*/); captureServiceConnection().onServiceConnected(mDreamName, mIBinder); mLooper.dispatchAll(); // Stop dream. mDreamController.stopDream(true /*immediate*/, "test stop dream" /*reason*/); // Verify that dream service is called to detach. verify(mIDreamService).detach(); } private ServiceConnection captureServiceConnection() { verify(mContext).bindServiceAsUser(any(), mServiceConnectionACaptor.capture(), anyInt(), any()); return mServiceConnectionACaptor.getValue(); } }