/* * 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.view.InsetsController.AnimationType; import static android.view.InsetsState.ITYPE_IME; import android.annotation.Nullable; import android.inputmethodservice.InputMethodService; import android.os.IBinder; import android.os.Parcel; import android.text.TextUtils; import android.view.SurfaceControl.Transaction; import android.view.inputmethod.EditorInfo; import android.view.inputmethod.InputMethodManager; import com.android.internal.annotations.VisibleForTesting; import java.util.Arrays; import java.util.function.Supplier; /** * Controls the visibility and animations of IME window insets source. * @hide */ public final class ImeInsetsSourceConsumer extends InsetsSourceConsumer { private EditorInfo mFocusedEditor; private EditorInfo mPreRenderedEditor; /** * Determines if IME would be shown next time IME is pre-rendered for currently focused * editor {@link #mFocusedEditor} if {@link #isServedEditorRendered} is {@code true}. */ private boolean mShowOnNextImeRender; /** * 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. */ private boolean mIsRequestedVisibleAwaitingControl; public ImeInsetsSourceConsumer( InsetsState state, Supplier transactionSupplier, InsetsController controller) { super(ITYPE_IME, state, transactionSupplier, controller); } public void onPreRendered(EditorInfo info) { mPreRenderedEditor = info; if (mShowOnNextImeRender) { mShowOnNextImeRender = false; if (isServedEditorRendered()) { applyImeVisibility(true /* setVisible */); } } } public void onServedEditorChanged(EditorInfo info) { if (isDummyOrEmptyEditor(info)) { mShowOnNextImeRender = false; } mFocusedEditor = info; } public void applyImeVisibility(boolean setVisible) { mController.applyImeVisibility(setVisible); } @Override public void onWindowFocusGained() { super.onWindowFocusGained(); getImm().registerImeConsumer(this); } @Override public void onWindowFocusLost() { super.onWindowFocusLost(); getImm().unregisterImeConsumer(this); mIsRequestedVisibleAwaitingControl = false; } @Override void hide(boolean animationFinished, @AnimationType int animationType) { super.hide(); if (animationFinished) { // remove IME surface as IME has finished hide animation. notifyHidden(); removeSurface(); } } /** * Request {@link InputMethodManager} to show the IME. * @return @see {@link android.view.InsetsSourceConsumer.ShowResult}. */ @Override public @ShowResult int requestShow(boolean fromIme) { // TODO: ResultReceiver for IME. // TODO: Set mShowOnNextImeRender to automatically show IME and guard it with a flag. if (getControl() == null) { // If control is null, schedule to show IME when control is available. mIsRequestedVisibleAwaitingControl = 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.getSource(getType()).isVisible() && getControl() != null) { return ShowResult.SHOW_IMMEDIATELY; } return getImm().requestImeShow(mController.getHost().getWindowToken()) ? ShowResult.IME_SHOW_DELAYED : ShowResult.IME_SHOW_FAILED; } /** * Notify {@link InputMethodService} that IME window is hidden. */ @Override void notifyHidden() { getImm().notifyImeHidden(mController.getHost().getWindowToken()); } @Override public void removeSurface() { final IBinder window = mController.getHost().getWindowToken(); if (window != null) { getImm().removeImeSurface(window); } } @Override public void setControl(@Nullable InsetsSourceControl control, int[] showTypes, int[] hideTypes) { super.setControl(control, showTypes, hideTypes); if (control == null && !mIsRequestedVisibleAwaitingControl) { hide(); removeSurface(); } } @Override protected boolean isRequestedVisibleAwaitingControl() { return mIsRequestedVisibleAwaitingControl || isRequestedVisible(); } @Override public void onPerceptible(boolean perceptible) { super.onPerceptible(perceptible); final IBinder window = mController.getHost().getWindowToken(); if (window != null) { getImm().reportPerceptible(window, perceptible); } } private boolean isDummyOrEmptyEditor(EditorInfo info) { // TODO(b/123044812): Handle dummy input gracefully in IME Insets API return info == null || (info.fieldId <= 0 && info.inputType <= 0); } private boolean isServedEditorRendered() { if (mFocusedEditor == null || mPreRenderedEditor == null || isDummyOrEmptyEditor(mFocusedEditor) || isDummyOrEmptyEditor(mPreRenderedEditor)) { // No view is focused or ready. return false; } return areEditorsSimilar(mFocusedEditor, mPreRenderedEditor); } @VisibleForTesting public static boolean areEditorsSimilar(EditorInfo info1, EditorInfo info2) { // We don't need to compare EditorInfo.fieldId (View#id) since that shouldn't change // IME views. boolean areOptionsSimilar = info1.imeOptions == info2.imeOptions && info1.inputType == info2.inputType && TextUtils.equals(info1.packageName, info2.packageName); areOptionsSimilar &= info1.privateImeOptions != null ? info1.privateImeOptions.equals(info2.privateImeOptions) : true; if (!areOptionsSimilar) { return false; } // compare bundle extras. if ((info1.extras == null && info2.extras == null) || info1.extras == info2.extras) { return true; } if ((info1.extras == null && info2.extras != null) || (info1.extras == null && info2.extras != null)) { return false; } if (info1.extras.hashCode() == info2.extras.hashCode() || info1.extras.equals(info1)) { return true; } if (info1.extras.size() != info2.extras.size()) { return false; } if (info1.extras.toString().equals(info2.extras.toString())) { return true; } // Compare bytes Parcel parcel1 = Parcel.obtain(); info1.extras.writeToParcel(parcel1, 0); parcel1.setDataPosition(0); Parcel parcel2 = Parcel.obtain(); info2.extras.writeToParcel(parcel2, 0); parcel2.setDataPosition(0); return Arrays.equals(parcel1.createByteArray(), parcel2.createByteArray()); } private InputMethodManager getImm() { return mController.getHost().getInputMethodManager(); } }