From 92d2dd36cdd44ddeb5db85a5c3aee6f2fd3000c0 Mon Sep 17 00:00:00 2001 From: Tarandeep Singh Date: Wed, 7 Aug 2019 14:45:01 -0700 Subject: [PATCH] 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 --- .../InputMethodService.java | 22 ++++++++++++----- .../android/view/ImeInsetsSourceConsumer.java | 10 ++------ core/java/android/view/InsetsController.java | 24 +++++++++++++------ .../android/view/InsetsSourceConsumer.java | 14 ++++++++--- core/java/android/view/InsetsState.java | 5 +++- .../android/view/InsetsControllerTest.java | 4 ++++ .../view/InsetsSourceConsumerTest.java | 19 +++++++++++---- 7 files changed, 68 insertions(+), 30 deletions(-) diff --git a/core/java/android/inputmethodservice/InputMethodService.java b/core/java/android/inputmethodservice/InputMethodService.java index 82d4d1d10d7e1..83391f368ac15 100644 --- a/core/java/android/inputmethodservice/InputMethodService.java +++ b/core/java/android/inputmethodservice/InputMethodService.java @@ -19,6 +19,7 @@ package android.inputmethodservice; 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.LayoutInflater; 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 @@ public class InputMethodService extends AbstractInputMethodService { 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 @@ public class InputMethodService extends AbstractInputMethodService { 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 @@ public class InputMethodService extends AbstractInputMethodService { /** * 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 @@ public class InputMethodService extends AbstractInputMethodService { 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 83abf1a33b2d1..3bf44ac92a58e 100644 --- a/core/java/android/view/ImeInsetsSourceConsumer.java +++ b/core/java/android/view/ImeInsetsSourceConsumer.java @@ -42,7 +42,6 @@ public final class ImeInsetsSourceConsumer extends InsetsSourceConsumer { * editor {@link #mFocusedEditor} if {@link #isServedEditorRendered} is {@code true}. */ private boolean mShowOnNextImeRender; - private boolean mHasWindowFocus; public ImeInsetsSourceConsumer( InsetsState state, Supplier transactionSupplier, @@ -68,23 +67,18 @@ public final class ImeInsetsSourceConsumer extends InsetsSourceConsumer { } 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 bf16e3dedd499..d0ecae9e34e4b 100644 --- a/core/java/android/view/InsetsController.java +++ b/core/java/android/view/InsetsController.java @@ -296,7 +296,8 @@ public class InsetsController implements WindowInsetsController { final ArraySet internalTypes = mState.toInternalType(types); final SparseArray consumers = new SparseArray<>(); - Pair typesReadyPair = collectConsumers(fromIme, internalTypes, consumers); + Pair typesReadyPair = collectConsumers( + fromIme, internalTypes, consumers, listener); int typesReady = typesReadyPair.first; boolean isReady = typesReadyPair.second; if (!isReady) { @@ -324,13 +325,16 @@ public class InsetsController implements WindowInsetsController { * @return Pair of (types ready to animate, is ready to animate). */ private Pair collectConsumers(boolean fromIme, - ArraySet internalTypes, SparseArray consumers) { + ArraySet internalTypes, SparseArray 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 @@ public class InsetsController implements WindowInsetsController { } 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 @@ public class InsetsController implements WindowInsetsController { @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 a7801583f643b..9edccb3fb221b 100644 --- a/core/java/android/view/InsetsSourceConsumer.java +++ b/core/java/android/view/InsetsSourceConsumer.java @@ -58,6 +58,7 @@ public class InsetsSourceConsumer { private final @InternalInsetType int mType; private final InsetsState mState; private @Nullable InsetsSourceControl mSourceControl; + private boolean mHasWindowFocus; public InsetsSourceConsumer(@InternalInsetType int type, InsetsState state, Supplier transactionSupplier, InsetsController controller) { @@ -104,12 +105,20 @@ public class InsetsSourceConsumer { /** * 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 @@ public class InsetsSourceConsumer { return; } mVisible = visible; - applyHiddenToControl(); applyLocalVisibilityOverride(); mController.notifyVisibilityChanged(); } diff --git a/core/java/android/view/InsetsState.java b/core/java/android/view/InsetsState.java index b76f2a1753463..a04c39b63c859 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 @@ public class InsetsState implements Parcelable { 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 1e558287e21be..337663e33cd03 100644 --- a/core/tests/coretests/src/android/view/InsetsControllerTest.java +++ b/core/tests/coretests/src/android/view/InsetsControllerTest.java @@ -145,6 +145,7 @@ public class InsetsControllerTest { 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 @@ public class InsetsControllerTest { 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 @@ public class InsetsControllerTest { 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 971e143927b37..e5fe2d021d9a8 100644 --- a/core/tests/coretests/src/android/view/InsetsSourceConsumerTest.java +++ b/core/tests/coretests/src/android/view/InsetsSourceConsumerTest.java @@ -18,8 +18,10 @@ package android.view; 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.Before; 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 @@ public class InsetsSourceConsumerTest { private SurfaceSession mSession = new SurfaceSession(); private SurfaceControl mLeash; @Mock Transaction mMockTransaction; + private InsetsSource mSpyInsetsSource; @Before public void setup() { @@ -77,7 +81,11 @@ public class InsetsSourceConsumerTest { } 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 @@ public class InsetsSourceConsumerTest { @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