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