From a6f3561667befbef3cdc160a4097c6e653c530a2 Mon Sep 17 00:00:00 2001 From: Tarandeep Singh Date: Fri, 11 Jan 2019 19:50:46 -0800 Subject: [PATCH] Reparent IME window and handle non-fullscreen windows correctly IME window is attached to the IME target if possible. This ensures a smooth enter/exit animation when the activity is coming in/going away. Furthermore, if the controlling window doesn't span the entire display, we can't offer controlling it in a frame-by-frame manner, and we need to do the inset calculations relative to the display frame. Test: adb shell setprop persist.wm.new_insets 1 adb shell setprop persist.pre_render_ime_views 0 Test: Open IME, go home, reopen app Test: Show dialog with EditText Bug: 111084606 Change-Id: Id40470f6f8284b48acfa4719049afd14fde332d6 --- core/java/android/view/InsetsController.java | 24 +++- core/java/android/view/InsetsState.java | 24 +++- .../android/view/InsetsControllerTest.java | 16 +++ .../src/android/view/InsetsStateTest.java | 117 ++++++++++++------ .../com/android/server/wm/DisplayContent.java | 37 ++++++ .../server/wm/InsetsStateController.java | 1 + .../server/wm/DisplayContentTests.java | 38 ++++++ .../android/view/test/InsetsModeSession.java | 37 ++++++ 8 files changed, 248 insertions(+), 46 deletions(-) create mode 100644 tests/utils/testutils/java/android/view/test/InsetsModeSession.java diff --git a/core/java/android/view/InsetsController.java b/core/java/android/view/InsetsController.java index 7ad97a6d393eb..1bf027e773796 100644 --- a/core/java/android/view/InsetsController.java +++ b/core/java/android/view/InsetsController.java @@ -133,6 +133,10 @@ public class InsetsController implements WindowInsetsController { } void onFrameChanged(Rect frame) { + if (mFrame.equals(frame)) { + return; + } + mViewRoot.notifyInsetsChanged(); mFrame.set(frame); } @@ -257,11 +261,21 @@ public class InsetsController implements WindowInsetsController { private void controlWindowInsetsAnimation(@InsetType int types, WindowInsetsAnimationControlListener listener, boolean fromIme) { + // If the frame of our window doesn't span the entire display, the control API makes very + // little sense, as we don't deal with negative insets. So just cancel immediately. + if (!mState.getDisplayFrame().equals(mFrame)) { + listener.onCancelled(); + return; + } + controlAnimationUnchecked(types, listener, mFrame, fromIme); + } + + private void controlAnimationUnchecked(@InsetType int types, + WindowInsetsAnimationControlListener listener, Rect frame, boolean fromIme) { if (types == 0) { // nothing to animate. return; } - // TODO: Check whether we already have a controller. final ArraySet internalTypes = mState.toInternalType(types); final SparseArray consumers = new SparseArray<>(); @@ -285,7 +299,7 @@ public class InsetsController implements WindowInsetsController { } final InsetsAnimationControlImpl controller = new InsetsAnimationControlImpl(consumers, - mFrame, mState, listener, typesReady, + frame, mState, listener, typesReady, () -> new SyncRtSurfaceTransactionApplier(mViewRoot.mView), this); mAnimationControls.add(controller); } @@ -436,6 +450,7 @@ public class InsetsController implements WindowInsetsController { // nothing to animate. return; } + WindowInsetsAnimationControlListener listener = new WindowInsetsAnimationControlListener() { @Override public void onReady(WindowInsetsAnimationController controller, int types) { @@ -479,7 +494,10 @@ public class InsetsController implements WindowInsetsController { // TODO: Instead of clearing this here, properly wire up // InsetsAnimationControlImpl.finish() to remove this from mAnimationControls. mAnimationControls.clear(); - controlWindowInsetsAnimation(types, listener, fromIme); + + // Show/hide animations always need to be relative to the display frame, in order that shown + // and hidden state insets are correct. + controlAnimationUnchecked(types, listener, mState.getDisplayFrame(), fromIme); } private void hideDirectly(@InsetType int types) { diff --git a/core/java/android/view/InsetsState.java b/core/java/android/view/InsetsState.java index 4f809fe6d54a3..c9273d8cc2a9e 100644 --- a/core/java/android/view/InsetsState.java +++ b/core/java/android/view/InsetsState.java @@ -39,6 +39,7 @@ import java.io.PrintWriter; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; +import java.util.Objects; /** * Holder for state of system windows that cause window insets for all other windows in the system. @@ -104,6 +105,11 @@ public class InsetsState implements Parcelable { private final ArrayMap mSources = new ArrayMap<>(); + /** + * The frame of the display these sources are relative to. + */ + private final Rect mDisplayFrame = new Rect(); + public InsetsState() { } @@ -209,6 +215,14 @@ public class InsetsState implements Parcelable { return mSources.computeIfAbsent(type, InsetsSource::new); } + public void setDisplayFrame(Rect frame) { + mDisplayFrame.set(frame); + } + + public Rect getDisplayFrame() { + return mDisplayFrame; + } + /** * Modifies the state of this class to exclude a certain type to make it ready for dispatching * to the client. @@ -224,6 +238,7 @@ public class InsetsState implements Parcelable { } public void set(InsetsState other, boolean copySources) { + mDisplayFrame.set(other.mDisplayFrame); mSources.clear(); if (copySources) { for (int i = 0; i < other.mSources.size(); i++) { @@ -323,6 +338,9 @@ public class InsetsState implements Parcelable { InsetsState state = (InsetsState) o; + if (!mDisplayFrame.equals(state.mDisplayFrame)) { + return false; + } if (mSources.size() != state.mSources.size()) { return false; } @@ -341,7 +359,7 @@ public class InsetsState implements Parcelable { @Override public int hashCode() { - return mSources.hashCode(); + return Objects.hash(mDisplayFrame, mSources); } public InsetsState(Parcel in) { @@ -355,9 +373,10 @@ public class InsetsState implements Parcelable { @Override public void writeToParcel(Parcel dest, int flags) { + dest.writeParcelable(mDisplayFrame, flags); dest.writeInt(mSources.size()); for (int i = 0; i < mSources.size(); i++) { - dest.writeParcelable(mSources.valueAt(i), 0 /* flags */); + dest.writeParcelable(mSources.valueAt(i), flags); } } @@ -374,6 +393,7 @@ public class InsetsState implements Parcelable { public void readFromParcel(Parcel in) { mSources.clear(); + mDisplayFrame.set(in.readParcelable(null /* loader */)); final int size = in.readInt(); for (int i = 0; i < size; i++) { final InsetsSource source = in.readParcelable(null /* loader */); diff --git a/core/tests/coretests/src/android/view/InsetsControllerTest.java b/core/tests/coretests/src/android/view/InsetsControllerTest.java index 6dad6a22f7ea2..3fc3773ff3daa 100644 --- a/core/tests/coretests/src/android/view/InsetsControllerTest.java +++ b/core/tests/coretests/src/android/view/InsetsControllerTest.java @@ -24,6 +24,11 @@ import static junit.framework.Assert.assertEquals; import static junit.framework.Assert.assertFalse; import static junit.framework.Assert.assertNull; import static junit.framework.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; import android.content.Context; import android.graphics.Insets; @@ -94,6 +99,17 @@ public class InsetsControllerTest { assertNull(mController.getSourceConsumer(TYPE_TOP_BAR).getControl()); } + @Test + public void testFrameDoesntMatchDisplay() { + mController.onFrameChanged(new Rect(0, 0, 100, 100)); + mController.getState().setDisplayFrame(new Rect(0, 0, 200, 200)); + WindowInsetsAnimationControlListener controlListener = + mock(WindowInsetsAnimationControlListener.class); + mController.controlWindowInsetsAnimation(0, controlListener); + verify(controlListener).onCancelled(); + verify(controlListener, never()).onReady(any(), anyInt()); + } + @Test public void testAnimationEndState() { InsetsSourceControl[] controls = prepareControls(); diff --git a/core/tests/coretests/src/android/view/InsetsStateTest.java b/core/tests/coretests/src/android/view/InsetsStateTest.java index 03af67df13d05..bfe6e32b6261e 100644 --- a/core/tests/coretests/src/android/view/InsetsStateTest.java +++ b/core/tests/coretests/src/android/view/InsetsStateTest.java @@ -37,6 +37,7 @@ import android.os.Parcel; import android.platform.test.annotations.Presubmit; import android.util.SparseIntArray; import android.view.WindowInsets.Type; +import android.view.test.InsetsModeSession; import androidx.test.filters.FlakyTest; import androidx.test.runner.AndroidJUnit4; @@ -53,49 +54,58 @@ public class InsetsStateTest { private InsetsState mState2 = new InsetsState(); @Test - public void testCalculateInsets() { - mState.getSource(TYPE_TOP_BAR).setFrame(new Rect(0, 0, 100, 100)); - mState.getSource(TYPE_TOP_BAR).setVisible(true); - mState.getSource(TYPE_IME).setFrame(new Rect(0, 200, 100, 300)); - mState.getSource(TYPE_IME).setVisible(true); - SparseIntArray typeSideMap = new SparseIntArray(); - WindowInsets insets = mState.calculateInsets(new Rect(0, 0, 100, 300), false, false, - DisplayCutout.NO_CUTOUT, null, null, typeSideMap); - assertEquals(Insets.of(0, 100, 0, 100), insets.getSystemWindowInsets()); - assertEquals(Insets.of(0, 100, 0, 100), insets.getInsets(Type.all())); - assertEquals(INSET_SIDE_TOP, typeSideMap.get(TYPE_TOP_BAR)); - assertEquals(INSET_SIDE_BOTTOM, typeSideMap.get(TYPE_IME)); - assertEquals(Insets.of(0, 100, 0, 0), insets.getInsets(Type.topBar())); - assertEquals(Insets.of(0, 0, 0, 100), insets.getInsets(Type.ime())); + public void testCalculateInsets() throws Exception { + try (final InsetsModeSession session = + new InsetsModeSession(ViewRootImpl.NEW_INSETS_MODE_FULL)) { + mState.getSource(TYPE_TOP_BAR).setFrame(new Rect(0, 0, 100, 100)); + mState.getSource(TYPE_TOP_BAR).setVisible(true); + mState.getSource(TYPE_IME).setFrame(new Rect(0, 200, 100, 300)); + mState.getSource(TYPE_IME).setVisible(true); + SparseIntArray typeSideMap = new SparseIntArray(); + WindowInsets insets = mState.calculateInsets(new Rect(0, 0, 100, 300), false, false, + DisplayCutout.NO_CUTOUT, null, null, typeSideMap); + assertEquals(Insets.of(0, 100, 0, 100), insets.getSystemWindowInsets()); + assertEquals(Insets.of(0, 100, 0, 100), insets.getInsets(Type.all())); + assertEquals(INSET_SIDE_TOP, typeSideMap.get(TYPE_TOP_BAR)); + assertEquals(INSET_SIDE_BOTTOM, typeSideMap.get(TYPE_IME)); + assertEquals(Insets.of(0, 100, 0, 0), insets.getInsets(Type.topBar())); + assertEquals(Insets.of(0, 0, 0, 100), insets.getInsets(Type.ime())); + } } @Test - public void testCalculateInsets_imeAndNav() { - mState.getSource(TYPE_NAVIGATION_BAR).setFrame(new Rect(0, 200, 100, 300)); - mState.getSource(TYPE_NAVIGATION_BAR).setVisible(true); - mState.getSource(TYPE_IME).setFrame(new Rect(0, 100, 100, 300)); - mState.getSource(TYPE_IME).setVisible(true); - WindowInsets insets = mState.calculateInsets(new Rect(0, 0, 100, 300), false, false, - DisplayCutout.NO_CUTOUT, null, null, null); - assertEquals(100, insets.getStableInsetBottom()); - assertEquals(Insets.of(0, 0, 0, 100), insets.getMaxInsets(Type.all())); - assertEquals(Insets.of(0, 0, 0, 200), insets.getSystemWindowInsets()); - assertEquals(Insets.of(0, 0, 0, 200), insets.getInsets(Type.all())); - assertEquals(Insets.of(0, 0, 0, 100), insets.getInsets(Type.sideBars())); - assertEquals(Insets.of(0, 0, 0, 200), insets.getInsets(Type.ime())); + public void testCalculateInsets_imeAndNav() throws Exception{ + try (final InsetsModeSession session = + new InsetsModeSession(ViewRootImpl.NEW_INSETS_MODE_FULL)) { + mState.getSource(TYPE_NAVIGATION_BAR).setFrame(new Rect(0, 200, 100, 300)); + mState.getSource(TYPE_NAVIGATION_BAR).setVisible(true); + mState.getSource(TYPE_IME).setFrame(new Rect(0, 100, 100, 300)); + mState.getSource(TYPE_IME).setVisible(true); + WindowInsets insets = mState.calculateInsets(new Rect(0, 0, 100, 300), false, false, + DisplayCutout.NO_CUTOUT, null, null, null); + assertEquals(100, insets.getStableInsetBottom()); + assertEquals(Insets.of(0, 0, 0, 100), insets.getMaxInsets(Type.systemBars())); + assertEquals(Insets.of(0, 0, 0, 200), insets.getSystemWindowInsets()); + assertEquals(Insets.of(0, 0, 0, 200), insets.getInsets(Type.all())); + assertEquals(Insets.of(0, 0, 0, 100), insets.getInsets(Type.sideBars())); + assertEquals(Insets.of(0, 0, 0, 200), insets.getInsets(Type.ime())); + } } @Test - public void testCalculateInsets_navRightStatusTop() { - mState.getSource(TYPE_TOP_BAR).setFrame(new Rect(0, 0, 100, 100)); - mState.getSource(TYPE_TOP_BAR).setVisible(true); - mState.getSource(TYPE_NAVIGATION_BAR).setFrame(new Rect(80, 0, 100, 300)); - mState.getSource(TYPE_NAVIGATION_BAR).setVisible(true); - WindowInsets insets = mState.calculateInsets(new Rect(0, 0, 100, 300), false, false, - DisplayCutout.NO_CUTOUT, null, null, null); - assertEquals(Insets.of(0, 100, 20, 0), insets.getSystemWindowInsets()); - assertEquals(Insets.of(0, 100, 0, 0), insets.getInsets(Type.topBar())); - assertEquals(Insets.of(0, 0, 20, 0), insets.getInsets(Type.sideBars())); + public void testCalculateInsets_navRightStatusTop() throws Exception { + try (final InsetsModeSession session = + new InsetsModeSession(ViewRootImpl.NEW_INSETS_MODE_FULL)) { + mState.getSource(TYPE_TOP_BAR).setFrame(new Rect(0, 0, 100, 100)); + mState.getSource(TYPE_TOP_BAR).setVisible(true); + mState.getSource(TYPE_NAVIGATION_BAR).setFrame(new Rect(80, 0, 100, 300)); + mState.getSource(TYPE_NAVIGATION_BAR).setVisible(true); + WindowInsets insets = mState.calculateInsets(new Rect(0, 0, 100, 300), false, false, + DisplayCutout.NO_CUTOUT, null, null, null); + assertEquals(Insets.of(0, 100, 20, 0), insets.getSystemWindowInsets()); + assertEquals(Insets.of(0, 100, 0, 0), insets.getInsets(Type.topBar())); + assertEquals(Insets.of(0, 0, 20, 0), insets.getInsets(Type.sideBars())); + } } @Test @@ -114,14 +124,14 @@ public class InsetsStateTest { public void testEquals_differentRect() { mState.getSource(TYPE_TOP_BAR).setFrame(new Rect(0, 0, 100, 100)); mState2.getSource(TYPE_TOP_BAR).setFrame(new Rect(0, 0, 10, 10)); - assertNotEquals(mState, mState2); + assertNotEqualsAndHashCode(); } @Test public void testEquals_differentSource() { mState.getSource(TYPE_TOP_BAR).setFrame(new Rect(0, 0, 100, 100)); mState2.getSource(TYPE_IME).setFrame(new Rect(0, 0, 100, 100)); - assertNotEquals(mState, mState2); + assertNotEqualsAndHashCode(); } @Test @@ -130,7 +140,7 @@ public class InsetsStateTest { mState.getSource(TYPE_IME).setFrame(new Rect(0, 0, 100, 100)); mState2.getSource(TYPE_IME).setFrame(new Rect(0, 0, 100, 100)); mState2.getSource(TYPE_TOP_BAR).setFrame(new Rect(0, 0, 100, 100)); - assertEquals(mState, mState2); + assertEqualsAndHashCode(); } @Test @@ -138,7 +148,21 @@ public class InsetsStateTest { mState.getSource(TYPE_IME).setFrame(new Rect(0, 0, 100, 100)); mState.getSource(TYPE_IME).setVisible(true); mState2.getSource(TYPE_IME).setFrame(new Rect(0, 0, 100, 100)); - assertNotEquals(mState, mState2); + assertNotEqualsAndHashCode(); + } + + @Test + public void testEquals_differentFrame() { + mState.setDisplayFrame(new Rect(0, 1, 2, 3)); + mState.setDisplayFrame(new Rect(4, 5, 6, 7)); + assertNotEqualsAndHashCode(); + } + + @Test + public void testEquals_sameFrame() { + mState.setDisplayFrame(new Rect(0, 1, 2, 3)); + mState2.setDisplayFrame(new Rect(0, 1, 2, 3)); + assertEqualsAndHashCode(); } @Test @@ -148,6 +172,7 @@ public class InsetsStateTest { mState.getSource(TYPE_TOP_BAR).setFrame(new Rect(0, 0, 100, 100)); Parcel p = Parcel.obtain(); mState.writeToParcel(p, 0 /* flags */); + p.setDataPosition(0); mState2.readFromParcel(p); p.recycle(); assertEquals(mState, mState2); @@ -161,4 +186,14 @@ public class InsetsStateTest { assertTrue(InsetsState.getDefaultVisibility(TYPE_SIDE_BAR_3)); assertFalse(InsetsState.getDefaultVisibility(TYPE_IME)); } + + private void assertEqualsAndHashCode() { + assertEquals(mState, mState2); + assertEquals(mState.hashCode(), mState2.hashCode()); + } + + private void assertNotEqualsAndHashCode() { + assertNotEquals(mState, mState2); + assertNotEquals(mState.hashCode(), mState2.hashCode()); + } } diff --git a/services/core/java/com/android/server/wm/DisplayContent.java b/services/core/java/com/android/server/wm/DisplayContent.java index 7a8fd49361b69..a67c72a79b783 100644 --- a/services/core/java/com/android/server/wm/DisplayContent.java +++ b/services/core/java/com/android/server/wm/DisplayContent.java @@ -38,6 +38,7 @@ import static android.view.Surface.ROTATION_180; import static android.view.Surface.ROTATION_270; import static android.view.Surface.ROTATION_90; import static android.view.View.GONE; +import static android.view.ViewRootImpl.NEW_INSETS_MODE_NONE; import static android.view.WindowManager.DOCKED_BOTTOM; import static android.view.WindowManager.DOCKED_INVALID; import static android.view.WindowManager.DOCKED_TOP; @@ -172,6 +173,7 @@ import android.view.SurfaceControl; import android.view.SurfaceControl.Transaction; import android.view.SurfaceSession; import android.view.View; +import android.view.ViewRootImpl; import android.view.WindowManager; import android.view.WindowManagerPolicyConstants.PointerEventListener; @@ -3251,6 +3253,36 @@ class DisplayContent extends WindowContainer= 0; i--) { mControllers.valueAt(i).onPostLayout(); } diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java index 3826fac22b05b..a62bc713db40a 100644 --- a/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java @@ -51,17 +51,22 @@ import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertThat; import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.eq; import android.annotation.SuppressLint; +import android.app.WindowConfiguration; import android.content.res.Configuration; import android.graphics.Rect; import android.os.SystemClock; import android.platform.test.annotations.Presubmit; import android.util.DisplayMetrics; import android.view.DisplayCutout; +import android.view.DisplayInfo; import android.view.Gravity; import android.view.MotionEvent; import android.view.Surface; +import android.view.ViewRootImpl; +import android.view.test.InsetsModeSession; import androidx.test.filters.FlakyTest; import androidx.test.filters.SmallTest; @@ -628,6 +633,39 @@ public class DisplayContentTests extends WindowTestsBase { eq(activityRecord), anyBoolean(), eq(dc.getDisplayId())); } + @Test + public void testComputeImeParent_app() throws Exception { + try (final InsetsModeSession session = + new InsetsModeSession(ViewRootImpl.NEW_INSETS_MODE_IME)) { + final DisplayContent dc = createNewDisplay(); + dc.mInputMethodTarget = createWindow(null, TYPE_BASE_APPLICATION, "app"); + assertEquals(dc.mInputMethodTarget.mAppToken.getSurfaceControl(), + dc.computeImeParent()); + } + } + + @Test + public void testComputeImeParent_app_notFullscreen() throws Exception { + try (final InsetsModeSession session = + new InsetsModeSession(ViewRootImpl.NEW_INSETS_MODE_IME)) { + final DisplayContent dc = createNewDisplay(); + dc.mInputMethodTarget = createWindow(null, TYPE_STATUS_BAR, "app"); + dc.mInputMethodTarget.setWindowingMode( + WindowConfiguration.WINDOWING_MODE_SPLIT_SCREEN_PRIMARY); + assertEquals(dc.getWindowingLayer(), dc.computeImeParent()); + } + } + + @Test + public void testComputeImeParent_noApp() throws Exception { + try (final InsetsModeSession session = + new InsetsModeSession(ViewRootImpl.NEW_INSETS_MODE_IME)) { + final DisplayContent dc = createNewDisplay(); + dc.mInputMethodTarget = createWindow(null, TYPE_STATUS_BAR, "statusBar"); + assertEquals(dc.getWindowingLayer(), dc.computeImeParent()); + } + } + private boolean isOptionsPanelAtRight(int displayId) { return (mWm.getPreferredOptionsPanelGravity(displayId) & Gravity.RIGHT) == Gravity.RIGHT; } diff --git a/tests/utils/testutils/java/android/view/test/InsetsModeSession.java b/tests/utils/testutils/java/android/view/test/InsetsModeSession.java new file mode 100644 index 0000000000000..c83dfa41d260f --- /dev/null +++ b/tests/utils/testutils/java/android/view/test/InsetsModeSession.java @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2019 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 android.view.test; + +import android.view.ViewRootImpl; + +/** + * Session to set insets mode for {@link ViewRootImpl#sNewInsetsMode}. + */ +public class InsetsModeSession implements AutoCloseable { + + private int mOldMode; + + public InsetsModeSession(int flag) { + mOldMode = ViewRootImpl.sNewInsetsMode; + ViewRootImpl.sNewInsetsMode = flag; + } + + @Override + public void close() throws Exception { + ViewRootImpl.sNewInsetsMode = mOldMode; + } +}