| /* |
| * Copyright (C) 2017 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package android.view.autofill; |
| |
| import static android.view.autofill.Helper.sVerbose; |
| |
| import android.annotation.NonNull; |
| import android.graphics.Rect; |
| import android.graphics.drawable.Drawable; |
| import android.os.IBinder; |
| import android.os.RemoteException; |
| import android.transition.Transition; |
| import android.util.Log; |
| import android.view.View; |
| import android.view.View.OnTouchListener; |
| import android.view.ViewTreeObserver; |
| import android.view.WindowManager; |
| import android.view.WindowManager.LayoutParams; |
| import android.widget.PopupWindow; |
| |
| /** |
| * Custom {@link PopupWindow} used to isolate its content from the autofilled app - the |
| * UI is rendered in a framework process, but it's controlled by the app. |
| * |
| * TODO(b/34943932): use an app surface control solution. |
| * |
| * @hide |
| */ |
| public class AutofillPopupWindow extends PopupWindow { |
| |
| private static final String TAG = "AutofillPopupWindow"; |
| |
| private final WindowPresenter mWindowPresenter; |
| private WindowManager.LayoutParams mWindowLayoutParams; |
| private boolean mFullScreen; |
| |
| private final View.OnAttachStateChangeListener mOnAttachStateChangeListener = |
| new View.OnAttachStateChangeListener() { |
| @Override |
| public void onViewAttachedToWindow(View v) { |
| /* ignore - handled by the super class */ |
| } |
| |
| @Override |
| public void onViewDetachedFromWindow(View v) { |
| dismiss(); |
| } |
| }; |
| |
| /** |
| * Creates a popup window with a presenter owning the window and responsible for |
| * showing/hiding/updating the backing window. This can be useful of the window is |
| * being shown by another process while the popup logic is in the process hosting |
| * the anchor view. |
| * <p> |
| * Using this constructor means that the presenter completely owns the content of |
| * the window and the following methods manipulating the window content shouldn't |
| * be used: {@link #getEnterTransition()}, {@link #setEnterTransition(Transition)}, |
| * {@link #getExitTransition()}, {@link #setExitTransition(Transition)}, |
| * {@link #getContentView()}, {@link #setContentView(View)}, {@link #getBackground()}, |
| * {@link #setBackgroundDrawable(Drawable)}, {@link #getElevation()}, |
| * {@link #setElevation(float)}, ({@link #getAnimationStyle()}, |
| * {@link #setAnimationStyle(int)}, {@link #setTouchInterceptor(OnTouchListener)}.</p> |
| */ |
| public AutofillPopupWindow(@NonNull IAutofillWindowPresenter presenter) { |
| mWindowPresenter = new WindowPresenter(presenter); |
| |
| // Here is a bit of voodoo - we want to show the window as system |
| // controlled one so it covers app windows, but at the same time it has to be |
| // an application type (so it's contained inside the application area). |
| // Hence, we set it to the application type with the highest z-order, which currently |
| // is TYPE_APPLICATION_ABOVE_SUB_PANEL. |
| setWindowLayoutType(WindowManager.LayoutParams.TYPE_APPLICATION_ABOVE_SUB_PANEL); |
| setTouchModal(false); |
| setOutsideTouchable(true); |
| setInputMethodMode(INPUT_METHOD_NOT_NEEDED); |
| setFocusable(true); |
| } |
| |
| @Override |
| protected boolean hasContentView() { |
| return true; |
| } |
| |
| @Override |
| protected boolean hasDecorView() { |
| return true; |
| } |
| |
| @Override |
| protected LayoutParams getDecorViewLayoutParams() { |
| return mWindowLayoutParams; |
| } |
| |
| /** |
| * The effective {@code update} method that should be called by its clients. |
| */ |
| public void update(View anchor, int offsetX, int offsetY, int width, int height, |
| Rect virtualBounds) { |
| mFullScreen = width == LayoutParams.MATCH_PARENT && height == LayoutParams.MATCH_PARENT; |
| // If we are showing the popup for a virtual view we use a fake view which |
| // delegates to the anchor but present itself with the same bounds as the |
| // virtual view. This ensures that the location logic in popup works |
| // symmetrically when the dropdown is below and above the anchor. |
| final View actualAnchor; |
| if (mFullScreen) { |
| offsetX = 0; |
| offsetY = 0; |
| actualAnchor = anchor; |
| } else if (virtualBounds != null) { |
| final int[] mLocationOnScreen = new int[] {virtualBounds.left, virtualBounds.top}; |
| actualAnchor = new View(anchor.getContext()) { |
| @Override |
| public void getLocationOnScreen(int[] location) { |
| location[0] = mLocationOnScreen[0]; |
| location[1] = mLocationOnScreen[1]; |
| } |
| |
| @Override |
| public int getAccessibilityViewId() { |
| return anchor.getAccessibilityViewId(); |
| } |
| |
| @Override |
| public ViewTreeObserver getViewTreeObserver() { |
| return anchor.getViewTreeObserver(); |
| } |
| |
| @Override |
| public IBinder getApplicationWindowToken() { |
| return anchor.getApplicationWindowToken(); |
| } |
| |
| @Override |
| public View getRootView() { |
| return anchor.getRootView(); |
| } |
| |
| @Override |
| public int getLayoutDirection() { |
| return anchor.getLayoutDirection(); |
| } |
| |
| @Override |
| public void getWindowDisplayFrame(Rect outRect) { |
| anchor.getWindowDisplayFrame(outRect); |
| } |
| |
| @Override |
| public void addOnAttachStateChangeListener( |
| OnAttachStateChangeListener listener) { |
| anchor.addOnAttachStateChangeListener(listener); |
| } |
| |
| @Override |
| public void removeOnAttachStateChangeListener( |
| OnAttachStateChangeListener listener) { |
| anchor.removeOnAttachStateChangeListener(listener); |
| } |
| |
| @Override |
| public boolean isAttachedToWindow() { |
| return anchor.isAttachedToWindow(); |
| } |
| |
| @Override |
| public boolean requestRectangleOnScreen(Rect rectangle, boolean immediate) { |
| return anchor.requestRectangleOnScreen(rectangle, immediate); |
| } |
| |
| @Override |
| public IBinder getWindowToken() { |
| return anchor.getWindowToken(); |
| } |
| }; |
| |
| actualAnchor.setLeftTopRightBottom( |
| virtualBounds.left, virtualBounds.top, |
| virtualBounds.right, virtualBounds.bottom); |
| actualAnchor.setScrollX(anchor.getScrollX()); |
| actualAnchor.setScrollY(anchor.getScrollY()); |
| |
| anchor.setOnScrollChangeListener((v, scrollX, scrollY, oldScrollX, oldScrollY) -> { |
| mLocationOnScreen[0] = mLocationOnScreen[0] - (scrollX - oldScrollX); |
| mLocationOnScreen[1] = mLocationOnScreen[1] - (scrollY - oldScrollY); |
| }); |
| actualAnchor.setWillNotDraw(true); |
| } else { |
| actualAnchor = anchor; |
| } |
| |
| if (!isShowing()) { |
| setWidth(width); |
| setHeight(height); |
| showAsDropDown(actualAnchor, offsetX, offsetY); |
| } else { |
| update(actualAnchor, offsetX, offsetY, width, height); |
| } |
| } |
| |
| @Override |
| protected void update(View anchor, WindowManager.LayoutParams params) { |
| final int layoutDirection = anchor != null ? anchor.getLayoutDirection() |
| : View.LAYOUT_DIRECTION_LOCALE; |
| mWindowPresenter.show(params, getTransitionEpicenter(), isLayoutInsetDecor(), |
| layoutDirection); |
| } |
| |
| @Override |
| protected boolean findDropDownPosition(View anchor, LayoutParams outParams, |
| int xOffset, int yOffset, int width, int height, int gravity, boolean allowScroll) { |
| if (mFullScreen) { |
| // Do not patch LayoutParams if force full screen |
| return false; |
| } |
| return super.findDropDownPosition(anchor, outParams, xOffset, yOffset, |
| width, height, gravity, allowScroll); |
| } |
| |
| @Override |
| public void showAsDropDown(View anchor, int xoff, int yoff, int gravity) { |
| if (sVerbose) { |
| Log.v(TAG, "showAsDropDown(): anchor=" + anchor + ", xoff=" + xoff + ", yoff=" + yoff |
| + ", isShowing(): " + isShowing()); |
| } |
| if (isShowing()) { |
| return; |
| } |
| |
| setShowing(true); |
| setDropDown(true); |
| attachToAnchor(anchor, xoff, yoff, gravity); |
| final WindowManager.LayoutParams p = mWindowLayoutParams = createPopupLayoutParams( |
| anchor.getWindowToken()); |
| final boolean aboveAnchor = findDropDownPosition(anchor, p, xoff, yoff, |
| p.width, p.height, gravity, getAllowScrollingAnchorParent()); |
| updateAboveAnchor(aboveAnchor); |
| p.accessibilityIdOfAnchor = anchor.getAccessibilityViewId(); |
| p.packageName = anchor.getContext().getPackageName(); |
| mWindowPresenter.show(p, getTransitionEpicenter(), isLayoutInsetDecor(), |
| anchor.getLayoutDirection()); |
| } |
| |
| @Override |
| protected void attachToAnchor(View anchor, int xoff, int yoff, int gravity) { |
| super.attachToAnchor(anchor, xoff, yoff, gravity); |
| anchor.addOnAttachStateChangeListener(mOnAttachStateChangeListener); |
| } |
| |
| @Override |
| protected void detachFromAnchor() { |
| final View anchor = getAnchor(); |
| if (anchor != null) { |
| anchor.removeOnAttachStateChangeListener(mOnAttachStateChangeListener); |
| } |
| super.detachFromAnchor(); |
| } |
| |
| @Override |
| public void dismiss() { |
| if (!isShowing() || isTransitioningToDismiss()) { |
| return; |
| } |
| |
| setShowing(false); |
| setTransitioningToDismiss(true); |
| |
| mWindowPresenter.hide(getTransitionEpicenter()); |
| detachFromAnchor(); |
| if (getOnDismissListener() != null) { |
| getOnDismissListener().onDismiss(); |
| } |
| } |
| |
| @Override |
| public int getAnimationStyle() { |
| throw new IllegalStateException("You can't call this!"); |
| } |
| |
| @Override |
| public Drawable getBackground() { |
| throw new IllegalStateException("You can't call this!"); |
| } |
| |
| @Override |
| public View getContentView() { |
| throw new IllegalStateException("You can't call this!"); |
| } |
| |
| @Override |
| public float getElevation() { |
| throw new IllegalStateException("You can't call this!"); |
| } |
| |
| @Override |
| public Transition getEnterTransition() { |
| throw new IllegalStateException("You can't call this!"); |
| } |
| |
| @Override |
| public Transition getExitTransition() { |
| throw new IllegalStateException("You can't call this!"); |
| } |
| |
| @Override |
| public void setAnimationStyle(int animationStyle) { |
| throw new IllegalStateException("You can't call this!"); |
| } |
| |
| @Override |
| public void setBackgroundDrawable(Drawable background) { |
| throw new IllegalStateException("You can't call this!"); |
| } |
| |
| @Override |
| public void setContentView(View contentView) { |
| if (contentView != null) { |
| throw new IllegalStateException("You can't call this!"); |
| } |
| } |
| |
| @Override |
| public void setElevation(float elevation) { |
| throw new IllegalStateException("You can't call this!"); |
| } |
| |
| @Override |
| public void setEnterTransition(Transition enterTransition) { |
| throw new IllegalStateException("You can't call this!"); |
| } |
| |
| @Override |
| public void setExitTransition(Transition exitTransition) { |
| throw new IllegalStateException("You can't call this!"); |
| } |
| |
| @Override |
| public void setTouchInterceptor(OnTouchListener l) { |
| throw new IllegalStateException("You can't call this!"); |
| } |
| |
| /** |
| * Contract between the popup window and a presenter that is responsible for |
| * showing/hiding/updating the actual window. |
| * |
| * <p>This can be useful if the anchor is in one process and the backing window is owned by |
| * another process. |
| */ |
| private class WindowPresenter { |
| final IAutofillWindowPresenter mPresenter; |
| |
| WindowPresenter(IAutofillWindowPresenter presenter) { |
| mPresenter = presenter; |
| } |
| |
| /** |
| * Shows the backing window. |
| * |
| * @param p The window layout params. |
| * @param transitionEpicenter The transition epicenter if animating. |
| * @param fitsSystemWindows Whether the content view should account for system decorations. |
| * @param layoutDirection The content layout direction to be consistent with the anchor. |
| */ |
| void show(WindowManager.LayoutParams p, Rect transitionEpicenter, boolean fitsSystemWindows, |
| int layoutDirection) { |
| try { |
| mPresenter.show(p, transitionEpicenter, fitsSystemWindows, layoutDirection); |
| } catch (RemoteException e) { |
| Log.w(TAG, "Error showing fill window", e); |
| e.rethrowFromSystemServer(); |
| } |
| } |
| |
| /** |
| * Hides the backing window. |
| * |
| * @param transitionEpicenter The transition epicenter if animating. |
| */ |
| void hide(Rect transitionEpicenter) { |
| try { |
| mPresenter.hide(transitionEpicenter); |
| } catch (RemoteException e) { |
| Log.w(TAG, "Error hiding fill window", e); |
| e.rethrowFromSystemServer(); |
| } |
| } |
| } |
| } |