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