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