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

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

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

parents 922e19de 01fbf6b7
Loading
Loading
Loading
Loading
+1 −1
Original line number Diff line number Diff line
@@ -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;
        }
+124 −72
Original line number Diff line number Diff line
@@ -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;

            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);
@@ -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.
            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);
            }
        };
    }
}
+0 −2
Original line number Diff line number Diff line
@@ -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);
+2 −5
Original line number Diff line number Diff line
@@ -21,7 +21,6 @@ import static android.app.ActivityTaskManager.RESIZE_MODE_FORCED;
import static android.app.ActivityTaskManager.RESIZE_MODE_SYSTEM_SCREEN_ROTATION;
import static android.app.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) {
+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();
    }
}