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

Commit 8bc078d7 authored by TreeHugger Robot's avatar TreeHugger Robot Committed by Android (Google) Code Review
Browse files

Merge "Improve AnchoredWindow with smart positioning logic."

parents 5a6a99d2 1e92f023
Loading
Loading
Loading
Loading
+2 −1
Original line number Diff line number Diff line
@@ -517,6 +517,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
@@ -2835,6 +2835,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" />
+216 −74
Original line number Diff line number Diff line
@@ -17,134 +17,276 @@ package com.android.server.autofill;

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

import android.annotation.Nullable;
import android.content.Context;
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;
    }
}
+3 −1
Original line number Diff line number Diff line
@@ -720,7 +720,9 @@ final class AutoFillManagerServiceImpl {
                    filterText = text.toString();
                }
            }
            getUiForShowing().showFillUi(viewState, response.getDatasets(), bounds, filterText);

            getUiForShowing().showFillUi(mActivityToken, viewState, response.getDatasets(),
                    bounds, filterText);
        }

        private void processResponseLocked(FillResponse response) {
+16 −28
Original line number Diff line number Diff line
@@ -17,6 +17,7 @@ package com.android.server.autofill;

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

import android.annotation.Nullable;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
@@ -71,8 +72,6 @@ final class AutoFillUI {
    private AnchoredWindow mFillWindow;
    private DatasetPicker mFillView;
    private ViewState mViewState;
    private Rect mBounds;
    private String mFilterText;

    private AutoFillUiCallback mCallback;
    private IBinder mActivityToken;
@@ -138,8 +137,6 @@ final class AutoFillUI {
        }

        mViewState = null;
        mBounds = null;
        mFilterText = null;
        mFillView = null;
        mFillWindow = null;
    }
@@ -147,17 +144,23 @@ final class AutoFillUI {
    /**
     * Shows the fill UI, removing the previous fill UI if the has changed.
     *
     * @param appToken the token of the app to be autofilled
     * @param viewState the view state, compared by reference to know if new UI should be shown
     * @param datasets the datasets to show, not used if viewState is the same
     * @param bounds bounds of the view to be filled, used if changed
     * @param filterText text of the view to be filled, used if changed
     */
    void showFillUi(ViewState viewState, ArraySet<Dataset> datasets, Rect bounds,
            String filterText) {
    void showFillUi(IBinder appToken, ViewState viewState, @Nullable ArraySet<Dataset> datasets,
            Rect bounds, String filterText) {
        if (!hasCallback()) {
            return;
        }

        UiThread.getHandler().runWithScissors(() -> {
            hideSnackbarUiThread();
            hideFillResponseAuthUiUiThread();
        }, 0);

        if (datasets == null) {
            // TODO(b/33197203): shouldn't be called, but keeping the WTF for a while just to be
            // safe, otherwise it would crash system server...
@@ -165,13 +168,10 @@ final class AutoFillUI {
            return;
        }

        // TODO(b/33197203): call to hideAll() was making it janky because then mViewState is set
        // to null and hence the first check inside the lambada fails, causing it to be displayed
        // twice in some cases.
        hideAll();

        UiThread.getHandler().runWithScissors(() -> {
            if (mViewState == null || !mViewState.mId.equals(viewState.mId)) {
                hideFillUiUiThread();

                mViewState = viewState;

                mFillView = new DatasetPicker(mContext, datasets,
@@ -183,25 +183,15 @@ final class AutoFillUI {
                            callback.fill(dataset);
                            hideFillUi();
                        });
                // TODO(b/33197203): No magical numbers
                mFillWindow = new AnchoredWindow(
                        mWm, mFillView, 800, ViewGroup.LayoutParams.WRAP_CONTENT);

                if (DEBUG) Slog.d(TAG, "show FillUi: " + viewState.mId);
            }
                mFillWindow = new AnchoredWindow(mWm, appToken, mFillView);

            // TODO(b/33197203): If bounds are the same we would not show, fix this
            if (!bounds.equals(mBounds)) {
                if (DEBUG) Slog.d(TAG, "update FillUi bounds: " + mBounds);
                mBounds = bounds;
                mFillWindow.show(mBounds);
                if (DEBUG) Slog.d(TAG, "showFillUi(): view changed");
            }

            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);
    }

@@ -268,8 +258,6 @@ final class AutoFillUI {
        pw.print(prefix); pw.print("mActivityToken: "); pw.println(mActivityToken);
        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