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.InsetsController.AnimationType;
20 import static android.view.InsetsState.ITYPE_IME;
21 
22 import android.annotation.Nullable;
23 import android.inputmethodservice.InputMethodService;
24 import android.os.IBinder;
25 import android.os.Parcel;
26 import android.text.TextUtils;
27 import android.view.SurfaceControl.Transaction;
28 import android.view.inputmethod.EditorInfo;
29 import android.view.inputmethod.InputMethodManager;
30 
31 import com.android.internal.annotations.VisibleForTesting;
32 
33 import java.util.Arrays;
34 import java.util.function.Supplier;
35 
36 /**
37  * Controls the visibility and animations of IME window insets source.
38  * @hide
39  */
40 public final class ImeInsetsSourceConsumer extends InsetsSourceConsumer {
41     private EditorInfo mFocusedEditor;
42     private EditorInfo mPreRenderedEditor;
43     /**
44      * Determines if IME would be shown next time IME is pre-rendered for currently focused
45      * editor {@link #mFocusedEditor} if {@link #isServedEditorRendered} is {@code true}.
46      */
47     private boolean mShowOnNextImeRender;
48 
49     /**
50      * Tracks whether we have an outstanding request from the IME to show, but weren't able to
51      * execute it because we didn't have control yet.
52      */
53     private boolean mIsRequestedVisibleAwaitingControl;
54 
ImeInsetsSourceConsumer( InsetsState state, Supplier<Transaction> transactionSupplier, InsetsController controller)55     public ImeInsetsSourceConsumer(
56             InsetsState state, Supplier<Transaction> transactionSupplier,
57             InsetsController controller) {
58         super(ITYPE_IME, state, transactionSupplier, controller);
59     }
60 
onPreRendered(EditorInfo info)61     public void onPreRendered(EditorInfo info) {
62         mPreRenderedEditor = info;
63         if (mShowOnNextImeRender) {
64             mShowOnNextImeRender = false;
65             if (isServedEditorRendered()) {
66                 applyImeVisibility(true /* setVisible */);
67             }
68         }
69     }
70 
onServedEditorChanged(EditorInfo info)71     public void onServedEditorChanged(EditorInfo info) {
72         if (isDummyOrEmptyEditor(info)) {
73             mShowOnNextImeRender = false;
74         }
75         mFocusedEditor = info;
76     }
77 
applyImeVisibility(boolean setVisible)78     public void applyImeVisibility(boolean setVisible) {
79         mController.applyImeVisibility(setVisible);
80     }
81 
82     @Override
onWindowFocusGained()83     public void onWindowFocusGained() {
84         super.onWindowFocusGained();
85         getImm().registerImeConsumer(this);
86     }
87 
88     @Override
onWindowFocusLost()89     public void onWindowFocusLost() {
90         super.onWindowFocusLost();
91         getImm().unregisterImeConsumer(this);
92         mIsRequestedVisibleAwaitingControl = false;
93     }
94 
95     @Override
hide(boolean animationFinished, @AnimationType int animationType)96     void hide(boolean animationFinished, @AnimationType int animationType) {
97         super.hide();
98 
99         if (animationFinished) {
100             // remove IME surface as IME has finished hide animation.
101             notifyHidden();
102             removeSurface();
103         }
104     }
105 
106     /**
107      * Request {@link InputMethodManager} to show the IME.
108      * @return @see {@link android.view.InsetsSourceConsumer.ShowResult}.
109      */
110     @Override
requestShow(boolean fromIme)111     public @ShowResult int requestShow(boolean fromIme) {
112         // TODO: ResultReceiver for IME.
113         // TODO: Set mShowOnNextImeRender to automatically show IME and guard it with a flag.
114 
115         if (getControl() == null) {
116             // If control is null, schedule to show IME when control is available.
117             mIsRequestedVisibleAwaitingControl = true;
118         }
119         // If we had a request before to show from IME (tracked with mImeRequestedShow), reaching
120         // this code here means that we now got control, so we can start the animation immediately.
121         // If client window is trying to control IME and IME is already visible, it is immediate.
122         if (fromIme || mState.getSource(getType()).isVisible() && getControl() != null) {
123             return ShowResult.SHOW_IMMEDIATELY;
124         }
125 
126         return getImm().requestImeShow(mController.getHost().getWindowToken())
127                 ? ShowResult.IME_SHOW_DELAYED : ShowResult.IME_SHOW_FAILED;
128     }
129 
130     /**
131      * Notify {@link InputMethodService} that IME window is hidden.
132      */
133     @Override
notifyHidden()134     void notifyHidden() {
135         getImm().notifyImeHidden(mController.getHost().getWindowToken());
136     }
137 
138     @Override
removeSurface()139     public void removeSurface() {
140         final IBinder window = mController.getHost().getWindowToken();
141         if (window != null) {
142             getImm().removeImeSurface(window);
143         }
144     }
145 
146     @Override
setControl(@ullable InsetsSourceControl control, int[] showTypes, int[] hideTypes)147     public void setControl(@Nullable InsetsSourceControl control, int[] showTypes,
148             int[] hideTypes) {
149         super.setControl(control, showTypes, hideTypes);
150         if (control == null && !mIsRequestedVisibleAwaitingControl) {
151             hide();
152             removeSurface();
153         }
154     }
155 
156     @Override
isRequestedVisibleAwaitingControl()157     protected boolean isRequestedVisibleAwaitingControl() {
158         return mIsRequestedVisibleAwaitingControl || isRequestedVisible();
159     }
160 
161     @Override
onPerceptible(boolean perceptible)162     public void onPerceptible(boolean perceptible) {
163         super.onPerceptible(perceptible);
164         final IBinder window = mController.getHost().getWindowToken();
165         if (window != null) {
166             getImm().reportPerceptible(window, perceptible);
167         }
168     }
169 
isDummyOrEmptyEditor(EditorInfo info)170     private boolean isDummyOrEmptyEditor(EditorInfo info) {
171         // TODO(b/123044812): Handle dummy input gracefully in IME Insets API
172         return info == null || (info.fieldId <= 0 && info.inputType <= 0);
173     }
174 
isServedEditorRendered()175     private boolean isServedEditorRendered() {
176         if (mFocusedEditor == null || mPreRenderedEditor == null
177                 || isDummyOrEmptyEditor(mFocusedEditor)
178                 || isDummyOrEmptyEditor(mPreRenderedEditor)) {
179             // No view is focused or ready.
180             return false;
181         }
182         return areEditorsSimilar(mFocusedEditor, mPreRenderedEditor);
183     }
184 
185     @VisibleForTesting
areEditorsSimilar(EditorInfo info1, EditorInfo info2)186     public static boolean areEditorsSimilar(EditorInfo info1, EditorInfo info2) {
187         // We don't need to compare EditorInfo.fieldId (View#id) since that shouldn't change
188         // IME views.
189         boolean areOptionsSimilar =
190                 info1.imeOptions == info2.imeOptions
191                 && info1.inputType == info2.inputType
192                 && TextUtils.equals(info1.packageName, info2.packageName);
193         areOptionsSimilar &= info1.privateImeOptions != null
194                 ? info1.privateImeOptions.equals(info2.privateImeOptions) : true;
195 
196         if (!areOptionsSimilar) {
197             return false;
198         }
199 
200         // compare bundle extras.
201         if ((info1.extras == null && info2.extras == null) || info1.extras == info2.extras) {
202             return true;
203         }
204         if ((info1.extras == null && info2.extras != null)
205                 || (info1.extras == null && info2.extras != null)) {
206             return false;
207         }
208         if (info1.extras.hashCode() == info2.extras.hashCode()
209                 || info1.extras.equals(info1)) {
210             return true;
211         }
212         if (info1.extras.size() != info2.extras.size()) {
213             return false;
214         }
215         if (info1.extras.toString().equals(info2.extras.toString())) {
216             return true;
217         }
218 
219         // Compare bytes
220         Parcel parcel1 = Parcel.obtain();
221         info1.extras.writeToParcel(parcel1, 0);
222         parcel1.setDataPosition(0);
223         Parcel parcel2 = Parcel.obtain();
224         info2.extras.writeToParcel(parcel2, 0);
225         parcel2.setDataPosition(0);
226 
227         return Arrays.equals(parcel1.createByteArray(), parcel2.createByteArray());
228     }
229 
getImm()230     private InputMethodManager getImm() {
231         return mController.getHost().getInputMethodManager();
232     }
233 }
234