From 0f7ec96b756a90def4c5dd786d18b798d343ad3d Mon Sep 17 00:00:00 2001 From: Winson Chung Date: Thu, 3 May 2018 18:03:15 -0700 Subject: [PATCH] Cancel any recents animation whenever a display's stack order changes - The recents animation holds a reference to the stack that the recents stack was previously behind (before the animation started) and should be restored behind after the animation ends. Due to the ordering of events, it is possible for a call to cancelRecentsAnimation() to come in after we have started the transition to home ie. 1) in app A 2) touch down on home button -> start recents animation, home stack saves A stack as the one to restore behind if animation doesn't finish 3) touch up to trigger button -> PWM to start home activity and simultaneously proxies the motion even to launcher which happens to call cancelRecentsAnimation (as a part of a catch all) 4) PWM start activity -> move home stack to top 5) cancelRecentsAnimation from launcher will try to restore the home stack behind A, when the rest of the system has already thought that the home stack was already moved to the front and focused Instead of depending on any particular ordering of events, we can just cancel the recents animation (without reordering) whenever the display's stacks change so as not to affect any upcoming changes. There is only one stack order changed listener (the recents animation, and there can only be one animation at a time; the previous animation is always canceled and unregisters the listener when cleaning up. - Revert the old workaround for Beta 1 in PWM as it doesn't catch other cases - Removed confusing code suggesting that we were using a non-default display (we are always only ever doing this animation for the default display since that's where we resolved the target stack). Bug: 73188263 Test: Was able to repro by artificially calling cancelRecentsAnimation immediately after starting home (and also by swiping up, holding, then invoking the home via adb (to trigger start activity) and then releasing (to trigger cancelRecentsAnimation)). Ensure that case now works. Test: atest FrameworksServicesTests:RecentsAnimationTest Test: atest FrameworksServicesTests:ActivityStackTests Change-Id: Iec1a29b5bb53737b4be3f1a3719c4e7c26313a11 --- .../android/server/am/ActivityDisplay.java | 41 ++++++- .../server/am/ActivityManagerService.java | 10 +- .../android/server/am/RecentsAnimation.java | 27 ++-- .../server/policy/PhoneWindowManager.java | 8 -- .../android/server/am/ActivityStackTests.java | 50 ++++++++ .../server/am/RecentsAnimationTest.java | 115 ++++++++++++++++++ 6 files changed, 227 insertions(+), 24 deletions(-) create mode 100644 services/tests/servicestests/src/com/android/server/am/RecentsAnimationTest.java diff --git a/services/core/java/com/android/server/am/ActivityDisplay.java b/services/core/java/com/android/server/am/ActivityDisplay.java index 0f42103e41782..27eae57302b0e 100644 --- a/services/core/java/com/android/server/am/ActivityDisplay.java +++ b/services/core/java/com/android/server/am/ActivityDisplay.java @@ -78,9 +78,13 @@ class ActivityDisplay extends ConfigurationContainer int mDisplayId; Display mDisplay; - /** All of the stacks on this display. Order matters, topmost stack is in front of all other - * stacks, bottommost behind. Accessed directly by ActivityManager package classes */ + /** + * All of the stacks on this display. Order matters, topmost stack is in front of all other + * stacks, bottommost behind. Accessed directly by ActivityManager package classes. Any calls + * changing the list should also call {@link #onStackOrderChanged()}. + */ private final ArrayList mStacks = new ArrayList<>(); + private ArrayList mStackOrderChangedCallbacks = new ArrayList<>(); /** Array of all UIDs that are present on the display. */ private IntArray mDisplayAccessUIDs = new IntArray(); @@ -145,6 +149,7 @@ class ActivityDisplay extends ConfigurationContainer mStacks.remove(stack); removeStackReferenceIfNeeded(stack); mSupervisor.mService.updateSleepIfNeededLocked(); + onStackOrderChanged(); } void positionChildAtTop(ActivityStack stack) { @@ -163,6 +168,7 @@ class ActivityDisplay extends ConfigurationContainer mStacks.add(insertPosition, stack); mWindowContainerController.positionChildAt(stack.getWindowContainerController(), insertPosition); + onStackOrderChanged(); } private int getTopInsertPosition(ActivityStack stack, int candidatePosition) { @@ -770,6 +776,30 @@ class ActivityDisplay extends ConfigurationContainer mSleeping = asleep; } + /** + * Adds a listener to be notified whenever the stack order in the display changes. Currently + * only used by the {@link RecentsAnimation} to determine whether to interrupt and cancel the + * current animation when the system state changes. + */ + void registerStackOrderChangedListener(OnStackOrderChangedListener listener) { + if (!mStackOrderChangedCallbacks.contains(listener)) { + mStackOrderChangedCallbacks.add(listener); + } + } + + /** + * Removes a previously registered stack order change listener. + */ + void unregisterStackOrderChangedListener(OnStackOrderChangedListener listener) { + mStackOrderChangedCallbacks.remove(listener); + } + + private void onStackOrderChanged() { + for (int i = mStackOrderChangedCallbacks.size() - 1; i >= 0; i--) { + mStackOrderChangedCallbacks.get(i).onStackOrderChanged(); + } + } + public void dump(PrintWriter pw, String prefix) { pw.println(prefix + "displayId=" + mDisplayId + " stacks=" + mStacks.size()); final String myPrefix = prefix + " "; @@ -806,4 +836,11 @@ class ActivityDisplay extends ConfigurationContainer } proto.end(token); } + + /** + * Callback for when the order of the stacks in the display changes. + */ + interface OnStackOrderChangedListener { + void onStackOrderChanged(); + } } diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java index a8e63f6695d46..9de876ec44082 100644 --- a/services/core/java/com/android/server/am/ActivityManagerService.java +++ b/services/core/java/com/android/server/am/ActivityManagerService.java @@ -5291,14 +5291,9 @@ public class ActivityManagerService extends IActivityManager.Stub final int callingPid = Binder.getCallingPid(); final long origId = Binder.clearCallingIdentity(); try { - final int recentsUid; - final String recentsPackage; - final List topVisibleActivities; synchronized (this) { final ComponentName recentsComponent = mRecentTasks.getRecentsComponent(); - recentsPackage = recentsComponent.getPackageName(); - recentsUid = mRecentTasks.getRecentsComponentUid(); - topVisibleActivities = mStackSupervisor.getTopVisibleActivities(); + final int recentsUid = mRecentTasks.getRecentsComponentUid(); // Start a new recents animation final RecentsAnimation anim = new RecentsAnimation(this, mStackSupervisor, @@ -5314,13 +5309,14 @@ public class ActivityManagerService extends IActivityManager.Stub @Override public void cancelRecentsAnimation(boolean restoreHomeStackPosition) { enforceCallerIsRecentsOrHasPermission(MANAGE_ACTIVITY_STACKS, "cancelRecentsAnimation()"); + final long callingUid = Binder.getCallingUid(); final long origId = Binder.clearCallingIdentity(); try { synchronized (this) { // Cancel the recents animation synchronously (do not hold the WM lock) mWindowManager.cancelRecentsAnimationSynchronously(restoreHomeStackPosition ? REORDER_MOVE_TO_ORIGINAL_POSITION - : REORDER_KEEP_IN_PLACE, "cancelRecentsAnimation"); + : REORDER_KEEP_IN_PLACE, "cancelRecentsAnimation/uid=" + callingUid); } } finally { Binder.restoreCallingIdentity(origId); diff --git a/services/core/java/com/android/server/am/RecentsAnimation.java b/services/core/java/com/android/server/am/RecentsAnimation.java index a88f4080a784c..a3d2173b294cb 100644 --- a/services/core/java/com/android/server/am/RecentsAnimation.java +++ b/services/core/java/com/android/server/am/RecentsAnimation.java @@ -49,7 +49,8 @@ import com.android.server.wm.WindowManagerService; * Manages the recents animation, including the reordering of the stacks for the transition and * cleanup. See {@link com.android.server.wm.RecentsAnimationController}. */ -class RecentsAnimation implements RecentsAnimationCallbacks { +class RecentsAnimation implements RecentsAnimationCallbacks, + ActivityDisplay.OnStackOrderChangedListener { private static final String TAG = RecentsAnimation.class.getSimpleName(); // TODO (b/73188263): Reset debugging flags private static final boolean DEBUG = true; @@ -140,13 +141,11 @@ class RecentsAnimation implements RecentsAnimationCallbacks { recentsUid, recentsComponent.getPackageName()); } - final ActivityDisplay display; if (hasExistingActivity) { // Move the recents activity into place for the animation if it is not top most - display = targetActivity.getDisplay(); - display.moveStackBehindBottomMostVisibleStack(targetStack); + mDefaultDisplay.moveStackBehindBottomMostVisibleStack(targetStack); if (DEBUG) Slog.d(TAG, "Moved stack=" + targetStack + " behind stack=" - + display.getStackAbove(targetStack)); + + mDefaultDisplay.getStackAbove(targetStack)); // If there are multiple tasks in the target stack (ie. the home stack, with 3p // and default launchers coexisting), then move the task to the top as a part of @@ -173,7 +172,6 @@ class RecentsAnimation implements RecentsAnimationCallbacks { targetActivity = mDefaultDisplay.getStack(WINDOWING_MODE_UNDEFINED, mTargetActivityType).getTopActivity(); - display = targetActivity.getDisplay(); // TODO: Maybe wait for app to draw in this particular case? @@ -190,7 +188,8 @@ class RecentsAnimation implements RecentsAnimationCallbacks { mWindowManager.cancelRecentsAnimationSynchronously(REORDER_MOVE_TO_ORIGINAL_POSITION, "startRecentsActivity"); mWindowManager.initializeRecentsAnimation(mTargetActivityType, recentsAnimationRunner, - this, display.mDisplayId, mStackSupervisor.mRecentTasks.getRecentTaskIds()); + this, mDefaultDisplay.mDisplayId, + mStackSupervisor.mRecentTasks.getRecentTaskIds()); // If we updated the launch-behind state, update the visibility of the activities after // we fetch the visible tasks to be controlled by the animation @@ -198,6 +197,9 @@ class RecentsAnimation implements RecentsAnimationCallbacks { mStackSupervisor.getActivityMetricsLogger().notifyActivityLaunched(START_TASK_TO_FRONT, targetActivity); + + // Register for stack order changes + mDefaultDisplay.registerStackOrderChangedListener(this); } catch (Exception e) { Slog.e(TAG, "Failed to start recents activity", e); throw e; @@ -219,6 +221,9 @@ class RecentsAnimation implements RecentsAnimationCallbacks { mAssistDataRequester = null; } + // Unregister for stack order changes + mDefaultDisplay.unregisterStackOrderChangedListener(this); + if (mWindowManager.getRecentsAnimationController() == null) return; // Just to be sure end the launch hint in case the target activity was never launched. @@ -316,6 +321,14 @@ class RecentsAnimation implements RecentsAnimationCallbacks { } } + @Override + public void onStackOrderChanged() { + // If the activity display stack order changes, cancel any running recents animation in + // place + mWindowManager.cancelRecentsAnimationSynchronously(REORDER_KEEP_IN_PLACE, + "stackOrderChanged"); + } + /** * Called only when the animation should be canceled prior to starting. */ diff --git a/services/core/java/com/android/server/policy/PhoneWindowManager.java b/services/core/java/com/android/server/policy/PhoneWindowManager.java index 18f3434ece490..e6d851b83aa8b 100644 --- a/services/core/java/com/android/server/policy/PhoneWindowManager.java +++ b/services/core/java/com/android/server/policy/PhoneWindowManager.java @@ -6092,14 +6092,6 @@ public class PhoneWindowManager implements WindowManagerPolicy { && (!isNavBarVirtKey || mNavBarVirtualKeyHapticFeedbackEnabled) && event.getRepeatCount() == 0; - // Cancel any pending remote recents animations before handling the button itself. In the - // case where we are going home and the recents animation has already started, just cancel - // the recents animation, leaving the home stack in place for the pending start activity - if (isNavBarVirtKey && !down && !canceled) { - boolean isHomeKey = keyCode == KeyEvent.KEYCODE_HOME; - mActivityManagerInternal.cancelRecentsAnimation(!isHomeKey); - } - // Handle special keys. switch (keyCode) { case KeyEvent.KEYCODE_BACK: { diff --git a/services/tests/servicestests/src/com/android/server/am/ActivityStackTests.java b/services/tests/servicestests/src/com/android/server/am/ActivityStackTests.java index 4b8dcc1e56c1a..01425ed51b559 100644 --- a/services/tests/servicestests/src/com/android/server/am/ActivityStackTests.java +++ b/services/tests/servicestests/src/com/android/server/am/ActivityStackTests.java @@ -565,6 +565,47 @@ public class ActivityStackTests extends ActivityTestsBase { false /* displaySleeping */, false /* expected*/); } + @Test + public void testStackOrderChangedOnRemoveStack() throws Exception { + StackOrderChangedListener listener = new StackOrderChangedListener(); + mDefaultDisplay.registerStackOrderChangedListener(listener); + try { + mDefaultDisplay.removeChild(mStack); + } finally { + mDefaultDisplay.unregisterStackOrderChangedListener(listener); + } + assertTrue(listener.changed); + } + + @Test + public void testStackOrderChangedOnAddPositionStack() throws Exception { + mDefaultDisplay.removeChild(mStack); + + StackOrderChangedListener listener = new StackOrderChangedListener(); + mDefaultDisplay.registerStackOrderChangedListener(listener); + try { + mDefaultDisplay.addChild(mStack, 0); + } finally { + mDefaultDisplay.unregisterStackOrderChangedListener(listener); + } + assertTrue(listener.changed); + } + + @Test + public void testStackOrderChangedOnPositionStack() throws Exception { + StackOrderChangedListener listener = new StackOrderChangedListener(); + try { + final TestActivityStack fullscreenStack1 = createStackForShouldBeVisibleTest( + mDefaultDisplay, WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_STANDARD, + true /* onTop */); + mDefaultDisplay.registerStackOrderChangedListener(listener); + mDefaultDisplay.positionChildAtBottom(fullscreenStack1); + } finally { + mDefaultDisplay.unregisterStackOrderChangedListener(listener); + } + assertTrue(listener.changed); + } + private void verifyShouldSleepActivities(boolean focusedStack, boolean keyguardGoingAway, boolean displaySleeping, boolean expected) { mSupervisor.mFocusedStack = focusedStack ? mStack : null; @@ -578,4 +619,13 @@ public class ActivityStackTests extends ActivityTestsBase { assertEquals(expected, mStack.shouldSleepActivities()); } + + private class StackOrderChangedListener implements ActivityDisplay.OnStackOrderChangedListener { + boolean changed = false; + + @Override + public void onStackOrderChanged() { + changed = true; + } + } } diff --git a/services/tests/servicestests/src/com/android/server/am/RecentsAnimationTest.java b/services/tests/servicestests/src/com/android/server/am/RecentsAnimationTest.java new file mode 100644 index 0000000000000..eefd973112f5a --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/am/RecentsAnimationTest.java @@ -0,0 +1,115 @@ +/* + * Copyright (C) 2017 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.am; + +import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME; +import static android.app.WindowConfiguration.ACTIVITY_TYPE_RECENTS; +import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD; +import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; +import static android.content.Intent.FLAG_ACTIVITY_MULTIPLE_TASK; +import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK; +import static com.android.server.wm.RecentsAnimationController.REORDER_KEEP_IN_PLACE; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.anyInt; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.platform.test.annotations.Presubmit; +import android.support.test.InstrumentationRegistry; +import android.support.test.filters.MediumTest; +import android.support.test.runner.AndroidJUnit4; +import android.view.IRecentsAnimationRunner; +import com.android.server.AttributeCache; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** + * atest FrameworksServicesTests:RecentsAnimationTest + */ +@MediumTest +@Presubmit +@RunWith(AndroidJUnit4.class) +public class RecentsAnimationTest extends ActivityTestsBase { + private static final int TEST_CALLING_PID = 3; + + private Context mContext = InstrumentationRegistry.getContext(); + private ActivityManagerService mService; + private ComponentName mRecentsComponent; + + @Before + @Override + public void setUp() throws Exception { + super.setUp(); + + mRecentsComponent = new ComponentName(mContext.getPackageName(), "RecentsActivity"); + mService = setupActivityManagerService(new MyTestActivityManagerService(mContext)); + AttributeCache.init(mContext); + } + + @Test + public void testCancelAnimationOnStackOrderChange() throws Exception { + ActivityStack fullscreenStack = mService.mStackSupervisor.getDefaultDisplay().createStack( + WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_STANDARD, true /* onTop */); + ActivityStack recentsStack = mService.mStackSupervisor.getDefaultDisplay().createStack( + WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_RECENTS, true /* onTop */); + ActivityRecord recentsActivity = new ActivityBuilder(mService) + .setComponent(mRecentsComponent) + .setCreateTask(true) + .setStack(recentsStack) + .build(); + ActivityStack fullscreenStack2 = mService.mStackSupervisor.getDefaultDisplay().createStack( + WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_STANDARD, true /* onTop */); + ActivityRecord fsActivity = new ActivityBuilder(mService) + .setComponent(new ComponentName(mContext.getPackageName(), "App1")) + .setCreateTask(true) + .setStack(fullscreenStack2) + .build(); + doReturn(true).when(mService.mWindowManager).canStartRecentsAnimation(); + + // Start the recents animation + Intent recentsIntent = new Intent(); + recentsIntent.setComponent(mRecentsComponent); + mService.startRecentsActivity(recentsIntent, null, mock(IRecentsAnimationRunner.class)); + + fullscreenStack.moveToFront("Activity start"); + + // Ensure that the recents animation was canceled + verify(mService.mWindowManager, times(1)).cancelRecentsAnimationSynchronously( + eq(REORDER_KEEP_IN_PLACE), any()); + } + + private class MyTestActivityManagerService extends TestActivityManagerService { + MyTestActivityManagerService(Context context) { + super(context); + } + + @Override + protected RecentTasks createRecentTasks() { + RecentTasks recents = mock(RecentTasks.class); + doReturn(mRecentsComponent).when(recents).getRecentsComponent(); + System.out.println(mRecentsComponent); + return recents; + } + } +}