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
17package android.view.autofill;
18
19import static android.view.autofill.Helper.sVerbose;
20
21import android.annotation.NonNull;
22import android.graphics.Point;
23import android.graphics.Rect;
24import android.graphics.drawable.Drawable;
25import android.os.IBinder;
26import android.os.RemoteException;
27import android.transition.Transition;
28import android.util.Log;
29import android.view.View;
30import android.view.View.OnTouchListener;
31import android.view.ViewTreeObserver;
32import android.view.WindowManager;
33import android.view.WindowManager.LayoutParams;
34import 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 */
44public 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     */
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
90    protected boolean hasContentView() {
91        return true;
92    }
93
94    @Override
95    protected boolean hasDecorView() {
96        return true;
97    }
98
99    @Override
100    protected LayoutParams getDecorViewLayoutParams() {
101        return mWindowLayoutParams;
102    }
103
104    /**
105     * The effective {@code update} method that should be called by its clients.
106     */
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
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
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
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
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
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
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
318    public int getAnimationStyle() {
319        throw new IllegalStateException("You can't call this!");
320    }
321
322    @Override
323    public Drawable getBackground() {
324        throw new IllegalStateException("You can't call this!");
325    }
326
327    @Override
328    public View getContentView() {
329        throw new IllegalStateException("You can't call this!");
330    }
331
332    @Override
333    public float getElevation() {
334        throw new IllegalStateException("You can't call this!");
335    }
336
337    @Override
338    public Transition getEnterTransition() {
339        throw new IllegalStateException("You can't call this!");
340    }
341
342    @Override
343    public Transition getExitTransition() {
344        throw new IllegalStateException("You can't call this!");
345    }
346
347    @Override
348    public void setBackgroundDrawable(Drawable background) {
349        throw new IllegalStateException("You can't call this!");
350    }
351
352    @Override
353    public void setContentView(View contentView) {
354        if (contentView != null) {
355            throw new IllegalStateException("You can't call this!");
356        }
357    }
358
359    @Override
360    public void setElevation(float elevation) {
361        throw new IllegalStateException("You can't call this!");
362    }
363
364    @Override
365    public void setEnterTransition(Transition enterTransition) {
366        throw new IllegalStateException("You can't call this!");
367    }
368
369    @Override
370    public void setExitTransition(Transition exitTransition) {
371        throw new IllegalStateException("You can't call this!");
372    }
373
374    @Override
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
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         */
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         */
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