Home | History | Annotate | Download | only in view
      1 /*
      2  * Copyright (C) 2019 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License
     15  */
     16 
     17 package android.view;
     18 
     19 import static android.view.InsetsState.TYPE_IME;
     20 
     21 import android.inputmethodservice.InputMethodService;
     22 import android.os.Parcel;
     23 import android.text.TextUtils;
     24 import android.view.SurfaceControl.Transaction;
     25 import android.view.inputmethod.EditorInfo;
     26 import android.view.inputmethod.InputMethodManager;
     27 
     28 import com.android.internal.annotations.VisibleForTesting;
     29 
     30 import java.util.Arrays;
     31 import java.util.function.Supplier;
     32 
     33 /**
     34  * Controls the visibility and animations of IME window insets source.
     35  * @hide
     36  */
     37 public final class ImeInsetsSourceConsumer extends InsetsSourceConsumer {
     38     private EditorInfo mFocusedEditor;
     39     private EditorInfo mPreRenderedEditor;
     40     /**
     41      * Determines if IME would be shown next time IME is pre-rendered for currently focused
     42      * editor {@link #mFocusedEditor} if {@link #isServedEditorRendered} is {@code true}.
     43      */
     44     private boolean mShowOnNextImeRender;
     45     private boolean mHasWindowFocus;
     46 
     47     public ImeInsetsSourceConsumer(
     48             InsetsState state, Supplier<Transaction> transactionSupplier,
     49             InsetsController controller) {
     50         super(TYPE_IME, state, transactionSupplier, controller);
     51     }
     52 
     53     public void onPreRendered(EditorInfo info) {
     54         mPreRenderedEditor = info;
     55         if (mShowOnNextImeRender) {
     56             mShowOnNextImeRender = false;
     57             if (isServedEditorRendered()) {
     58                 applyImeVisibility(true /* setVisible */);
     59             }
     60         }
     61     }
     62 
     63     public void onServedEditorChanged(EditorInfo info) {
     64         if (isDummyOrEmptyEditor(info)) {
     65             mShowOnNextImeRender = false;
     66         }
     67         mFocusedEditor = info;
     68     }
     69 
     70     public void applyImeVisibility(boolean setVisible) {
     71         if (!mHasWindowFocus) {
     72             // App window doesn't have focus, any visibility changes would be no-op.
     73             return;
     74         }
     75 
     76         mController.applyImeVisibility(setVisible);
     77     }
     78 
     79     @Override
     80     public void onWindowFocusGained() {
     81         mHasWindowFocus = true;
     82         getImm().registerImeConsumer(this);
     83     }
     84 
     85     @Override
     86     public void onWindowFocusLost() {
     87         mHasWindowFocus = false;
     88         getImm().unregisterImeConsumer(this);
     89     }
     90 
     91     /**
     92      * Request {@link InputMethodManager} to show the IME.
     93      * @return @see {@link android.view.InsetsSourceConsumer.ShowResult}.
     94      */
     95     @Override
     96     @ShowResult int requestShow(boolean fromIme) {
     97         // TODO: ResultReceiver for IME.
     98         // TODO: Set mShowOnNextImeRender to automatically show IME and guard it with a flag.
     99         if (fromIme) {
    100             return ShowResult.SHOW_IMMEDIATELY;
    101         }
    102 
    103         return getImm().requestImeShow(null /* resultReceiver */)
    104                 ? ShowResult.SHOW_DELAYED : ShowResult.SHOW_FAILED;
    105     }
    106 
    107     /**
    108      * Notify {@link InputMethodService} that IME window is hidden.
    109      */
    110     @Override
    111     void notifyHidden() {
    112         getImm().notifyImeHidden();
    113     }
    114 
    115     private boolean isDummyOrEmptyEditor(EditorInfo info) {
    116         // TODO(b/123044812): Handle dummy input gracefully in IME Insets API
    117         return info == null || (info.fieldId <= 0 && info.inputType <= 0);
    118     }
    119 
    120     private boolean isServedEditorRendered() {
    121         if (mFocusedEditor == null || mPreRenderedEditor == null
    122                 || isDummyOrEmptyEditor(mFocusedEditor)
    123                 || isDummyOrEmptyEditor(mPreRenderedEditor)) {
    124             // No view is focused or ready.
    125             return false;
    126         }
    127         return areEditorsSimilar(mFocusedEditor, mPreRenderedEditor);
    128     }
    129 
    130     @VisibleForTesting
    131     public static boolean areEditorsSimilar(EditorInfo info1, EditorInfo info2) {
    132         // We don't need to compare EditorInfo.fieldId (View#id) since that shouldn't change
    133         // IME views.
    134         boolean areOptionsSimilar =
    135                 info1.imeOptions == info2.imeOptions
    136                 && info1.inputType == info2.inputType
    137                 && TextUtils.equals(info1.packageName, info2.packageName);
    138         areOptionsSimilar &= info1.privateImeOptions != null
    139                 ? info1.privateImeOptions.equals(info2.privateImeOptions) : true;
    140 
    141         if (!areOptionsSimilar) {
    142             return false;
    143         }
    144 
    145         // compare bundle extras.
    146         if ((info1.extras == null && info2.extras == null) || info1.extras == info2.extras) {
    147             return true;
    148         }
    149         if ((info1.extras == null && info2.extras != null)
    150                 || (info1.extras == null && info2.extras != null)) {
    151             return false;
    152         }
    153         if (info1.extras.hashCode() == info2.extras.hashCode()
    154                 || info1.extras.equals(info1)) {
    155             return true;
    156         }
    157         if (info1.extras.size() != info2.extras.size()) {
    158             return false;
    159         }
    160         if (info1.extras.toString().equals(info2.extras.toString())) {
    161             return true;
    162         }
    163 
    164         // Compare bytes
    165         Parcel parcel1 = Parcel.obtain();
    166         info1.extras.writeToParcel(parcel1, 0);
    167         parcel1.setDataPosition(0);
    168         Parcel parcel2 = Parcel.obtain();
    169         info2.extras.writeToParcel(parcel2, 0);
    170         parcel2.setDataPosition(0);
    171 
    172         return Arrays.equals(parcel1.createByteArray(), parcel2.createByteArray());
    173     }
    174 
    175     private InputMethodManager getImm() {
    176         return mController.getViewRoot().mContext.getSystemService(InputMethodManager.class);
    177     }
    178 }
    179