1 /* 2 * Copyright (C) 2017 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.view.autofill; 18 19 import static android.view.autofill.Helper.sVerbose; 20 21 import android.annotation.NonNull; 22 import android.graphics.Point; 23 import android.graphics.Rect; 24 import android.graphics.drawable.Drawable; 25 import android.os.IBinder; 26 import android.os.RemoteException; 27 import android.transition.Transition; 28 import android.util.Log; 29 import android.view.View; 30 import android.view.View.OnTouchListener; 31 import android.view.ViewTreeObserver; 32 import android.view.WindowManager; 33 import android.view.WindowManager.LayoutParams; 34 import android.widget.PopupWindow; 35 36 /** 37 * Custom {@link PopupWindow} used to isolate its content from the autofilled app - the 38 * UI is rendered in a framework process, but it's controlled by the app. 39 * 40 * TODO(b/34943932): use an app surface control solution. 41 * 42 * @hide 43 */ 44 public class AutofillPopupWindow extends PopupWindow { 45 46 private static final String TAG = "AutofillPopupWindow"; 47 48 private final WindowPresenter mWindowPresenter; 49 private WindowManager.LayoutParams mWindowLayoutParams; 50 private boolean mFullScreen; 51 52 private final View.OnAttachStateChangeListener mOnAttachStateChangeListener = 53 new View.OnAttachStateChangeListener() { 54 @Override 55 public void onViewAttachedToWindow(View v) { 56 /* ignore - handled by the super class */ 57 } 58 59 @Override 60 public void onViewDetachedFromWindow(View v) { 61 dismiss(); 62 } 63 }; 64 65 /** 66 * Creates a popup window with a presenter owning the window and responsible for 67 * showing/hiding/updating the backing window. This can be useful of the window is 68 * being shown by another process while the popup logic is in the process hosting 69 * the anchor view. 70 * <p> 71 * Using this constructor means that the presenter completely owns the content of 72 * the window and the following methods manipulating the window content shouldn't 73 * be used: {@link #getEnterTransition()}, {@link #setEnterTransition(Transition)}, 74 * {@link #getExitTransition()}, {@link #setExitTransition(Transition)}, 75 * {@link #getContentView()}, {@link #setContentView(View)}, {@link #getBackground()}, 76 * {@link #setBackgroundDrawable(Drawable)}, {@link #getElevation()}, 77 * {@link #setElevation(float)}, ({@link #getAnimationStyle()}, 78 * {@link #setAnimationStyle(int)}, {@link #setTouchInterceptor(OnTouchListener)}.</p> 79 */ AutofillPopupWindow(@onNull IAutofillWindowPresenter presenter)80 public AutofillPopupWindow(@NonNull IAutofillWindowPresenter presenter) { 81 mWindowPresenter = new WindowPresenter(presenter); 82 83 setTouchModal(false); 84 setOutsideTouchable(true); 85 setInputMethodMode(INPUT_METHOD_NOT_NEEDED); 86 setFocusable(true); 87 } 88 89 @Override hasContentView()90 protected boolean hasContentView() { 91 return true; 92 } 93 94 @Override hasDecorView()95 protected boolean hasDecorView() { 96 return true; 97 } 98 99 @Override getDecorViewLayoutParams()100 protected LayoutParams getDecorViewLayoutParams() { 101 return mWindowLayoutParams; 102 } 103 104 /** 105 * The effective {@code update} method that should be called by its clients. 106 */ update(View anchor, int offsetX, int offsetY, int width, int height, Rect virtualBounds)107 public void update(View anchor, int offsetX, int offsetY, int width, int height, 108 Rect virtualBounds) { 109 mFullScreen = width == LayoutParams.MATCH_PARENT; 110 // For no fullscreen autofill window, we want to show the window as system controlled one 111 // so it covers app windows, but it has to be an application type (so it's contained inside 112 // the application area). Hence, we set it to the application type with the highest z-order, 113 // which currently is TYPE_APPLICATION_ABOVE_SUB_PANEL. 114 // For fullscreen mode, autofill window is at the bottom of screen, it should not be 115 // clipped by app activity window. Fullscreen autofill window does not need to follow app 116 // anchor view position. 117 setWindowLayoutType(mFullScreen ? WindowManager.LayoutParams.TYPE_SYSTEM_DIALOG 118 : WindowManager.LayoutParams.TYPE_APPLICATION_ABOVE_SUB_PANEL); 119 // If we are showing the popup for a virtual view we use a fake view which 120 // delegates to the anchor but present itself with the same bounds as the 121 // virtual view. This ensures that the location logic in popup works 122 // symmetrically when the dropdown is below and above the anchor. 123 final View actualAnchor; 124 if (mFullScreen) { 125 offsetX = 0; 126 offsetY = 0; 127 // If it is not fullscreen height, put window at bottom. Computes absolute position. 128 // Note that we cannot easily change default gravity from Gravity.TOP to 129 // Gravity.BOTTOM because PopupWindow base class does not expose computeGravity(). 130 final Point outPoint = new Point(); 131 anchor.getContext().getDisplay().getSize(outPoint); 132 width = outPoint.x; 133 if (height != LayoutParams.MATCH_PARENT) { 134 offsetY = outPoint.y - height; 135 } 136 actualAnchor = anchor; 137 } else if (virtualBounds != null) { 138 final int[] mLocationOnScreen = new int[] {virtualBounds.left, virtualBounds.top}; 139 actualAnchor = new View(anchor.getContext()) { 140 @Override 141 public void getLocationOnScreen(int[] location) { 142 location[0] = mLocationOnScreen[0]; 143 location[1] = mLocationOnScreen[1]; 144 } 145 146 @Override 147 public int getAccessibilityViewId() { 148 return anchor.getAccessibilityViewId(); 149 } 150 151 @Override 152 public ViewTreeObserver getViewTreeObserver() { 153 return anchor.getViewTreeObserver(); 154 } 155 156 @Override 157 public IBinder getApplicationWindowToken() { 158 return anchor.getApplicationWindowToken(); 159 } 160 161 @Override 162 public View getRootView() { 163 return anchor.getRootView(); 164 } 165 166 @Override 167 public int getLayoutDirection() { 168 return anchor.getLayoutDirection(); 169 } 170 171 @Override 172 public void getWindowDisplayFrame(Rect outRect) { 173 anchor.getWindowDisplayFrame(outRect); 174 } 175 176 @Override 177 public void addOnAttachStateChangeListener( 178 OnAttachStateChangeListener listener) { 179 anchor.addOnAttachStateChangeListener(listener); 180 } 181 182 @Override 183 public void removeOnAttachStateChangeListener( 184 OnAttachStateChangeListener listener) { 185 anchor.removeOnAttachStateChangeListener(listener); 186 } 187 188 @Override 189 public boolean isAttachedToWindow() { 190 return anchor.isAttachedToWindow(); 191 } 192 193 @Override 194 public boolean requestRectangleOnScreen(Rect rectangle, boolean immediate) { 195 return anchor.requestRectangleOnScreen(rectangle, immediate); 196 } 197 198 @Override 199 public IBinder getWindowToken() { 200 return anchor.getWindowToken(); 201 } 202 }; 203 204 actualAnchor.setLeftTopRightBottom( 205 virtualBounds.left, virtualBounds.top, 206 virtualBounds.right, virtualBounds.bottom); 207 actualAnchor.setScrollX(anchor.getScrollX()); 208 actualAnchor.setScrollY(anchor.getScrollY()); 209 210 anchor.setOnScrollChangeListener((v, scrollX, scrollY, oldScrollX, oldScrollY) -> { 211 mLocationOnScreen[0] = mLocationOnScreen[0] - (scrollX - oldScrollX); 212 mLocationOnScreen[1] = mLocationOnScreen[1] - (scrollY - oldScrollY); 213 }); 214 actualAnchor.setWillNotDraw(true); 215 } else { 216 actualAnchor = anchor; 217 } 218 219 if (!mFullScreen) { 220 // No fullscreen window animation is controlled by PopupWindow. 221 setAnimationStyle(-1); 222 } else if (height == LayoutParams.MATCH_PARENT) { 223 // Complete fullscreen autofill window has no animation. 224 setAnimationStyle(0); 225 } else { 226 // Slide half screen height autofill window from bottom. 227 setAnimationStyle(com.android.internal.R.style.AutofillHalfScreenAnimation); 228 } 229 if (!isShowing()) { 230 setWidth(width); 231 setHeight(height); 232 showAsDropDown(actualAnchor, offsetX, offsetY); 233 } else { 234 update(actualAnchor, offsetX, offsetY, width, height); 235 } 236 } 237 238 @Override update(View anchor, WindowManager.LayoutParams params)239 protected void update(View anchor, WindowManager.LayoutParams params) { 240 final int layoutDirection = anchor != null ? anchor.getLayoutDirection() 241 : View.LAYOUT_DIRECTION_LOCALE; 242 mWindowPresenter.show(params, getTransitionEpicenter(), isLayoutInsetDecor(), 243 layoutDirection); 244 } 245 246 @Override findDropDownPosition(View anchor, LayoutParams outParams, int xOffset, int yOffset, int width, int height, int gravity, boolean allowScroll)247 protected boolean findDropDownPosition(View anchor, LayoutParams outParams, 248 int xOffset, int yOffset, int width, int height, int gravity, boolean allowScroll) { 249 if (mFullScreen) { 250 // In fullscreen mode, don't need consider the anchor view. 251 outParams.x = xOffset; 252 outParams.y = yOffset; 253 outParams.width = width; 254 outParams.height = height; 255 outParams.gravity = gravity; 256 return false; 257 } 258 return super.findDropDownPosition(anchor, outParams, xOffset, yOffset, 259 width, height, gravity, allowScroll); 260 } 261 262 @Override showAsDropDown(View anchor, int xoff, int yoff, int gravity)263 public void showAsDropDown(View anchor, int xoff, int yoff, int gravity) { 264 if (sVerbose) { 265 Log.v(TAG, "showAsDropDown(): anchor=" + anchor + ", xoff=" + xoff + ", yoff=" + yoff 266 + ", isShowing(): " + isShowing()); 267 } 268 if (isShowing()) { 269 return; 270 } 271 272 setShowing(true); 273 setDropDown(true); 274 attachToAnchor(anchor, xoff, yoff, gravity); 275 final WindowManager.LayoutParams p = mWindowLayoutParams = createPopupLayoutParams( 276 anchor.getWindowToken()); 277 final boolean aboveAnchor = findDropDownPosition(anchor, p, xoff, yoff, 278 p.width, p.height, gravity, getAllowScrollingAnchorParent()); 279 updateAboveAnchor(aboveAnchor); 280 p.accessibilityIdOfAnchor = anchor.getAccessibilityViewId(); 281 p.packageName = anchor.getContext().getPackageName(); 282 mWindowPresenter.show(p, getTransitionEpicenter(), isLayoutInsetDecor(), 283 anchor.getLayoutDirection()); 284 } 285 286 @Override attachToAnchor(View anchor, int xoff, int yoff, int gravity)287 protected void attachToAnchor(View anchor, int xoff, int yoff, int gravity) { 288 super.attachToAnchor(anchor, xoff, yoff, gravity); 289 anchor.addOnAttachStateChangeListener(mOnAttachStateChangeListener); 290 } 291 292 @Override detachFromAnchor()293 protected void detachFromAnchor() { 294 final View anchor = getAnchor(); 295 if (anchor != null) { 296 anchor.removeOnAttachStateChangeListener(mOnAttachStateChangeListener); 297 } 298 super.detachFromAnchor(); 299 } 300 301 @Override dismiss()302 public void dismiss() { 303 if (!isShowing() || isTransitioningToDismiss()) { 304 return; 305 } 306 307 setShowing(false); 308 setTransitioningToDismiss(true); 309 310 mWindowPresenter.hide(getTransitionEpicenter()); 311 detachFromAnchor(); 312 if (getOnDismissListener() != null) { 313 getOnDismissListener().onDismiss(); 314 } 315 } 316 317 @Override getAnimationStyle()318 public int getAnimationStyle() { 319 throw new IllegalStateException("You can't call this!"); 320 } 321 322 @Override getBackground()323 public Drawable getBackground() { 324 throw new IllegalStateException("You can't call this!"); 325 } 326 327 @Override getContentView()328 public View getContentView() { 329 throw new IllegalStateException("You can't call this!"); 330 } 331 332 @Override getElevation()333 public float getElevation() { 334 throw new IllegalStateException("You can't call this!"); 335 } 336 337 @Override getEnterTransition()338 public Transition getEnterTransition() { 339 throw new IllegalStateException("You can't call this!"); 340 } 341 342 @Override getExitTransition()343 public Transition getExitTransition() { 344 throw new IllegalStateException("You can't call this!"); 345 } 346 347 @Override setBackgroundDrawable(Drawable background)348 public void setBackgroundDrawable(Drawable background) { 349 throw new IllegalStateException("You can't call this!"); 350 } 351 352 @Override setContentView(View contentView)353 public void setContentView(View contentView) { 354 if (contentView != null) { 355 throw new IllegalStateException("You can't call this!"); 356 } 357 } 358 359 @Override setElevation(float elevation)360 public void setElevation(float elevation) { 361 throw new IllegalStateException("You can't call this!"); 362 } 363 364 @Override setEnterTransition(Transition enterTransition)365 public void setEnterTransition(Transition enterTransition) { 366 throw new IllegalStateException("You can't call this!"); 367 } 368 369 @Override setExitTransition(Transition exitTransition)370 public void setExitTransition(Transition exitTransition) { 371 throw new IllegalStateException("You can't call this!"); 372 } 373 374 @Override setTouchInterceptor(OnTouchListener l)375 public void setTouchInterceptor(OnTouchListener l) { 376 throw new IllegalStateException("You can't call this!"); 377 } 378 379 /** 380 * Contract between the popup window and a presenter that is responsible for 381 * showing/hiding/updating the actual window. 382 * 383 * <p>This can be useful if the anchor is in one process and the backing window is owned by 384 * another process. 385 */ 386 private class WindowPresenter { 387 final IAutofillWindowPresenter mPresenter; 388 WindowPresenter(IAutofillWindowPresenter presenter)389 WindowPresenter(IAutofillWindowPresenter presenter) { 390 mPresenter = presenter; 391 } 392 393 /** 394 * Shows the backing window. 395 * 396 * @param p The window layout params. 397 * @param transitionEpicenter The transition epicenter if animating. 398 * @param fitsSystemWindows Whether the content view should account for system decorations. 399 * @param layoutDirection The content layout direction to be consistent with the anchor. 400 */ show(WindowManager.LayoutParams p, Rect transitionEpicenter, boolean fitsSystemWindows, int layoutDirection)401 void show(WindowManager.LayoutParams p, Rect transitionEpicenter, boolean fitsSystemWindows, 402 int layoutDirection) { 403 try { 404 mPresenter.show(p, transitionEpicenter, fitsSystemWindows, layoutDirection); 405 } catch (RemoteException e) { 406 Log.w(TAG, "Error showing fill window", e); 407 e.rethrowFromSystemServer(); 408 } 409 } 410 411 /** 412 * Hides the backing window. 413 * 414 * @param transitionEpicenter The transition epicenter if animating. 415 */ hide(Rect transitionEpicenter)416 void hide(Rect transitionEpicenter) { 417 try { 418 mPresenter.hide(transitionEpicenter); 419 } catch (RemoteException e) { 420 Log.w(TAG, "Error hiding fill window", e); 421 e.rethrowFromSystemServer(); 422 } 423 } 424 } 425 } 426