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