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
This commit is contained in:
Adrian Roos
2020-06-12 18:48:10 +02:00
parent 70ad33113f
commit c22eec9d35
15 changed files with 203 additions and 0 deletions

View File

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

View File

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

View File

@@ -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<InsetsSourceControl> 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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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 {
/**

View File

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

View File

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

View File

@@ -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) {
}
};
/**

View File

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

View File

@@ -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,

View File

@@ -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<DisplayContent.DisplayChildWindowCo
private void updateImeControlTarget() {
mInputMethodControlTarget = computeImeControlTarget();
mInsetsStateController.onImeControlTargetChanged(mInputMethodControlTarget);
final WindowState win = mInputMethodControlTarget != null
? mInputMethodControlTarget.getWindow() : null;
final IBinder token = win != null ? win.mClient.asBinder() : null;
// Note: not allowed to call into IMMS with the WM lock held, hence the post.
mWmService.mH.post(() ->
InputMethodManagerInternal.get().reportImeControl(token)
);
}
private void updateImeParent() {

View File

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