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