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

Original change: https://googleplex-android-review.googlesource.com/c/platform/frameworks/base/+/20041857

Change-Id: Idbbd394f57016b4609450819729f22ee102db478
Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
This commit is contained in:
TreeHugger Robot
2022-10-22 07:19:42 +00:00
committed by Automerger Merge Worker
5 changed files with 287 additions and 80 deletions

View File

@@ -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;
}

View File

@@ -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();
mHandler.post(() -> mListener.onDreamStopped(oldDream.mToken));
// Current dream stopped, device no longer dreaming.
if (dream == mCurrentDream) {
mCurrentDream = null;
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.
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);
}
};
}
}

View File

@@ -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);

View File

@@ -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;
@@ -5856,12 +5855,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) {

View File

@@ -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();
}
}