From c22eec9d35e797417b8630fff178ec53e7c29b65 Mon Sep 17 00:00:00 2001 From: Adrian Roos Date: Fri, 12 Jun 2020 18:48:10 +0200 Subject: [PATCH] IME animation: hide IME-related navbar icons until perceptible During transitions and while the IME is controlled by the app, the IME may be "visible" as far as the IME framework is concerned, but not actually perceptible by the user due to offsets or alpha applied to the leash. This may lead to out-of-place navbar symbols for the IME, especially in gesture nav. To avoid this, the ImeInsetsSourceConsumer now notifies the IMF of whether it has made the IME unperceptible to the user. For now, we ignore the cases where the IME is controlled by something other than the client window - in that case, we just revert to the previous behavior of it being always considered perceptible. Fixes: 158079255 Test: manual, launch email compose activity, observe that back button only indicates once IME appears Test: atest InsetsAnimationControlImplTest Change-Id: I4dc9d6513d0559156f7da39244f3fc5ebc952ed4 --- .../android/view/ImeInsetsSourceConsumer.java | 10 ++++ .../view/InsetsAnimationControlCallbacks.java | 12 +++++ .../view/InsetsAnimationControlImpl.java | 14 ++++++ .../InsetsAnimationThreadControlRunner.java | 5 ++ core/java/android/view/InsetsController.java | 19 ++++++++ .../android/view/InsetsSourceConsumer.java | 10 ++++ .../view/ViewRootInsetsControllerHost.java | 13 +++++ .../view/inputmethod/InputMethodManager.java | 13 +++++ .../internal/view/IInputMethodManager.aidl | 1 + .../view/InsetsAnimationControlImplTest.java | 18 +++++++ .../InputMethodManagerInternal.java | 15 ++++++ .../InputMethodManagerService.java | 48 +++++++++++++++++++ .../MultiClientInputMethodManagerService.java | 10 ++++ .../com/android/server/wm/DisplayContent.java | 9 ++++ .../com/android/server/wm/InsetsPolicy.java | 6 +++ 15 files changed, 203 insertions(+) diff --git a/core/java/android/view/ImeInsetsSourceConsumer.java b/core/java/android/view/ImeInsetsSourceConsumer.java index 8b5af29517cb5..ef9d990168d2c 100644 --- a/core/java/android/view/ImeInsetsSourceConsumer.java +++ b/core/java/android/view/ImeInsetsSourceConsumer.java @@ -21,6 +21,7 @@ import static android.view.InsetsState.ITYPE_IME; import android.annotation.Nullable; import android.inputmethodservice.InputMethodService; +import android.os.IBinder; import android.os.Parcel; import android.text.TextUtils; import android.view.SurfaceControl.Transaction; @@ -153,6 +154,15 @@ public final class ImeInsetsSourceConsumer extends InsetsSourceConsumer { return mIsRequestedVisibleAwaitingControl || isRequestedVisible(); } + @Override + public void onPerceptible(boolean perceptible) { + super.onPerceptible(perceptible); + final IBinder window = mController.getHost().getWindowToken(); + if (window != null) { + getImm().reportPerceptible(window, perceptible); + } + } + private boolean isDummyOrEmptyEditor(EditorInfo info) { // TODO(b/123044812): Handle dummy input gracefully in IME Insets API return info == null || (info.fieldId <= 0 && info.inputType <= 0); diff --git a/core/java/android/view/InsetsAnimationControlCallbacks.java b/core/java/android/view/InsetsAnimationControlCallbacks.java index 74c186948b2f2..3431c3ecc310f 100644 --- a/core/java/android/view/InsetsAnimationControlCallbacks.java +++ b/core/java/android/view/InsetsAnimationControlCallbacks.java @@ -16,6 +16,7 @@ package android.view; +import android.view.WindowInsets.Type.InsetsType; import android.view.WindowInsetsAnimation.Bounds; /** @@ -64,4 +65,15 @@ public interface InsetsAnimationControlCallbacks { * previous calls to applySurfaceParams. */ void releaseSurfaceControlFromRt(SurfaceControl sc); + + /** + * Reports that the perceptibility of the given types has changed to the given value. + * + * A type is perceptible if it is not (almost) entirely off-screen and not (almost) entirely + * transparent. + * + * @param types the (public) types whose perceptibility has changed + * @param perceptible true, if the types are now perceptible, false if they are not perceptible + */ + void reportPerceptible(@InsetsType int types, boolean perceptible); } diff --git a/core/java/android/view/InsetsAnimationControlImpl.java b/core/java/android/view/InsetsAnimationControlImpl.java index baeae2fd2cdde..31da83ad5137e 100644 --- a/core/java/android/view/InsetsAnimationControlImpl.java +++ b/core/java/android/view/InsetsAnimationControlImpl.java @@ -87,6 +87,7 @@ public class InsetsAnimationControlImpl implements WindowInsetsAnimationControll private float mPendingAlpha = 1.0f; @VisibleForTesting(visibility = PACKAGE) public boolean mReadyDispatched; + private Boolean mPerceptible; @VisibleForTesting public InsetsAnimationControlImpl(SparseArray controls, Rect frame, @@ -121,6 +122,14 @@ public class InsetsAnimationControlImpl implements WindowInsetsAnimationControll new Bounds(mHiddenInsets, mShownInsets)); } + private boolean calculatePerceptible(Insets currentInsets, float currentAlpha) { + return 100 * currentInsets.left >= 5 * (mShownInsets.left - mHiddenInsets.left) + && 100 * currentInsets.top >= 5 * (mShownInsets.top - mHiddenInsets.top) + && 100 * currentInsets.right >= 5 * (mShownInsets.right - mHiddenInsets.right) + && 100 * currentInsets.bottom >= 5 * (mShownInsets.bottom - mHiddenInsets.bottom) + && currentAlpha >= 0.5f; + } + @Override public boolean hasZeroInsetsIme() { return mHasZeroInsetsIme; @@ -175,6 +184,11 @@ public class InsetsAnimationControlImpl implements WindowInsetsAnimationControll mPendingInsets = sanitize(insets); mPendingAlpha = sanitize(alpha); mController.scheduleApplyChangeInsets(this); + boolean perceptible = calculatePerceptible(mPendingInsets, mPendingAlpha); + if (mPerceptible == null || perceptible != mPerceptible) { + mController.reportPerceptible(mTypes, perceptible); + mPerceptible = perceptible; + } } @VisibleForTesting diff --git a/core/java/android/view/InsetsAnimationThreadControlRunner.java b/core/java/android/view/InsetsAnimationThreadControlRunner.java index 0e71b7643b7d9..123604489da49 100644 --- a/core/java/android/view/InsetsAnimationThreadControlRunner.java +++ b/core/java/android/view/InsetsAnimationThreadControlRunner.java @@ -90,6 +90,11 @@ public class InsetsAnimationThreadControlRunner implements InsetsAnimationContro // Since we don't push the SurfaceParams to the RT we can release directly sc.release(); } + + @Override + public void reportPerceptible(int types, boolean perceptible) { + mMainThreadHandler.post(() -> mOuterCallbacks.reportPerceptible(types, perceptible)); + } }; @UiThread diff --git a/core/java/android/view/InsetsController.java b/core/java/android/view/InsetsController.java index 5f99bfe432a42..a3c88bacb6277 100644 --- a/core/java/android/view/InsetsController.java +++ b/core/java/android/view/InsetsController.java @@ -35,6 +35,7 @@ import android.graphics.Insets; import android.graphics.Rect; import android.os.CancellationSignal; import android.os.Handler; +import android.os.IBinder; import android.os.Trace; import android.util.ArraySet; import android.util.Log; @@ -165,6 +166,12 @@ public class InsetsController implements WindowInsetsController, InsetsAnimation /** @see ViewRootImpl#dipToPx */ int dipToPx(int dips); + + /** + * @return token associated with the host, if it has one. + */ + @Nullable + IBinder getWindowToken(); } private static final String TAG = "InsetsController"; @@ -1353,6 +1360,18 @@ public class InsetsController implements WindowInsetsController, InsetsAnimation mHost.releaseSurfaceControlFromRt(sc); } + @Override + public void reportPerceptible(int types, boolean perceptible) { + final ArraySet internalTypes = toInternalType(types); + final int size = mSourceConsumers.size(); + for (int i = 0; i < size; i++) { + final InsetsSourceConsumer consumer = mSourceConsumers.valueAt(i); + if (internalTypes.contains(consumer.getType())) { + consumer.onPerceptible(perceptible); + } + } + } + Host getHost() { return mHost; } diff --git a/core/java/android/view/InsetsSourceConsumer.java b/core/java/android/view/InsetsSourceConsumer.java index 565846638acc0..b62e67c8f9e1f 100644 --- a/core/java/android/view/InsetsSourceConsumer.java +++ b/core/java/android/view/InsetsSourceConsumer.java @@ -260,6 +260,15 @@ public class InsetsSourceConsumer { return ShowResult.SHOW_IMMEDIATELY; } + /** + * Reports that this source's perceptibility has changed + * + * @param perceptible true if the source is perceptible, false otherwise. + * @see InsetsAnimationControlCallbacks#reportPerceptible + */ + public void onPerceptible(boolean perceptible) { + } + /** * Notify listeners that window is now hidden. */ @@ -339,5 +348,6 @@ public class InsetsSourceConsumer { t.hide(mSourceControl.getLeash()); } t.apply(); + onPerceptible(mRequestedVisible); } } diff --git a/core/java/android/view/ViewRootInsetsControllerHost.java b/core/java/android/view/ViewRootInsetsControllerHost.java index f7ca3c2b7ddf3..90a80cefc54d7 100644 --- a/core/java/android/view/ViewRootInsetsControllerHost.java +++ b/core/java/android/view/ViewRootInsetsControllerHost.java @@ -22,6 +22,7 @@ import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_BEHAVIOR_CONT import android.annotation.NonNull; import android.os.Handler; +import android.os.IBinder; import android.os.RemoteException; import android.util.Log; import android.view.inputmethod.InputMethodManager; @@ -236,4 +237,16 @@ public class ViewRootInsetsControllerHost implements InsetsController.Host { } return 0; } + + @Override + public IBinder getWindowToken() { + if (mViewRoot == null) { + return null; + } + final View view = mViewRoot.getView(); + if (view == null) { + return null; + } + return view.getWindowToken(); + } } diff --git a/core/java/android/view/inputmethod/InputMethodManager.java b/core/java/android/view/inputmethod/InputMethodManager.java index 821dd742db9ab..aedb59bfee422 100644 --- a/core/java/android/view/inputmethod/InputMethodManager.java +++ b/core/java/android/view/inputmethod/InputMethodManager.java @@ -541,6 +541,19 @@ public final class InputMethodManager { return servedView.hasWindowFocus() || isAutofillUIShowing(servedView); } + /** + * Reports whether the IME is currently perceptible or not, according to the leash applied by + * {@link android.view.WindowInsetsController}. + * @hide + */ + public void reportPerceptible(IBinder windowToken, boolean perceptible) { + try { + mService.reportPerceptible(windowToken, perceptible); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + private final class DelegateImpl implements ImeFocusController.InputMethodManagerDelegate { /** diff --git a/core/java/com/android/internal/view/IInputMethodManager.aidl b/core/java/com/android/internal/view/IInputMethodManager.aidl index d22f942133380..8ec51b89d2400 100644 --- a/core/java/com/android/internal/view/IInputMethodManager.aidl +++ b/core/java/com/android/internal/view/IInputMethodManager.aidl @@ -72,5 +72,6 @@ interface IInputMethodManager { void reportActivityView(in IInputMethodClient parentClient, int childDisplayId, in float[] matrixValues); + oneway void reportPerceptible(in IBinder windowToken, boolean perceptible); void removeImeSurface(); } diff --git a/core/tests/coretests/src/android/view/InsetsAnimationControlImplTest.java b/core/tests/coretests/src/android/view/InsetsAnimationControlImplTest.java index 8eca650398bf2..a2b1e3d69cd2e 100644 --- a/core/tests/coretests/src/android/view/InsetsAnimationControlImplTest.java +++ b/core/tests/coretests/src/android/view/InsetsAnimationControlImplTest.java @@ -234,6 +234,24 @@ public class InsetsAnimationControlImplTest { verify(mMockListener).onFinished(mController); } + @Test + public void testPerceptible_insets() { + mController.setInsetsAndAlpha(mController.getHiddenStateInsets(), 1f, 1f); + verify(mMockController).reportPerceptible(systemBars(), false); + + mController.setInsetsAndAlpha(mController.getShownStateInsets(), 1f, 1f); + verify(mMockController).reportPerceptible(systemBars(), true); + } + + @Test + public void testPerceptible_alpha() { + mController.setInsetsAndAlpha(mController.getShownStateInsets(), 0f, 1f); + verify(mMockController).reportPerceptible(systemBars(), false); + + mController.setInsetsAndAlpha(mController.getShownStateInsets(), 1f, 1f); + verify(mMockController).reportPerceptible(systemBars(), true); + } + private void assertPosition(Matrix m, Rect original, Rect transformed) { RectF rect = new RectF(original); rect.offsetTo(0, 0); diff --git a/services/core/java/com/android/server/inputmethod/InputMethodManagerInternal.java b/services/core/java/com/android/server/inputmethod/InputMethodManagerInternal.java index de13bd86a4155..70f0399d10709 100644 --- a/services/core/java/com/android/server/inputmethod/InputMethodManagerInternal.java +++ b/services/core/java/com/android/server/inputmethod/InputMethodManagerInternal.java @@ -17,6 +17,7 @@ package com.android.server.inputmethod; import android.annotation.NonNull; +import android.annotation.Nullable; import android.annotation.UserIdInt; import android.os.IBinder; import android.view.inputmethod.InlineSuggestionsRequest; @@ -108,6 +109,16 @@ public abstract class InputMethodManagerInternal { public abstract boolean transferTouchFocusToImeWindow(@NonNull IBinder sourceInputToken, int displayId); + /** + * Reports that IME control has transferred to the given window token, or if null that + * control has been taken away from client windows (and is instead controlled by the policy + * or SystemUI). + * + * @param windowToken the window token that is now in control, or {@code null} if no client + * window is in control of the IME. + */ + public abstract void reportImeControl(@Nullable IBinder windowToken); + /** * Fake implementation of {@link InputMethodManagerInternal}. All the methods do nothing. */ @@ -151,6 +162,10 @@ public abstract class InputMethodManagerInternal { int displayId) { return false; } + + @Override + public void reportImeControl(@Nullable IBinder windowToken) { + } }; /** diff --git a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java index fea7980c1c246..9acb47538043c 100644 --- a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java +++ b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java @@ -179,6 +179,7 @@ import java.util.Collections; import java.util.Date; import java.util.List; import java.util.Locale; +import java.util.Objects; import java.util.WeakHashMap; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.atomic.AtomicInteger; @@ -592,6 +593,11 @@ public class InputMethodManagerService extends IInputMethodManager.Stub // Was the keyguard locked when this client became current? private boolean mCurClientInKeyguard; + /** + * {@code true} if the IME has not been mostly hidden via {@link android.view.InsetsController} + */ + private boolean mCurPerceptible; + /** * Set to true if our ServiceConnection is currently actively bound to * a service (whether or not we have gotten its IBinder back yet). @@ -2936,6 +2942,9 @@ public class InputMethodManagerService extends IInputMethodManager.Stub if (vis != 0 && isKeyguardLocked() && !mCurClientInKeyguard) { vis = 0; } + if (!mCurPerceptible) { + vis = 0; + } // mImeWindowVis should be updated before calling shouldShowImeSwitcherLocked(). final boolean needsToShowImeSwitcher = shouldShowImeSwitcherLocked(vis); if (mStatusBar != null) { @@ -3148,6 +3157,28 @@ public class InputMethodManagerService extends IInputMethodManager.Stub } } + @BinderThread + @Override + public void reportPerceptible(IBinder windowToken, boolean perceptible) { + Objects.requireNonNull(windowToken, "windowToken must not be null"); + int uid = Binder.getCallingUid(); + synchronized (mMethodMap) { + if (!calledFromValidUserLocked()) { + return; + } + final long ident = Binder.clearCallingIdentity(); + try { + if (mCurFocusedWindow == windowToken + && mCurPerceptible != perceptible) { + mCurPerceptible = perceptible; + updateSystemUiLocked(mImeWindowVis, mBackDisposition); + } + } finally { + Binder.restoreCallingIdentity(ident); + } + } + } + @GuardedBy("mMethodMap") boolean showCurrentInputLocked(IBinder windowToken, int flags, ResultReceiver resultReceiver, @SoftInputShowHideReason int reason) { @@ -3451,6 +3482,7 @@ public class InputMethodManagerService extends IInputMethodManager.Stub mCurFocusedWindow = windowToken; mCurFocusedWindowSoftInputMode = softInputMode; mCurFocusedWindowClient = cs; + mCurPerceptible = true; // Should we auto-show the IME even if the caller has not // specified what should be done with it? @@ -5010,6 +5042,16 @@ public class InputMethodManagerService extends IInputMethodManager.Stub return mInputManagerInternal.transferTouchFocus(sourceInputToken, curHostInputToken); } + private void reportImeControl(@Nullable IBinder windowToken) { + synchronized (mMethodMap) { + if (mCurFocusedWindow != windowToken) { + // mCurPerceptible was set by the focused window, but it is no longer in control, + // so we reset mCurPerceptible. + mCurPerceptible = true; + } + } + } + private static final class LocalServiceImpl extends InputMethodManagerInternal { @NonNull private final InputMethodManagerService mService; @@ -5062,6 +5104,11 @@ public class InputMethodManagerService extends IInputMethodManager.Stub int displayId) { return mService.transferTouchFocusToImeWindow(sourceInputToken, displayId); } + + @Override + public void reportImeControl(@Nullable IBinder windowToken) { + mService.reportImeControl(windowToken); + } } @BinderThread @@ -5164,6 +5211,7 @@ public class InputMethodManagerService extends IInputMethodManager.Stub p.println(" mCurMethodId=" + mCurMethodId); client = mCurClient; p.println(" mCurClient=" + client + " mCurSeq=" + mCurSeq); + p.println(" mCurPerceptible=" + mCurPerceptible); p.println(" mCurFocusedWindow=" + mCurFocusedWindow + " softInputMode=" + InputMethodDebug.softInputModeToString(mCurFocusedWindowSoftInputMode) diff --git a/services/core/java/com/android/server/inputmethod/MultiClientInputMethodManagerService.java b/services/core/java/com/android/server/inputmethod/MultiClientInputMethodManagerService.java index 33c78e403ce3c..2e3d3963c9dfe 100644 --- a/services/core/java/com/android/server/inputmethod/MultiClientInputMethodManagerService.java +++ b/services/core/java/com/android/server/inputmethod/MultiClientInputMethodManagerService.java @@ -221,6 +221,10 @@ public final class MultiClientInputMethodManagerService { reportNotSupported(); return false; } + + @Override + public void reportImeControl(@Nullable IBinder windowToken) { + } }); } @@ -1751,6 +1755,12 @@ public final class MultiClientInputMethodManagerService { reportNotSupported(); } + @BinderThread + @Override + public void reportPerceptible(IBinder windowClient, boolean perceptible) { + reportNotSupported(); + } + @BinderThread @Override public void onShellCommand(@Nullable FileDescriptor in, @Nullable FileDescriptor out, diff --git a/services/core/java/com/android/server/wm/DisplayContent.java b/services/core/java/com/android/server/wm/DisplayContent.java index ed055eb7eb099..9b5d94ebb1ac9 100644 --- a/services/core/java/com/android/server/wm/DisplayContent.java +++ b/services/core/java/com/android/server/wm/DisplayContent.java @@ -210,6 +210,7 @@ import com.android.internal.util.function.pooled.PooledConsumer; import com.android.internal.util.function.pooled.PooledFunction; import com.android.internal.util.function.pooled.PooledLambda; import com.android.internal.util.function.pooled.PooledPredicate; +import com.android.server.inputmethod.InputMethodManagerInternal; import com.android.server.policy.WindowManagerPolicy; import com.android.server.protolog.common.ProtoLog; import com.android.server.wm.utils.DisplayRotationUtil; @@ -3566,6 +3567,14 @@ class DisplayContent extends WindowContainer + InputMethodManagerInternal.get().reportImeControl(token) + ); } private void updateImeParent() { diff --git a/services/core/java/com/android/server/wm/InsetsPolicy.java b/services/core/java/com/android/server/wm/InsetsPolicy.java index c7b91067d59fc..254356d673e35 100644 --- a/services/core/java/com/android/server/wm/InsetsPolicy.java +++ b/services/core/java/com/android/server/wm/InsetsPolicy.java @@ -482,6 +482,12 @@ class InsetsPolicy { WindowInsetsAnimation animation, Bounds bounds) { } + + @Override + public void reportPerceptible(int types, boolean perceptible) { + // No-op for now - only client windows report perceptibility for now, with policy + // controllers assumed to always be perceptible. + } } } }