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 
ImeInsetsSourceConsumer( InsetsState state, Supplier<Transaction> transactionSupplier, InsetsController controller)47     public ImeInsetsSourceConsumer(
48             InsetsState state, Supplier<Transaction> transactionSupplier,
49             InsetsController controller) {
50         super(TYPE_IME, state, transactionSupplier, controller);
51     }
52 
onPreRendered(EditorInfo info)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 
onServedEditorChanged(EditorInfo info)63     public void onServedEditorChanged(EditorInfo info) {
64         if (isDummyOrEmptyEditor(info)) {
65             mShowOnNextImeRender = false;
66         }
67         mFocusedEditor = info;
68     }
69 
applyImeVisibility(boolean setVisible)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
onWindowFocusGained()80     public void onWindowFocusGained() {
81         mHasWindowFocus = true;
82         getImm().registerImeConsumer(this);
83     }
84 
85     @Override
onWindowFocusLost()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
requestShow(boolean fromIme)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
notifyHidden()111     void notifyHidden() {
112         getImm().notifyImeHidden();
113     }
114 
isDummyOrEmptyEditor(EditorInfo info)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 
isServedEditorRendered()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
areEditorsSimilar(EditorInfo info1, EditorInfo info2)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 
getImm()175     private InputMethodManager getImm() {
176         return mController.getViewRoot().mContext.getSystemService(InputMethodManager.class);
177     }
178 }
179