/* * 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.InsetsState.TYPE_IME; import android.inputmethodservice.InputMethodService; 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; private boolean mHasWindowFocus; public ImeInsetsSourceConsumer( InsetsState state, Supplier transactionSupplier, InsetsController controller) { super(TYPE_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) { if (!mHasWindowFocus) { // App window doesn't have focus, any visibility changes would be no-op. return; } mController.applyImeVisibility(setVisible); } @Override public void onWindowFocusGained() { mHasWindowFocus = true; getImm().registerImeConsumer(this); } @Override public void onWindowFocusLost() { mHasWindowFocus = false; getImm().unregisterImeConsumer(this); } /** * Request {@link InputMethodManager} to show the IME. * @return @see {@link android.view.InsetsSourceConsumer.ShowResult}. */ @Override @ShowResult int requestShow(boolean fromIme) { // TODO: ResultReceiver for IME. // TODO: Set mShowOnNextImeRender to automatically show IME and guard it with a flag. if (fromIme) { return ShowResult.SHOW_IMMEDIATELY; } return getImm().requestImeShow(null /* resultReceiver */) ? ShowResult.SHOW_DELAYED : ShowResult.SHOW_FAILED; } /** * Notify {@link InputMethodService} that IME window is hidden. */ @Override void notifyHidden() { getImm().notifyImeHidden(); } 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.getViewRoot().mContext.getSystemService(InputMethodManager.class); } }