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