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:
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
/**
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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) {
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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.
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user