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