1 /*
2  * Copyright (C) 2007-2008 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License"); you may not
5  * use this file except in compliance with the License. You may obtain a copy of
6  * 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, WITHOUT
12  * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13  * License for the specific language governing permissions and limitations under
14  * the License.
15  */
16 
17 package android.inputmethodservice;
18 
19 import static java.lang.annotation.RetentionPolicy.SOURCE;
20 
21 import android.annotation.IntDef;
22 import android.app.Dialog;
23 import android.content.Context;
24 import android.graphics.Rect;
25 import android.os.Debug;
26 import android.os.IBinder;
27 import android.util.Log;
28 import android.view.Gravity;
29 import android.view.KeyEvent;
30 import android.view.MotionEvent;
31 import android.view.View;
32 import android.view.WindowManager;
33 
34 import java.lang.annotation.Retention;
35 
36 /**
37  * A SoftInputWindow is a Dialog that is intended to be used for a top-level input
38  * method window.  It will be displayed along the edge of the screen, moving
39  * the application user interface away from it so that the focused item is
40  * always visible.
41  * @hide
42  */
43 public class SoftInputWindow extends Dialog {
44     private static final boolean DEBUG = false;
45     private static final String TAG = "SoftInputWindow";
46 
47     final String mName;
48     final Callback mCallback;
49     final KeyEvent.Callback mKeyEventCallback;
50     final KeyEvent.DispatcherState mDispatcherState;
51     final int mWindowType;
52     final int mGravity;
53     final boolean mTakesFocus;
54     private final Rect mBounds = new Rect();
55 
56     @Retention(SOURCE)
57     @IntDef(value = {SoftInputWindowState.TOKEN_PENDING, SoftInputWindowState.TOKEN_SET,
58             SoftInputWindowState.SHOWN_AT_LEAST_ONCE, SoftInputWindowState.REJECTED_AT_LEAST_ONCE})
59     private @interface SoftInputWindowState {
60         /**
61          * The window token is not set yet.
62          */
63         int TOKEN_PENDING = 0;
64         /**
65          * The window token was set, but the window is not shown yet.
66          */
67         int TOKEN_SET = 1;
68         /**
69          * The window was shown at least once.
70          */
71         int SHOWN_AT_LEAST_ONCE = 2;
72         /**
73          * {@link android.view.WindowManager.BadTokenException} was sent when calling
74          * {@link Dialog#show()} at least once.
75          */
76         int REJECTED_AT_LEAST_ONCE = 3;
77         /**
78          * The window is considered destroyed.  Any incoming request should be ignored.
79          */
80         int DESTROYED = 4;
81     }
82 
83     @SoftInputWindowState
84     private int mWindowState = SoftInputWindowState.TOKEN_PENDING;
85 
86     public interface Callback {
onBackPressed()87         public void onBackPressed();
88     }
89 
setToken(IBinder token)90     public void setToken(IBinder token) {
91         switch (mWindowState) {
92             case SoftInputWindowState.TOKEN_PENDING:
93                 // Normal scenario.  Nothing to worry about.
94                 WindowManager.LayoutParams lp = getWindow().getAttributes();
95                 lp.token = token;
96                 getWindow().setAttributes(lp);
97                 updateWindowState(SoftInputWindowState.TOKEN_SET);
98 
99                 // As soon as we have a token, make sure the window is added (but not shown) by
100                 // setting visibility to INVISIBLE and calling show() on Dialog. Note that
101                 // WindowInsetsController.OnControllableInsetsChangedListener relies on the window
102                 // being added to function.
103                 getWindow().getDecorView().setVisibility(View.INVISIBLE);
104                 show();
105                 return;
106             case SoftInputWindowState.TOKEN_SET:
107             case SoftInputWindowState.SHOWN_AT_LEAST_ONCE:
108             case SoftInputWindowState.REJECTED_AT_LEAST_ONCE:
109                 throw new IllegalStateException("setToken can be called only once");
110             case SoftInputWindowState.DESTROYED:
111                 // Just ignore.  Since there are multiple event queues from the token is issued
112                 // in the system server to the timing when it arrives here, it can be delivered
113                 // after the is already destroyed.  No one should be blamed because of such an
114                 // unfortunate but possible scenario.
115                 Log.i(TAG, "Ignoring setToken() because window is already destroyed.");
116                 return;
117             default:
118                 throw new IllegalStateException("Unexpected state=" + mWindowState);
119         }
120     }
121 
122     /**
123      * Create a SoftInputWindow that uses a custom style.
124      *
125      * @param context The Context in which the DockWindow should run. In
126      *        particular, it uses the window manager and theme from this context
127      *        to present its UI.
128      * @param theme A style resource describing the theme to use for the window.
129      *        See <a href="{@docRoot}reference/available-resources.html#stylesandthemes">Style
130      *        and Theme Resources</a> for more information about defining and
131      *        using styles. This theme is applied on top of the current theme in
132      *        <var>context</var>. If 0, the default dialog theme will be used.
133      */
SoftInputWindow(Context context, String name, int theme, Callback callback, KeyEvent.Callback keyEventCallback, KeyEvent.DispatcherState dispatcherState, int windowType, int gravity, boolean takesFocus)134     public SoftInputWindow(Context context, String name, int theme, Callback callback,
135             KeyEvent.Callback keyEventCallback, KeyEvent.DispatcherState dispatcherState,
136             int windowType, int gravity, boolean takesFocus) {
137         super(context, theme);
138         mName = name;
139         mCallback = callback;
140         mKeyEventCallback = keyEventCallback;
141         mDispatcherState = dispatcherState;
142         mWindowType = windowType;
143         mGravity = gravity;
144         mTakesFocus = takesFocus;
145         initDockWindow();
146     }
147 
148     @Override
onWindowFocusChanged(boolean hasFocus)149     public void onWindowFocusChanged(boolean hasFocus) {
150         super.onWindowFocusChanged(hasFocus);
151         mDispatcherState.reset();
152     }
153 
154     @Override
dispatchTouchEvent(MotionEvent ev)155     public boolean dispatchTouchEvent(MotionEvent ev) {
156         getWindow().getDecorView().getHitRect(mBounds);
157 
158         if (ev.isWithinBoundsNoHistory(mBounds.left, mBounds.top,
159                 mBounds.right - 1, mBounds.bottom - 1)) {
160             return super.dispatchTouchEvent(ev);
161         } else {
162             MotionEvent temp = ev.clampNoHistory(mBounds.left, mBounds.top,
163                     mBounds.right - 1, mBounds.bottom - 1);
164             boolean handled = super.dispatchTouchEvent(temp);
165             temp.recycle();
166             return handled;
167         }
168     }
169 
170     /**
171      * Set which boundary of the screen the DockWindow sticks to.
172      *
173      * @param gravity The boundary of the screen to stick. See {@link
174      *        android.view.Gravity.LEFT}, {@link android.view.Gravity.TOP},
175      *        {@link android.view.Gravity.BOTTOM}, {@link
176      *        android.view.Gravity.RIGHT}.
177      */
setGravity(int gravity)178     public void setGravity(int gravity) {
179         WindowManager.LayoutParams lp = getWindow().getAttributes();
180         lp.gravity = gravity;
181         updateWidthHeight(lp);
182         getWindow().setAttributes(lp);
183     }
184 
getGravity()185     public int getGravity() {
186         return getWindow().getAttributes().gravity;
187     }
188 
updateWidthHeight(WindowManager.LayoutParams lp)189     private void updateWidthHeight(WindowManager.LayoutParams lp) {
190         if (lp.gravity == Gravity.TOP || lp.gravity == Gravity.BOTTOM) {
191             lp.width = WindowManager.LayoutParams.MATCH_PARENT;
192             lp.height = WindowManager.LayoutParams.WRAP_CONTENT;
193         } else {
194             lp.width = WindowManager.LayoutParams.WRAP_CONTENT;
195             lp.height = WindowManager.LayoutParams.MATCH_PARENT;
196         }
197     }
198 
onKeyDown(int keyCode, KeyEvent event)199     public boolean onKeyDown(int keyCode, KeyEvent event) {
200         if (mKeyEventCallback != null && mKeyEventCallback.onKeyDown(keyCode, event)) {
201             return true;
202         }
203         return super.onKeyDown(keyCode, event);
204     }
205 
onKeyLongPress(int keyCode, KeyEvent event)206     public boolean onKeyLongPress(int keyCode, KeyEvent event) {
207         if (mKeyEventCallback != null && mKeyEventCallback.onKeyLongPress(keyCode, event)) {
208             return true;
209         }
210         return super.onKeyLongPress(keyCode, event);
211     }
212 
onKeyUp(int keyCode, KeyEvent event)213     public boolean onKeyUp(int keyCode, KeyEvent event) {
214         if (mKeyEventCallback != null && mKeyEventCallback.onKeyUp(keyCode, event)) {
215             return true;
216         }
217         return super.onKeyUp(keyCode, event);
218     }
219 
onKeyMultiple(int keyCode, int count, KeyEvent event)220     public boolean onKeyMultiple(int keyCode, int count, KeyEvent event) {
221         if (mKeyEventCallback != null && mKeyEventCallback.onKeyMultiple(keyCode, count, event)) {
222             return true;
223         }
224         return super.onKeyMultiple(keyCode, count, event);
225     }
226 
onBackPressed()227     public void onBackPressed() {
228         if (mCallback != null) {
229             mCallback.onBackPressed();
230         } else {
231             super.onBackPressed();
232         }
233     }
234 
initDockWindow()235     private void initDockWindow() {
236         WindowManager.LayoutParams lp = getWindow().getAttributes();
237 
238         lp.type = mWindowType;
239         lp.setTitle(mName);
240 
241         lp.gravity = mGravity;
242         updateWidthHeight(lp);
243 
244         getWindow().setAttributes(lp);
245 
246         int windowSetFlags = WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN;
247         int windowModFlags = WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN |
248                 WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE |
249                 WindowManager.LayoutParams.FLAG_DIM_BEHIND;
250 
251         if (!mTakesFocus) {
252             windowSetFlags |= WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
253         } else {
254             windowSetFlags |= WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL;
255             windowModFlags |= WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL;
256         }
257 
258         getWindow().setFlags(windowSetFlags, windowModFlags);
259     }
260 
261     @Override
show()262     public final void show() {
263         switch (mWindowState) {
264             case SoftInputWindowState.TOKEN_PENDING:
265                 throw new IllegalStateException("Window token is not set yet.");
266             case SoftInputWindowState.TOKEN_SET:
267             case SoftInputWindowState.SHOWN_AT_LEAST_ONCE:
268                 // Normal scenario.  Nothing to worry about.
269                 try {
270                     super.show();
271                     updateWindowState(SoftInputWindowState.SHOWN_AT_LEAST_ONCE);
272                 } catch (WindowManager.BadTokenException e) {
273                     // Just ignore this exception.  Since show() can be requested from other
274                     // components such as the system and there could be multiple event queues before
275                     // the request finally arrives here, the system may have already invalidated the
276                     // window token attached to our window.  In such a scenario, receiving
277                     // BadTokenException here is an expected behavior.  We just ignore it and update
278                     // the state so that we do not touch this window later.
279                     Log.i(TAG, "Probably the IME window token is already invalidated."
280                             + " show() does nothing.");
281                     updateWindowState(SoftInputWindowState.REJECTED_AT_LEAST_ONCE);
282                 }
283                 return;
284             case SoftInputWindowState.REJECTED_AT_LEAST_ONCE:
285                 // Just ignore.  In general we cannot completely avoid this kind of race condition.
286                 Log.i(TAG, "Not trying to call show() because it was already rejected once.");
287                 return;
288             case SoftInputWindowState.DESTROYED:
289                 // Just ignore.  In general we cannot completely avoid this kind of race condition.
290                 Log.i(TAG, "Ignoring show() because the window is already destroyed.");
291                 return;
292             default:
293                 throw new IllegalStateException("Unexpected state=" + mWindowState);
294         }
295     }
296 
dismissForDestroyIfNecessary()297     final void dismissForDestroyIfNecessary() {
298         switch (mWindowState) {
299             case SoftInputWindowState.TOKEN_PENDING:
300             case SoftInputWindowState.TOKEN_SET:
301                 // nothing to do because the window has never been shown.
302                 updateWindowState(SoftInputWindowState.DESTROYED);
303                 return;
304             case SoftInputWindowState.SHOWN_AT_LEAST_ONCE:
305                 // Disable exit animation for the current IME window
306                 // to avoid the race condition between the exit and enter animations
307                 // when the current IME is being switched to another one.
308                 try {
309                     getWindow().setWindowAnimations(0);
310                     dismiss();
311                 } catch (WindowManager.BadTokenException e) {
312                     // Just ignore this exception.  Since show() can be requested from other
313                     // components such as the system and there could be multiple event queues before
314                     // the request finally arrives here, the system may have already invalidated the
315                     // window token attached to our window.  In such a scenario, receiving
316                     // BadTokenException here is an expected behavior.  We just ignore it and update
317                     // the state so that we do not touch this window later.
318                     Log.i(TAG, "Probably the IME window token is already invalidated. "
319                             + "No need to dismiss it.");
320                 }
321                 // Either way, consider that the window is destroyed.
322                 updateWindowState(SoftInputWindowState.DESTROYED);
323                 return;
324             case SoftInputWindowState.REJECTED_AT_LEAST_ONCE:
325                 // Just ignore.  In general we cannot completely avoid this kind of race condition.
326                 Log.i(TAG,
327                         "Not trying to dismiss the window because it is most likely unnecessary.");
328                 // Anyway, consider that the window is destroyed.
329                 updateWindowState(SoftInputWindowState.DESTROYED);
330                 return;
331             case SoftInputWindowState.DESTROYED:
332                 throw new IllegalStateException(
333                         "dismissForDestroyIfNecessary can be called only once");
334             default:
335                 throw new IllegalStateException("Unexpected state=" + mWindowState);
336         }
337     }
338 
updateWindowState(@oftInputWindowState int newState)339     private void updateWindowState(@SoftInputWindowState int newState) {
340         if (DEBUG) {
341             if (mWindowState != newState) {
342                 Log.d(TAG, "WindowState: " + stateToString(mWindowState) + " -> "
343                         + stateToString(newState) + " @ " + Debug.getCaller());
344             }
345         }
346         mWindowState = newState;
347     }
348 
stateToString(@oftInputWindowState int state)349     private static String stateToString(@SoftInputWindowState int state) {
350         switch (state) {
351             case SoftInputWindowState.TOKEN_PENDING:
352                 return "TOKEN_PENDING";
353             case SoftInputWindowState.TOKEN_SET:
354                 return "TOKEN_SET";
355             case SoftInputWindowState.SHOWN_AT_LEAST_ONCE:
356                 return "SHOWN_AT_LEAST_ONCE";
357             case SoftInputWindowState.REJECTED_AT_LEAST_ONCE:
358                 return "REJECTED_AT_LEAST_ONCE";
359             case SoftInputWindowState.DESTROYED:
360                 return "DESTROYED";
361             default:
362                 throw new IllegalStateException("Unknown state=" + state);
363         }
364     }
365 }
366