Merge "Disable user animations on insets whose visible frame is empty (refined)" into rvc-dev am: 77190b77f6
Original change: https://googleplex-android-review.googlesource.com/c/platform/frameworks/base/+/11908304 Change-Id: I26f41c07925b94a5eb06e12ab131896b661c9db4
This commit is contained in:
@@ -471,6 +471,10 @@ public class InputMethodService extends AbstractInputMethodService {
|
|||||||
|
|
||||||
final ViewTreeObserver.OnComputeInternalInsetsListener mInsetsComputer = info -> {
|
final ViewTreeObserver.OnComputeInternalInsetsListener mInsetsComputer = info -> {
|
||||||
onComputeInsets(mTmpInsets);
|
onComputeInsets(mTmpInsets);
|
||||||
|
if (!mViewsCreated) {
|
||||||
|
// The IME views are not ready, keep visible insets untouched.
|
||||||
|
mTmpInsets.visibleTopInsets = 0;
|
||||||
|
}
|
||||||
if (isExtractViewShown()) {
|
if (isExtractViewShown()) {
|
||||||
// In true fullscreen mode, we just say the window isn't covering
|
// In true fullscreen mode, we just say the window isn't covering
|
||||||
// any content so we don't impact whatever is behind.
|
// any content so we don't impact whatever is behind.
|
||||||
|
|||||||
@@ -514,6 +514,12 @@ public class InsetsController implements WindowInsetsController, InsetsAnimation
|
|||||||
/** Set of inset types for which an animation was started since last resetting this field */
|
/** Set of inset types for which an animation was started since last resetting this field */
|
||||||
private @InsetsType int mLastStartedAnimTypes;
|
private @InsetsType int mLastStartedAnimTypes;
|
||||||
|
|
||||||
|
/** Set of inset types which cannot be controlled by the user animation */
|
||||||
|
private @InsetsType int mDisabledUserAnimationInsetsTypes;
|
||||||
|
|
||||||
|
private Runnable mInvokeControllableInsetsChangedListeners =
|
||||||
|
this::invokeControllableInsetsChangedListeners;
|
||||||
|
|
||||||
public InsetsController(Host host) {
|
public InsetsController(Host host) {
|
||||||
this(host, (controller, type) -> {
|
this(host, (controller, type) -> {
|
||||||
if (type == ITYPE_IME) {
|
if (type == ITYPE_IME) {
|
||||||
@@ -628,22 +634,57 @@ public class InsetsController implements WindowInsetsController, InsetsAnimation
|
|||||||
|
|
||||||
private void updateState(InsetsState newState) {
|
private void updateState(InsetsState newState) {
|
||||||
mState.setDisplayFrame(newState.getDisplayFrame());
|
mState.setDisplayFrame(newState.getDisplayFrame());
|
||||||
for (int i = 0; i < InsetsState.SIZE; i++) {
|
@InsetsType int disabledUserAnimationTypes = 0;
|
||||||
InsetsSource source = newState.peekSource(i);
|
@InsetsType int[] cancelledUserAnimationTypes = {0};
|
||||||
if (source == null) continue;;
|
for (@InternalInsetsType int type = 0; type < InsetsState.SIZE; type++) {
|
||||||
getSourceConsumer(source.getType()).updateSource(source);
|
InsetsSource source = newState.peekSource(type);
|
||||||
}
|
|
||||||
for (int i = 0; i < InsetsState.SIZE; i++) {
|
|
||||||
InsetsSource source = mState.peekSource(i);
|
|
||||||
if (source == null) continue;
|
if (source == null) continue;
|
||||||
if (newState.peekSource(source.getType()) == null) {
|
@AnimationType int animationType = getAnimationType(type);
|
||||||
mState.removeSource(source.getType());
|
if (!source.isUserControllable()) {
|
||||||
|
@InsetsType int insetsType = toPublicType(type);
|
||||||
|
// The user animation is not allowed when visible frame is empty.
|
||||||
|
disabledUserAnimationTypes |= insetsType;
|
||||||
|
if (animationType == ANIMATION_TYPE_USER) {
|
||||||
|
// Existing user animation needs to be cancelled.
|
||||||
|
animationType = ANIMATION_TYPE_NONE;
|
||||||
|
cancelledUserAnimationTypes[0] |= insetsType;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
getSourceConsumer(type).updateSource(source, animationType);
|
||||||
|
}
|
||||||
|
for (@InternalInsetsType int type = 0; type < InsetsState.SIZE; type++) {
|
||||||
|
InsetsSource source = mState.peekSource(type);
|
||||||
|
if (source == null) continue;
|
||||||
|
if (newState.peekSource(type) == null) {
|
||||||
|
mState.removeSource(type);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (mCaptionInsetsHeight != 0) {
|
if (mCaptionInsetsHeight != 0) {
|
||||||
mState.getSource(ITYPE_CAPTION_BAR).setFrame(new Rect(mFrame.left, mFrame.top,
|
mState.getSource(ITYPE_CAPTION_BAR).setFrame(new Rect(mFrame.left, mFrame.top,
|
||||||
mFrame.right, mFrame.top + mCaptionInsetsHeight));
|
mFrame.right, mFrame.top + mCaptionInsetsHeight));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
updateDisabledUserAnimationTypes(disabledUserAnimationTypes);
|
||||||
|
|
||||||
|
if (cancelledUserAnimationTypes[0] != 0) {
|
||||||
|
mHandler.post(() -> show(cancelledUserAnimationTypes[0]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateDisabledUserAnimationTypes(@InsetsType int disabledUserAnimationTypes) {
|
||||||
|
@InsetsType int diff = mDisabledUserAnimationInsetsTypes ^ disabledUserAnimationTypes;
|
||||||
|
if (diff != 0) {
|
||||||
|
for (int i = mSourceConsumers.size() - 1; i >= 0; i--) {
|
||||||
|
InsetsSourceConsumer consumer = mSourceConsumers.valueAt(i);
|
||||||
|
if (consumer.getControl() != null
|
||||||
|
&& (toPublicType(consumer.getType()) & diff) != 0) {
|
||||||
|
mHandler.removeCallbacks(mInvokeControllableInsetsChangedListeners);
|
||||||
|
mHandler.post(mInvokeControllableInsetsChangedListeners);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mDisabledUserAnimationInsetsTypes = disabledUserAnimationTypes;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean captionInsetsUnchanged() {
|
private boolean captionInsetsUnchanged() {
|
||||||
@@ -847,6 +888,18 @@ public class InsetsController implements WindowInsetsController, InsetsAnimation
|
|||||||
+ " while an existing " + Type.toString(mTypesBeingCancelled)
|
+ " while an existing " + Type.toString(mTypesBeingCancelled)
|
||||||
+ " is being cancelled.");
|
+ " is being cancelled.");
|
||||||
}
|
}
|
||||||
|
if (animationType == ANIMATION_TYPE_USER) {
|
||||||
|
final @InsetsType int disabledTypes = types & mDisabledUserAnimationInsetsTypes;
|
||||||
|
if (DEBUG) Log.d(TAG, "user animation disabled types: " + disabledTypes);
|
||||||
|
types &= ~mDisabledUserAnimationInsetsTypes;
|
||||||
|
|
||||||
|
if (fromIme && (disabledTypes & ime()) != 0
|
||||||
|
&& !mState.getSource(ITYPE_IME).isVisible()) {
|
||||||
|
// We've requested IMM to show IME, but the IME is not controllable. We need to
|
||||||
|
// cancel the request.
|
||||||
|
getSourceConsumer(ITYPE_IME).hide(true, animationType);
|
||||||
|
}
|
||||||
|
}
|
||||||
if (types == 0) {
|
if (types == 0) {
|
||||||
// nothing to animate.
|
// nothing to animate.
|
||||||
listener.onCancelled(null);
|
listener.onCancelled(null);
|
||||||
@@ -1320,7 +1373,8 @@ public class InsetsController implements WindowInsetsController, InsetsAnimation
|
|||||||
@InsetsType int result = 0;
|
@InsetsType int result = 0;
|
||||||
for (int i = mSourceConsumers.size() - 1; i >= 0; i--) {
|
for (int i = mSourceConsumers.size() - 1; i >= 0; i--) {
|
||||||
InsetsSourceConsumer consumer = mSourceConsumers.valueAt(i);
|
InsetsSourceConsumer consumer = mSourceConsumers.valueAt(i);
|
||||||
if (consumer.getControl() != null) {
|
InsetsSource source = mState.peekSource(consumer.mType);
|
||||||
|
if (consumer.getControl() != null && source != null && source.isUserControllable()) {
|
||||||
result |= toPublicType(consumer.mType);
|
result |= toPublicType(consumer.mType);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1331,6 +1385,7 @@ public class InsetsController implements WindowInsetsController, InsetsAnimation
|
|||||||
* @return The types that are now animating due to a listener invoking control/show/hide
|
* @return The types that are now animating due to a listener invoking control/show/hide
|
||||||
*/
|
*/
|
||||||
private @InsetsType int invokeControllableInsetsChangedListeners() {
|
private @InsetsType int invokeControllableInsetsChangedListeners() {
|
||||||
|
mHandler.removeCallbacks(mInvokeControllableInsetsChangedListeners);
|
||||||
mLastStartedAnimTypes = 0;
|
mLastStartedAnimTypes = 0;
|
||||||
@InsetsType int types = calculateControllableTypes();
|
@InsetsType int types = calculateControllableTypes();
|
||||||
int size = mControllableInsetsChangedListeners.size();
|
int size = mControllableInsetsChangedListeners.size();
|
||||||
|
|||||||
@@ -92,6 +92,11 @@ public class InsetsSource implements Parcelable {
|
|||||||
return mVisible;
|
return mVisible;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
boolean isUserControllable() {
|
||||||
|
// If mVisibleFrame is null, it will be the same area as mFrame.
|
||||||
|
return mVisibleFrame == null || !mVisibleFrame.isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Calculates the insets this source will cause to a client window.
|
* Calculates the insets this source will cause to a client window.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -18,8 +18,8 @@ package android.view;
|
|||||||
|
|
||||||
import static android.view.InsetsController.ANIMATION_TYPE_NONE;
|
import static android.view.InsetsController.ANIMATION_TYPE_NONE;
|
||||||
import static android.view.InsetsController.AnimationType;
|
import static android.view.InsetsController.AnimationType;
|
||||||
import static android.view.InsetsState.getDefaultVisibility;
|
|
||||||
import static android.view.InsetsController.DEBUG;
|
import static android.view.InsetsController.DEBUG;
|
||||||
|
import static android.view.InsetsState.getDefaultVisibility;
|
||||||
import static android.view.InsetsState.toPublicType;
|
import static android.view.InsetsState.toPublicType;
|
||||||
|
|
||||||
import static com.android.internal.annotations.VisibleForTesting.Visibility.PACKAGE;
|
import static com.android.internal.annotations.VisibleForTesting.Visibility.PACKAGE;
|
||||||
@@ -284,9 +284,9 @@ public class InsetsSourceConsumer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@VisibleForTesting(visibility = PACKAGE)
|
@VisibleForTesting(visibility = PACKAGE)
|
||||||
public void updateSource(InsetsSource newSource) {
|
public void updateSource(InsetsSource newSource, @AnimationType int animationType) {
|
||||||
InsetsSource source = mState.peekSource(mType);
|
InsetsSource source = mState.peekSource(mType);
|
||||||
if (source == null || mController.getAnimationType(mType) == ANIMATION_TYPE_NONE
|
if (source == null || animationType == ANIMATION_TYPE_NONE
|
||||||
|| source.getFrame().equals(newSource.getFrame())) {
|
|| source.getFrame().equals(newSource.getFrame())) {
|
||||||
mPendingFrame = null;
|
mPendingFrame = null;
|
||||||
mPendingVisibleFrame = null;
|
mPendingVisibleFrame = null;
|
||||||
@@ -295,7 +295,7 @@ public class InsetsSourceConsumer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Frame is changing while animating. Keep note of the new frame but keep existing frame
|
// Frame is changing while animating. Keep note of the new frame but keep existing frame
|
||||||
// until animaition is finished.
|
// until animation is finished.
|
||||||
newSource = new InsetsSource(newSource);
|
newSource = new InsetsSource(newSource);
|
||||||
mPendingFrame = new Rect(newSource.getFrame());
|
mPendingFrame = new Rect(newSource.getFrame());
|
||||||
mPendingVisibleFrame = newSource.getVisibleFrame() != null
|
mPendingVisibleFrame = newSource.getVisibleFrame() != null
|
||||||
|
|||||||
@@ -26,13 +26,11 @@ import static junit.framework.Assert.assertEquals;
|
|||||||
import static junit.framework.TestCase.assertFalse;
|
import static junit.framework.TestCase.assertFalse;
|
||||||
import static junit.framework.TestCase.assertTrue;
|
import static junit.framework.TestCase.assertTrue;
|
||||||
|
|
||||||
import static org.mockito.ArgumentMatchers.anyInt;
|
|
||||||
import static org.mockito.ArgumentMatchers.eq;
|
import static org.mockito.ArgumentMatchers.eq;
|
||||||
import static org.mockito.Mockito.mock;
|
import static org.mockito.Mockito.mock;
|
||||||
import static org.mockito.Mockito.reset;
|
import static org.mockito.Mockito.reset;
|
||||||
import static org.mockito.Mockito.verify;
|
import static org.mockito.Mockito.verify;
|
||||||
import static org.mockito.Mockito.verifyZeroInteractions;
|
import static org.mockito.Mockito.verifyZeroInteractions;
|
||||||
import static org.mockito.Mockito.when;
|
|
||||||
|
|
||||||
import android.app.Instrumentation;
|
import android.app.Instrumentation;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
@@ -135,37 +133,29 @@ public class InsetsSourceConsumerTest {
|
|||||||
InsetsSourceConsumer consumer = new InsetsSourceConsumer(
|
InsetsSourceConsumer consumer = new InsetsSourceConsumer(
|
||||||
ITYPE_IME, state, null, controller);
|
ITYPE_IME, state, null, controller);
|
||||||
|
|
||||||
when(controller.getAnimationType(anyInt())).thenReturn(ANIMATION_TYPE_NONE);
|
|
||||||
|
|
||||||
InsetsSource source = new InsetsSource(ITYPE_IME);
|
InsetsSource source = new InsetsSource(ITYPE_IME);
|
||||||
source.setFrame(0, 1, 2, 3);
|
source.setFrame(0, 1, 2, 3);
|
||||||
consumer.updateSource(new InsetsSource(source));
|
consumer.updateSource(new InsetsSource(source), ANIMATION_TYPE_NONE);
|
||||||
|
|
||||||
when(controller.getAnimationType(anyInt())).thenReturn(ANIMATION_TYPE_USER);
|
|
||||||
|
|
||||||
// While we're animating, updates are delayed
|
// While we're animating, updates are delayed
|
||||||
source.setFrame(4, 5, 6, 7);
|
source.setFrame(4, 5, 6, 7);
|
||||||
consumer.updateSource(new InsetsSource(source));
|
consumer.updateSource(new InsetsSource(source), ANIMATION_TYPE_USER);
|
||||||
assertEquals(new Rect(0, 1, 2, 3), state.peekSource(ITYPE_IME).getFrame());
|
assertEquals(new Rect(0, 1, 2, 3), state.peekSource(ITYPE_IME).getFrame());
|
||||||
|
|
||||||
// Finish the animation, now the pending frame should be applied
|
// Finish the animation, now the pending frame should be applied
|
||||||
when(controller.getAnimationType(anyInt())).thenReturn(ANIMATION_TYPE_NONE);
|
|
||||||
assertTrue(consumer.notifyAnimationFinished());
|
assertTrue(consumer.notifyAnimationFinished());
|
||||||
assertEquals(new Rect(4, 5, 6, 7), state.peekSource(ITYPE_IME).getFrame());
|
assertEquals(new Rect(4, 5, 6, 7), state.peekSource(ITYPE_IME).getFrame());
|
||||||
|
|
||||||
when(controller.getAnimationType(anyInt())).thenReturn(ANIMATION_TYPE_USER);
|
|
||||||
|
|
||||||
// Animating again, updates are delayed
|
// Animating again, updates are delayed
|
||||||
source.setFrame(8, 9, 10, 11);
|
source.setFrame(8, 9, 10, 11);
|
||||||
consumer.updateSource(new InsetsSource(source));
|
consumer.updateSource(new InsetsSource(source), ANIMATION_TYPE_USER);
|
||||||
assertEquals(new Rect(4, 5, 6, 7), state.peekSource(ITYPE_IME).getFrame());
|
assertEquals(new Rect(4, 5, 6, 7), state.peekSource(ITYPE_IME).getFrame());
|
||||||
|
|
||||||
// Updating with the current frame triggers a different code path, verify this clears
|
// Updating with the current frame triggers a different code path, verify this clears
|
||||||
// the pending 8, 9, 10, 11 frame:
|
// the pending 8, 9, 10, 11 frame:
|
||||||
source.setFrame(4, 5, 6, 7);
|
source.setFrame(4, 5, 6, 7);
|
||||||
consumer.updateSource(new InsetsSource(source));
|
consumer.updateSource(new InsetsSource(source), ANIMATION_TYPE_USER);
|
||||||
|
|
||||||
when(controller.getAnimationType(anyInt())).thenReturn(ANIMATION_TYPE_NONE);
|
|
||||||
assertFalse(consumer.notifyAnimationFinished());
|
assertFalse(consumer.notifyAnimationFinished());
|
||||||
assertEquals(new Rect(4, 5, 6, 7), state.peekSource(ITYPE_IME).getFrame());
|
assertEquals(new Rect(4, 5, 6, 7), state.peekSource(ITYPE_IME).getFrame());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ package com.android.systemui.globalactions;
|
|||||||
|
|
||||||
import static android.view.WindowInsets.Type.ime;
|
import static android.view.WindowInsets.Type.ime;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
import static org.junit.Assert.assertNotNull;
|
import static org.junit.Assert.assertNotNull;
|
||||||
import static org.junit.Assert.assertTrue;
|
import static org.junit.Assert.assertTrue;
|
||||||
import static org.junit.Assert.fail;
|
import static org.junit.Assert.fail;
|
||||||
@@ -39,6 +40,7 @@ import androidx.test.rule.ActivityTestRule;
|
|||||||
|
|
||||||
import com.android.systemui.SysuiTestCase;
|
import com.android.systemui.SysuiTestCase;
|
||||||
|
|
||||||
|
import org.junit.After;
|
||||||
import org.junit.Rule;
|
import org.junit.Rule;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
|
|
||||||
@@ -52,6 +54,11 @@ public class GlobalActionsImeTest extends SysuiTestCase {
|
|||||||
public ActivityTestRule<TestActivity> mActivityTestRule = new ActivityTestRule<>(
|
public ActivityTestRule<TestActivity> mActivityTestRule = new ActivityTestRule<>(
|
||||||
TestActivity.class, false, false);
|
TestActivity.class, false, false);
|
||||||
|
|
||||||
|
@After
|
||||||
|
public void tearDown() {
|
||||||
|
executeShellCommand("input keyevent HOME");
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This test verifies that GlobalActions, which is frequently used to capture bugreports,
|
* This test verifies that GlobalActions, which is frequently used to capture bugreports,
|
||||||
* doesn't interfere with the IME, i.e. soft-keyboard state.
|
* doesn't interfere with the IME, i.e. soft-keyboard state.
|
||||||
@@ -68,6 +75,9 @@ public class GlobalActionsImeTest extends SysuiTestCase {
|
|||||||
waitUntil("Ime is not visible", activity::isImeVisible);
|
waitUntil("Ime is not visible", activity::isImeVisible);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// In some cases, IME is not controllable. e.g., floating IME or fullscreen IME.
|
||||||
|
final boolean activityControlledIme = activity.mControlsIme;
|
||||||
|
|
||||||
executeShellCommand("input keyevent --longpress POWER");
|
executeShellCommand("input keyevent --longpress POWER");
|
||||||
|
|
||||||
waitUntil("activity loses focus", () -> !activity.mHasFocus);
|
waitUntil("activity loses focus", () -> !activity.mHasFocus);
|
||||||
@@ -77,9 +87,9 @@ public class GlobalActionsImeTest extends SysuiTestCase {
|
|||||||
|
|
||||||
runAssertionOnMainThread(() -> {
|
runAssertionOnMainThread(() -> {
|
||||||
assertTrue("IME should remain visible behind GlobalActions, but didn't",
|
assertTrue("IME should remain visible behind GlobalActions, but didn't",
|
||||||
activity.mControlsIme);
|
|
||||||
assertTrue("App behind GlobalActions should remain in control of IME, but didn't",
|
|
||||||
activity.mImeVisible);
|
activity.mImeVisible);
|
||||||
|
assertEquals("App behind GlobalActions should remain in control of IME, but didn't",
|
||||||
|
activityControlledIme, activity.mControlsIme);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -181,7 +191,7 @@ public class GlobalActionsImeTest extends SysuiTestCase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
boolean isImeVisible() {
|
boolean isImeVisible() {
|
||||||
return mHasFocus && mControlsIme && mImeVisible;
|
return mHasFocus && mImeVisible;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
Reference in New Issue
Block a user