diff --git a/core/res/res/values/dimens.xml b/core/res/res/values/dimens.xml index 927988f712832..c2cd895b5e3cc 100644 --- a/core/res/res/values/dimens.xml +++ b/core/res/res/values/dimens.xml @@ -520,6 +520,7 @@ 800dp - 2dp + 4dp 64dp + 16dp diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml index b95c20b05d9ff..24f22cdddaa1d 100644 --- a/core/res/res/values/symbols.xml +++ b/core/res/res/values/symbols.xml @@ -2834,6 +2834,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..e53165d4b567e 100644 --- a/services/autofill/java/com/android/server/autofill/AnchoredWindow.java +++ b/services/autofill/java/com/android/server/autofill/AnchoredWindow.java @@ -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); + 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 42e4fd3b2b7f9..ea74582ff62fd 100644 --- a/services/autofill/java/com/android/server/autofill/AutoFillManagerServiceImpl.java +++ b/services/autofill/java/com/android/server/autofill/AutoFillManagerServiceImpl.java @@ -813,7 +813,7 @@ final class AutoFillManagerServiceImpl { final int mId; private Session(int id, IBinder activityToken) { - mUi = new AutoFillUI(mContext, this); + mUi = new AutoFillUI(mContext, this, activityToken); mId = id; mActivityToken = new WeakReference<>(activityToken); } diff --git a/services/autofill/java/com/android/server/autofill/AutoFillUI.java b/services/autofill/java/com/android/server/autofill/AutoFillUI.java index 76c291687ec3d..73f488d39f154 100644 --- a/services/autofill/java/com/android/server/autofill/AutoFillUI.java +++ b/services/autofill/java/com/android/server/autofill/AutoFillUI.java @@ -33,6 +33,7 @@ import android.graphics.Rect; import android.graphics.PixelFormat; import android.os.Binder; import android.os.Bundle; +import android.os.IBinder; import android.util.Slog; import android.view.autofill.AutoFillId; import android.view.autofill.Dataset; @@ -63,23 +64,23 @@ final class AutoFillUI { private final Context mContext; private final Session mSession; + private final IBinder mAppToken; private final WindowManager mWm; // Fill UI variables 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. */ private View mSnackbar; - AutoFillUI(Context context, Session session) { + AutoFillUI(Context context, Session session, IBinder appToken) { mContext = context; mSession = session; + mAppToken = appToken; mWm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); } @@ -114,8 +115,6 @@ final class AutoFillUI { } mViewState = null; - mBounds = null; - mFilterText = null; mFillView = null; mFillWindow = null; } @@ -143,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); } @@ -248,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 diff --git a/services/autofill/java/com/android/server/autofill/DatasetPicker.java b/services/autofill/java/com/android/server/autofill/DatasetPicker.java index 7245aaabfd306..db516d85efd7b 100644 --- a/services/autofill/java/com/android/server/autofill/DatasetPicker.java +++ b/services/autofill/java/com/android/server/autofill/DatasetPicker.java @@ -38,8 +38,6 @@ import java.util.List; * 3) the View is detached. */ final class DatasetPicker extends ListView implements OnItemClickListener { - private static final String TAG = "DatasetPicker"; - interface Listener { void onDatasetPicked(Dataset dataset); } @@ -80,7 +78,7 @@ final class DatasetPicker extends ListView implements OnItemClickListener { adapter.getFilter().filter(prefix, new FilterListener() { @Override public void onFilterComplete(int count) { - DatasetPicker.this.requestLayout(); + setVisibility(count > 0 ? View.VISIBLE : View.GONE); } }); }