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