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