Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit eb3c9344 authored by Jason Long's avatar Jason Long
Browse files

Improve AnchoredWindow with smart positioning logic.

* Add appToken.
* Add mWindowSizeListenerView for getting the displayable window size
  (screen - system windows - soft input).
* Add mContentView when we have all the necesssary info.
* Change mContentView only if something changes (height, bounds,
  displayBounds).

Bug: 34637800
Test: CTS
Change-Id: Iade6179f6303a6d4777e99fa6c600ba6352ee863
parent 8f60ea63
Loading
Loading
Loading
Loading
+2 −1
Original line number Diff line number Diff line
@@ -520,6 +520,7 @@
    <dimen name="item_touch_helper_swipe_escape_max_velocity">800dp</dimen>

    <!-- The elevation of AutoFill fill window-->
    <dimen name="autofill_fill_elevation">2dp</dimen>
    <dimen name="autofill_fill_elevation">4dp</dimen>
    <dimen name="autofill_fill_item_height">64dp</dimen>
    <dimen name="autofill_fill_min_margin">16dp</dimen>
</resources>
+1 −0
Original line number Diff line number Diff line
@@ -2833,6 +2833,7 @@
  <!-- com.android.server.autofill -->
  <java-symbol type="dimen" name="autofill_fill_elevation" />
  <java-symbol type="dimen" name="autofill_fill_item_height" />
  <java-symbol type="dimen" name="autofill_fill_min_margin" />
  <java-symbol type="layout" name="autofill_save"/>
  <java-symbol type="id" name="autofill_save_title" />
  <java-symbol type="id" name="autofill_save_no" />
+217 −74
Original line number Diff line number Diff line
@@ -17,134 +17,277 @@ package com.android.server.autofill;

import static com.android.server.autofill.Helper.DEBUG;

import android.annotation.Nullable;
import android.content.Context;
import android.graphics.Color;
import android.graphics.PixelFormat;
import android.graphics.Rect;
import android.os.IBinder;
import android.util.Slog;
import android.view.Gravity;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.View.MeasureSpec;
import android.view.WindowManager;
import android.view.WindowManager.LayoutParams;
import android.widget.FrameLayout;

import java.io.PrintWriter;
/**
 * A window above the application that is smartly anchored to a rectangular region.
 */
final class AnchoredWindow {
final class AnchoredWindow implements View.OnLayoutChangeListener, View.OnTouchListener {
    private static final String TAG = "AutoFill";

    private static final int NULL_HEIGHT = -1;

    private final WindowManager mWm;
    private final View mRootView;
    private final View mView;
    private final int mWidth;
    private final int mHeight;
    private boolean mIsShowing = false;
    private final IBinder mAppToken;
    private final View mContentView;

    private final View mWindowSizeListenerView;
    private final int mMinMargin;

    private int mLastHeight = NULL_HEIGHT;
    @Nullable
    private Rect mLastBounds;
    @Nullable
    private Rect mLastDisplayBounds;

    /**
     * Constructor.
     *
     * @param wm window manager that draws the view on a window
     * @param view singleton view in the window
     * @param width requested width of the view
     * @param height requested height of the view
     * @param wm window manager that draws the content on a window
     * @param appToken token to pass to window manager
     * @param contentView content of the window
     */
    AnchoredWindow(WindowManager wm, View view, int width, int height) {
    AnchoredWindow(WindowManager wm, IBinder appToken, View contentView) {
        mWm = wm;
        mRootView = wrapView(view, width, height);
        mView = view;
        mWidth = width;
        mHeight = height;
        mAppToken = appToken;
        mContentView = contentView;

        mContentView.addOnLayoutChangeListener(this);

        Context context = contentView.getContext();

        mWindowSizeListenerView = new FrameLayout(context);
        mWindowSizeListenerView.addOnLayoutChangeListener(this);

        mMinMargin = context.getResources().getDimensionPixelSize(
                com.android.internal.R.dimen.autofill_fill_min_margin);
    }

    /**
     * Shows the window.
     *
     * @param bounds the rectangular region this window should be anchored to
     * @param bounds the region the window should be anchored to
     */
    void show(Rect bounds) {
        final LayoutParams params = createBaseLayoutParams();
        params.x = bounds.left;
        params.y = bounds.bottom;
        if (DEBUG) Slog.d(TAG, "show bounds=" + bounds);

        if (!mIsShowing) {
            if (DEBUG) Slog.d(TAG, "adding view " + mView);
            mWm.addView(mRootView, params);
        } else {
            if (DEBUG) Slog.d(TAG, "updating view " + mView);
            mWm.updateViewLayout(mRootView, params);
        if (!mWindowSizeListenerView.isAttachedToWindow()) {
            if (DEBUG) Slog.d(TAG, "adding mWindowSizeListenerView");
            LayoutParams params = createWindowLayoutParams(
                    mAppToken,
                    LayoutParams.FLAG_NOT_TOUCHABLE); // not touchable
            params.gravity = Gravity.LEFT | Gravity.TOP;
            params.x = 0;
            params.y = 0;
            params.width = LayoutParams.MATCH_PARENT;
            params.height = LayoutParams.MATCH_PARENT;
            mWm.addView(mWindowSizeListenerView, params);
        }
        mIsShowing = true;

        updateBounds(bounds);
    }

    /**
     * Hides the window.
     */
    void hide() {
        if (DEBUG) Slog.d(TAG, "removing view " + mView);
        if (DEBUG) Slog.d(TAG, "hide");

        if (mIsShowing) {
            mWm.removeView(mRootView);
        }
        mIsShowing = false;
        mLastHeight = NULL_HEIGHT;
        mLastBounds = null;
        mLastDisplayBounds = null;

        if (mWindowSizeListenerView.isAttachedToWindow()) {
            if (DEBUG) Slog.d(TAG, "removing mWindowSizeListenerView");
            mWm.removeView(mWindowSizeListenerView);
        }

    /**
     * Wraps a view with a SelfRemovingView and sets its requested width and height.
     */
    private View wrapView(View view, int width, int height) {
        final ViewGroup viewGroup = new SelfRemovingView(view.getContext());
        viewGroup.addView(view, new ViewGroup.LayoutParams(width, height));
        return viewGroup;
        if (mContentView.isAttachedToWindow()) {
            if (DEBUG) Slog.d(TAG, "removing mContentView");
            mContentView.setOnTouchListener(null);
            mWm.removeView(mContentView);
        }
    }

    private static LayoutParams createBaseLayoutParams() {
        final LayoutParams params = new LayoutParams();
        // TODO(b/33197203): LayoutParams.TYPE_AUTOFILL
        params.type = LayoutParams.TYPE_SYSTEM_ALERT;
        params.flags =
                LayoutParams.SOFT_INPUT_STATE_UNCHANGED
                | LayoutParams.FLAG_LAYOUT_IN_SCREEN
                | LayoutParams.FLAG_LAYOUT_NO_LIMITS
                | LayoutParams.FLAG_NOT_FOCUSABLE
                | LayoutParams.FLAG_NOT_TOUCH_MODAL
                | LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH;
        params.gravity = Gravity.TOP | Gravity.LEFT;
        params.width = LayoutParams.WRAP_CONTENT;
        params.height = LayoutParams.WRAP_CONTENT;
        return params;
    @Override
    public void onLayoutChange(View view, int left, int top, int right, int bottom,
            int oldLeft, int oldTop, int oldRight, int oldBottom) {
        if (view == mWindowSizeListenerView) {
            if (DEBUG) Slog.d(TAG, "onLayoutChange() for mWindowSizeListenerView");
            // mWindowSizeListenerView layout changed, get the size of the display bounds and update
            // the window.
            final Rect displayBounds = new Rect();
            view.getBoundsOnScreen(displayBounds);
            updateDisplayBounds(displayBounds);
        } else if (view == mContentView) {
            // mContentView layout changed, update the window in case its height changed.
            if (DEBUG) Slog.d(TAG, "onLayoutChange() for mContentView");
            updateHeight();
        }
    }

    // When the window is touched outside, hide the window.
    @Override
    public String toString() {
        if (!DEBUG) return super.toString();
    public boolean onTouch(View view, MotionEvent event) {
        if (view == mContentView && event.getAction() == MotionEvent.ACTION_OUTSIDE) {
            hide();
            return true;
        }
        return false;
    }

        return "AnchoredWindow: [width=" + mWidth + ", height=" + mHeight + ", view=" + mView + "]";
    private boolean updateHeight() {
        final Rect displayBounds = mLastDisplayBounds;
        if (displayBounds == null) {
            return false;
        }

    void dump(PrintWriter pw) {
        pw.println("Anchored Window");
        final String prefix = "  ";
        pw.print(prefix); pw.print("width: "); pw.println(mWidth);
        pw.print(prefix); pw.print("height: "); pw.println(mHeight);
        pw.print(prefix); pw.print("visible: "); pw.println(mIsShowing);
        mContentView.measure(
                MeasureSpec.makeMeasureSpec(displayBounds.width(), MeasureSpec.AT_MOST),
                MeasureSpec.makeMeasureSpec(displayBounds.height(), MeasureSpec.AT_MOST));
        int height = mContentView.getMeasuredHeight();
        if (height != mLastHeight) {
            if (DEBUG) Slog.d(TAG, "update height=" + height);
            mLastHeight = height;
            update(height, mLastBounds, displayBounds);
            return true;
        } else {
            return false;
        }
    }

    private void updateBounds(Rect bounds) {
        if (!bounds.equals(mLastBounds)) {
            if (DEBUG) Slog.d(TAG, "update bounds=" + bounds);
            mLastBounds = bounds;

    /** FrameLayout that listens for touch events removes itself if the touch event is outside. */
    private final class SelfRemovingView extends FrameLayout {
        public SelfRemovingView(Context context) {
            super(context);
            update(mLastHeight, bounds, mLastDisplayBounds);
        }
    }

        @Override
        public boolean onTouchEvent(MotionEvent event) {
            if (event.getAction() == MotionEvent.ACTION_OUTSIDE) {
                hide();
                return true;
    private void updateDisplayBounds(Rect displayBounds) {
        if (!displayBounds.equals(mLastDisplayBounds)) {
            if (DEBUG) Slog.d(TAG, "update displayBounds=" + displayBounds);
            mLastDisplayBounds = displayBounds;

            if (!updateHeight()) {
                update(mLastHeight, mLastBounds, displayBounds);
            }
        }
    }

    // Updates the window if height, bounds, and displayBounds are not null.
    // Caller should ensure that something changed before calling.
    private void update(int height, @Nullable Rect bounds, @Nullable Rect displayBounds) {
        if (height == NULL_HEIGHT || bounds == null || displayBounds == null) {
            return;
        }

        if (DEBUG) Slog.d(TAG, "update height=" + height + ", bounds=" + bounds
                + ", displayBounds=" + displayBounds);

        final LayoutParams params = createWindowLayoutParams(mAppToken,
                LayoutParams.FLAG_NOT_TOUCH_MODAL // outside touches go to windows behind us
                | LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH); // outside touches trigger MotionEvent
        params.setTitle("AutoFill Fill"); // used for debugging
        updatePosition(params, height, mMinMargin, bounds, displayBounds);
        if (!mContentView.isAttachedToWindow()) {
            if (DEBUG) Slog.d(TAG, "adding mContentView");
            mWm.addView(mContentView, params);
            mContentView.setOnTouchListener(this);
        } else {
            if (DEBUG) Slog.d(TAG, "updating mContentView");
            mWm.updateViewLayout(mContentView, params);
        }
    }

    /**
     * Updates the position of the window by altering the {@link LayoutParams}.
     *
     * <p>The window can be anchored either above or below the bounds. Anchoring the window below
     * the bounds is preferred, if it fits. Otherwise, anchor the window on the side with more
     * space.
     *
     * @param params the params to update
     * @param height the requested height of the window
     * @param minMargin the minimum margin between the window and the display bounds
     * @param bounds the region the window should be anchored to
     * @param displayBounds the region in which the window may be displayed
     */
    private static void updatePosition(
            LayoutParams params,
            int height,
            int minMargin,
            Rect bounds,
            Rect displayBounds) {
        boolean below;
        int verticalSpace;
        final int verticalSpaceBelow = displayBounds.bottom - bounds.bottom - minMargin;
        if (height <= verticalSpaceBelow) {
            // Fits below bounds.
            below = true;
            verticalSpace = height;
        } else {
            final int verticalSpaceAbove = bounds.top - displayBounds.top - minMargin;
            if (height <= verticalSpaceAbove) {
                // Fits above bounds.
                below = false;
                verticalSpace = height;
            } else {
                // Pick above/below based on which has the most space.
                if (verticalSpaceBelow >= verticalSpaceAbove) {
                    below = true;
                    verticalSpace = verticalSpaceBelow;
                } else {
                return super.onTouchEvent(event);
                    below = false;
                    verticalSpace = verticalSpaceAbove;
                }
            }
        }

        int gravity;
        int y;
        if (below) {
            if (DEBUG) Slog.d(TAG, "anchorBelow");
            gravity = Gravity.TOP | Gravity.LEFT;
            y = bounds.bottom - displayBounds.top;
        } else {
            if (DEBUG) Slog.d(TAG, "anchorAbove");
            gravity = Gravity.BOTTOM | Gravity.LEFT;
            y = displayBounds.bottom - bounds.top;
        }

        final int x = bounds.left - displayBounds.left;

        params.gravity = gravity;
        params.x = x;
        params.y = y;
        params.width = bounds.width();
        params.height = verticalSpace;
    }

    private static LayoutParams createWindowLayoutParams(IBinder appToken, int flags) {
        final LayoutParams params = new LayoutParams();
        params.token = appToken;
        params.type = LayoutParams.TYPE_PHONE;
        params.flags =
                flags
                | LayoutParams.FLAG_NOT_FOCUSABLE // don't receive input events
                | LayoutParams.FLAG_ALT_FOCUSABLE_IM; // resize for soft input
        params.format = PixelFormat.TRANSLUCENT;
        return params;
    }
}
+5 −20
Original line number Diff line number Diff line
@@ -71,8 +71,6 @@ final class AutoFillUI {
    private AnchoredWindow mFillWindow;
    private DatasetPicker mFillView;
    private ViewState mViewState;
    private Rect mBounds;
    private String mFilterText;

    /**
     * Custom snackbar UI used for saving autofill or other informational messages.
@@ -117,8 +115,6 @@ final class AutoFillUI {
        }

        mViewState = null;
        mBounds = null;
        mFilterText = null;
        mFillView = null;
        mFillWindow = null;
    }
@@ -146,23 +142,14 @@ final class AutoFillUI {
                            mSession.autoFillApp(dataset);
                            hideFillUi();
                        });
                mFillWindow = new AnchoredWindow(
                        mWm, mFillView, 800, ViewGroup.LayoutParams.WRAP_CONTENT);
                mFillWindow = new AnchoredWindow(mWm, mAppToken, mFillView);

                if (DEBUG) Slog.d(TAG, "show FillUi");
                if (DEBUG) Slog.d(TAG, "showFillUi(): view changed");
            }

            if (!bounds.equals(mBounds)) {
                if (DEBUG) Slog.d(TAG, "update FillUi bounds: " + mBounds);
                mBounds = bounds;
                mFillWindow.show(mBounds);
            }

            if (!filterText.equals(mFilterText)) {
                if (DEBUG) Slog.d(TAG, "update FillUi filter text: " + mFilterText);
                mFilterText = filterText;
                mFillView.update(mFilterText);
            }
            if (DEBUG) Slog.d(TAG, "showFillUi(): bounds=" + bounds + ", filterText=" + filterText);
            mFillView.update(filterText);
            mFillWindow.show(bounds);
        }, 0);
    }

@@ -251,8 +238,6 @@ final class AutoFillUI {
        pw.print(prefix); pw.print("mSessionId: "); pw.println(mSession.mId);
        pw.print(prefix); pw.print("mSnackBar: "); pw.println(mSnackbar);
        pw.print(prefix); pw.print("mViewState: "); pw.println(mViewState);
        pw.print(prefix); pw.print("mBounds: "); pw.println(mBounds);
        pw.print(prefix); pw.print("mFilterText: "); pw.println(mFilterText);
    }

    //similar to a snackbar, but can be a bit custom since it is more than just text. This will