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