blob: 147c15bea076a4fb6a9ecdf96611e61a8e24873f [file] [log] [blame]
/*
* Copyright (C) 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package android.view;
import static android.inputmethodservice.InputMethodService.ENABLE_HIDE_IME_CAPTION_BAR;
import static android.os.Trace.TRACE_TAG_VIEW;
import static android.view.InsetsControllerProto.CONTROL;
import static android.view.InsetsControllerProto.STATE;
import static android.view.InsetsSource.ID_IME;
import static android.view.InsetsSource.ID_IME_CAPTION_BAR;
import static android.view.ViewRootImpl.CAPTION_ON_SHELL;
import static android.view.WindowInsets.Type.FIRST;
import static android.view.WindowInsets.Type.LAST;
import static android.view.WindowInsets.Type.all;
import static android.view.WindowInsets.Type.captionBar;
import static android.view.WindowInsets.Type.ime;
import static android.view.inputmethod.ImeTracker.PHASE_CLIENT_ANIMATION_CANCEL;
import android.animation.AnimationHandler;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.TypeEvaluator;
import android.animation.ValueAnimator;
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.ActivityThread;
import android.content.Context;
import android.content.res.CompatibilityInfo;
import android.graphics.Insets;
import android.graphics.Point;
import android.graphics.Rect;
import android.os.CancellationSignal;
import android.os.Handler;
import android.os.IBinder;
import android.os.Process;
import android.os.Trace;
import android.text.TextUtils;
import android.util.IntArray;
import android.util.Log;
import android.util.Pair;
import android.util.SparseArray;
import android.util.proto.ProtoOutputStream;
import android.view.InsetsSourceConsumer.ShowResult;
import android.view.SurfaceControl.Transaction;
import android.view.WindowInsets.Type;
import android.view.WindowInsets.Type.InsetsType;
import android.view.WindowInsetsAnimation.Bounds;
import android.view.WindowManager.LayoutParams.SoftInputModeFlags;
import android.view.animation.Interpolator;
import android.view.animation.LinearInterpolator;
import android.view.animation.PathInterpolator;
import android.view.inputmethod.ImeTracker;
import android.view.inputmethod.ImeTracker.InputMethodJankContext;
import android.view.inputmethod.InputMethodManager;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.graphics.SfVsyncFrameCallbackProvider;
import com.android.internal.inputmethod.ImeTracing;
import com.android.internal.inputmethod.SoftInputShowHideReason;
import com.android.internal.util.function.TriFunction;
import java.io.PrintWriter;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
/**
* Implements {@link WindowInsetsController} on the client.
* @hide
*/
public class InsetsController implements WindowInsetsController, InsetsAnimationControlCallbacks {
private int mTypesBeingCancelled;
public interface Host {
Handler getHandler();
/**
* Notifies host that {@link InsetsController#getState()} has changed.
*/
void notifyInsetsChanged();
void dispatchWindowInsetsAnimationPrepare(@NonNull WindowInsetsAnimation animation);
Bounds dispatchWindowInsetsAnimationStart(
@NonNull WindowInsetsAnimation animation, @NonNull Bounds bounds);
WindowInsets dispatchWindowInsetsAnimationProgress(@NonNull WindowInsets insets,
@NonNull List<WindowInsetsAnimation> runningAnimations);
void dispatchWindowInsetsAnimationEnd(@NonNull WindowInsetsAnimation animation);
/**
* Requests host to apply surface params in synchronized manner.
*/
void applySurfaceParams(final SyncRtSurfaceTransactionApplier.SurfaceParams... params);
/**
* @see ViewRootImpl#updateCompatSysUiVisibility(int, int, int)
*/
default void updateCompatSysUiVisibility(@InsetsType int visibleTypes,
@InsetsType int requestedVisibleTypes, @InsetsType int controllableTypes) { }
/**
* Called when the requested visibilities of insets have been modified by the client.
* The visibilities should be reported back to WM.
*
* @param types Bitwise flags of types requested visible.
*/
void updateRequestedVisibleTypes(@InsetsType int types);
/**
* @return Whether the host has any callbacks it wants to synchronize the animations with.
* If there are no callbacks, the animation will be off-loaded to another thread and
* slightly different animation curves are picked.
*/
boolean hasAnimationCallbacks();
/**
* @see WindowInsetsController#setSystemBarsAppearance
*/
void setSystemBarsAppearance(@Appearance int appearance, @Appearance int mask);
/**
* @see WindowInsetsController#getSystemBarsAppearance()
*/
@Appearance int getSystemBarsAppearance();
default boolean isSystemBarsAppearanceControlled() {
return false;
}
/**
* @see WindowInsetsController#setSystemBarsBehavior
*/
void setSystemBarsBehavior(@Behavior int behavior);
/**
* @see WindowInsetsController#getSystemBarsBehavior
*/
@Behavior int getSystemBarsBehavior();
default boolean isSystemBarsBehaviorControlled() {
return false;
}
/**
* Releases a surface and ensure that this is done after {@link #applySurfaceParams} has
* finished applying params.
*/
void releaseSurfaceControlFromRt(SurfaceControl surfaceControl);
/**
* If this host is a view hierarchy, adds a pre-draw runnable to ensure proper ordering as
* described in {@link WindowInsetsAnimation.Callback#onPrepare}.
*
* If this host isn't a view hierarchy, the runnable can be executed immediately.
*/
void addOnPreDrawRunnable(Runnable r);
/**
* Adds a runnbale to be executed during {@link Choreographer#CALLBACK_INSETS_ANIMATION}
* phase.
*/
void postInsetsAnimationCallback(Runnable r);
/**
* Obtains {@link InputMethodManager} instance from host.
*/
InputMethodManager getInputMethodManager();
/**
* @return title of the rootView, if it has one.
* Note: this method is for debugging purposes only.
*/
@Nullable
String getRootViewTitle();
/**
* @return the context related to the rootView.
*/
@Nullable
default Context getRootViewContext() {
return null;
}
/** @see ViewRootImpl#dipToPx */
int dipToPx(int dips);
/**
* @return token associated with the host, if it has one.
*/
@Nullable
IBinder getWindowToken();
/**
* @return Translator associated with the host, if it has one.
*/
@Nullable
default CompatibilityInfo.Translator getTranslator() {
return null;
}
/**
* Notifies when the state of running animation is changed. The state is either "running" or
* "idle".
*
* @param running {@code true} if there is any animation running; {@code false} otherwise.
*/
default void notifyAnimationRunningStateChanged(boolean running) {}
}
private static final String TAG = "InsetsController";
private static final int ANIMATION_DURATION_MOVE_IN_MS = 275;
private static final int ANIMATION_DURATION_MOVE_OUT_MS = 340;
private static final int ANIMATION_DURATION_FADE_IN_MS = 500;
private static final int ANIMATION_DURATION_FADE_OUT_MS = 1500;
/** Visible for WindowManagerWrapper */
public static final int ANIMATION_DURATION_RESIZE = 300;
private static final int ANIMATION_DELAY_DIM_MS = 500;
private static final int ANIMATION_DURATION_SYNC_IME_MS = 285;
private static final int ANIMATION_DURATION_UNSYNC_IME_MS = 200;
private static final int PENDING_CONTROL_TIMEOUT_MS = 2000;
private static final Interpolator SYSTEM_BARS_INSETS_INTERPOLATOR =
new PathInterpolator(0.4f, 0f, 0.2f, 1f);
private static final Interpolator SYSTEM_BARS_ALPHA_INTERPOLATOR =
new PathInterpolator(0.3f, 0f, 1f, 1f);
private static final Interpolator SYSTEM_BARS_DIM_INTERPOLATOR = alphaFraction -> {
// While playing dim animation, alphaFraction is changed from 1f to 0f. Here changes it to
// time-based fraction for computing delay and interpolation.
float fraction = 1 - alphaFraction;
final float fractionDelay = (float) ANIMATION_DELAY_DIM_MS / ANIMATION_DURATION_FADE_OUT_MS;
if (fraction <= fractionDelay) {
return 1f;
} else {
float innerFraction = (fraction - fractionDelay) / (1f - fractionDelay);
return 1f - SYSTEM_BARS_ALPHA_INTERPOLATOR.getInterpolation(innerFraction);
}
};
private static final Interpolator SYNC_IME_INTERPOLATOR =
new PathInterpolator(0.2f, 0f, 0f, 1f);
private static final Interpolator LINEAR_OUT_SLOW_IN_INTERPOLATOR =
new PathInterpolator(0, 0, 0.2f, 1f);
private static final Interpolator FAST_OUT_LINEAR_IN_INTERPOLATOR =
new PathInterpolator(0.4f, 0f, 1f, 1f);
/** Visible for WindowManagerWrapper */
public static final Interpolator RESIZE_INTERPOLATOR = new LinearInterpolator();
/** The amount IME will move up/down when animating in floating mode. */
private static final int FLOATING_IME_BOTTOM_INSET_DP = -80;
private static final int ID_CAPTION_BAR =
InsetsSource.createId(null /* owner */, 0 /* index */, captionBar());
static final boolean DEBUG = false;
static final boolean WARN = false;
/**
* Layout mode during insets animation: The views should be laid out as if the changing inset
* types are fully shown. Before starting the animation, {@link View#onApplyWindowInsets} will
* be called as if the changing insets types are shown, which will result in the views being
* laid out as if the insets are fully shown.
*/
public static final int LAYOUT_INSETS_DURING_ANIMATION_SHOWN = 0;
/**
* Layout mode during insets animation: The views should be laid out as if the changing inset
* types are fully hidden. Before starting the animation, {@link View#onApplyWindowInsets} will
* be called as if the changing insets types are hidden, which will result in the views being
* laid out as if the insets are fully hidden.
*/
public static final int LAYOUT_INSETS_DURING_ANIMATION_HIDDEN = 1;
/**
* Determines the behavior of how the views should be laid out during an insets animation that
* is controlled by the application by calling {@link #controlWindowInsetsAnimation}.
* <p>
* When the animation is system-initiated, the layout mode is always chosen such that the
* pre-animation layout will represent the opposite of the starting state, i.e. when insets
* are appearing, {@link #LAYOUT_INSETS_DURING_ANIMATION_SHOWN} will be used. When insets
* are disappearing, {@link #LAYOUT_INSETS_DURING_ANIMATION_HIDDEN} will be used.
*/
@Retention(RetentionPolicy.SOURCE)
@IntDef(value = {LAYOUT_INSETS_DURING_ANIMATION_SHOWN,
LAYOUT_INSETS_DURING_ANIMATION_HIDDEN})
@interface LayoutInsetsDuringAnimation {
}
/** Not running an animation. */
@VisibleForTesting
public static final int ANIMATION_TYPE_NONE = -1;
/** Running animation will show insets */
public static final int ANIMATION_TYPE_SHOW = 0;
/** Running animation will hide insets */
public static final int ANIMATION_TYPE_HIDE = 1;
/** Running animation is controlled by user via {@link #controlWindowInsetsAnimation} */
@VisibleForTesting
public static final int ANIMATION_TYPE_USER = 2;
/** Running animation will resize insets */
@VisibleForTesting
public static final int ANIMATION_TYPE_RESIZE = 3;
@Retention(RetentionPolicy.SOURCE)
@IntDef(value = {ANIMATION_TYPE_NONE, ANIMATION_TYPE_SHOW, ANIMATION_TYPE_HIDE,
ANIMATION_TYPE_USER, ANIMATION_TYPE_RESIZE})
public @interface AnimationType {
}
/**
* Translation animation evaluator.
*/
private static TypeEvaluator<Insets> sEvaluator = (fraction, startValue, endValue) -> Insets.of(
(int) (startValue.left + fraction * (endValue.left - startValue.left)),
(int) (startValue.top + fraction * (endValue.top - startValue.top)),
(int) (startValue.right + fraction * (endValue.right - startValue.right)),
(int) (startValue.bottom + fraction * (endValue.bottom - startValue.bottom)));
/** Logging listener. */
private WindowInsetsAnimationControlListener mLoggingListener;
/** Context for {@link android.view.inputmethod.ImeTracker.ImeJankTracker} to monitor jank. */
private final InputMethodJankContext mJankContext = new InputMethodJankContext() {
@Override
public Context getDisplayContext() {
return mHost != null ? mHost.getRootViewContext() : null;
}
@Override
public SurfaceControl getTargetSurfaceControl() {
final InsetsSourceControl imeSourceControl = getImeSourceConsumer().getControl();
return imeSourceControl != null ? imeSourceControl.getLeash() : null;
}
@Override
public String getHostPackageName() {
return mHost != null ? mHost.getRootViewContext().getPackageName() : null;
}
};
/**
* The default implementation of listener, to be used by InsetsController and InsetsPolicy to
* animate insets.
*/
public static class InternalAnimationControlListener
implements WindowInsetsAnimationControlListener {
private WindowInsetsAnimationController mController;
private ValueAnimator mAnimator;
private final boolean mShow;
private final boolean mHasAnimationCallbacks;
private final @InsetsType int mRequestedTypes;
private final @Behavior int mBehavior;
private final long mDurationMs;
private final boolean mDisable;
private final int mFloatingImeBottomInset;
private final WindowInsetsAnimationControlListener mLoggingListener;
private final InputMethodJankContext mInputMethodJankContext;
private final ThreadLocal<AnimationHandler> mSfAnimationHandlerThreadLocal =
new ThreadLocal<AnimationHandler>() {
@Override
protected AnimationHandler initialValue() {
AnimationHandler handler = new AnimationHandler();
handler.setProvider(new SfVsyncFrameCallbackProvider());
return handler;
}
};
public InternalAnimationControlListener(boolean show, boolean hasAnimationCallbacks,
@InsetsType int requestedTypes, @Behavior int behavior, boolean disable,
int floatingImeBottomInset, WindowInsetsAnimationControlListener loggingListener,
@Nullable InputMethodJankContext jankContext) {
mShow = show;
mHasAnimationCallbacks = hasAnimationCallbacks;
mRequestedTypes = requestedTypes;
mBehavior = behavior;
mDurationMs = calculateDurationMs();
mDisable = disable;
mFloatingImeBottomInset = floatingImeBottomInset;
mLoggingListener = loggingListener;
mInputMethodJankContext = jankContext;
}
@Override
public void onReady(WindowInsetsAnimationController controller, int types) {
mController = controller;
if (DEBUG) Log.d(TAG, "default animation onReady types: " + types);
if (mLoggingListener != null) {
mLoggingListener.onReady(controller, types);
}
if (mDisable) {
onAnimationFinish();
return;
}
mAnimator = ValueAnimator.ofFloat(0f, 1f);
mAnimator.setDuration(mDurationMs);
mAnimator.setInterpolator(new LinearInterpolator());
Insets hiddenInsets = controller.getHiddenStateInsets();
// IME with zero insets is a special case: it will animate-in from offscreen and end
// with final insets of zero and vice-versa.
hiddenInsets = controller.hasZeroInsetsIme()
? Insets.of(hiddenInsets.left, hiddenInsets.top, hiddenInsets.right,
mFloatingImeBottomInset)
: hiddenInsets;
Insets start = mShow
? hiddenInsets
: controller.getShownStateInsets();
Insets end = mShow
? controller.getShownStateInsets()
: hiddenInsets;
Interpolator insetsInterpolator = getInsetsInterpolator();
Interpolator alphaInterpolator = getAlphaInterpolator();
mAnimator.addUpdateListener(animation -> {
float rawFraction = animation.getAnimatedFraction();
float alphaFraction = mShow
? rawFraction
: 1 - rawFraction;
float insetsFraction = insetsInterpolator.getInterpolation(rawFraction);
controller.setInsetsAndAlpha(
sEvaluator.evaluate(insetsFraction, start, end),
alphaInterpolator.getInterpolation(alphaFraction),
rawFraction);
if (DEBUG) Log.d(TAG, "Default animation setInsetsAndAlpha fraction: "
+ insetsFraction);
});
mAnimator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationStart(Animator animation) {
if (mInputMethodJankContext == null) return;
ImeTracker.forJank().onRequestAnimation(
mInputMethodJankContext,
getAnimationType(),
!mHasAnimationCallbacks);
}
@Override
public void onAnimationCancel(Animator animation) {
if (mInputMethodJankContext == null) return;
ImeTracker.forJank().onCancelAnimation(getAnimationType());
}
@Override
public void onAnimationEnd(Animator animation) {
onAnimationFinish();
if (mInputMethodJankContext == null) return;
ImeTracker.forJank().onFinishAnimation(getAnimationType());
}
});
if (!mHasAnimationCallbacks) {
mAnimator.setAnimationHandler(mSfAnimationHandlerThreadLocal.get());
}
mAnimator.start();
}
@Override
public void onFinished(WindowInsetsAnimationController controller) {
if (DEBUG) Log.d(TAG, "InternalAnimationControlListener onFinished types:"
+ Type.toString(mRequestedTypes));
if (mLoggingListener != null) {
mLoggingListener.onFinished(controller);
}
}
@Override
public void onCancelled(WindowInsetsAnimationController controller) {
// Animator can be null when it is cancelled before onReady() completes.
if (mAnimator != null) {
mAnimator.cancel();
}
if (DEBUG) Log.d(TAG, "InternalAnimationControlListener onCancelled types:"
+ mRequestedTypes);
if (mLoggingListener != null) {
mLoggingListener.onCancelled(controller);
}
}
protected Interpolator getInsetsInterpolator() {
if ((mRequestedTypes & ime()) != 0) {
if (mHasAnimationCallbacks) {
return SYNC_IME_INTERPOLATOR;
} else if (mShow) {
return LINEAR_OUT_SLOW_IN_INTERPOLATOR;
} else {
return FAST_OUT_LINEAR_IN_INTERPOLATOR;
}
} else {
if (mBehavior == BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE) {
return SYSTEM_BARS_INSETS_INTERPOLATOR;
} else {
// Makes insets stay at the shown position.
return input -> mShow ? 1f : 0f;
}
}
}
Interpolator getAlphaInterpolator() {
if ((mRequestedTypes & ime()) != 0) {
if (mHasAnimationCallbacks) {
return input -> 1f;
} else if (mShow) {
// Alpha animation takes half the time with linear interpolation;
return input -> Math.min(1f, 2 * input);
} else {
return FAST_OUT_LINEAR_IN_INTERPOLATOR;
}
} else {
if (mBehavior == BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE) {
return input -> 1f;
} else {
if (mShow) {
return SYSTEM_BARS_ALPHA_INTERPOLATOR;
} else {
return SYSTEM_BARS_DIM_INTERPOLATOR;
}
}
}
}
protected void onAnimationFinish() {
mController.finish(mShow);
if (DEBUG) Log.d(TAG, "onAnimationFinish showOnFinish: " + mShow);
}
/**
* To get the animation duration in MS.
*/
public long getDurationMs() {
return mDurationMs;
}
private long calculateDurationMs() {
if ((mRequestedTypes & ime()) != 0) {
if (mHasAnimationCallbacks) {
return ANIMATION_DURATION_SYNC_IME_MS;
} else {
return ANIMATION_DURATION_UNSYNC_IME_MS;
}
} else {
if (mBehavior == BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE) {
return mShow ? ANIMATION_DURATION_MOVE_IN_MS : ANIMATION_DURATION_MOVE_OUT_MS;
} else {
return mShow ? ANIMATION_DURATION_FADE_IN_MS : ANIMATION_DURATION_FADE_OUT_MS;
}
}
}
/**
* Returns the current animation type.
*/
@AnimationType
private int getAnimationType() {
return mShow ? ANIMATION_TYPE_SHOW : ANIMATION_TYPE_HIDE;
}
}
/**
* Represents a running animation
*/
private static class RunningAnimation {
RunningAnimation(InsetsAnimationControlRunner runner, int type) {
this.runner = runner;
this.type = type;
}
final InsetsAnimationControlRunner runner;
final @AnimationType int type;
/**
* Whether {@link WindowInsetsAnimation.Callback#onStart(WindowInsetsAnimation, Bounds)} has
* been dispatched already for this animation.
*/
boolean startDispatched;
}
/**
* Represents a control request that we had to defer because we are waiting for the IME to
* process our show request.
*/
private static class PendingControlRequest {
PendingControlRequest(@InsetsType int types, WindowInsetsAnimationControlListener listener,
long durationMs, Interpolator interpolator, @AnimationType int animationType,
@LayoutInsetsDuringAnimation int layoutInsetsDuringAnimation,
CancellationSignal cancellationSignal, boolean useInsetsAnimationThread) {
this.types = types;
this.listener = listener;
this.durationMs = durationMs;
this.interpolator = interpolator;
this.animationType = animationType;
this.layoutInsetsDuringAnimation = layoutInsetsDuringAnimation;
this.cancellationSignal = cancellationSignal;
this.useInsetsAnimationThread = useInsetsAnimationThread;
}
@InsetsType int types;
final WindowInsetsAnimationControlListener listener;
final long durationMs;
final Interpolator interpolator;
final @AnimationType int animationType;
final @LayoutInsetsDuringAnimation int layoutInsetsDuringAnimation;
final CancellationSignal cancellationSignal;
final boolean useInsetsAnimationThread;
}
/** The local state */
private final InsetsState mState = new InsetsState();
/** The state dispatched from server */
private final InsetsState mLastDispatchedState = new InsetsState();
private final Rect mFrame = new Rect();
private final TriFunction<InsetsController, Integer, Integer, InsetsSourceConsumer>
mConsumerCreator;
private final SparseArray<InsetsSourceConsumer> mSourceConsumers = new SparseArray<>();
private final InsetsSourceConsumer mImeSourceConsumer;
private final Host mHost;
private final Handler mHandler;
private final SparseArray<InsetsSourceControl> mTmpControlArray = new SparseArray<>();
private final ArrayList<RunningAnimation> mRunningAnimations = new ArrayList<>();
private WindowInsets mLastInsets;
private boolean mAnimCallbackScheduled;
private final Runnable mAnimCallback;
/** Pending control request that is waiting on IME to be ready to be shown */
private PendingControlRequest mPendingImeControlRequest;
private int mWindowType;
private int mLastLegacySoftInputMode;
private int mLastLegacyWindowFlags;
private int mLastLegacySystemUiFlags;
private int mLastActivityType;
private boolean mStartingAnimation;
private int mCaptionInsetsHeight = 0;
private int mImeCaptionBarInsetsHeight = 0;
private boolean mAnimationsDisabled;
private boolean mCompatSysUiVisibilityStaled;
private final Runnable mPendingControlTimeout = this::abortPendingImeControlRequest;
private final ArrayList<OnControllableInsetsChangedListener> mControllableInsetsChangedListeners
= new ArrayList<>();
/** Set of inset types for which an animation was started since last resetting this field */
private @InsetsType int mLastStartedAnimTypes;
/** Set of inset types which are existing */
private @InsetsType int mExistingTypes = 0;
/** Set of inset types which are visible */
private @InsetsType int mVisibleTypes = WindowInsets.Type.defaultVisible();
/** Set of inset types which are requested visible */
private @InsetsType int mRequestedVisibleTypes = WindowInsets.Type.defaultVisible();
/** Set of inset types which are requested visible which are reported to the host */
private @InsetsType int mReportedRequestedVisibleTypes = WindowInsets.Type.defaultVisible();
/** Set of inset types that we have controls of */
private @InsetsType int mControllableTypes;
private final Runnable mInvokeControllableInsetsChangedListeners =
this::invokeControllableInsetsChangedListeners;
private final InsetsState.OnTraverseCallbacks mRemoveGoneSources =
new InsetsState.OnTraverseCallbacks() {
private final IntArray mPendingRemoveIndexes = new IntArray();
@Override
public void onIdNotFoundInState2(int index1, InsetsSource source1) {
if (!CAPTION_ON_SHELL && source1.getType() == captionBar()) {
return;
}
if (source1.getId() == ID_IME_CAPTION_BAR) {
return;
}
// Don't change the indexes of the sources while traversing. Remove it later.
mPendingRemoveIndexes.add(index1);
}
@Override
public void onFinish(InsetsState state1, InsetsState state2) {
for (int i = mPendingRemoveIndexes.size() - 1; i >= 0; i--) {
state1.removeSourceAt(mPendingRemoveIndexes.get(i));
}
mPendingRemoveIndexes.clear();
}
};
private final InsetsState.OnTraverseCallbacks mStartResizingAnimationIfNeeded =
new InsetsState.OnTraverseCallbacks() {
private @InsetsType int mTypes;
private InsetsState mToState;
@Override
public void onStart(InsetsState state1, InsetsState state2) {
mTypes = 0;
mToState = null;
}
@Override
public void onIdMatch(InsetsSource source1, InsetsSource source2) {
final @InsetsType int type = source1.getType();
if ((type & Type.systemBars()) == 0
|| !source1.isVisible() || !source2.isVisible()
|| source1.getFrame().equals(source2.getFrame())
|| !(Rect.intersects(mFrame, source1.getFrame())
|| Rect.intersects(mFrame, source2.getFrame()))) {
return;
}
mTypes |= type;
if (mToState == null) {
mToState = new InsetsState();
}
mToState.addSource(new InsetsSource(source2));
}
@Override
public void onFinish(InsetsState state1, InsetsState state2) {
if (mTypes == 0) {
return;
}
cancelExistingControllers(mTypes);
final InsetsAnimationControlRunner runner = new InsetsResizeAnimationRunner(
mFrame, state1, mToState, RESIZE_INTERPOLATOR,
ANIMATION_DURATION_RESIZE, mTypes, InsetsController.this);
if (mRunningAnimations.isEmpty()) {
mHost.notifyAnimationRunningStateChanged(true);
}
mRunningAnimations.add(new RunningAnimation(runner, runner.getAnimationType()));
}
};
public InsetsController(Host host) {
this(host, (controller, id, type) -> {
if (type == ime()) {
return new ImeInsetsSourceConsumer(id, controller.mState,
Transaction::new, controller);
} else {
return new InsetsSourceConsumer(id, type, controller.mState,
Transaction::new, controller);
}
}, host.getHandler());
}
@VisibleForTesting
public InsetsController(Host host,
TriFunction<InsetsController, Integer, Integer, InsetsSourceConsumer> consumerCreator,
Handler handler) {
mHost = host;
mConsumerCreator = consumerCreator;
mHandler = handler;
mAnimCallback = () -> {
mAnimCallbackScheduled = false;
if (mRunningAnimations.isEmpty()) {
return;
}
final List<WindowInsetsAnimation> runningAnimations = new ArrayList<>();
final List<WindowInsetsAnimation> finishedAnimations = new ArrayList<>();
final InsetsState state = new InsetsState(mState, true /* copySources */);
for (int i = mRunningAnimations.size() - 1; i >= 0; i--) {
RunningAnimation runningAnimation = mRunningAnimations.get(i);
if (DEBUG) Log.d(TAG, "Running animation type: " + runningAnimation.type);
final InsetsAnimationControlRunner runner = runningAnimation.runner;
if (runner instanceof WindowInsetsAnimationController) {
// Keep track of running animation to be dispatched. Aggregate it here such that
// if it gets finished within applyChangeInsets we still dispatch it to
// onProgress.
if (runningAnimation.startDispatched) {
runningAnimations.add(runner.getAnimation());
}
if (((InternalInsetsAnimationController) runner).applyChangeInsets(state)) {
finishedAnimations.add(runner.getAnimation());
}
}
}
WindowInsets insets = state.calculateInsets(mFrame,
mState /* ignoringVisibilityState */, mLastInsets.isRound(),
mLastLegacySoftInputMode, mLastLegacyWindowFlags, mLastLegacySystemUiFlags,
mWindowType, mLastActivityType, null /* idSideMap */);
mHost.dispatchWindowInsetsAnimationProgress(insets,
Collections.unmodifiableList(runningAnimations));
if (DEBUG) {
for (WindowInsetsAnimation anim : runningAnimations) {
Log.d(TAG, String.format("Running animation type: %d, progress: %f",
anim.getTypeMask(), anim.getInterpolatedFraction()));
}
}
for (int i = finishedAnimations.size() - 1; i >= 0; i--) {
dispatchAnimationEnd(finishedAnimations.get(i));
}
};
// Make mImeSourceConsumer always non-null.
mImeSourceConsumer = getSourceConsumer(ID_IME, ime());
}
@VisibleForTesting
public void onFrameChanged(Rect frame) {
if (mFrame.equals(frame)) {
return;
}
if (mImeCaptionBarInsetsHeight != 0) {
setImeCaptionBarInsetsHeight(mImeCaptionBarInsetsHeight);
}
mHost.notifyInsetsChanged();
mFrame.set(frame);
}
@Override
public InsetsState getState() {
return mState;
}
@Override
public @InsetsType int getRequestedVisibleTypes() {
return mRequestedVisibleTypes;
}
public InsetsState getLastDispatchedState() {
return mLastDispatchedState;
}
public boolean onStateChanged(InsetsState state) {
boolean stateChanged = false;
if (!CAPTION_ON_SHELL) {
stateChanged = !mState.equals(state, true /* excludesCaptionBar */,
false /* excludesInvisibleIme */)
|| captionInsetsUnchanged();
} else {
stateChanged = !mState.equals(state, false /* excludesCaptionBar */,
false /* excludesInvisibleIme */);
}
if (!stateChanged && mLastDispatchedState.equals(state)) {
return false;
}
if (DEBUG) Log.d(TAG, "onStateChanged: " + state);
mLastDispatchedState.set(state, true /* copySources */);
final InsetsState lastState = new InsetsState(mState, true /* copySources */);
updateState(state);
applyLocalVisibilityOverride();
updateCompatSysUiVisibility();
if (!mState.equals(lastState, false /* excludesCaptionBar */,
true /* excludesInvisibleIme */)) {
if (DEBUG) Log.d(TAG, "onStateChanged, notifyInsetsChanged");
mHost.notifyInsetsChanged();
if (lastState.getDisplayFrame().equals(mState.getDisplayFrame())) {
InsetsState.traverse(lastState, mState, mStartResizingAnimationIfNeeded);
}
}
return true;
}
private void updateState(InsetsState newState) {
mState.set(newState, 0 /* types */);
@InsetsType int existingTypes = 0;
@InsetsType int visibleTypes = 0;
@InsetsType int[] cancelledUserAnimationTypes = {0};
for (int i = 0, size = newState.sourceSize(); i < size; i++) {
final InsetsSource source = newState.sourceAt(i);
@InsetsType int type = source.getType();
@AnimationType int animationType = getAnimationType(type);
final InsetsSourceConsumer consumer = mSourceConsumers.get(source.getId());
if (consumer != null) {
consumer.updateSource(source, animationType);
} else {
mState.addSource(source);
}
existingTypes |= type;
if (source.isVisible()) {
visibleTypes |= type;
}
}
// If a type doesn't have a source, treat it as visible if it is visible by default.
visibleTypes |= WindowInsets.Type.defaultVisible() & ~existingTypes;
if (mVisibleTypes != visibleTypes) {
if (WindowInsets.Type.hasCompatSystemBars(mVisibleTypes ^ visibleTypes)) {
mCompatSysUiVisibilityStaled = true;
}
mVisibleTypes = visibleTypes;
}
if (mExistingTypes != existingTypes) {
if (WindowInsets.Type.hasCompatSystemBars(mExistingTypes ^ existingTypes)) {
mCompatSysUiVisibilityStaled = true;
}
mExistingTypes = existingTypes;
}
InsetsState.traverse(mState, newState, mRemoveGoneSources);
if (cancelledUserAnimationTypes[0] != 0) {
mHandler.post(() -> show(cancelledUserAnimationTypes[0]));
}
}
private boolean captionInsetsUnchanged() {
if (CAPTION_ON_SHELL) {
return false;
}
final InsetsSource source = mState.peekSource(ID_CAPTION_BAR);
if (source == null && mCaptionInsetsHeight == 0) {
return false;
}
if (source != null && mCaptionInsetsHeight == source.getFrame().height()) {
return false;
}
return true;
}
/**
* @see InsetsState#calculateInsets(Rect, InsetsState, boolean, int, int, int, int, int,
* android.util.SparseIntArray)
*/
@VisibleForTesting
public WindowInsets calculateInsets(boolean isScreenRound, int windowType, int activityType,
int legacySoftInputMode, int legacyWindowFlags, int legacySystemUiFlags) {
mWindowType = windowType;
mLastActivityType = activityType;
mLastLegacySoftInputMode = legacySoftInputMode;
mLastLegacyWindowFlags = legacyWindowFlags;
mLastLegacySystemUiFlags = legacySystemUiFlags;
mLastInsets = mState.calculateInsets(mFrame, null /* ignoringVisibilityState */,
isScreenRound, legacySoftInputMode, legacyWindowFlags,
legacySystemUiFlags, windowType, activityType, null /* idSideMap */);
return mLastInsets;
}
/**
* @see InsetsState#calculateVisibleInsets(Rect, int, int, int, int)
*/
public Insets calculateVisibleInsets(int windowType, int activityType,
@SoftInputModeFlags int softInputMode, int windowFlags) {
return mState.calculateVisibleInsets(mFrame, windowType, activityType, softInputMode,
windowFlags);
}
/**
* Called when the server has dispatched us a new set of inset controls.
*/
public void onControlsChanged(InsetsSourceControl[] activeControls) {
if (activeControls != null) {
for (InsetsSourceControl activeControl : activeControls) {
if (activeControl != null) {
// TODO(b/122982984): Figure out why it can be null.
mTmpControlArray.put(activeControl.getId(), activeControl);
}
}
}
@InsetsType int controllableTypes = 0;
int consumedControlCount = 0;
final @InsetsType int[] showTypes = new int[1];
final @InsetsType int[] hideTypes = new int[1];
// Ensure to update all existing source consumers
for (int i = mSourceConsumers.size() - 1; i >= 0; i--) {
final InsetsSourceConsumer consumer = mSourceConsumers.valueAt(i);
if (consumer.getId() == ID_IME_CAPTION_BAR) {
// The inset control for the IME caption bar will never be dispatched
// by the server.
continue;
}
final InsetsSourceControl control = mTmpControlArray.get(consumer.getId());
if (control != null) {
controllableTypes |= control.getType();
consumedControlCount++;
}
// control may be null, but we still need to update the control to null if it got
// revoked.
consumer.setControl(control, showTypes, hideTypes);
}
// Ensure to create source consumers if not available yet.
if (consumedControlCount != mTmpControlArray.size()) {
for (int i = mTmpControlArray.size() - 1; i >= 0; i--) {
final InsetsSourceControl control = mTmpControlArray.valueAt(i);
getSourceConsumer(control.getId(), control.getType())
.setControl(control, showTypes, hideTypes);
}
}
if (mTmpControlArray.size() > 0) {
// Update surface positions for animations.
for (int i = mRunningAnimations.size() - 1; i >= 0; i--) {
mRunningAnimations.get(i).runner.updateSurfacePosition(mTmpControlArray);
}
}
mTmpControlArray.clear();
// Do not override any animations that the app started in the OnControllableInsetsChanged
// listeners.
int animatingTypes = invokeControllableInsetsChangedListeners();
showTypes[0] &= ~animatingTypes;
hideTypes[0] &= ~animatingTypes;
if (showTypes[0] != 0) {
applyAnimation(showTypes[0], true /* show */, false /* fromIme */,
null /* statsToken */);
}
if (hideTypes[0] != 0) {
applyAnimation(hideTypes[0], false /* show */, false /* fromIme */,
null /* statsToken */);
}
if (mControllableTypes != controllableTypes) {
if (WindowInsets.Type.hasCompatSystemBars(mControllableTypes ^ controllableTypes)) {
mCompatSysUiVisibilityStaled = true;
}
mControllableTypes = controllableTypes;
}
// InsetsSourceConsumer#setControl might change the requested visibility.
reportRequestedVisibleTypes();
}
@Override
public void show(@InsetsType int types) {
ImeTracker.Token statsToken = null;
if ((types & ime()) != 0) {
statsToken = ImeTracker.forLogging().onRequestShow(null /* component */,
Process.myUid(), ImeTracker.ORIGIN_CLIENT_SHOW_SOFT_INPUT,
SoftInputShowHideReason.SHOW_SOFT_INPUT_BY_INSETS_API);
}
show(types, false /* fromIme */, statsToken);
}
@VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
public void show(@InsetsType int types, boolean fromIme,
@Nullable ImeTracker.Token statsToken) {
if ((types & ime()) != 0) {
Log.d(TAG, "show(ime(), fromIme=" + fromIme + ")");
}
if (fromIme) {
ImeTracing.getInstance().triggerClientDump("InsetsController#show",
mHost.getInputMethodManager(), null /* icProto */);
Trace.asyncTraceEnd(TRACE_TAG_VIEW, "IC.showRequestFromApiToImeReady", 0);
Trace.asyncTraceBegin(TRACE_TAG_VIEW, "IC.showRequestFromIme", 0);
} else {
Trace.asyncTraceBegin(TRACE_TAG_VIEW, "IC.showRequestFromApi", 0);
Trace.asyncTraceBegin(TRACE_TAG_VIEW, "IC.showRequestFromApiToImeReady", 0);
}
// Handle pending request ready in case there was one set.
if (fromIme && mPendingImeControlRequest != null) {
if ((types & Type.ime()) != 0) {
ImeTracker.forLatency().onShown(statsToken, ActivityThread::currentApplication);
}
handlePendingControlRequest(statsToken);
return;
}
// TODO: Support a ResultReceiver for IME.
// TODO(b/123718661): Make show() work for multi-session IME.
int typesReady = 0;
final boolean imeVisible = mState.isSourceOrDefaultVisible(
mImeSourceConsumer.getId(), ime());
for (int type = FIRST; type <= LAST; type = type << 1) {
if ((types & type) == 0) {
continue;
}
@AnimationType final int animationType = getAnimationType(type);
final boolean requestedVisible = (type & mRequestedVisibleTypes) != 0;
final boolean isIme = type == ime();
var alreadyVisible = requestedVisible && (!isIme || imeVisible)
&& animationType == ANIMATION_TYPE_NONE;
var alreadyAnimatingShow = animationType == ANIMATION_TYPE_SHOW;
if (alreadyVisible || alreadyAnimatingShow) {
// no-op: already shown or animating in (because window visibility is
// applied before starting animation).
if (DEBUG) Log.d(TAG, String.format(
"show ignored for type: %d animType: %d requestedVisible: %s",
type, animationType, requestedVisible));
if (isIme) {
ImeTracker.forLogging().onCancelled(statsToken,
ImeTracker.PHASE_CLIENT_APPLY_ANIMATION);
}
continue;
}
if (fromIme && animationType == ANIMATION_TYPE_USER) {
// App is already controlling the IME, don't cancel it.
if (isIme) {
ImeTracker.forLogging().onFailed(
statsToken, ImeTracker.PHASE_CLIENT_APPLY_ANIMATION);
}
continue;
}
if (isIme) {
ImeTracker.forLogging().onProgress(
statsToken, ImeTracker.PHASE_CLIENT_APPLY_ANIMATION);
}
typesReady |= type;
}
if (DEBUG) Log.d(TAG, "show typesReady: " + typesReady);
if (fromIme && (typesReady & Type.ime()) != 0) {
ImeTracker.forLatency().onShown(statsToken, ActivityThread::currentApplication);
}
applyAnimation(typesReady, true /* show */, fromIme, statsToken);
}
/**
* Handle the {@link #mPendingImeControlRequest} when
* - The IME insets is ready to show.
* - The IME insets has being requested invisible.
*/
private void handlePendingControlRequest(@Nullable ImeTracker.Token statsToken) {
PendingControlRequest pendingRequest = mPendingImeControlRequest;
mPendingImeControlRequest = null;
mHandler.removeCallbacks(mPendingControlTimeout);
// We are about to playing the default animation. Passing a null frame indicates the
// controlled types should be animated regardless of the frame.
controlAnimationUnchecked(
pendingRequest.types, pendingRequest.cancellationSignal,
pendingRequest.listener, null /* frame */,
true /* fromIme */, pendingRequest.durationMs, pendingRequest.interpolator,
pendingRequest.animationType,
pendingRequest.layoutInsetsDuringAnimation,
pendingRequest.useInsetsAnimationThread, statsToken);
}
@Override
public void hide(@InsetsType int types) {
ImeTracker.Token statsToken = null;
if ((types & ime()) != 0) {
statsToken = ImeTracker.forLogging().onRequestHide(null /* component */,
Process.myUid(), ImeTracker.ORIGIN_CLIENT_HIDE_SOFT_INPUT,
SoftInputShowHideReason.HIDE_SOFT_INPUT_BY_INSETS_API);
}
hide(types, false /* fromIme */, statsToken);
}
@VisibleForTesting
public void hide(@InsetsType int types, boolean fromIme,
@Nullable ImeTracker.Token statsToken) {
if (fromIme) {
ImeTracing.getInstance().triggerClientDump("InsetsController#hide",
mHost.getInputMethodManager(), null /* icProto */);
Trace.asyncTraceBegin(TRACE_TAG_VIEW, "IC.hideRequestFromIme", 0);
} else {
Trace.asyncTraceBegin(TRACE_TAG_VIEW, "IC.hideRequestFromApi", 0);
}
int typesReady = 0;
boolean hasImeRequestedHidden = false;
final boolean hadPendingImeControlRequest = mPendingImeControlRequest != null;
for (int type = FIRST; type <= LAST; type = type << 1) {
if ((types & type) == 0) {
continue;
}
@AnimationType final int animationType = getAnimationType(type);
final boolean requestedVisible = (type & mRequestedVisibleTypes) != 0;
final boolean isImeAnimation = type == ime();
if (mPendingImeControlRequest != null && !requestedVisible) {
// Remove the hide insets type from the pending show request.
mPendingImeControlRequest.types &= ~type;
if (mPendingImeControlRequest.types == 0) {
abortPendingImeControlRequest();
}
}
if (isImeAnimation && !requestedVisible && animationType == ANIMATION_TYPE_NONE) {
hasImeRequestedHidden = true;
// Ensure to request hide IME in case there is any pending requested visible
// being applied from setControl when receiving the insets control.
if (hadPendingImeControlRequest
|| getImeSourceConsumer().isRequestedVisibleAwaitingControl()) {
getImeSourceConsumer().requestHide(fromIme, statsToken);
}
}
if (!requestedVisible && animationType == ANIMATION_TYPE_NONE
|| animationType == ANIMATION_TYPE_HIDE) {
// no-op: already hidden or animating out (because window visibility is
// applied before starting animation).
if (isImeAnimation) {
ImeTracker.forLogging().onCancelled(statsToken,
ImeTracker.PHASE_CLIENT_APPLY_ANIMATION);
}
continue;
}
if (isImeAnimation) {
ImeTracker.forLogging().onProgress(
statsToken, ImeTracker.PHASE_CLIENT_APPLY_ANIMATION);
}
typesReady |= type;
}
if (hasImeRequestedHidden && mPendingImeControlRequest != null) {
// Handle the pending show request for other insets types since the IME insets has being
// requested hidden.
handlePendingControlRequest(statsToken);
getImeSourceConsumer().removeSurface();
}
applyAnimation(typesReady, false /* show */, fromIme, statsToken);
}
@Override
public void controlWindowInsetsAnimation(@InsetsType int types, long durationMillis,
@Nullable Interpolator interpolator,
@Nullable CancellationSignal cancellationSignal,
@NonNull WindowInsetsAnimationControlListener listener) {
controlWindowInsetsAnimation(types, cancellationSignal, listener,
false /* fromIme */, durationMillis, interpolator, ANIMATION_TYPE_USER);
}
private void controlWindowInsetsAnimation(@InsetsType int types,
@Nullable CancellationSignal cancellationSignal,
WindowInsetsAnimationControlListener listener,
boolean fromIme, long durationMs, @Nullable Interpolator interpolator,
@AnimationType int animationType) {
if ((mState.calculateUncontrollableInsetsFromFrame(mFrame) & types) != 0) {
listener.onCancelled(null);
return;
}
if (fromIme) {
ImeTracing.getInstance().triggerClientDump(
"InsetsController#controlWindowInsetsAnimation",
mHost.getInputMethodManager(), null /* icProto */);
}
controlAnimationUnchecked(types, cancellationSignal, listener, mFrame, fromIme, durationMs,
interpolator, animationType, getLayoutInsetsDuringAnimationMode(types),
false /* useInsetsAnimationThread */, null /* statsToken */);
}
private void controlAnimationUnchecked(@InsetsType int types,
@Nullable CancellationSignal cancellationSignal,
WindowInsetsAnimationControlListener listener, @Nullable Rect frame, boolean fromIme,
long durationMs, Interpolator interpolator,
@AnimationType int animationType,
@LayoutInsetsDuringAnimation int layoutInsetsDuringAnimation,
boolean useInsetsAnimationThread, @Nullable ImeTracker.Token statsToken) {
final boolean visible = layoutInsetsDuringAnimation == LAYOUT_INSETS_DURING_ANIMATION_SHOWN;
// Basically, we accept the requested visibilities from the upstream callers...
setRequestedVisibleTypes(visible ? types : 0, types);
// However, we might reject the request in some cases, such as delaying showing IME or
// rejecting showing IME.
controlAnimationUncheckedInner(types, cancellationSignal, listener, frame, fromIme,
durationMs, interpolator, animationType, layoutInsetsDuringAnimation,
useInsetsAnimationThread, statsToken);
// We are finishing setting the requested visible types. Report them to the server and/or
// the app.
reportRequestedVisibleTypes();
}
private void controlAnimationUncheckedInner(@InsetsType int types,
@Nullable CancellationSignal cancellationSignal,
WindowInsetsAnimationControlListener listener, @Nullable Rect frame, boolean fromIme,
long durationMs, Interpolator interpolator,
@AnimationType int animationType,
@LayoutInsetsDuringAnimation int layoutInsetsDuringAnimation,
boolean useInsetsAnimationThread, @Nullable ImeTracker.Token statsToken) {
if ((types & mTypesBeingCancelled) != 0) {
final boolean monitoredAnimation =
animationType == ANIMATION_TYPE_SHOW || animationType == ANIMATION_TYPE_HIDE;
if (monitoredAnimation && (types & Type.ime()) != 0) {
if (animationType == ANIMATION_TYPE_SHOW) {
ImeTracker.forLatency().onShowCancelled(statsToken,
PHASE_CLIENT_ANIMATION_CANCEL, ActivityThread::currentApplication);
} else {
ImeTracker.forLatency().onHideCancelled(statsToken,
PHASE_CLIENT_ANIMATION_CANCEL, ActivityThread::currentApplication);
}
ImeTracker.forLogging().onCancelled(statsToken,
ImeTracker.PHASE_CLIENT_CONTROL_ANIMATION);
}
throw new IllegalStateException("Cannot start a new insets animation of "
+ Type.toString(types)
+ " while an existing " + Type.toString(mTypesBeingCancelled)
+ " is being cancelled.");
}
ImeTracker.forLogging().onProgress(statsToken, ImeTracker.PHASE_CLIENT_CONTROL_ANIMATION);
if (types == 0) {
// nothing to animate.
listener.onCancelled(null);
if (DEBUG) Log.d(TAG, "no types to animate in controlAnimationUnchecked");
Trace.asyncTraceEnd(TRACE_TAG_VIEW, "IC.showRequestFromApi", 0);
Trace.asyncTraceEnd(TRACE_TAG_VIEW, "IC.showRequestFromApiToImeReady", 0);
return;
}
if (DEBUG) Log.d(TAG, "controlAnimation types: " + types);
mLastStartedAnimTypes |= types;
final SparseArray<InsetsSourceControl> controls = new SparseArray<>();
Pair<Integer, Boolean> typesReadyPair = collectSourceControls(
fromIme, types, controls, animationType, statsToken);
int typesReady = typesReadyPair.first;
boolean imeReady = typesReadyPair.second;
if (DEBUG) Log.d(TAG, String.format(
"controlAnimationUnchecked, typesReady: %s imeReady: %s", typesReady, imeReady));
if (!imeReady) {
// IME isn't ready, all requested types will be animated once IME is ready
abortPendingImeControlRequest();
final PendingControlRequest request = new PendingControlRequest(types,
listener, durationMs,
interpolator, animationType, layoutInsetsDuringAnimation, cancellationSignal,
useInsetsAnimationThread);
mPendingImeControlRequest = request;
mHandler.postDelayed(mPendingControlTimeout, PENDING_CONTROL_TIMEOUT_MS);
if (DEBUG) Log.d(TAG, "Ime not ready. Create pending request");
if (cancellationSignal != null) {
cancellationSignal.setOnCancelListener(() -> {
if (mPendingImeControlRequest == request) {
if (DEBUG) Log.d(TAG,
"Cancellation signal abortPendingImeControlRequest");
abortPendingImeControlRequest();
}
});
}
// The requested visibilities should be delayed as well. Otherwise, we might override
// the insets visibility before playing animation.
setRequestedVisibleTypes(mReportedRequestedVisibleTypes, types);
Trace.asyncTraceEnd(TRACE_TAG_VIEW, "IC.showRequestFromApi", 0);
if (!fromIme) {
Trace.asyncTraceEnd(TRACE_TAG_VIEW, "IC.showRequestFromApiToImeReady", 0);
}
return;
}
if (typesReady == 0) {
if (DEBUG) Log.d(TAG, "No types ready. onCancelled()");
listener.onCancelled(null);
Trace.asyncTraceEnd(TRACE_TAG_VIEW, "IC.showRequestFromApi", 0);
if (!fromIme) {
Trace.asyncTraceEnd(TRACE_TAG_VIEW, "IC.showRequestFromApiToImeReady", 0);
}
return;
}
cancelExistingControllers(typesReady);
final InsetsAnimationControlRunner runner = useInsetsAnimationThread
? new InsetsAnimationThreadControlRunner(controls,
frame, mState, listener, typesReady, this, durationMs, interpolator,
animationType, layoutInsetsDuringAnimation, mHost.getTranslator(),
mHost.getHandler(), statsToken)
: new InsetsAnimationControlImpl(controls,
frame, mState, listener, typesReady, this, durationMs, interpolator,
animationType, layoutInsetsDuringAnimation, mHost.getTranslator(),
statsToken);
if ((typesReady & WindowInsets.Type.ime()) != 0) {
ImeTracing.getInstance().triggerClientDump("InsetsAnimationControlImpl",
mHost.getInputMethodManager(), null /* icProto */);
if (animationType == ANIMATION_TYPE_HIDE) {
ImeTracker.forLatency().onHidden(statsToken, ActivityThread::currentApplication);
}
}
ImeTracker.forLogging().onProgress(statsToken, ImeTracker.PHASE_CLIENT_ANIMATION_RUNNING);
if (mRunningAnimations.isEmpty()) {
mHost.notifyAnimationRunningStateChanged(true);
}
mRunningAnimations.add(new RunningAnimation(runner, animationType));
if (DEBUG) Log.d(TAG, "Animation added to runner. useInsetsAnimationThread: "
+ useInsetsAnimationThread);
if (cancellationSignal != null) {
cancellationSignal.setOnCancelListener(() -> {
cancelAnimation(runner, true /* invokeCallback */);
});
} else {
Trace.asyncTraceBegin(TRACE_TAG_VIEW, "IC.pendingAnim", 0);
}
onAnimationStateChanged(types, true /* running */);
if (fromIme) {
switch (animationType) {
case ANIMATION_TYPE_SHOW:
Trace.asyncTraceEnd(TRACE_TAG_VIEW, "IC.showRequestFromIme", 0);
break;
case ANIMATION_TYPE_HIDE:
Trace.asyncTraceEnd(TRACE_TAG_VIEW, "IC.hideRequestFromIme", 0);
break;
}
} else if (animationType == ANIMATION_TYPE_HIDE) {
Trace.asyncTraceEnd(TRACE_TAG_VIEW, "IC.hideRequestFromApi", 0);
}
}
// TODO(b/242962223): Make this setter restrictive.
@Override
public void setSystemDrivenInsetsAnimationLoggingListener(
@Nullable WindowInsetsAnimationControlListener listener) {
mLoggingListener = listener;
}
/**
* @return Pair of (types ready to animate, IME ready to animate).
*/
private Pair<Integer, Boolean> collectSourceControls(boolean fromIme, @InsetsType int types,
SparseArray<InsetsSourceControl> controls, @AnimationType int animationType,
@Nullable ImeTracker.Token statsToken) {
ImeTracker.forLogging().onProgress(statsToken,
ImeTracker.PHASE_CLIENT_COLLECT_SOURCE_CONTROLS);
int typesReady = 0;
boolean imeReady = true;
for (int i = mSourceConsumers.size() - 1; i >= 0; i--) {
final InsetsSourceConsumer consumer = mSourceConsumers.valueAt(i);
if ((consumer.getType() & types) == 0) {
continue;
}
boolean show = animationType == ANIMATION_TYPE_SHOW
|| animationType == ANIMATION_TYPE_USER;
boolean canRun = true;
if (show) {
// Show request
switch(consumer.requestShow(fromIme, statsToken)) {
case ShowResult.SHOW_IMMEDIATELY:
break;
case ShowResult.IME_SHOW_DELAYED:
imeReady = false;
if (DEBUG) Log.d(TAG, "requestShow IME_SHOW_DELAYED");
break;
case ShowResult.IME_SHOW_FAILED:
if (WARN) Log.w(TAG, "requestShow IME_SHOW_FAILED. fromIme: "
+ fromIme);
// IME cannot be shown (since it didn't have focus), proceed
// with animation of other types.
canRun = false;
// Reject the show request.
setRequestedVisibleTypes(0 /* visibleTypes */, consumer.getType());
break;
}
} else {
consumer.requestHide(fromIme, statsToken);
}
if (!canRun) {
if (WARN) Log.w(TAG, String.format(
"collectSourceControls can't continue show for type: %s fromIme: %b",
WindowInsets.Type.toString(consumer.getType()), fromIme));
continue;
}
final InsetsSourceControl control = consumer.getControl();
if (control != null
&& (control.getLeash() != null || control.getId() == ID_IME_CAPTION_BAR)) {
controls.put(control.getId(), new InsetsSourceControl(control));
typesReady |= consumer.getType();
}
}
return new Pair<>(typesReady, imeReady);
}
private @LayoutInsetsDuringAnimation int getLayoutInsetsDuringAnimationMode(
@InsetsType int types) {
// Generally, we want to layout the opposite of the current state. This is to make animation
// callbacks easy to use: The can capture the layout values and then treat that as end-state
// during the animation.
//
// However, if controlling multiple sources, we want to treat it as shown if any of the
// types is currently hidden.
return (mRequestedVisibleTypes & types) != types
? LAYOUT_INSETS_DURING_ANIMATION_SHOWN
: LAYOUT_INSETS_DURING_ANIMATION_HIDDEN;
}
private void cancelExistingControllers(@InsetsType int types) {
final int originalmTypesBeingCancelled = mTypesBeingCancelled;
mTypesBeingCancelled |= types;
try {
for (int i = mRunningAnimations.size() - 1; i >= 0; i--) {
InsetsAnimationControlRunner control = mRunningAnimations.get(i).runner;
if ((control.getTypes() & types) != 0) {
cancelAnimation(control, true /* invokeCallback */);
}
}
if ((types & ime()) != 0) {
abortPendingImeControlRequest();
}
} finally {
mTypesBeingCancelled = originalmTypesBeingCancelled;
}
}
private void abortPendingImeControlRequest() {
if (mPendingImeControlRequest != null) {
mPendingImeControlRequest.listener.onCancelled(null);
mPendingImeControlRequest = null;
mHandler.removeCallbacks(mPendingControlTimeout);
if (DEBUG) Log.d(TAG, "abortPendingImeControlRequest");
}
}
@VisibleForTesting
@Override
public void notifyFinished(InsetsAnimationControlRunner runner, boolean shown) {
setRequestedVisibleTypes(shown ? runner.getTypes() : 0, runner.getTypes());
cancelAnimation(runner, false /* invokeCallback */);
if (DEBUG) Log.d(TAG, "notifyFinished. shown: " + shown);
if (runner.getAnimationType() == ANIMATION_TYPE_RESIZE) {
// The resize animation doesn't show or hide the insets. We shouldn't change the
// requested visibility.
return;
}
final ImeTracker.Token statsToken = runner.getStatsToken();
if (shown) {
ImeTracker.forLogging().onProgress(statsToken,
ImeTracker.PHASE_CLIENT_ANIMATION_FINISHED_SHOW);
ImeTracker.forLogging().onShown(statsToken);
} else {
ImeTracker.forLogging().onProgress(statsToken,
ImeTracker.PHASE_CLIENT_ANIMATION_FINISHED_HIDE);
ImeTracker.forLogging().onHidden(statsToken);
}
reportRequestedVisibleTypes();
}
@Override
public void applySurfaceParams(final SyncRtSurfaceTransactionApplier.SurfaceParams... params) {
mHost.applySurfaceParams(params);
}
void notifyControlRevoked(InsetsSourceConsumer consumer) {
final @InsetsType int type = consumer.getType();
for (int i = mRunningAnimations.size() - 1; i >= 0; i--) {
InsetsAnimationControlRunner control = mRunningAnimations.get(i).runner;
control.notifyControlRevoked(type);
if (control.getControllingTypes() == 0) {
cancelAnimation(control, true /* invokeCallback */);
}
}
if (type == ime()) {
abortPendingImeControlRequest();
}
if (consumer.getType() != ime()) {
// IME consumer should always be there since we need to communicate with
// InputMethodManager no matter we have the control or not.
mSourceConsumers.remove(consumer.getId());
}
}
private void cancelAnimation(InsetsAnimationControlRunner control, boolean invokeCallback) {
if (invokeCallback) {
ImeTracker.forLogging().onCancelled(control.getStatsToken(),
PHASE_CLIENT_ANIMATION_CANCEL);
control.cancel();
} else {
// Succeeds if invokeCallback is false (i.e. when called from notifyFinished).
ImeTracker.forLogging().onProgress(control.getStatsToken(),
PHASE_CLIENT_ANIMATION_CANCEL);
}
if (DEBUG) {
Log.d(TAG, TextUtils.formatSimple(
"cancelAnimation of types: %d, animType: %d, host: %s",
control.getTypes(), control.getAnimationType(), mHost.getRootViewTitle()));
}
@InsetsType int removedTypes = 0;
for (int i = mRunningAnimations.size() - 1; i >= 0; i--) {
RunningAnimation runningAnimation = mRunningAnimations.get(i);
if (runningAnimation.runner == control) {
mRunningAnimations.remove(i);
removedTypes = control.getTypes();
if (invokeCallback) {
dispatchAnimationEnd(runningAnimation.runner.getAnimation());
}
break;
}
}
if (mRunningAnimations.isEmpty()) {
mHost.notifyAnimationRunningStateChanged(false);
}
onAnimationStateChanged(removedTypes, false /* running */);
}
private void onAnimationStateChanged(@InsetsType int types, boolean running) {
boolean insetsChanged = false;
for (int i = mSourceConsumers.size() - 1; i >= 0; i--) {
final InsetsSourceConsumer consumer = mSourceConsumers.valueAt(i);
if ((consumer.getType() & types) != 0) {
insetsChanged |= consumer.onAnimationStateChanged(running);
}
}
if (insetsChanged) {
notifyVisibilityChanged();
}
}
private void applyLocalVisibilityOverride() {
for (int i = mSourceConsumers.size() - 1; i >= 0; i--) {
final InsetsSourceConsumer consumer = mSourceConsumers.valueAt(i);
consumer.applyLocalVisibilityOverride();
}
}
@VisibleForTesting
public @NonNull InsetsSourceConsumer getSourceConsumer(int id, int type) {
InsetsSourceConsumer consumer = mSourceConsumers.get(id);
if (consumer != null) {
return consumer;
}
if (type == ime() && mImeSourceConsumer != null) {
// WindowInsets.Type.ime() should be only provided by one source.
mSourceConsumers.remove(mImeSourceConsumer.getId());
consumer = mImeSourceConsumer;
consumer.setId(id);
} else {
consumer = mConsumerCreator.apply(this, id, type);
}
mSourceConsumers.put(id, consumer);
return consumer;
}
@VisibleForTesting
public @NonNull InsetsSourceConsumer getImeSourceConsumer() {
return mImeSourceConsumer;
}
void notifyVisibilityChanged() {
mHost.notifyInsetsChanged();
}
/**
* @see ViewRootImpl#updateCompatSysUiVisibility(int, int, int)
*/
public void updateCompatSysUiVisibility() {
if (mCompatSysUiVisibilityStaled) {
mCompatSysUiVisibilityStaled = false;
mHost.updateCompatSysUiVisibility(
// Treat non-existing types as controllable types for compatibility.
mVisibleTypes, mRequestedVisibleTypes, mControllableTypes | ~mExistingTypes);
}
}
/**
* Called when current window gains focus.
*/
public void onWindowFocusGained(boolean hasViewFocused) {
mImeSourceConsumer.onWindowFocusGained(hasViewFocused);
}
/**
* Called when current window loses focus.
*/
public void onWindowFocusLost() {
mImeSourceConsumer.onWindowFocusLost();
}
@VisibleForTesting
public @AnimationType int getAnimationType(@InsetsType int type) {
for (int i = mRunningAnimations.size() - 1; i >= 0; i--) {
InsetsAnimationControlRunner control = mRunningAnimations.get(i).runner;
if (control.controlsType(type)) {
return mRunningAnimations.get(i).type;
}
}
return ANIMATION_TYPE_NONE;
}
@VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
public void setRequestedVisibleTypes(@InsetsType int visibleTypes, @InsetsType int mask) {
final @InsetsType int requestedVisibleTypes =
(mRequestedVisibleTypes & ~mask) | (visibleTypes & mask);
if (mRequestedVisibleTypes != requestedVisibleTypes) {
mRequestedVisibleTypes = requestedVisibleTypes;
}
}
/**
* Called when finishing setting requested visible types or finishing setting controls.
*/
private void reportRequestedVisibleTypes() {
if (mReportedRequestedVisibleTypes != mRequestedVisibleTypes) {
final @InsetsType int diff = mRequestedVisibleTypes ^ mReportedRequestedVisibleTypes;
if (WindowInsets.Type.hasCompatSystemBars(diff)) {
mCompatSysUiVisibilityStaled = true;
}
mReportedRequestedVisibleTypes = mRequestedVisibleTypes;
mHost.updateRequestedVisibleTypes(mReportedRequestedVisibleTypes);
}
updateCompatSysUiVisibility();
}
@VisibleForTesting
public void applyAnimation(@InsetsType final int types, boolean show, boolean fromIme,
@Nullable ImeTracker.Token statsToken) {
// TODO(b/166736352): We should only skip the animation of specific types, not all types.
boolean skipAnim = false;
if ((types & ime()) != 0) {
final InsetsSourceControl imeControl = mImeSourceConsumer.getControl();
// Skip showing animation once that made by system for some reason.
// (e.g. starting window with IME snapshot)
if (imeControl != null) {
skipAnim = imeControl.getAndClearSkipAnimationOnce() && show
&& mImeSourceConsumer.hasViewFocusWhenWindowFocusGain();
}
}
applyAnimation(types, show, fromIme, skipAnim, statsToken);
}
@VisibleForTesting
public void applyAnimation(@InsetsType final int types, boolean show, boolean fromIme,
boolean skipAnim, @Nullable ImeTracker.Token statsToken) {
if (types == 0) {
// nothing to animate.
if (DEBUG) Log.d(TAG, "applyAnimation, nothing to animate");
Trace.asyncTraceEnd(TRACE_TAG_VIEW, "IC.showRequestFromApi", 0);
if (!fromIme) {
Trace.asyncTraceEnd(TRACE_TAG_VIEW, "IC.showRequestFromApiToImeReady", 0);
}
return;
}
boolean hasAnimationCallbacks = mHost.hasAnimationCallbacks();
final InternalAnimationControlListener listener = new InternalAnimationControlListener(
show, hasAnimationCallbacks, types, mHost.getSystemBarsBehavior(),
skipAnim || mAnimationsDisabled, mHost.dipToPx(FLOATING_IME_BOTTOM_INSET_DP),
mLoggingListener, mJankContext);
// We are about to playing the default animation (show/hide). Passing a null frame indicates
// the controlled types should be animated regardless of the frame.
controlAnimationUnchecked(
types, null /* cancellationSignal */, listener, null /* frame */, fromIme,
listener.getDurationMs(), listener.getInsetsInterpolator(),
show ? ANIMATION_TYPE_SHOW : ANIMATION_TYPE_HIDE,
show ? LAYOUT_INSETS_DURING_ANIMATION_SHOWN : LAYOUT_INSETS_DURING_ANIMATION_HIDDEN,
!hasAnimationCallbacks /* useInsetsAnimationThread */, statsToken);
}
/**
* Cancel on-going animation to show/hide {@link InsetsType}.
*/
@VisibleForTesting
public void cancelExistingAnimations() {
cancelExistingControllers(all());
}
void dump(String prefix, PrintWriter pw) {
pw.print(prefix); pw.println("InsetsController:");
mState.dump(prefix + " ", pw);
}
void dumpDebug(ProtoOutputStream proto, long fieldId) {
final long token = proto.start(fieldId);
mState.dumpDebug(proto, STATE);
for (int i = mRunningAnimations.size() - 1; i >= 0; i--) {
InsetsAnimationControlRunner runner = mRunningAnimations.get(i).runner;
runner.dumpDebug(proto, CONTROL);
}
proto.end(token);
}
@VisibleForTesting
@Override
public <T extends InsetsAnimationControlRunner & InternalInsetsAnimationController>
void startAnimation(T runner, WindowInsetsAnimationControlListener listener, int types,
WindowInsetsAnimation animation, Bounds bounds) {
mHost.dispatchWindowInsetsAnimationPrepare(animation);
mHost.addOnPreDrawRunnable(() -> {
if (runner.isCancelled()) {
if (WARN) Log.w(TAG, "startAnimation canceled before preDraw");
return;
}
Trace.asyncTraceBegin(TRACE_TAG_VIEW,
"InsetsAnimation: " + WindowInsets.Type.toString(types), types);
for (int i = mRunningAnimations.size() - 1; i >= 0; i--) {
RunningAnimation runningAnimation = mRunningAnimations.get(i);
if (runningAnimation.runner == runner) {
runningAnimation.startDispatched = true;
}
}
Trace.asyncTraceEnd(TRACE_TAG_VIEW, "IC.pendingAnim", 0);
mHost.dispatchWindowInsetsAnimationStart(animation, bounds);
mStartingAnimation = true;
runner.setReadyDispatched(true);
listener.onReady(runner, types);
mStartingAnimation = false;
});
}
@VisibleForTesting
public void dispatchAnimationEnd(WindowInsetsAnimation animation) {
Trace.asyncTraceEnd(TRACE_TAG_VIEW,
"InsetsAnimation: " + WindowInsets.Type.toString(animation.getTypeMask()),
animation.getTypeMask());
mHost.dispatchWindowInsetsAnimationEnd(animation);
}
@VisibleForTesting
@Override
public void scheduleApplyChangeInsets(InsetsAnimationControlRunner runner) {
if (mStartingAnimation || runner.getAnimationType() == ANIMATION_TYPE_USER) {
mAnimCallback.run();
mAnimCallbackScheduled = false;
return;
}
if (!mAnimCallbackScheduled) {
mHost.postInsetsAnimationCallback(mAnimCallback);
mAnimCallbackScheduled = true;
}
}
@Override
public void setSystemBarsAppearance(@Appearance int appearance, @Appearance int mask) {
mHost.setSystemBarsAppearance(appearance, mask);
}
@Override
public @Appearance int getSystemBarsAppearance() {
if (!mHost.isSystemBarsAppearanceControlled()) {
// We only return the requested appearance, not the implied one.
return 0;
}
return mHost.getSystemBarsAppearance();
}
@Override
public void setCaptionInsetsHeight(int height) {
// This method is to be removed once the caption is moved to the shell.
if (CAPTION_ON_SHELL) {
return;
}
if (mCaptionInsetsHeight != height) {
mCaptionInsetsHeight = height;
if (mCaptionInsetsHeight != 0) {
mState.getOrCreateSource(ID_CAPTION_BAR, captionBar()).setFrame(
mFrame.left, mFrame.top, mFrame.right, mFrame.top + mCaptionInsetsHeight);
} else {
mState.removeSource(ID_CAPTION_BAR);
}
mHost.notifyInsetsChanged();
}
}
@Override
public void setImeCaptionBarInsetsHeight(int height) {
if (!ENABLE_HIDE_IME_CAPTION_BAR) {
return;
}
Rect newFrame = new Rect(mFrame.left, mFrame.bottom - height, mFrame.right, mFrame.bottom);
InsetsSource source = mState.peekSource(ID_IME_CAPTION_BAR);
if (mImeCaptionBarInsetsHeight != height
|| (source != null && !newFrame.equals(source.getFrame()))) {
mImeCaptionBarInsetsHeight = height;
if (mImeCaptionBarInsetsHeight != 0) {
mState.getOrCreateSource(ID_IME_CAPTION_BAR, captionBar())
.setFrame(newFrame);
getSourceConsumer(ID_IME_CAPTION_BAR, captionBar()).setControl(
new InsetsSourceControl(ID_IME_CAPTION_BAR, captionBar(),
null /* leash */, false /* initialVisible */,
new Point(), Insets.NONE),
new int[1], new int[1]);
} else {
mState.removeSource(ID_IME_CAPTION_BAR);
InsetsSourceConsumer sourceConsumer = mSourceConsumers.get(ID_IME_CAPTION_BAR);
if (sourceConsumer != null) {
sourceConsumer.setControl(null, new int[1], new int[1]);
}
}
mHost.notifyInsetsChanged();
}
}
@Override
public void setSystemBarsBehavior(@Behavior int behavior) {
mHost.setSystemBarsBehavior(behavior);
}
@Override
public @Behavior int getSystemBarsBehavior() {
if (!mHost.isSystemBarsBehaviorControlled()) {
// We only return the requested behavior, not the implied one.
return 0;
}
return mHost.getSystemBarsBehavior();
}
@Override
public void setAnimationsDisabled(boolean disable) {
mAnimationsDisabled = disable;
}
private @InsetsType int calculateControllableTypes() {
@InsetsType int result = 0;
for (int i = mSourceConsumers.size() - 1; i >= 0; i--) {
InsetsSourceConsumer consumer = mSourceConsumers.valueAt(i);
InsetsSource source = mState.peekSource(consumer.getId());
if (consumer.getControl() != null && source != null) {
result |= consumer.getType();
}
}
return result & ~mState.calculateUncontrollableInsetsFromFrame(mFrame);
}
/**
* @return The types that are now animating due to a listener invoking control/show/hide
*/
private @InsetsType int invokeControllableInsetsChangedListeners() {
mHandler.removeCallbacks(mInvokeControllableInsetsChangedListeners);
mLastStartedAnimTypes = 0;
@InsetsType int types = calculateControllableTypes();
int size = mControllableInsetsChangedListeners.size();
for (int i = 0; i < size; i++) {
mControllableInsetsChangedListeners.get(i).onControllableInsetsChanged(this, types);
}
return mLastStartedAnimTypes;
}
@Override
public void addOnControllableInsetsChangedListener(
OnControllableInsetsChangedListener listener) {
Objects.requireNonNull(listener);
mControllableInsetsChangedListeners.add(listener);
listener.onControllableInsetsChanged(this, calculateControllableTypes());
}
@Override
public void removeOnControllableInsetsChangedListener(
OnControllableInsetsChangedListener listener) {
Objects.requireNonNull(listener);
mControllableInsetsChangedListeners.remove(listener);
}
@Override
public void releaseSurfaceControlFromRt(SurfaceControl sc) {
mHost.releaseSurfaceControlFromRt(sc);
}
@Override
public void reportPerceptible(@InsetsType int types, boolean perceptible) {
final int size = mSourceConsumers.size();
for (int i = 0; i < size; i++) {
final InsetsSourceConsumer consumer = mSourceConsumers.valueAt(i);
if ((consumer.getType() & types) != 0) {
consumer.onPerceptible(perceptible);
}
}
}
Host getHost() {
return mHost;
}
}