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
This commit is contained in:
Tarandeep Singh
2019-01-11 19:50:46 -08:00
committed by Jorim Jaggi
parent 437ed07bb1
commit a6f3561667
8 changed files with 248 additions and 46 deletions

View File

@@ -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<Integer> internalTypes = mState.toInternalType(types);
final SparseArray<InsetsSourceConsumer> 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) {

View File

@@ -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<Integer, InsetsSource> 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 */);

View File

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

View File

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

View File

@@ -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<DisplayContent.DisplayChildWindowCo
mInputMethodTargetWaitingAnim = targetWaitingAnim;
assignWindowLayers(false /* setLayoutNeeded */);
mInsetsStateController.onImeTargetChanged(target);
updateImeParent();
}
private void updateImeParent() {
if (ViewRootImpl.sNewInsetsMode == NEW_INSETS_MODE_NONE) {
return;
}
final SurfaceControl newParent = computeImeParent();
if (newParent != null) {
mPendingTransaction.reparent(mImeWindowsContainers.mSurfaceControl, newParent);
scheduleAnimation();
}
}
/**
* Computes the window the IME should be attached to.
*/
@VisibleForTesting
SurfaceControl computeImeParent() {
// Attach it to app if the target is part of an app and such app is covering the entire
// screen. If it's not covering the entire screen the IME might extend beyond the apps
// bounds.
if (mInputMethodTarget != null && mInputMethodTarget.mAppToken != null &&
mInputMethodTarget.getWindowingMode() == WINDOWING_MODE_FULLSCREEN) {
return mInputMethodTarget.mAppToken.getSurfaceControl();
}
// Otherwise, we just attach it to the display.
return mWindowingLayer;
}
boolean getNeedsMenu(WindowState top, WindowManagerPolicy.WindowState bottom) {
@@ -4885,6 +4917,11 @@ class DisplayContent extends WindowContainer<DisplayContent.DisplayChildWindowCo
.reparent(mWindowingLayer, sc).reparent(mOverlayLayer, sc);
}
@VisibleForTesting
SurfaceControl getWindowingLayer() {
return mWindowingLayer;
}
/**
* Create a portal window handle for input. This window transports any touch to the display
* indicated by {@link InputWindowHandle#portalToDisplayId} if the touch hits this window.

View File

@@ -112,6 +112,7 @@ class InsetsStateController {
* Called when a layout pass has occurred.
*/
void onPostLayout() {
mState.setDisplayFrame(mDisplayContent.getBounds());
for (int i = mControllers.size() - 1; i>= 0; i--) {
mControllers.valueAt(i).onPostLayout();
}

View File

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

View File

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