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