Merge "Improve AnchoredWindow with smart positioning logic."
This commit is contained in:
committed by
Android (Google) Code Review
commit
8bc078d734
@@ -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>
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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}.
|
||||
*
|
||||
* <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 {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user