1 /* 2 * Copyright (C) 2007 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of 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, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package android.inputmethodservice; 18 19 import static android.inputmethodservice.SoftInputWindowProto.BOUNDS; 20 import static android.inputmethodservice.SoftInputWindowProto.WINDOW_STATE; 21 22 import static java.lang.annotation.RetentionPolicy.SOURCE; 23 24 import android.annotation.IntDef; 25 import android.app.Dialog; 26 import android.graphics.Rect; 27 import android.os.Debug; 28 import android.os.IBinder; 29 import android.util.Log; 30 import android.util.proto.ProtoOutputStream; 31 import android.view.KeyEvent; 32 import android.view.MotionEvent; 33 import android.view.View; 34 import android.view.WindowManager; 35 36 import java.lang.annotation.Retention; 37 38 /** 39 * A {@link SoftInputWindow} is a {@link Dialog} that is intended to be used for a top-level input 40 * method window. It will be displayed along the edge of the screen, moving the application user 41 * interface away from it so that the focused item is always visible. 42 */ 43 final class SoftInputWindow extends Dialog { 44 private static final boolean DEBUG = false; 45 private static final String TAG = "SoftInputWindow"; 46 47 private final KeyEvent.DispatcherState mDispatcherState; 48 private final Rect mBounds = new Rect(); 49 private final InputMethodService mService; 50 51 @Retention(SOURCE) 52 @IntDef(value = {WindowState.TOKEN_PENDING, WindowState.TOKEN_SET, 53 WindowState.SHOWN_AT_LEAST_ONCE, WindowState.REJECTED_AT_LEAST_ONCE, 54 WindowState.DESTROYED}) 55 private @interface WindowState { 56 /** 57 * The window token is not set yet. 58 */ 59 int TOKEN_PENDING = 0; 60 /** 61 * The window token was set, but the window is not shown yet. 62 */ 63 int TOKEN_SET = 1; 64 /** 65 * The window was shown at least once. 66 */ 67 int SHOWN_AT_LEAST_ONCE = 2; 68 /** 69 * {@link android.view.WindowManager.BadTokenException} was sent when calling 70 * {@link Dialog#show()} at least once. 71 */ 72 int REJECTED_AT_LEAST_ONCE = 3; 73 /** 74 * The window is considered destroyed. Any incoming request should be ignored. 75 */ 76 int DESTROYED = 4; 77 } 78 79 @WindowState 80 private int mWindowState = WindowState.TOKEN_PENDING; 81 82 @Override allowsRegisterDefaultOnBackInvokedCallback()83 protected boolean allowsRegisterDefaultOnBackInvokedCallback() { 84 // Do not register OnBackInvokedCallback from Dialog#onStart, InputMethodService will 85 // register CompatOnBackInvokedCallback for input method window. 86 return false; 87 } 88 89 /** 90 * Set {@link IBinder} window token to the window. 91 * 92 * <p>This method can be called only once.</p> 93 * @param token {@link IBinder} token to be associated with the window. 94 */ setToken(IBinder token)95 void setToken(IBinder token) { 96 switch (mWindowState) { 97 case WindowState.TOKEN_PENDING: 98 // Normal scenario. Nothing to worry about. 99 WindowManager.LayoutParams lp = getWindow().getAttributes(); 100 lp.token = token; 101 getWindow().setAttributes(lp); 102 updateWindowState(WindowState.TOKEN_SET); 103 104 // As soon as we have a token, make sure the window is added (but not shown) by 105 // setting visibility to INVISIBLE and calling show() on Dialog. Note that 106 // WindowInsetsController.OnControllableInsetsChangedListener relies on the window 107 // being added to function. 108 getWindow().getDecorView().setVisibility(View.INVISIBLE); 109 show(); 110 return; 111 case WindowState.TOKEN_SET: 112 case WindowState.SHOWN_AT_LEAST_ONCE: 113 case WindowState.REJECTED_AT_LEAST_ONCE: 114 throw new IllegalStateException("setToken can be called only once"); 115 case WindowState.DESTROYED: 116 // Just ignore. Since there are multiple event queues from the token is issued 117 // in the system server to the timing when it arrives here, it can be delivered 118 // after the is already destroyed. No one should be blamed because of such an 119 // unfortunate but possible scenario. 120 Log.i(TAG, "Ignoring setToken() because window is already destroyed."); 121 return; 122 default: 123 throw new IllegalStateException("Unexpected state=" + mWindowState); 124 } 125 } 126 127 /** 128 * Create a SoftInputWindow that uses a custom style. 129 * 130 * @param service The {@link InputMethodService} in which the DockWindow should run. In 131 * particular, it uses the window manager and theme from this context 132 * to present its UI. 133 * @param theme A style resource describing the theme to use for the window. 134 * See <a href="{@docRoot}reference/available-resources.html#stylesandthemes">Style 135 * and Theme Resources</a> for more information about defining and 136 * using styles. This theme is applied on top of the current theme in 137 * <var>context</var>. If 0, the default dialog theme will be used. 138 */ SoftInputWindow(InputMethodService service, int theme, KeyEvent.DispatcherState dispatcherState)139 SoftInputWindow(InputMethodService service, int theme, 140 KeyEvent.DispatcherState dispatcherState) { 141 super(service, theme); 142 mService = service; 143 mDispatcherState = dispatcherState; 144 } 145 146 @Override onWindowFocusChanged(boolean hasFocus)147 public void onWindowFocusChanged(boolean hasFocus) { 148 super.onWindowFocusChanged(hasFocus); 149 mDispatcherState.reset(); 150 } 151 152 @Override dispatchTouchEvent(MotionEvent ev)153 public boolean dispatchTouchEvent(MotionEvent ev) { 154 getWindow().getDecorView().getHitRect(mBounds); 155 156 if (ev.isWithinBoundsNoHistory(mBounds.left, mBounds.top, 157 mBounds.right - 1, mBounds.bottom - 1)) { 158 return super.dispatchTouchEvent(ev); 159 } else { 160 MotionEvent temp = ev.clampNoHistory(mBounds.left, mBounds.top, 161 mBounds.right - 1, mBounds.bottom - 1); 162 boolean handled = super.dispatchTouchEvent(temp); 163 temp.recycle(); 164 return handled; 165 } 166 } 167 168 @Override show()169 public void show() { 170 switch (mWindowState) { 171 case WindowState.TOKEN_PENDING: 172 throw new IllegalStateException("Window token is not set yet."); 173 case WindowState.TOKEN_SET: 174 case WindowState.SHOWN_AT_LEAST_ONCE: 175 // Normal scenario. Nothing to worry about. 176 try { 177 super.show(); 178 updateWindowState(WindowState.SHOWN_AT_LEAST_ONCE); 179 } catch (WindowManager.BadTokenException e) { 180 // Just ignore this exception. Since show() can be requested from other 181 // components such as the system and there could be multiple event queues before 182 // the request finally arrives here, the system may have already invalidated the 183 // window token attached to our window. In such a scenario, receiving 184 // BadTokenException here is an expected behavior. We just ignore it and update 185 // the state so that we do not touch this window later. 186 Log.i(TAG, "Probably the IME window token is already invalidated." 187 + " show() does nothing."); 188 updateWindowState(WindowState.REJECTED_AT_LEAST_ONCE); 189 } 190 return; 191 case WindowState.REJECTED_AT_LEAST_ONCE: 192 // Just ignore. In general we cannot completely avoid this kind of race condition. 193 Log.i(TAG, "Not trying to call show() because it was already rejected once."); 194 return; 195 case WindowState.DESTROYED: 196 // Just ignore. In general we cannot completely avoid this kind of race condition. 197 Log.i(TAG, "Ignoring show() because the window is already destroyed."); 198 return; 199 default: 200 throw new IllegalStateException("Unexpected state=" + mWindowState); 201 } 202 } 203 dismissForDestroyIfNecessary()204 void dismissForDestroyIfNecessary() { 205 switch (mWindowState) { 206 case WindowState.TOKEN_PENDING: 207 case WindowState.TOKEN_SET: 208 // nothing to do because the window has never been shown. 209 updateWindowState(WindowState.DESTROYED); 210 return; 211 case WindowState.SHOWN_AT_LEAST_ONCE: 212 // Disable exit animation for the current IME window 213 // to avoid the race condition between the exit and enter animations 214 // when the current IME is being switched to another one. 215 try { 216 getWindow().setWindowAnimations(0); 217 dismiss(); 218 } catch (WindowManager.BadTokenException e) { 219 // Just ignore this exception. Since show() can be requested from other 220 // components such as the system and there could be multiple event queues before 221 // the request finally arrives here, the system may have already invalidated the 222 // window token attached to our window. In such a scenario, receiving 223 // BadTokenException here is an expected behavior. We just ignore it and update 224 // the state so that we do not touch this window later. 225 Log.i(TAG, "Probably the IME window token is already invalidated. " 226 + "No need to dismiss it."); 227 } 228 // Either way, consider that the window is destroyed. 229 updateWindowState(WindowState.DESTROYED); 230 return; 231 case WindowState.REJECTED_AT_LEAST_ONCE: 232 // Just ignore. In general we cannot completely avoid this kind of race condition. 233 Log.i(TAG, 234 "Not trying to dismiss the window because it is most likely unnecessary."); 235 // Anyway, consider that the window is destroyed. 236 updateWindowState(WindowState.DESTROYED); 237 return; 238 case WindowState.DESTROYED: 239 throw new IllegalStateException( 240 "dismissForDestroyIfNecessary can be called only once"); 241 default: 242 throw new IllegalStateException("Unexpected state=" + mWindowState); 243 } 244 } 245 updateWindowState(@indowState int newState)246 private void updateWindowState(@WindowState int newState) { 247 if (DEBUG) { 248 if (mWindowState != newState) { 249 Log.d(TAG, "WindowState: " + stateToString(mWindowState) + " -> " 250 + stateToString(newState) + " @ " + Debug.getCaller()); 251 } 252 } 253 mWindowState = newState; 254 } 255 stateToString(@indowState int state)256 private static String stateToString(@WindowState int state) { 257 switch (state) { 258 case WindowState.TOKEN_PENDING: 259 return "TOKEN_PENDING"; 260 case WindowState.TOKEN_SET: 261 return "TOKEN_SET"; 262 case WindowState.SHOWN_AT_LEAST_ONCE: 263 return "SHOWN_AT_LEAST_ONCE"; 264 case WindowState.REJECTED_AT_LEAST_ONCE: 265 return "REJECTED_AT_LEAST_ONCE"; 266 case WindowState.DESTROYED: 267 return "DESTROYED"; 268 default: 269 throw new IllegalStateException("Unknown state=" + state); 270 } 271 } 272 dumpDebug(ProtoOutputStream proto, long fieldId)273 void dumpDebug(ProtoOutputStream proto, long fieldId) { 274 final long token = proto.start(fieldId); 275 mBounds.dumpDebug(proto, BOUNDS); 276 proto.write(WINDOW_STATE, mWindowState); 277 proto.end(token); 278 } 279 } 280