blob: 34b288460a67bac11d8b3b85e66e104eb2931b30 [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.view.InsetsController.ANIMATION_TYPE_NONE;
import static android.view.InsetsController.AnimationType;
import static android.view.InsetsController.DEBUG;
import static android.view.InsetsSourceConsumerProto.ANIMATION_STATE;
import static android.view.InsetsSourceConsumerProto.HAS_WINDOW_FOCUS;
import static android.view.InsetsSourceConsumerProto.INTERNAL_INSETS_TYPE;
import static android.view.InsetsSourceConsumerProto.IS_REQUESTED_VISIBLE;
import static android.view.InsetsSourceConsumerProto.PENDING_FRAME;
import static android.view.InsetsSourceConsumerProto.PENDING_VISIBLE_FRAME;
import static android.view.InsetsSourceConsumerProto.SOURCE_CONTROL;
import static com.android.internal.annotations.VisibleForTesting.Visibility.PACKAGE;
import android.annotation.IntDef;
import android.annotation.Nullable;
import android.graphics.Rect;
import android.util.Log;
import android.util.proto.ProtoOutputStream;
import android.view.SurfaceControl.Transaction;
import android.view.WindowInsets.Type.InsetsType;
import android.view.inputmethod.ImeTracker;
import com.android.internal.annotations.VisibleForTesting;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.Objects;
import java.util.function.Supplier;
/**
* Controls the visibility and animations of a single window insets source.
* @hide
*/
public class InsetsSourceConsumer {
@Retention(RetentionPolicy.SOURCE)
@IntDef(value = {
ShowResult.SHOW_IMMEDIATELY,
ShowResult.IME_SHOW_DELAYED,
ShowResult.IME_SHOW_FAILED
})
@interface ShowResult {
/**
* Window type is ready to be shown, will be shown immediately.
*/
int SHOW_IMMEDIATELY = 0;
/**
* Result will be delayed. Window needs to be prepared or request is not from controller.
* Request will be delegated to controller and may or may not be shown.
*/
int IME_SHOW_DELAYED = 1;
/**
* Window will not be shown because one of the conditions couldn't be met.
* (e.g. in IME's case, when no editor is focused.)
*/
int IME_SHOW_FAILED = 2;
}
protected static final int ANIMATION_STATE_NONE = 0;
protected static final int ANIMATION_STATE_SHOW = 1;
protected static final int ANIMATION_STATE_HIDE = 2;
protected int mAnimationState = ANIMATION_STATE_NONE;
protected final InsetsController mController;
protected final InsetsState mState;
private int mId;
@InsetsType
private final int mType;
private static final String TAG = "InsetsSourceConsumer";
private final Supplier<Transaction> mTransactionSupplier;
@Nullable
private InsetsSourceControl mSourceControl;
private boolean mHasWindowFocus;
/**
* Whether the view has focus returned by {@link #onWindowFocusGained(boolean)}.
*/
private boolean mHasViewFocusWhenWindowFocusGain;
private Rect mPendingFrame;
private Rect mPendingVisibleFrame;
/**
* @param id The ID of the consumed insets.
* @param type The {@link InsetsType} of the consumed insets.
* @param state The current {@link InsetsState} of the consumed insets.
* @param transactionSupplier The source of new {@link Transaction} instances. The supplier
* must provide *new* instances, which will be explicitly closed by this class.
* @param controller The {@link InsetsController} to use for insets interaction.
*/
public InsetsSourceConsumer(int id, @InsetsType int type, InsetsState state,
Supplier<Transaction> transactionSupplier, InsetsController controller) {
mId = id;
mType = type;
mState = state;
mTransactionSupplier = transactionSupplier;
mController = controller;
}
/**
* Updates the control delivered from the server.
* @param showTypes An integer array with a single entry that determines which types a show
* animation should be run after setting the control.
* @param hideTypes An integer array with a single entry that determines which types a hide
* animation should be run after setting the control.
* @return Whether the control has changed from the server
*/
public boolean setControl(@Nullable InsetsSourceControl control,
@InsetsType int[] showTypes, @InsetsType int[] hideTypes) {
if (Objects.equals(mSourceControl, control)) {
if (mSourceControl != null && mSourceControl != control) {
mSourceControl.release(SurfaceControl::release);
mSourceControl = control;
}
return false;
}
final InsetsSourceControl lastControl = mSourceControl;
mSourceControl = control;
if (control != null) {
if (DEBUG) Log.d(TAG, String.format("setControl -> %s on %s",
WindowInsets.Type.toString(control.getType()),
mController.getHost().getRootViewTitle()));
}
if (mSourceControl == null) {
// We are loosing control
mController.notifyControlRevoked(this);
// Check if we need to restore server visibility.
final InsetsSource localSource = mState.peekSource(mId);
final InsetsSource serverSource = mController.getLastDispatchedState().peekSource(mId);
final boolean localVisible = localSource != null && localSource.isVisible();
final boolean serverVisible = serverSource != null && serverSource.isVisible();
if (localSource != null) {
localSource.setVisible(serverVisible);
}
if (localVisible != serverVisible) {
mController.notifyVisibilityChanged();
}
} else {
final boolean requestedVisible = isRequestedVisibleAwaitingControl();
final SurfaceControl oldLeash = lastControl != null ? lastControl.getLeash() : null;
final SurfaceControl newLeash = control.getLeash();
if (newLeash != null && (oldLeash == null || !newLeash.isSameSurface(oldLeash))
&& requestedVisible != control.isInitiallyVisible()) {
// We are gaining leash, and need to run an animation since previous state
// didn't match.
if (DEBUG) Log.d(TAG, String.format("Gaining leash in %s, requestedVisible: %b",
mController.getHost().getRootViewTitle(), requestedVisible));
if (requestedVisible) {
showTypes[0] |= mType;
} else {
hideTypes[0] |= mType;
}
} else {
// We are gaining control, but don't need to run an animation.
// However make sure that the leash visibility is still up to date.
if (applyLocalVisibilityOverride()) {
mController.notifyVisibilityChanged();
}
// If we have a new leash, make sure visibility is up-to-date, even though we
// didn't want to run an animation above.
if (mController.getAnimationType(mType) == ANIMATION_TYPE_NONE) {
applyRequestedVisibilityToControl();
}
// Remove the surface that owned by last control when it lost.
if (!requestedVisible && lastControl == null) {
removeSurface();
}
}
}
if (lastControl != null) {
lastControl.release(SurfaceControl::release);
}
return true;
}
@VisibleForTesting(visibility = PACKAGE)
public InsetsSourceControl getControl() {
return mSourceControl;
}
/**
* Determines if the consumer will be shown after control is available.
*
* @return {@code true} if consumer has a pending show.
*/
protected boolean isRequestedVisibleAwaitingControl() {
return (mController.getRequestedVisibleTypes() & mType) != 0;
}
int getId() {
return mId;
}
void setId(int id) {
mId = id;
}
@InsetsType int getType() {
return mType;
}
/**
* Called right after the animation is started or finished.
*/
@VisibleForTesting(visibility = PACKAGE)
public boolean onAnimationStateChanged(boolean running) {
boolean insetsChanged = false;
if (!running && mPendingFrame != null) {
final InsetsSource source = mState.peekSource(mId);
if (source != null) {
source.setFrame(mPendingFrame);
source.setVisibleFrame(mPendingVisibleFrame);
insetsChanged = true;
}
mPendingFrame = null;
mPendingVisibleFrame = null;
}
final boolean showRequested = isShowRequested();
final boolean cancelledForNewAnimation = !running && showRequested
? mAnimationState == ANIMATION_STATE_HIDE
: mAnimationState == ANIMATION_STATE_SHOW;
mAnimationState = running
? (showRequested ? ANIMATION_STATE_SHOW : ANIMATION_STATE_HIDE)
: ANIMATION_STATE_NONE;
// We apply the visibility override after the animation is started. We don't do this before
// that because we need to know the initial insets state while creating the animation.
// We also need to apply the override after the animation is finished because the requested
// visibility can be set when finishing the user animation.
// If the animation is cancelled because we are going to play a new animation with an
// opposite direction, don't apply it now but after the new animation is started.
if (!cancelledForNewAnimation) {
insetsChanged |= applyLocalVisibilityOverride();
}
return insetsChanged;
}
protected boolean isShowRequested() {
return (mController.getRequestedVisibleTypes() & getType()) != 0;
}
/**
* Called when current window gains focus
*/
public void onWindowFocusGained(boolean hasViewFocus) {
mHasWindowFocus = true;
mHasViewFocusWhenWindowFocusGain = hasViewFocus;
}
/**
* Called when current window loses focus.
*/
public void onWindowFocusLost() {
mHasWindowFocus = false;
}
boolean hasViewFocusWhenWindowFocusGain() {
return mHasViewFocusWhenWindowFocusGain;
}
@VisibleForTesting(visibility = PACKAGE)
public boolean applyLocalVisibilityOverride() {
final InsetsSource source = mState.peekSource(mId);
if (source == null) {
return false;
}
final boolean requestedVisible = (mController.getRequestedVisibleTypes() & mType) != 0;
// If we don't have control, we are not able to change the visibility.
if (mSourceControl == null) {
if (DEBUG) Log.d(TAG, "applyLocalVisibilityOverride: No control in "
+ mController.getHost().getRootViewTitle()
+ " requestedVisible=" + requestedVisible);
return false;
}
if (source.isVisible() == requestedVisible) {
return false;
}
if (DEBUG) Log.d(TAG, String.format("applyLocalVisibilityOverride: %s requestedVisible: %b",
mController.getHost().getRootViewTitle(), requestedVisible));
source.setVisible(requestedVisible);
return true;
}
/**
* Request to show current window type.
*
* @param fromController {@code true} if request is coming from controller.
* (e.g. in IME case, controller is
* {@link android.inputmethodservice.InputMethodService}).
* @param statsToken the token tracking the current IME show request or {@code null} otherwise.
*
* @implNote The {@code statsToken} is ignored here, and only handled in
* {@link ImeInsetsSourceConsumer} for IME animations only.
*
* @return @see {@link ShowResult}.
*/
@VisibleForTesting(visibility = PACKAGE)
@ShowResult
public int requestShow(boolean fromController, @Nullable ImeTracker.Token statsToken) {
return ShowResult.SHOW_IMMEDIATELY;
}
void requestHide(boolean fromController, @Nullable ImeTracker.Token statsToken) {
// no-op for types that always 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) {
}
/**
* Remove surface on which this consumer type is drawn.
*/
public void removeSurface() {
// no-op for types that always return ShowResult#SHOW_IMMEDIATELY.
}
@VisibleForTesting(visibility = PACKAGE)
public void updateSource(InsetsSource newSource, @AnimationType int animationType) {
InsetsSource source = mState.peekSource(mId);
if (source == null || animationType == ANIMATION_TYPE_NONE
|| source.getFrame().equals(newSource.getFrame())) {
mPendingFrame = null;
mPendingVisibleFrame = null;
mState.addSource(newSource);
return;
}
// Frame is changing while animating. Keep note of the new frame but keep existing frame
// until animation is finished.
newSource = new InsetsSource(newSource);
mPendingFrame = new Rect(newSource.getFrame());
mPendingVisibleFrame = newSource.getVisibleFrame() != null
? new Rect(newSource.getVisibleFrame())
: null;
newSource.setFrame(source.getFrame());
newSource.setVisibleFrame(source.getVisibleFrame());
mState.addSource(newSource);
if (DEBUG) Log.d(TAG, "updateSource: " + newSource);
}
private void applyRequestedVisibilityToControl() {
if (mSourceControl == null || mSourceControl.getLeash() == null) {
return;
}
final boolean requestedVisible = (mController.getRequestedVisibleTypes() & mType) != 0;
try (Transaction t = mTransactionSupplier.get()) {
if (DEBUG) Log.d(TAG, "applyRequestedVisibilityToControl: " + requestedVisible);
if (requestedVisible) {
t.show(mSourceControl.getLeash());
} else {
t.hide(mSourceControl.getLeash());
}
// Ensure the alpha value is aligned with the actual requested visibility.
t.setAlpha(mSourceControl.getLeash(), requestedVisible ? 1 : 0);
t.apply();
}
onPerceptible(requestedVisible);
}
void dumpDebug(ProtoOutputStream proto, long fieldId) {
final long token = proto.start(fieldId);
proto.write(INTERNAL_INSETS_TYPE, WindowInsets.Type.toString(mType));
proto.write(HAS_WINDOW_FOCUS, mHasWindowFocus);
proto.write(IS_REQUESTED_VISIBLE, isShowRequested());
if (mSourceControl != null) {
mSourceControl.dumpDebug(proto, SOURCE_CONTROL);
}
if (mPendingFrame != null) {
mPendingFrame.dumpDebug(proto, PENDING_FRAME);
}
if (mPendingVisibleFrame != null) {
mPendingVisibleFrame.dumpDebug(proto, PENDING_VISIBLE_FRAME);
}
proto.write(ANIMATION_STATE, mAnimationState);
proto.end(token);
}
}