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