diff --git a/services/core/java/com/android/server/wm/RecentsAnimationController.java b/services/core/java/com/android/server/wm/RecentsAnimationController.java index e8f4545e1c27a..318b3d293a57a 100644 --- a/services/core/java/com/android/server/wm/RecentsAnimationController.java +++ b/services/core/java/com/android/server/wm/RecentsAnimationController.java @@ -51,6 +51,7 @@ import android.view.RemoteAnimationTarget; import android.view.SurfaceControl; import android.view.SurfaceControl.Transaction; +import com.android.internal.annotations.VisibleForTesting; import com.google.android.collect.Sets; import com.android.server.wm.SurfaceAnimator.OnAnimationFinishedCallback; @@ -290,8 +291,10 @@ public class RecentsAnimationController implements DeathRecipient { mService.mWindowPlacerLocked.performSurfacePlacement(); } - private void addAnimation(Task task, boolean isRecentTaskInvisible) { + @VisibleForTesting + AnimationAdapter addAnimation(Task task, boolean isRecentTaskInvisible) { if (DEBUG) Log.d(TAG, "addAnimation(" + task.getName() + ")"); + // TODO: Refactor this to use the task's animator final SurfaceAnimator anim = new SurfaceAnimator(task, null /* animationFinishedCallback */, mService); final TaskAnimationAdapter taskAdapter = new TaskAnimationAdapter(task, @@ -299,6 +302,15 @@ public class RecentsAnimationController implements DeathRecipient { anim.startAnimation(task.getPendingTransaction(), taskAdapter, false /* hidden */); task.commitPendingTransaction(); mPendingAnimations.add(taskAdapter); + return taskAdapter; + } + + @VisibleForTesting + void removeAnimation(TaskAnimationAdapter taskAdapter) { + if (DEBUG) Log.d(TAG, "removeAnimation(" + taskAdapter.mTask.getName() + ")"); + taskAdapter.mTask.setCanAffectSystemUiFlags(true); + taskAdapter.mCapturedFinishCallback.onAnimationFinished(taskAdapter); + mPendingAnimations.remove(taskAdapter); } void startAnimation() { @@ -311,12 +323,21 @@ public class RecentsAnimationController implements DeathRecipient { try { final ArrayList appAnimations = new ArrayList<>(); for (int i = mPendingAnimations.size() - 1; i >= 0; i--) { - final RemoteAnimationTarget target = - mPendingAnimations.get(i).createRemoteAnimationApp(); + final TaskAnimationAdapter taskAdapter = mPendingAnimations.get(i); + final RemoteAnimationTarget target = taskAdapter.createRemoteAnimationApp(); if (target != null) { appAnimations.add(target); + } else { + removeAnimation(taskAdapter); } } + + // Skip the animation if there is nothing to animate + if (appAnimations.isEmpty()) { + cancelAnimation(REORDER_MOVE_TO_ORIGINAL_POSITION); + return; + } + final RemoteAnimationTarget[] appTargets = appAnimations.toArray( new RemoteAnimationTarget[appAnimations.size()]); mPendingStart = false; @@ -365,14 +386,12 @@ public class RecentsAnimationController implements DeathRecipient { if (DEBUG) Log.d(TAG, "cleanupAnimation(): mPendingAnimations=" + mPendingAnimations.size()); for (int i = mPendingAnimations.size() - 1; i >= 0; i--) { - final TaskAnimationAdapter adapter = mPendingAnimations.get(i); - adapter.mTask.setCanAffectSystemUiFlags(true); + final TaskAnimationAdapter taskAdapter = mPendingAnimations.get(i); if (reorderMode == REORDER_MOVE_TO_TOP || reorderMode == REORDER_KEEP_IN_PLACE) { - adapter.mTask.dontAnimateDimExit(); + taskAdapter.mTask.dontAnimateDimExit(); } - adapter.mCapturedFinishCallback.onAnimationFinished(adapter); + removeAnimation(taskAdapter); } - mPendingAnimations.clear(); mRunner.asBinder().unlinkToDeath(this, 0); // Clear associated input consumers @@ -457,7 +476,8 @@ public class RecentsAnimationController implements DeathRecipient { return false; } - private class TaskAnimationAdapter implements AnimationAdapter { + @VisibleForTesting + class TaskAnimationAdapter implements AnimationAdapter { private final Task mTask; private SurfaceControl mCapturedLeash; diff --git a/services/tests/servicestests/src/com/android/server/wm/RecentsAnimationControllerTest.java b/services/tests/servicestests/src/com/android/server/wm/RecentsAnimationControllerTest.java new file mode 100644 index 0000000000000..fbf6691f5bd28 --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/wm/RecentsAnimationControllerTest.java @@ -0,0 +1,89 @@ +/* + * Copyright (C) 2018 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.wm; + +import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD; +import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; +import static android.view.Display.DEFAULT_DISPLAY; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.atLeast; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; + +import android.os.Binder; +import android.os.IInterface; +import android.platform.test.annotations.Presubmit; +import android.support.test.filters.SmallTest; +import android.support.test.runner.AndroidJUnit4; +import android.view.IRecentsAnimationRunner; +import android.view.SurfaceControl; +import com.android.server.wm.SurfaceAnimator.OnAnimationFinishedCallback; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +/** + * atest FrameworksServicesTests:com.android.server.wm.RecentsAnimationControllerTest + */ +@SmallTest +@Presubmit +@RunWith(AndroidJUnit4.class) +public class RecentsAnimationControllerTest extends WindowTestsBase { + + @Mock SurfaceControl mMockLeash; + @Mock SurfaceControl.Transaction mMockTransaction; + @Mock OnAnimationFinishedCallback mFinishedCallback; + @Mock IRecentsAnimationRunner mMockRunner; + @Mock RecentsAnimationController.RecentsAnimationCallbacks mAnimationCallbacks; + private RecentsAnimationController mController; + + @Before + public void setUp() throws Exception { + super.setUp(); + MockitoAnnotations.initMocks(this); + when(mMockRunner.asBinder()).thenReturn(new Binder()); + mController = new RecentsAnimationController(sWm, mMockRunner, mAnimationCallbacks, + DEFAULT_DISPLAY); + } + + @Test + public void testRemovedBeforeStarted_expectCanceled() throws Exception { + final AppWindowToken appWindow = createAppWindowToken(mDisplayContent, + WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_STANDARD); + AnimationAdapter adapter = mController.addAnimation(appWindow.getTask(), + false /* isRecentTaskInvisible */); + adapter.startAnimation(mMockLeash, mMockTransaction, mFinishedCallback); + + // Remove the app window so that the animation target can not be created + appWindow.removeImmediately(); + mController.startAnimation(); + + // Verify that the finish callback to reparent the leash is called + verify(mFinishedCallback).onAnimationFinished(eq(adapter)); + // Verify the animation canceled callback to the app was made + verify(mMockRunner).onAnimationCanceled(); + verifyNoMoreInteractionsExceptAsBinder(mMockRunner); + } + + private static void verifyNoMoreInteractionsExceptAsBinder(IInterface binder) { + verify(binder, atLeast(0)).asBinder(); + verifyNoMoreInteractions(binder); + } +}