The Android Open Source Project | 9066cfe | 2009-03-03 19:31:44 -0800 | [diff] [blame] | 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 | |
Yohei Yukawa | 13a9ffb | 2018-08-29 19:56:02 -0700 | [diff] [blame] | 19 | import static java.lang.annotation.RetentionPolicy.SOURCE; |
| 20 | |
| 21 | import android.annotation.IntDef; |
The Android Open Source Project | 9066cfe | 2009-03-03 19:31:44 -0800 | [diff] [blame] | 22 | import android.app.Dialog; |
| 23 | import android.content.Context; |
Tadashi G. Takaoka | 7bd6c20 | 2011-01-25 18:02:55 +0900 | [diff] [blame] | 24 | import android.graphics.Rect; |
Yohei Yukawa | 13a9ffb | 2018-08-29 19:56:02 -0700 | [diff] [blame] | 25 | import android.os.Debug; |
The Android Open Source Project | 9066cfe | 2009-03-03 19:31:44 -0800 | [diff] [blame] | 26 | import android.os.IBinder; |
Yohei Yukawa | 13a9ffb | 2018-08-29 19:56:02 -0700 | [diff] [blame] | 27 | import android.util.Log; |
The Android Open Source Project | 9066cfe | 2009-03-03 19:31:44 -0800 | [diff] [blame] | 28 | import android.view.Gravity; |
Dianne Hackborn | 83fe3f5 | 2009-09-12 23:38:30 -0700 | [diff] [blame] | 29 | import android.view.KeyEvent; |
Tadashi G. Takaoka | 7bd6c20 | 2011-01-25 18:02:55 +0900 | [diff] [blame] | 30 | import android.view.MotionEvent; |
The Android Open Source Project | 9066cfe | 2009-03-03 19:31:44 -0800 | [diff] [blame] | 31 | import android.view.WindowManager; |
| 32 | |
Yohei Yukawa | 13a9ffb | 2018-08-29 19:56:02 -0700 | [diff] [blame] | 33 | import java.lang.annotation.Retention; |
| 34 | |
The Android Open Source Project | 9066cfe | 2009-03-03 19:31:44 -0800 | [diff] [blame] | 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. |
Dianne Hackborn | c03c916 | 2014-05-02 10:45:59 -0700 | [diff] [blame] | 40 | * @hide |
The Android Open Source Project | 9066cfe | 2009-03-03 19:31:44 -0800 | [diff] [blame] | 41 | */ |
Dianne Hackborn | c03c916 | 2014-05-02 10:45:59 -0700 | [diff] [blame] | 42 | public class SoftInputWindow extends Dialog { |
Yohei Yukawa | 13a9ffb | 2018-08-29 19:56:02 -0700 | [diff] [blame] | 43 | private static final boolean DEBUG = false; |
| 44 | private static final String TAG = "SoftInputWindow"; |
| 45 | |
Dianne Hackborn | c03c916 | 2014-05-02 10:45:59 -0700 | [diff] [blame] | 46 | final String mName; |
| 47 | final Callback mCallback; |
| 48 | final KeyEvent.Callback mKeyEventCallback; |
Dianne Hackborn | 83fe3f5 | 2009-09-12 23:38:30 -0700 | [diff] [blame] | 49 | final KeyEvent.DispatcherState mDispatcherState; |
Dianne Hackborn | e30e02f | 2014-05-27 18:24:45 -0700 | [diff] [blame] | 50 | final int mWindowType; |
| 51 | final int mGravity; |
Dianne Hackborn | c03c916 | 2014-05-02 10:45:59 -0700 | [diff] [blame] | 52 | final boolean mTakesFocus; |
Tadashi G. Takaoka | 7bd6c20 | 2011-01-25 18:02:55 +0900 | [diff] [blame] | 53 | private final Rect mBounds = new Rect(); |
Dianne Hackborn | c03c916 | 2014-05-02 10:45:59 -0700 | [diff] [blame] | 54 | |
Yohei Yukawa | 13a9ffb | 2018-08-29 19:56:02 -0700 | [diff] [blame] | 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 | |
Dianne Hackborn | c03c916 | 2014-05-02 10:45:59 -0700 | [diff] [blame] | 85 | public interface Callback { |
| 86 | public void onBackPressed(); |
| 87 | } |
| 88 | |
The Android Open Source Project | 9066cfe | 2009-03-03 19:31:44 -0800 | [diff] [blame] | 89 | public void setToken(IBinder token) { |
Yohei Yukawa | 13a9ffb | 2018-08-29 19:56:02 -0700 | [diff] [blame] | 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 | } |
The Android Open Source Project | 9066cfe | 2009-03-03 19:31:44 -0800 | [diff] [blame] | 112 | } |
Yohei Yukawa | 13a9ffb | 2018-08-29 19:56:02 -0700 | [diff] [blame] | 113 | |
The Android Open Source Project | 9066cfe | 2009-03-03 19:31:44 -0800 | [diff] [blame] | 114 | /** |
Dianne Hackborn | 2c84cfc | 2011-10-31 15:39:59 -0700 | [diff] [blame] | 115 | * Create a SoftInputWindow that uses a custom style. |
The Android Open Source Project | 9066cfe | 2009-03-03 19:31:44 -0800 | [diff] [blame] | 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 | */ |
Dianne Hackborn | c03c916 | 2014-05-02 10:45:59 -0700 | [diff] [blame] | 126 | public SoftInputWindow(Context context, String name, int theme, Callback callback, |
| 127 | KeyEvent.Callback keyEventCallback, KeyEvent.DispatcherState dispatcherState, |
Dianne Hackborn | e30e02f | 2014-05-27 18:24:45 -0700 | [diff] [blame] | 128 | int windowType, int gravity, boolean takesFocus) { |
The Android Open Source Project | 9066cfe | 2009-03-03 19:31:44 -0800 | [diff] [blame] | 129 | super(context, theme); |
Dianne Hackborn | c03c916 | 2014-05-02 10:45:59 -0700 | [diff] [blame] | 130 | mName = name; |
| 131 | mCallback = callback; |
| 132 | mKeyEventCallback = keyEventCallback; |
Dianne Hackborn | 83fe3f5 | 2009-09-12 23:38:30 -0700 | [diff] [blame] | 133 | mDispatcherState = dispatcherState; |
Dianne Hackborn | e30e02f | 2014-05-27 18:24:45 -0700 | [diff] [blame] | 134 | mWindowType = windowType; |
| 135 | mGravity = gravity; |
Dianne Hackborn | c03c916 | 2014-05-02 10:45:59 -0700 | [diff] [blame] | 136 | mTakesFocus = takesFocus; |
The Android Open Source Project | 9066cfe | 2009-03-03 19:31:44 -0800 | [diff] [blame] | 137 | initDockWindow(); |
| 138 | } |
| 139 | |
Dianne Hackborn | 83fe3f5 | 2009-09-12 23:38:30 -0700 | [diff] [blame] | 140 | @Override |
| 141 | public void onWindowFocusChanged(boolean hasFocus) { |
| 142 | super.onWindowFocusChanged(hasFocus); |
| 143 | mDispatcherState.reset(); |
| 144 | } |
| 145 | |
Tadashi G. Takaoka | 7bd6c20 | 2011-01-25 18:02:55 +0900 | [diff] [blame] | 146 | @Override |
| 147 | public boolean dispatchTouchEvent(MotionEvent ev) { |
| 148 | getWindow().getDecorView().getHitRect(mBounds); |
Jeff Brown | fe9f8ab | 2011-05-06 18:20:01 -0700 | [diff] [blame] | 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 | } |
Tadashi G. Takaoka | 7bd6c20 | 2011-01-25 18:02:55 +0900 | [diff] [blame] | 160 | } |
| 161 | |
The Android Open Source Project | 9066cfe | 2009-03-03 19:31:44 -0800 | [diff] [blame] | 162 | /** |
The Android Open Source Project | 9066cfe | 2009-03-03 19:31:44 -0800 | [diff] [blame] | 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(); |
The Android Open Source Project | 9066cfe | 2009-03-03 19:31:44 -0800 | [diff] [blame] | 172 | lp.gravity = gravity; |
Dianne Hackborn | e30e02f | 2014-05-27 18:24:45 -0700 | [diff] [blame] | 173 | updateWidthHeight(lp); |
| 174 | getWindow().setAttributes(lp); |
| 175 | } |
The Android Open Source Project | 9066cfe | 2009-03-03 19:31:44 -0800 | [diff] [blame] | 176 | |
Dianne Hackborn | 20d9474 | 2014-05-29 18:35:45 -0700 | [diff] [blame] | 177 | public int getGravity() { |
| 178 | return getWindow().getAttributes().gravity; |
| 179 | } |
| 180 | |
Dianne Hackborn | e30e02f | 2014-05-27 18:24:45 -0700 | [diff] [blame] | 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; |
The Android Open Source Project | 9066cfe | 2009-03-03 19:31:44 -0800 | [diff] [blame] | 188 | } |
| 189 | } |
| 190 | |
Dianne Hackborn | c03c916 | 2014-05-02 10:45:59 -0700 | [diff] [blame] | 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 | |
The Android Open Source Project | 9066cfe | 2009-03-03 19:31:44 -0800 | [diff] [blame] | 227 | private void initDockWindow() { |
| 228 | WindowManager.LayoutParams lp = getWindow().getAttributes(); |
| 229 | |
Dianne Hackborn | e30e02f | 2014-05-27 18:24:45 -0700 | [diff] [blame] | 230 | lp.type = mWindowType; |
Dianne Hackborn | c03c916 | 2014-05-02 10:45:59 -0700 | [diff] [blame] | 231 | lp.setTitle(mName); |
The Android Open Source Project | 9066cfe | 2009-03-03 19:31:44 -0800 | [diff] [blame] | 232 | |
Dianne Hackborn | e30e02f | 2014-05-27 18:24:45 -0700 | [diff] [blame] | 233 | lp.gravity = mGravity; |
| 234 | updateWidthHeight(lp); |
The Android Open Source Project | 9066cfe | 2009-03-03 19:31:44 -0800 | [diff] [blame] | 235 | |
| 236 | getWindow().setAttributes(lp); |
Dianne Hackborn | c03c916 | 2014-05-02 10:45:59 -0700 | [diff] [blame] | 237 | |
| 238 | int windowSetFlags = WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN; |
| 239 | int windowModFlags = WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN | |
The Android Open Source Project | 9066cfe | 2009-03-03 19:31:44 -0800 | [diff] [blame] | 240 | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | |
Dianne Hackborn | c03c916 | 2014-05-02 10:45:59 -0700 | [diff] [blame] | 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); |
The Android Open Source Project | 9066cfe | 2009-03-03 19:31:44 -0800 | [diff] [blame] | 251 | } |
Yohei Yukawa | 13a9ffb | 2018-08-29 19:56:02 -0700 | [diff] [blame] | 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 | } |
The Android Open Source Project | 9066cfe | 2009-03-03 19:31:44 -0800 | [diff] [blame] | 357 | } |