1 /*
2  * Copyright (C) 2021 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.service.voice;
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.graphics.Rect;
25 import android.os.Debug;
26 import android.os.IBinder;
27 import android.util.Log;
28 import android.view.Gravity;
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 VoiceInteractionWindow} is a {@link Dialog} that is intended to be used for a top-level
38  * {@link VoiceInteractionSession}. It will be displayed along the edge of the screen, moving the
39  * application user interface away from it so that the focused item is always visible.
40  */
41 final class VoiceInteractionWindow extends Dialog {
42     private static final boolean DEBUG = false;
43     private static final String TAG = "VoiceInteractionWindow";
44 
45     private final String mName;
46     private final Callback mCallback;
47     private final KeyEvent.Callback mKeyEventCallback;
48     private final KeyEvent.DispatcherState mDispatcherState;
49     private final int mWindowType;
50     private final int mGravity;
51     private final boolean mTakesFocus;
52     private final Rect mBounds = new Rect();
53 
54     @Retention(SOURCE)
55     @IntDef(value = {WindowState.TOKEN_PENDING, WindowState.TOKEN_SET,
56             WindowState.SHOWN_AT_LEAST_ONCE, WindowState.REJECTED_AT_LEAST_ONCE,
57             WindowState.DESTROYED})
58     private @interface WindowState {
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 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     @WindowState
83     private int mWindowState = WindowState.TOKEN_PENDING;
84 
85     /**
86      * Used to provide callbacks.
87      */
88     interface Callback {
89         /**
90          * Used to be notified when {@link Dialog#onBackPressed()} gets called.
91          */
onBackPressed()92         void onBackPressed();
93     }
94 
95     /**
96      * Set {@link IBinder} window token to the window.
97      *
98      * <p>This method can be called only once.</p>
99      * @param token {@link IBinder} token to be associated with the window.
100      */
setToken(IBinder token)101     void setToken(IBinder token) {
102         switch (mWindowState) {
103             case WindowState.TOKEN_PENDING:
104                 // Normal scenario.  Nothing to worry about.
105                 WindowManager.LayoutParams lp = getWindow().getAttributes();
106                 lp.token = token;
107                 getWindow().setAttributes(lp);
108                 updateWindowState(WindowState.TOKEN_SET);
109 
110                 // As soon as we have a token, make sure the window is added (but not shown) by
111                 // setting visibility to INVISIBLE and calling show() on Dialog. Note that
112                 // WindowInsetsController.OnControllableInsetsChangedListener relies on the window
113                 // being added to function.
114                 getWindow().getDecorView().setVisibility(View.INVISIBLE);
115                 show();
116                 return;
117             case WindowState.TOKEN_SET:
118             case WindowState.SHOWN_AT_LEAST_ONCE:
119             case WindowState.REJECTED_AT_LEAST_ONCE:
120                 throw new IllegalStateException("setToken can be called only once");
121             case WindowState.DESTROYED:
122                 // Just ignore.  Since there are multiple event queues from the token is issued
123                 // in the system server to the timing when it arrives here, it can be delivered
124                 // after the is already destroyed.  No one should be blamed because of such an
125                 // unfortunate but possible scenario.
126                 Log.i(TAG, "Ignoring setToken() because window is already destroyed.");
127                 return;
128             default:
129                 throw new IllegalStateException("Unexpected state=" + mWindowState);
130         }
131     }
132 
133     /**
134      * Create a {@link VoiceInteractionWindow} that uses a custom style.
135      *
136      * @param context The Context in which the DockWindow should run. In
137      *        particular, it uses the window manager and theme from this context
138      *        to present its UI.
139      * @param theme A style resource describing the theme to use for the window.
140      *        See <a href="{@docRoot}reference/available-resources.html#stylesandthemes">Style
141      *        and Theme Resources</a> for more information about defining and
142      *        using styles. This theme is applied on top of the current theme in
143      *        <var>context</var>. If 0, the default dialog theme will be used.
144      */
VoiceInteractionWindow(Context context, String name, int theme, Callback callback, KeyEvent.Callback keyEventCallback, KeyEvent.DispatcherState dispatcherState, int windowType, int gravity, boolean takesFocus)145     VoiceInteractionWindow(Context context, String name, int theme, Callback callback,
146             KeyEvent.Callback keyEventCallback, KeyEvent.DispatcherState dispatcherState,
147             int windowType, int gravity, boolean takesFocus) {
148         super(context, theme);
149         mName = name;
150         mCallback = callback;
151         mKeyEventCallback = keyEventCallback;
152         mDispatcherState = dispatcherState;
153         mWindowType = windowType;
154         mGravity = gravity;
155         mTakesFocus = takesFocus;
156         initDockWindow();
157     }
158 
159     @Override
onWindowFocusChanged(boolean hasFocus)160     public void onWindowFocusChanged(boolean hasFocus) {
161         super.onWindowFocusChanged(hasFocus);
162         mDispatcherState.reset();
163     }
164 
165     @Override
dispatchTouchEvent(MotionEvent ev)166     public boolean dispatchTouchEvent(MotionEvent ev) {
167         getWindow().getDecorView().getHitRect(mBounds);
168 
169         if (ev.isWithinBoundsNoHistory(mBounds.left, mBounds.top,
170                 mBounds.right - 1, mBounds.bottom - 1)) {
171             return super.dispatchTouchEvent(ev);
172         } else {
173             MotionEvent temp = ev.clampNoHistory(mBounds.left, mBounds.top,
174                     mBounds.right - 1, mBounds.bottom - 1);
175             boolean handled = super.dispatchTouchEvent(temp);
176             temp.recycle();
177             return handled;
178         }
179     }
180 
updateWidthHeight(WindowManager.LayoutParams lp)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;
188         }
189     }
190 
191     @Override
onKeyDown(int keyCode, KeyEvent event)192     public boolean onKeyDown(int keyCode, KeyEvent event) {
193         if (mKeyEventCallback != null && mKeyEventCallback.onKeyDown(keyCode, event)) {
194             return true;
195         }
196         return super.onKeyDown(keyCode, event);
197     }
198 
199     @Override
onKeyLongPress(int keyCode, KeyEvent event)200     public boolean onKeyLongPress(int keyCode, KeyEvent event) {
201         if (mKeyEventCallback != null && mKeyEventCallback.onKeyLongPress(keyCode, event)) {
202             return true;
203         }
204         return super.onKeyLongPress(keyCode, event);
205     }
206 
207     @Override
onKeyUp(int keyCode, KeyEvent event)208     public boolean onKeyUp(int keyCode, KeyEvent event) {
209         if (mKeyEventCallback != null && mKeyEventCallback.onKeyUp(keyCode, event)) {
210             return true;
211         }
212         return super.onKeyUp(keyCode, event);
213     }
214 
215     @Override
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 
223     @Override
onBackPressed()224     public void onBackPressed() {
225         if (mCallback != null) {
226             mCallback.onBackPressed();
227         } else {
228             super.onBackPressed();
229         }
230     }
231 
initDockWindow()232     private void initDockWindow() {
233         WindowManager.LayoutParams lp = getWindow().getAttributes();
234 
235         lp.type = mWindowType;
236         lp.setTitle(mName);
237 
238         lp.gravity = mGravity;
239         updateWidthHeight(lp);
240 
241         getWindow().setAttributes(lp);
242 
243         int windowSetFlags = WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN;
244         int windowModFlags = WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN
245                 | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
246                 | WindowManager.LayoutParams.FLAG_DIM_BEHIND;
247 
248         if (!mTakesFocus) {
249             windowSetFlags |= WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
250         } else {
251             windowSetFlags |= WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL;
252             windowModFlags |= WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL;
253         }
254 
255         getWindow().setFlags(windowSetFlags, windowModFlags);
256     }
257 
258     @Override
show()259     public void show() {
260         switch (mWindowState) {
261             case WindowState.TOKEN_PENDING:
262                 throw new IllegalStateException("Window token is not set yet.");
263             case WindowState.TOKEN_SET:
264             case WindowState.SHOWN_AT_LEAST_ONCE:
265                 // Normal scenario.  Nothing to worry about.
266                 try {
267                     super.show();
268                     updateWindowState(WindowState.SHOWN_AT_LEAST_ONCE);
269                 } catch (WindowManager.BadTokenException e) {
270                     // Just ignore this exception.  Since show() can be requested from other
271                     // components such as the system and there could be multiple event queues before
272                     // the request finally arrives here, the system may have already invalidated the
273                     // window token attached to our window.  In such a scenario, receiving
274                     // BadTokenException here is an expected behavior.  We just ignore it and update
275                     // the state so that we do not touch this window later.
276                     Log.i(TAG, "Probably the IME window token is already invalidated."
277                             + " show() does nothing.");
278                     updateWindowState(WindowState.REJECTED_AT_LEAST_ONCE);
279                 }
280                 return;
281             case WindowState.REJECTED_AT_LEAST_ONCE:
282                 // Just ignore.  In general we cannot completely avoid this kind of race condition.
283                 Log.i(TAG, "Not trying to call show() because it was already rejected once.");
284                 return;
285             case WindowState.DESTROYED:
286                 // Just ignore.  In general we cannot completely avoid this kind of race condition.
287                 Log.i(TAG, "Ignoring show() because the window is already destroyed.");
288                 return;
289             default:
290                 throw new IllegalStateException("Unexpected state=" + mWindowState);
291         }
292     }
293 
updateWindowState(@indowState int newState)294     private void updateWindowState(@WindowState int newState) {
295         if (DEBUG) {
296             if (mWindowState != newState) {
297                 Log.d(TAG, "WindowState: " + stateToString(mWindowState) + " -> "
298                         + stateToString(newState) + " @ " + Debug.getCaller());
299             }
300         }
301         mWindowState = newState;
302     }
303 
stateToString(@indowState int state)304     private static String stateToString(@WindowState int state) {
305         switch (state) {
306             case WindowState.TOKEN_PENDING:
307                 return "TOKEN_PENDING";
308             case WindowState.TOKEN_SET:
309                 return "TOKEN_SET";
310             case WindowState.SHOWN_AT_LEAST_ONCE:
311                 return "SHOWN_AT_LEAST_ONCE";
312             case WindowState.REJECTED_AT_LEAST_ONCE:
313                 return "REJECTED_AT_LEAST_ONCE";
314             case WindowState.DESTROYED:
315                 return "DESTROYED";
316             default:
317                 throw new IllegalStateException("Unknown state=" + state);
318         }
319     }
320 }
321