blob: d63c25a093829cf39e686d6e938496709c3bb117 [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.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 android.view.InsetsState.ITYPE_IME;
import static android.view.InsetsState.getDefaultVisibility;
import static android.view.InsetsState.toPublicType;
import static com.android.internal.annotations.VisibleForTesting.Visibility.PACKAGE;
import android.annotation.IntDef;
import android.annotation.Nullable;
import android.graphics.Rect;
import android.util.ArraySet;
import android.util.Log;
import android.util.proto.ProtoOutputStream;
import android.view.InsetsState.InternalInsetsType;
import android.view.SurfaceControl.Transaction;
import android.view.WindowInsets.Type.InsetsType;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.inputmethod.ImeTracing;
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 immidiately.
*/
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 final InsetsController mController;
protected boolean mRequestedVisible;
protected final InsetsState mState;
protected final @InternalInsetsType int mType;
private static final String TAG = "InsetsSourceConsumer";
private final Supplier<Transaction> mTransactionSupplier;
private @Nullable 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 type The {@link InternalInsetsType} 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(@InternalInsetsType int type, InsetsState state,
Supplier<Transaction> transactionSupplier, InsetsController controller) {
mType = type;
mState = state;
mTransactionSupplier = transactionSupplier;
mController = controller;
mRequestedVisible = getDefaultVisibility(type);
}
/**
* 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 (mType == ITYPE_IME) {
ImeTracing.getInstance().triggerClientDump("InsetsSourceConsumer#setControl",
mController.getHost().getInputMethodManager(), null /* icProto */);
}
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",
InsetsState.typeToString(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 source = mState.getSource(mType);
final boolean serverVisibility =
mController.getLastDispatchedState().getSourceOrDefaultVisibility(mType);
if (source.isVisible() != serverVisibility) {
source.setVisible(serverVisibility);
mController.notifyVisibilityChanged();
}
// For updateCompatSysUiVisibility
applyLocalVisibilityOverride();
} 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] |= toPublicType(getType());
} else {
hideTypes[0] |= toPublicType(getType());
}
} 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.
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
public InsetsSourceControl getControl() {
return mSourceControl;
}
/**
* Determines if the consumer will be shown after control is available.
* Note: for system bars this method is same as {@link #isRequestedVisible()}.
*
* @return {@code true} if consumer has a pending show.
*/
protected boolean isRequestedVisibleAwaitingControl() {
return isRequestedVisible();
}
int getType() {
return mType;
}
@VisibleForTesting
public void show(boolean fromIme) {
if (DEBUG) Log.d(TAG, String.format("Call show() for type: %s fromIme: %b ",
InsetsState.typeToString(mType), fromIme));
setRequestedVisible(true);
}
@VisibleForTesting
public void hide() {
if (DEBUG) Log.d(TAG, String.format("Call hide for %s on %s",
InsetsState.typeToString(mType), mController.getHost().getRootViewTitle()));
setRequestedVisible(false);
}
void hide(boolean animationFinished, @AnimationType int animationType) {
hide();
}
/**
* 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;
}
boolean applyLocalVisibilityOverride() {
final InsetsSource source = mState.peekSource(mType);
final boolean isVisible = source != null ? source.isVisible() : getDefaultVisibility(mType);
final boolean hasControl = mSourceControl != null;
if (mType == ITYPE_IME) {
ImeTracing.getInstance().triggerClientDump(
"InsetsSourceConsumer#applyLocalVisibilityOverride",
mController.getHost().getInputMethodManager(), null /* icProto */);
}
updateCompatSysUiVisibility(hasControl, source, isVisible);
// If we don't have control, we are not able to change the visibility.
if (!hasControl) {
if (DEBUG) Log.d(TAG, "applyLocalVisibilityOverride: No control in "
+ mController.getHost().getRootViewTitle()
+ " requestedVisible " + mRequestedVisible);
return false;
}
if (isVisible == mRequestedVisible) {
return false;
}
if (DEBUG) Log.d(TAG, String.format("applyLocalVisibilityOverride: %s requestedVisible: %b",
mController.getHost().getRootViewTitle(), mRequestedVisible));
mState.getSource(mType).setVisible(mRequestedVisible);
return true;
}
private void updateCompatSysUiVisibility(boolean hasControl, InsetsSource source,
boolean visible) {
final @InsetsType int publicType = InsetsState.toPublicType(mType);
if (publicType != WindowInsets.Type.statusBars()
&& publicType != WindowInsets.Type.navigationBars()) {
// System UI visibility only controls status bars and navigation bars.
return;
}
final boolean compatVisible;
if (hasControl) {
compatVisible = mRequestedVisible;
} else if (source != null && !source.getFrame().isEmpty()) {
compatVisible = visible;
} else {
final ArraySet<Integer> types = InsetsState.toInternalType(publicType);
for (int i = types.size() - 1; i >= 0; i--) {
final InsetsSource s = mState.peekSource(types.valueAt(i));
if (s != null && !s.getFrame().isEmpty()) {
// The compat system UI visibility would be updated by another consumer which
// handles the same public insets type.
return;
}
}
// No one provides the public type. Use the requested visibility for making the callback
// behavior compatible.
compatVisible = mRequestedVisible;
}
mController.updateCompatSysUiVisibility(mType, compatVisible, hasControl);
}
@VisibleForTesting
public boolean isRequestedVisible() {
return mRequestedVisible;
}
/**
* 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}).
* @return @see {@link ShowResult}.
*/
@VisibleForTesting
public @ShowResult int requestShow(boolean fromController) {
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.
*/
void notifyHidden() {
// no-op for types that always return ShowResult#SHOW_IMMEDIATELY.
}
/**
* 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(mType);
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);
}
@VisibleForTesting(visibility = PACKAGE)
public boolean notifyAnimationFinished() {
if (mPendingFrame != null) {
InsetsSource source = mState.getSource(mType);
source.setFrame(mPendingFrame);
source.setVisibleFrame(mPendingVisibleFrame);
mPendingFrame = null;
mPendingVisibleFrame = null;
return true;
}
return false;
}
/**
* Sets requested visibility from the client, regardless of whether we are able to control it at
* the moment.
*/
protected void setRequestedVisible(boolean requestedVisible) {
if (mRequestedVisible != requestedVisible) {
mRequestedVisible = requestedVisible;
mController.onRequestedVisibilityChanged(this);
if (DEBUG) Log.d(TAG, "setRequestedVisible: " + requestedVisible);
}
if (applyLocalVisibilityOverride()) {
mController.notifyVisibilityChanged();
}
}
private void applyRequestedVisibilityToControl() {
if (mSourceControl == null || mSourceControl.getLeash() == null) {
return;
}
try (Transaction t = mTransactionSupplier.get()) {
if (DEBUG) Log.d(TAG, "applyRequestedVisibilityToControl: " + mRequestedVisible);
if (mRequestedVisible) {
t.show(mSourceControl.getLeash());
} else {
t.hide(mSourceControl.getLeash());
}
// Ensure the alpha value is aligned with the actual requested visibility.
t.setAlpha(mSourceControl.getLeash(), mRequestedVisible ? 1 : 0);
t.apply();
}
onPerceptible(mRequestedVisible);
}
void dumpDebug(ProtoOutputStream proto, long fieldId) {
final long token = proto.start(fieldId);
proto.write(INTERNAL_INSETS_TYPE, InsetsState.typeToString(mType));
proto.write(HAS_WINDOW_FOCUS, mHasWindowFocus);
proto.write(IS_REQUESTED_VISIBLE, mRequestedVisible);
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.end(token);
}
}