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