From 1e92f023878e6b5240a0af904faa9a7a89c5b03b Mon Sep 17 00:00:00 2001 From: Jason Long Date: Wed, 1 Feb 2017 09:29:39 -0800 Subject: [PATCH] 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: Icef5eb8d8e0b54376c84cbcaa5719aee0520e345 --- core/res/res/values/dimens.xml | 3 +- core/res/res/values/symbols.xml | 1 + .../server/autofill/AnchoredWindow.java | 298 +++++++++++++----- .../autofill/AutoFillManagerServiceImpl.java | 4 +- .../android/server/autofill/AutoFillUI.java | 46 +-- 5 files changed, 243 insertions(+), 109 deletions(-) diff --git a/core/res/res/values/dimens.xml b/core/res/res/values/dimens.xml index 9824051685231..de5fc9511010e 100644 --- a/core/res/res/values/dimens.xml +++ b/core/res/res/values/dimens.xml @@ -517,6 +517,7 @@ 800dp - 2dp + 4dp 64dp + 16dp diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml index 414335565bfdf..55a6092ceeae7 100644 --- a/core/res/res/values/symbols.xml +++ b/core/res/res/values/symbols.xml @@ -2835,6 +2835,7 @@ + diff --git a/services/autofill/java/com/android/server/autofill/AnchoredWindow.java b/services/autofill/java/com/android/server/autofill/AnchoredWindow.java index c68ac60bb26bb..ed0d234359844 100644 --- a/services/autofill/java/com/android/server/autofill/AnchoredWindow.java +++ b/services/autofill/java/com/android/server/autofill/AnchoredWindow.java @@ -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); + mLastHeight = NULL_HEIGHT; + mLastBounds = null; + mLastDisplayBounds = null; + + if (mWindowSizeListenerView.isAttachedToWindow()) { + if (DEBUG) Slog.d(TAG, "removing mWindowSizeListenerView"); + mWm.removeView(mWindowSizeListenerView); } - mIsShowing = false; - } - /** - * 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; - } - - 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; + if (mContentView.isAttachedToWindow()) { + if (DEBUG) Slog.d(TAG, "removing mContentView"); + mContentView.setOnTouchListener(null); + mWm.removeView(mContentView); + } } @Override - public String toString() { - if (!DEBUG) return super.toString(); - - return "AnchoredWindow: [width=" + mWidth + ", height=" + mHeight + ", view=" + mView + "]"; + 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(); + } } - 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); + // When the window is touched outside, hide the window. + @Override + public boolean onTouch(View view, MotionEvent event) { + if (view == mContentView && event.getAction() == MotionEvent.ACTION_OUTSIDE) { + hide(); + return true; + } + return false; } - /** 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); + private boolean updateHeight() { + final Rect displayBounds = mLastDisplayBounds; + if (displayBounds == null) { + return false; } - @Override - public boolean onTouchEvent(MotionEvent event) { - if (event.getAction() == MotionEvent.ACTION_OUTSIDE) { - hide(); - return true; - } else { - return super.onTouchEvent(event); + 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; + + update(mLastHeight, bounds, mLastDisplayBounds); + } + } + + 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}. + * + *

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 { + 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; + } } diff --git a/services/autofill/java/com/android/server/autofill/AutoFillManagerServiceImpl.java b/services/autofill/java/com/android/server/autofill/AutoFillManagerServiceImpl.java index 88f1bda39664c..439268ed50a57 100644 --- a/services/autofill/java/com/android/server/autofill/AutoFillManagerServiceImpl.java +++ b/services/autofill/java/com/android/server/autofill/AutoFillManagerServiceImpl.java @@ -709,7 +709,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) { diff --git a/services/autofill/java/com/android/server/autofill/AutoFillUI.java b/services/autofill/java/com/android/server/autofill/AutoFillUI.java index c482c402e775a..e9fc0440ba3a9 100644 --- a/services/autofill/java/com/android/server/autofill/AutoFillUI.java +++ b/services/autofill/java/com/android/server/autofill/AutoFillUI.java @@ -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 datasets, Rect bounds, - String filterText) { + void showFillUi(IBinder appToken, ViewState viewState, @Nullable ArraySet 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); + + if (DEBUG) Slog.d(TAG, "showFillUi(): view changed"); } - // 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 (!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