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 8f3ba87384067..89269aad8277e 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 55470d13be438..8c74532532e14 100644
--- a/services/autofill/java/com/android/server/autofill/AutoFillManagerServiceImpl.java
+++ b/services/autofill/java/com/android/server/autofill/AutoFillManagerServiceImpl.java
@@ -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) {
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