blob: 85eecdf722434404a4eebd2bed8f1e90731aa7d2 [file] [log] [blame]
/*
* Copyright (C) 2017 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.server.autofill.ui;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.Context;
import android.graphics.PixelFormat;
import android.graphics.Point;
import android.graphics.Rect;
import android.os.IBinder;
import android.service.autofill.Dataset;
import android.service.autofill.FillResponse;
import android.util.Slog;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.View.MeasureSpec;
import android.view.ViewGroup;
import android.view.WindowManager;
import android.view.autofill.AutofillId;
import android.view.autofill.AutofillValue;
import android.widget.ArrayAdapter;
import android.widget.ListView;
import android.widget.RemoteViews;
import com.android.internal.R;
import libcore.util.Objects;
import java.io.PrintWriter;
import java.util.ArrayList;
final class FillUi {
private static final String TAG = "FillUi";
private static final int VISIBLE_OPTIONS_MAX_COUNT = 3;
interface Callback {
void onResponsePicked(@NonNull FillResponse response);
void onDatasetPicked(@NonNull Dataset dataset);
void onCanceled();
void onDestroy();
}
private final Rect mAnchorBounds = new Rect();
private final @NonNull AnchoredWindow mWindow;
private final @NonNull Callback mCallback;
private final @NonNull ListView mListView;
private final @Nullable ArrayAdapter<ViewItem> mAdapter;
private @Nullable String mFilterText;
private final String mAccessibilityTitle;
private int mContentWidth;
private int mContentHeight;
private boolean mDestroyed;
FillUi(@NonNull Context context, @NonNull FillResponse response,
@NonNull AutofillId focusedViewId, @NonNull IBinder windowToken,
@NonNull Rect anchorBounds, @Nullable String filterText,
@NonNull Callback callback) {
mAnchorBounds.set(anchorBounds);
mCallback = callback;
mAccessibilityTitle = context.getString(R.string.autofill_picker_accessibility_title);
if (response.getAuthentication() != null) {
mListView = null;
mAdapter = null;
final View content;
try {
content = response.getPresentation().apply(context, null);
} catch (RuntimeException e) {
callback.onCanceled();
Slog.e(TAG, "Error inflating remote views", e);
mWindow = null;
return;
}
final int widthMeasureSpec = MeasureSpec.makeMeasureSpec(MeasureSpec.UNSPECIFIED, 0);
final int heightMeasureSpec = MeasureSpec.makeMeasureSpec(MeasureSpec.UNSPECIFIED, 0);
content.measure(widthMeasureSpec, heightMeasureSpec);
content.setOnClickListener(v -> mCallback.onResponsePicked(response));
mContentWidth = content.getMeasuredWidth();
mContentHeight = content.getMeasuredHeight();
mWindow = new AnchoredWindow(windowToken, content);
mWindow.show(mContentWidth, mContentHeight, mAnchorBounds);
} else {
final int datasetCount = response.getDatasets().size();
final ArrayList<ViewItem> items = new ArrayList<>(datasetCount);
for (int i = 0; i < datasetCount; i++) {
final Dataset dataset = response.getDatasets().get(i);
final int index = dataset.getFieldIds().indexOf(focusedViewId);
if (index >= 0) {
final RemoteViews presentation = dataset.getFieldPresentation(index);
final View view;
try {
view = presentation.apply(context, null);
} catch (RuntimeException e) {
Slog.e(TAG, "Error inflating remote views", e);
continue;
}
final AutofillValue value = dataset.getFieldValues().get(index);
String valueText = null;
if (value.isText()) {
valueText = value.getTextValue().toString().toLowerCase();
}
items.add(new ViewItem(dataset, valueText, view));
}
}
mAdapter = new ArrayAdapter<ViewItem>(context, 0, items) {
@Override
public View getView(int position, View convertView, ViewGroup parent) {
return getItem(position).getView();
}
};
final LayoutInflater inflater = LayoutInflater.from(context);
mListView = (ListView) inflater.inflate(
com.android.internal.R.layout.autofill_dataset_picker, null);
mListView.setAdapter(mAdapter);
mListView.setOnItemClickListener((adapter, view, position, id) -> {
final ViewItem vi = mAdapter.getItem(position);
mCallback.onDatasetPicked(vi.getDataset());
});
if (filterText == null) {
mFilterText = null;
} else {
mFilterText = filterText.toLowerCase();
}
applyNewFilterText();
mWindow = new AnchoredWindow(windowToken, mListView);
}
}
public void update(@NonNull Rect anchorBounds) {
throwIfDestroyed();
if (!mAnchorBounds.equals(anchorBounds)) {
mAnchorBounds.set(anchorBounds);
mWindow.show(mContentWidth, mContentHeight, anchorBounds);
}
}
private void applyNewFilterText() {
mAdapter.getFilter().filter(mFilterText, (count) -> {
if (mDestroyed) {
return;
}
if (count <= 0) {
mWindow.hide();
} else {
if (updateContentSize()) {
mWindow.show(mContentWidth, mContentHeight, mAnchorBounds);
}
if (mAdapter.getCount() > VISIBLE_OPTIONS_MAX_COUNT) {
mListView.setVerticalScrollBarEnabled(true);
mListView.onVisibilityAggregated(true);
} else {
mListView.setVerticalScrollBarEnabled(false);
}
}
});
}
public void setFilterText(@Nullable String filterText) {
throwIfDestroyed();
if (mAdapter == null) {
return;
}
if (filterText == null) {
filterText = null;
} else {
filterText = filterText.toLowerCase();
}
if (Objects.equal(mFilterText, filterText)) {
return;
}
mFilterText = filterText;
applyNewFilterText();
}
public void destroy() {
throwIfDestroyed();
mCallback.onDestroy();
mWindow.hide();
mDestroyed = true;
}
private boolean updateContentSize() {
if (mAdapter == null) {
return false;
}
boolean changed = false;
if (mAdapter.getCount() <= 0) {
if (mContentWidth != 0) {
mContentWidth = 0;
changed = true;
}
if (mContentHeight != 0) {
mContentHeight = 0;
changed = true;
}
return changed;
}
mContentWidth = 0;
mContentHeight = 0;
final int widthMeasureSpec = MeasureSpec.makeMeasureSpec(MeasureSpec.UNSPECIFIED, 0);
final int heightMeasureSpec = MeasureSpec.makeMeasureSpec(MeasureSpec.UNSPECIFIED, 0);
final int itemCount = Math.min(mAdapter.getCount(), VISIBLE_OPTIONS_MAX_COUNT);
for (int i = 0; i < itemCount; i++) {
View view = mAdapter.getItem(i).getView();
view.measure(widthMeasureSpec, heightMeasureSpec);
final int newContentWidth = Math.max(mContentWidth, view.getMeasuredWidth());
if (newContentWidth != mContentWidth) {
mContentWidth = newContentWidth;
changed = true;
}
final int newContentHeight = mContentHeight + view.getMeasuredHeight();
if (newContentHeight != mContentHeight) {
mContentHeight = newContentHeight;
changed = true;
}
}
return changed;
}
private void throwIfDestroyed() {
if (mDestroyed) {
throw new IllegalStateException("cannot interact with a destroyed instance");
}
}
private static class ViewItem {
private final String mValue;
private final Dataset mDataset;
private final View mView;
ViewItem(Dataset dataset, String value, View view) {
mDataset = dataset;
mValue = value;
mView = view;
}
public View getView() {
return mView;
}
public Dataset getDataset() {
return mDataset;
}
@Override
public String toString() {
// Used for filtering in the adapter
return mValue;
}
}
final class AnchoredWindow implements View.OnTouchListener {
private final Point mTempPoint = new Point();
private final WindowManager mWm;
private final IBinder mActivityToken;
private final View mContentView;
/**
* Constructor.
*
* @param activityToken token to pass to window manager
* @param contentView content of the window
*/
AnchoredWindow(IBinder activityToken, View contentView) {
mWm = contentView.getContext().getSystemService(WindowManager.class);
mActivityToken = activityToken;
mContentView = contentView;
}
/**
* Hides the window.
*/
void hide() {
if (mContentView.isAttachedToWindow()) {
mContentView.setOnTouchListener(null);
mWm.removeView(mContentView);
}
}
@Override
public boolean onTouch(View view, MotionEvent event) {
// When the window is touched outside, hide the window.
if (view == mContentView && event.getAction() == MotionEvent.ACTION_OUTSIDE) {
mCallback.onCanceled();
return true;
}
return false;
}
public void show(int desiredWidth, int desiredHeight, Rect anchorBounds) {
final WindowManager.LayoutParams params = new WindowManager.LayoutParams();
params.setTitle("FillUi");
params.token = mActivityToken;
params.type = WindowManager.LayoutParams.TYPE_APPLICATION_ATTACHED_DIALOG;
params.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
| WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM
| WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
| WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH
| WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN;
params.accessibilityTitle = mAccessibilityTitle;
mWm.getDefaultDisplay().getRealSize(mTempPoint);
final int screenWidth = mTempPoint.x;
final int screenHeight = mTempPoint.y;
// Try to place the window at the start of the anchor view if
// there is space to fit the content, otherwise fit as much of
// the window as possible moving it to the left using all available
// screen width.
params.x = Math.min(anchorBounds.left, Math.max(screenWidth - desiredWidth, 0));
params.width = Math.min(screenWidth, desiredWidth);
// Try to fit below using all available space with top-start gravity
// and if that fails try to fit above using all available space with
// bottom-start gravity.
final int verticalSpaceBelow = screenHeight - anchorBounds.bottom;
if (desiredHeight <= verticalSpaceBelow) {
// Fits below bounds.
params.height = desiredHeight;
params.gravity = Gravity.TOP | Gravity.START;
params.y = anchorBounds.bottom;
} else {
final int verticalSpaceAbove = anchorBounds.top;
if (desiredHeight <= verticalSpaceAbove) {
// Fits above bounds.
params.height = desiredHeight;
params.gravity = Gravity.BOTTOM | Gravity.START;
params.y = anchorBounds.top + desiredHeight;
} else {
// Pick above/below based on which has the most space.
if (verticalSpaceBelow >= verticalSpaceAbove) {
params.height = verticalSpaceBelow;
params.gravity = Gravity.TOP | Gravity.START;
params.y = anchorBounds.bottom;
} else {
params.height = verticalSpaceAbove;
params.gravity = Gravity.BOTTOM | Gravity.START;
params.y = anchorBounds.top + desiredHeight;
}
}
}
if (!mContentView.isAttachedToWindow()) {
mWm.addView(mContentView, params);
mContentView.setOnTouchListener(this);
} else {
mWm.updateViewLayout(mContentView, params);
}
}
}
public void dump(PrintWriter pw, String prefix) {
pw.print(prefix); pw.print("mAnchorBounds: "); pw.println(mAnchorBounds);
pw.print(prefix); pw.print("mCallback: "); pw.println(mCallback != null);
pw.print(prefix); pw.print("mListView: "); pw.println(mListView);
pw.print(prefix); pw.print("mAdapter: "); pw.println(mAdapter != null);
pw.print(prefix); pw.print("mFilterText: "); pw.println(mFilterText);
pw.print(prefix); pw.print("mAccessibilityTitle: "); pw.println(mAccessibilityTitle);
pw.print(prefix); pw.print("mContentWidth: "); pw.println(mContentWidth);
pw.print(prefix); pw.print("mContentHeight: "); pw.println(mContentHeight);
pw.print(prefix); pw.print("mDestroyed: "); pw.println(mDestroyed);
}
}