/* * Copyright (C) 2019 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.os.Trace.TRACE_TAG_VIEW; import static android.view.ImeInsetsSourceConsumerProto.HAS_PENDING_REQUEST; import static android.view.ImeInsetsSourceConsumerProto.INSETS_SOURCE_CONSUMER; import static android.view.ImeInsetsSourceConsumerProto.IS_REQUESTED_VISIBLE_AWAITING_CONTROL; import android.annotation.NonNull; import android.annotation.Nullable; import android.os.IBinder; import android.os.Trace; import android.util.proto.ProtoOutputStream; import android.view.SurfaceControl.Transaction; import android.view.inputmethod.ImeTracker; import android.view.inputmethod.InputMethodManager; import com.android.internal.inputmethod.ImeTracing; import com.android.internal.inputmethod.SoftInputShowHideReason; import java.util.function.Supplier; /** * Controls the visibility and animations of IME window insets source. * @hide */ public final class ImeInsetsSourceConsumer extends InsetsSourceConsumer { /** * Tracks whether are requested to show during the hide animation or requested to hide during * the show animation. If this is true, we should not remove the surface. */ private boolean mHasPendingRequest; /** * Tracks whether we have an outstanding request from the IME to show, but weren't able to * execute it because we didn't have control yet, or we didn't have a leash on the control yet. */ private boolean mIsRequestedVisibleAwaitingLeash; public ImeInsetsSourceConsumer( int id, InsetsState state, Supplier transactionSupplier, InsetsController controller) { super(id, WindowInsets.Type.ime(), state, transactionSupplier, controller); } @Override public boolean onAnimationStateChanged(boolean running) { if (!running) { ImeTracing.getInstance().triggerClientDump( "ImeInsetsSourceConsumer#onAnimationFinished", mController.getHost().getInputMethodManager(), null /* icProto */); } boolean insetsChanged = super.onAnimationStateChanged(running); if (running && !isShowRequested() && mController.isPredictiveBackImeHideAnimInProgress()) { // IME predictive back animation switched from pre-commit to post-commit. insetsChanged |= applyLocalVisibilityOverride(); } if (!isShowRequested()) { mIsRequestedVisibleAwaitingLeash = false; if (!running && !mHasPendingRequest) { final var statsToken = ImeTracker.forLogging().onStart(ImeTracker.TYPE_HIDE, ImeTracker.ORIGIN_CLIENT, SoftInputShowHideReason.HIDE_SOFT_INPUT_ON_ANIMATION_STATE_CHANGED, mController.getHost().isHandlingPointerEvent() /* fromUser */); notifyHidden(statsToken); removeSurface(); } } // This method is called // (1) after the animation starts. // (2) after the animation ends (including the case of cancel). // (3) if the IME is not controllable (running == false in this case). // We should reset mHasPendingRequest in all cases. mHasPendingRequest = false; return insetsChanged; } @Override public void onWindowFocusGained(boolean hasViewFocus) { super.onWindowFocusGained(hasViewFocus); getImm().registerImeConsumer(this); if ((mController.getRequestedVisibleTypes() & getType()) != 0 && !hasLeash()) { mIsRequestedVisibleAwaitingLeash = true; } } @Override public void onWindowFocusLost() { super.onWindowFocusLost(); getImm().unregisterImeConsumer(this); mIsRequestedVisibleAwaitingLeash = false; } @Override public boolean applyLocalVisibilityOverride() { ImeTracing.getInstance().triggerClientDump( "ImeInsetsSourceConsumer#applyLocalVisibilityOverride", mController.getHost().getInputMethodManager(), null /* icProto */); return super.applyLocalVisibilityOverride(); } /** * Request {@link InputMethodManager} to show the IME. * @return @see {@link android.view.InsetsSourceConsumer.ShowResult}. */ @Override @ShowResult public int requestShow(boolean fromIme, @Nullable ImeTracker.Token statsToken) { if (fromIme) { ImeTracing.getInstance().triggerClientDump( "ImeInsetsSourceConsumer#requestShow", mController.getHost().getInputMethodManager(), null /* icProto */); } onShowRequested(); // TODO: ResultReceiver for IME. // TODO: Set mShowOnNextImeRender to automatically show IME and guard it with a flag. ImeTracker.forLogging().onProgress(statsToken, ImeTracker.PHASE_CLIENT_INSETS_CONSUMER_REQUEST_SHOW); if (!hasLeash()) { // If control or leash is null, schedule to show IME when both available. mIsRequestedVisibleAwaitingLeash = true; } // If we had a request before to show from IME (tracked with mImeRequestedShow), reaching // this code here means that we now got control, so we can start the animation immediately. // If client window is trying to control IME and IME is already visible, it is immediate. if (fromIme || (mState.isSourceOrDefaultVisible(getId(), getType()) && hasLeash())) { return ShowResult.SHOW_IMMEDIATELY; } return getImm().requestImeShow(mController.getHost().getWindowToken(), statsToken) ? ShowResult.IME_SHOW_DELAYED : ShowResult.IME_SHOW_FAILED; } void requestHide(boolean fromIme, @Nullable ImeTracker.Token statsToken) { if (!fromIme) { // Create a new token to track the hide request when we have control and leash, // as we use the passed in token for the insets animation already. final var notifyStatsToken = hasLeash() ? ImeTracker.forLogging().onStart(ImeTracker.TYPE_HIDE, ImeTracker.ORIGIN_CLIENT, SoftInputShowHideReason.HIDE_SOFT_INPUT_REQUEST_HIDE_WITH_CONTROL, mController.getHost().isHandlingPointerEvent() /* fromUser */) : statsToken; // The insets might be controlled by a remote target. Let the server know we are // requested to hide. notifyHidden(notifyStatsToken); } if (mAnimationState == ANIMATION_STATE_SHOW) { mHasPendingRequest = true; } } /** * Notify {@link com.android.server.inputmethod.InputMethodManagerService} that * IME insets are hidden. * * @param statsToken the token tracking the current IME request or {@code null} otherwise. */ private void notifyHidden(@NonNull ImeTracker.Token statsToken) { ImeTracker.forLogging().onProgress(statsToken, ImeTracker.PHASE_CLIENT_INSETS_CONSUMER_NOTIFY_HIDDEN); getImm().notifyImeHidden(mController.getHost().getWindowToken(), statsToken); mIsRequestedVisibleAwaitingLeash = false; Trace.asyncTraceEnd(TRACE_TAG_VIEW, "IC.hideRequestFromApi", 0); } @Override public void removeSurface() { final IBinder window = mController.getHost().getWindowToken(); if (window != null) { getImm().removeImeSurface(window); } } @Override public boolean setControl(@Nullable InsetsSourceControl control, int[] showTypes, int[] hideTypes) { ImeTracing.getInstance().triggerClientDump("ImeInsetsSourceConsumer#setControl", mController.getHost().getInputMethodManager(), null /* icProto */); if (!super.setControl(control, showTypes, hideTypes)) { return false; } if (control == null && !mIsRequestedVisibleAwaitingLeash) { mController.setRequestedVisibleTypes(0 /* visibleTypes */, getType()); removeSurface(); } final boolean hasLeash = control != null && control.getLeash() != null; if (hasLeash) { mIsRequestedVisibleAwaitingLeash = false; } return true; } @Override protected boolean isRequestedVisibleAwaitingControl() { return super.isRequestedVisibleAwaitingControl() || mIsRequestedVisibleAwaitingLeash; } /** * Checks whether the consumer has an insets source control with a leash. */ private boolean hasLeash() { final var control = getControl(); return control != null && control.getLeash() != null; } @Override public void onPerceptible(boolean perceptible) { super.onPerceptible(perceptible); final IBinder window = mController.getHost().getWindowToken(); if (window != null) { getImm().reportPerceptible(window, perceptible); } } @Override public void dumpDebug(ProtoOutputStream proto, long fieldId) { final long token = proto.start(fieldId); super.dumpDebug(proto, INSETS_SOURCE_CONSUMER); proto.write(IS_REQUESTED_VISIBLE_AWAITING_CONTROL, mIsRequestedVisibleAwaitingLeash); proto.write(HAS_PENDING_REQUEST, mHasPendingRequest); proto.end(token); } /** * Called when {@link #requestShow(boolean, ImeTracker.Token)} or * {@link InputMethodManager#showSoftInput(View, int)} is called. */ public void onShowRequested() { if (mAnimationState == ANIMATION_STATE_HIDE || mController.isPredictiveBackImeHideAnimInProgress()) { mHasPendingRequest = true; } } private InputMethodManager getImm() { return mController.getHost().getInputMethodManager(); } }