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 { 86 public void onBackPressed(); 87 } 88 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 */ 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 141 public void onWindowFocusChanged(boolean hasFocus) { 142 super.onWindowFocusChanged(hasFocus); 143 mDispatcherState.reset(); 144 } 145 146 @Override 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 */ 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 177 public int getGravity() { 178 return getWindow().getAttributes().gravity; 179 } 180 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 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 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 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 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 219 public void onBackPressed() { 220 if (mCallback != null) { 221 mCallback.onBackPressed(); 222 } else { 223 super.onBackPressed(); 224 } 225 } 226 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 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 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 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 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