IME transitions without pre-rendering.
Support new IME inset api transitions without using pre-rendering.
This would be the default behavior when ViewRootImpl#sNewInsetsMode > 0
and pre-rendering is not enabled.
Bug: 111084606
Bug: 118599175
Test: Manually verify by just enabling Insets API and keeping
pre-rendering off.
1. Build and flash
2. adb shell setprop persist.wm.new_insets 1
3. adb reboot
4. Make sure tapping on edit text brings keyboard up with new
transition and back closes IME with various apps.
5. Make sure IME behavior is unchanged for apps with
ADJUST_RESIZE like whatsapp.
Test: atest CtsInputMethodTestCases CtsInputMethodServiceHostTestCases
Change-Id: If33e9dd45e549e49757237fa66051351b858875d
diff --git a/core/java/android/inputmethodservice/InputMethodService.java b/core/java/android/inputmethodservice/InputMethodService.java
index 82d4d1d..83391f3 100644
--- a/core/java/android/inputmethodservice/InputMethodService.java
+++ b/core/java/android/inputmethodservice/InputMethodService.java
@@ -19,6 +19,7 @@
import static android.view.Display.DEFAULT_DISPLAY;
import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT;
+import static android.view.ViewRootImpl.NEW_INSETS_MODE_NONE;
import static android.view.WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS;
import static java.lang.annotation.RetentionPolicy.SOURCE;
@@ -62,6 +63,7 @@
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
+import android.view.ViewRootImpl;
import android.view.ViewTreeObserver;
import android.view.Window;
import android.view.WindowManager;
@@ -595,12 +597,12 @@
if (DEBUG) Log.v(TAG, "hideSoftInput()");
final boolean wasVisible = mIsPreRendered
? mDecorViewVisible && mWindowVisible : isInputViewShown();
+ applyVisibilityInInsetsConsumerIfNecessary(false /* setVisible */);
if (mIsPreRendered) {
if (DEBUG) {
Log.v(TAG, "Making IME window invisible");
}
setImeWindowStatus(IME_ACTIVE | IME_INVISIBLE, mBackDisposition);
- applyVisibilityInInsetsConsumer(false /* setVisible */);
onPreRenderedWindowVisibilityChanged(false /* setVisible */);
} else {
mShowInputFlags = 0;
@@ -632,11 +634,11 @@
if (DEBUG) {
Log.v(TAG, "Making IME window visible");
}
- applyVisibilityInInsetsConsumer(true /* setVisible */);
onPreRenderedWindowVisibilityChanged(true /* setVisible */);
} else {
showWindow(true);
}
+ applyVisibilityInInsetsConsumerIfNecessary(true /* setVisible */);
}
// If user uses hard keyboard, IME button should always be shown.
setImeWindowStatus(mapToImeWindowStatus(), mBackDisposition);
@@ -1974,16 +1976,20 @@
/**
* Apply the IME visibility in {@link android.view.ImeInsetsSourceConsumer} when
- * pre-rendering is enabled.
+ * {@link ViewRootImpl.sNewInsetsMode} is enabled.
* @param setVisible {@code true} to make it visible, false to hide it.
*/
- private void applyVisibilityInInsetsConsumer(boolean setVisible) {
- if (!mIsPreRendered) {
+ private void applyVisibilityInInsetsConsumerIfNecessary(boolean setVisible) {
+ if (!isVisibilityAppliedUsingInsetsConsumer()) {
return;
}
mPrivOps.applyImeVisibility(setVisible);
}
+ private boolean isVisibilityAppliedUsingInsetsConsumer() {
+ return ViewRootImpl.sNewInsetsMode > NEW_INSETS_MODE_NONE;
+ }
+
private void finishViews(boolean finishingInput) {
if (mInputViewStarted) {
if (DEBUG) Log.v(TAG, "CALL: onFinishInputView");
@@ -2007,7 +2013,11 @@
mWindowVisible = false;
finishViews(false /* finishingInput */);
if (mDecorViewVisible) {
- mWindow.hide();
+ // When insets API is enabled, it is responsible for client and server side
+ // visibility of IME window.
+ if (!isVisibilityAppliedUsingInsetsConsumer()) {
+ mWindow.hide();
+ }
mDecorViewVisible = false;
onWindowHidden();
mDecorViewWasVisible = false;
diff --git a/core/java/android/view/ImeInsetsSourceConsumer.java b/core/java/android/view/ImeInsetsSourceConsumer.java
index 83abf1a..3bf44ac 100644
--- a/core/java/android/view/ImeInsetsSourceConsumer.java
+++ b/core/java/android/view/ImeInsetsSourceConsumer.java
@@ -42,7 +42,6 @@
* editor {@link #mFocusedEditor} if {@link #isServedEditorRendered} is {@code true}.
*/
private boolean mShowOnNextImeRender;
- private boolean mHasWindowFocus;
public ImeInsetsSourceConsumer(
InsetsState state, Supplier<Transaction> transactionSupplier,
@@ -68,23 +67,18 @@
}
public void applyImeVisibility(boolean setVisible) {
- if (!mHasWindowFocus) {
- // App window doesn't have focus, any visibility changes would be no-op.
- return;
- }
-
mController.applyImeVisibility(setVisible);
}
@Override
public void onWindowFocusGained() {
- mHasWindowFocus = true;
+ super.onWindowFocusGained();
getImm().registerImeConsumer(this);
}
@Override
public void onWindowFocusLost() {
- mHasWindowFocus = false;
+ super.onWindowFocusLost();
getImm().unregisterImeConsumer(this);
}
diff --git a/core/java/android/view/InsetsController.java b/core/java/android/view/InsetsController.java
index bf16e3d..d0ecae9 100644
--- a/core/java/android/view/InsetsController.java
+++ b/core/java/android/view/InsetsController.java
@@ -296,7 +296,8 @@
final ArraySet<Integer> internalTypes = mState.toInternalType(types);
final SparseArray<InsetsSourceConsumer> consumers = new SparseArray<>();
- Pair<Integer, Boolean> typesReadyPair = collectConsumers(fromIme, internalTypes, consumers);
+ Pair<Integer, Boolean> typesReadyPair = collectConsumers(
+ fromIme, internalTypes, consumers, listener);
int typesReady = typesReadyPair.first;
boolean isReady = typesReadyPair.second;
if (!isReady) {
@@ -324,13 +325,16 @@
* @return Pair of (types ready to animate, is ready to animate).
*/
private Pair<Integer, Boolean> collectConsumers(boolean fromIme,
- ArraySet<Integer> internalTypes, SparseArray<InsetsSourceConsumer> consumers) {
+ ArraySet<Integer> internalTypes, SparseArray<InsetsSourceConsumer> consumers,
+ WindowInsetsAnimationControlListener listener) {
int typesReady = 0;
boolean isReady = true;
for (int i = internalTypes.size() - 1; i >= 0; i--) {
InsetsSourceConsumer consumer = getSourceConsumer(internalTypes.valueAt(i));
- if (consumer.getControl() != null) {
- if (!consumer.isVisible()) {
+ // Double check for IME that IME target window has focus.
+ if (consumer.getType() != TYPE_IME || consumer.hasWindowFocus()) {
+ boolean setVisible = !consumer.isVisible();
+ if (setVisible) {
// Show request
switch(consumer.requestShow(fromIme)) {
case ShowResult.SHOW_IMMEDIATELY:
@@ -357,8 +361,11 @@
}
consumers.put(consumer.getType(), consumer);
} else {
- // TODO: Let calling app know it's not possible, or wait
- // TODO: Remove it from types
+ // window doesnt have focus, no-op.
+ isReady = false;
+ // TODO: Let the calling app know that window has lost focus and
+ // show()/hide()/controlWindowInsetsAnimation requests will be ignored.
+ typesReady &= ~InsetsState.toPublicType(consumer.getType());
}
}
return new Pair<>(typesReady, isReady);
@@ -533,7 +540,10 @@
@Override
public void onCancelled() {
- mAnimator.cancel();
+ // Animator can be null when it is cancelled before onReady() completes.
+ if (mAnimator != null) {
+ mAnimator.cancel();
+ }
}
private void onAnimationFinish() {
diff --git a/core/java/android/view/InsetsSourceConsumer.java b/core/java/android/view/InsetsSourceConsumer.java
index a780158..9edccb3 100644
--- a/core/java/android/view/InsetsSourceConsumer.java
+++ b/core/java/android/view/InsetsSourceConsumer.java
@@ -58,6 +58,7 @@
private final @InternalInsetType int mType;
private final InsetsState mState;
private @Nullable InsetsSourceControl mSourceControl;
+ private boolean mHasWindowFocus;
public InsetsSourceConsumer(@InternalInsetType int type, InsetsState state,
Supplier<Transaction> transactionSupplier, InsetsController controller) {
@@ -104,12 +105,20 @@
/**
* Called when current window gains focus
*/
- public void onWindowFocusGained() {}
+ public void onWindowFocusGained() {
+ mHasWindowFocus = true;
+ }
/**
* Called when current window loses focus.
*/
- public void onWindowFocusLost() {}
+ public void onWindowFocusLost() {
+ mHasWindowFocus = false;
+ }
+
+ boolean hasWindowFocus() {
+ return mHasWindowFocus;
+ }
boolean applyLocalVisibilityOverride() {
@@ -153,7 +162,6 @@
return;
}
mVisible = visible;
- applyHiddenToControl();
applyLocalVisibilityOverride();
mController.notifyVisibilityChanged();
}
diff --git a/core/java/android/view/InsetsState.java b/core/java/android/view/InsetsState.java
index b76f2a1..a04c39b 100644
--- a/core/java/android/view/InsetsState.java
+++ b/core/java/android/view/InsetsState.java
@@ -17,6 +17,7 @@
package android.view;
import static android.view.ViewRootImpl.NEW_INSETS_MODE_FULL;
+import static android.view.ViewRootImpl.NEW_INSETS_MODE_IME;
import static android.view.ViewRootImpl.NEW_INSETS_MODE_NONE;
import static android.view.WindowInsets.Type.MANDATORY_SYSTEM_GESTURES;
import static android.view.WindowInsets.Type.SIZE;
@@ -161,13 +162,15 @@
continue;
}
+ boolean skipNonImeInImeMode = ViewRootImpl.sNewInsetsMode == NEW_INSETS_MODE_IME
+ && source.getType() != TYPE_IME;
boolean skipSystemBars = ViewRootImpl.sNewInsetsMode != NEW_INSETS_MODE_FULL
&& (type == TYPE_TOP_BAR || type == TYPE_NAVIGATION_BAR);
boolean skipIme = source.getType() == TYPE_IME
&& (legacySoftInputMode & LayoutParams.SOFT_INPUT_ADJUST_RESIZE) == 0;
boolean skipLegacyTypes = ViewRootImpl.sNewInsetsMode == NEW_INSETS_MODE_NONE
&& (toPublicType(type) & Type.compatSystemInsets()) != 0;
- if (skipSystemBars || skipIme || skipLegacyTypes) {
+ if (skipSystemBars || skipIme || skipLegacyTypes || skipNonImeInImeMode) {
typeVisibilityMap[indexOf(toPublicType(type))] = source.isVisible();
continue;
}
diff --git a/core/tests/coretests/src/android/view/InsetsControllerTest.java b/core/tests/coretests/src/android/view/InsetsControllerTest.java
index 1e55828..337663e 100644
--- a/core/tests/coretests/src/android/view/InsetsControllerTest.java
+++ b/core/tests/coretests/src/android/view/InsetsControllerTest.java
@@ -145,6 +145,7 @@
InsetsSourceControl ime = controls[2];
InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> {
+ mController.getSourceConsumer(TYPE_IME).onWindowFocusGained();
// since there is no focused view, forcefully make IME visible.
mController.applyImeVisibility(true /* setVisible */);
mController.show(Type.all());
@@ -160,6 +161,7 @@
assertFalse(mController.getSourceConsumer(navBar.getType()).isVisible());
assertFalse(mController.getSourceConsumer(topBar.getType()).isVisible());
assertFalse(mController.getSourceConsumer(ime.getType()).isVisible());
+ mController.getSourceConsumer(TYPE_IME).onWindowFocusLost();
});
InstrumentationRegistry.getInstrumentation().waitForIdleSync();
}
@@ -172,12 +174,14 @@
controls[0] = ime;
mController.onControlsChanged(controls);
InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> {
+ mController.getSourceConsumer(TYPE_IME).onWindowFocusGained();
mController.applyImeVisibility(true);
mController.cancelExistingAnimation();
assertTrue(mController.getSourceConsumer(ime.getType()).isVisible());
mController.applyImeVisibility(false);
mController.cancelExistingAnimation();
assertFalse(mController.getSourceConsumer(ime.getType()).isVisible());
+ mController.getSourceConsumer(TYPE_IME).onWindowFocusLost();
});
InstrumentationRegistry.getInstrumentation().waitForIdleSync();
}
diff --git a/core/tests/coretests/src/android/view/InsetsSourceConsumerTest.java b/core/tests/coretests/src/android/view/InsetsSourceConsumerTest.java
index 971e143..e5fe2d0 100644
--- a/core/tests/coretests/src/android/view/InsetsSourceConsumerTest.java
+++ b/core/tests/coretests/src/android/view/InsetsSourceConsumerTest.java
@@ -18,8 +18,10 @@
import static android.view.InsetsState.TYPE_TOP_BAR;
+import static junit.framework.TestCase.assertFalse;
+import static junit.framework.TestCase.assertTrue;
+
import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyZeroInteractions;
@@ -40,6 +42,7 @@
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
+import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;
/**
@@ -60,6 +63,7 @@
private SurfaceSession mSession = new SurfaceSession();
private SurfaceControl mLeash;
@Mock Transaction mMockTransaction;
+ private InsetsSource mSpyInsetsSource;
@Before
public void setup() {
@@ -77,7 +81,11 @@
} catch (BadTokenException e) {
// activity isn't running, lets ignore BadTokenException.
}
- mConsumer = new InsetsSourceConsumer(TYPE_TOP_BAR, new InsetsState(),
+ InsetsState state = new InsetsState();
+ mSpyInsetsSource = Mockito.spy(new InsetsSource(TYPE_TOP_BAR));
+ state.addSource(mSpyInsetsSource);
+
+ mConsumer = new InsetsSourceConsumer(TYPE_TOP_BAR, state,
() -> mMockTransaction, new InsetsController(viewRootImpl));
});
instrumentation.waitForIdleSync();
@@ -88,14 +96,15 @@
@Test
public void testHide() {
mConsumer.hide();
- verify(mMockTransaction).hide(eq(mLeash));
+ assertFalse("Consumer should not be visible", mConsumer.isVisible());
+ verify(mSpyInsetsSource).setVisible(eq(false));
}
@Test
public void testShow() {
- mConsumer.hide();
mConsumer.show();
- verify(mMockTransaction, atLeastOnce()).show(eq(mLeash));
+ assertTrue("Consumer should be visible", mConsumer.isVisible());
+ verify(mSpyInsetsSource).setVisible(eq(true));
}
@Test