// Copyright 2013 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.ui.autofill;
import android.content.Context;
import android.graphics.Paint;
import android.graphics.Rect;
import android.text.TextUtils;
import android.view.LayoutInflater;
import android.view.View;
import android.view.View.OnLayoutChangeListener;
import android.widget.AdapterView;
import android.widget.ListPopupWindow;
import android.widget.TextView;
import org.chromium.ui.R;
import org.chromium.ui.base.ViewAndroidDelegate;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
/**
* The Autofill suggestion popup that lists relevant suggestions.
*/
public class AutofillPopup extends ListPopupWindow implements AdapterView.OnItemClickListener {
/**
* Constants defining types of Autofill suggestion entries.
* Has to be kept in sync with enum in WebAutofillClient.h
*
* Not supported: MenuItemIDWarningMessage, MenuItemIDClearForm, and
* MenuItemIDAutofillOptions.
*/
private static final int ITEM_ID_AUTOCOMPLETE_ENTRY = 0;
private static final int ITEM_ID_PASSWORD_ENTRY = -2;
private static final int ITEM_ID_SEPARATOR_ENTRY = -3;
private static final int ITEM_ID_DATA_LIST_ENTRY = -6;
private static final int TEXT_PADDING_DP = 30;
private final AutofillPopupDelegate mAutofillCallback;
private final Context mContext;
private final ViewAndroidDelegate mViewAndroidDelegate;
private View mAnchorView;
private float mAnchorWidth;
private float mAnchorHeight;
private float mAnchorX;
private float mAnchorY;
private Paint mLabelViewPaint;
private Paint mSublabelViewPaint;
private OnLayoutChangeListener mLayoutChangeListener;
private List<AutofillSuggestion> mSuggestions;
/**
* An interface that can be injected to log field names selected by
* the autofill.
*/
public interface AutofillLogger {
public void logSuggestionSelected(String fieldName);
}
private static AutofillLogger sAutofillLogger = null;
public static void setAutofillLogger(AutofillLogger autofillLogger) {
sAutofillLogger = autofillLogger;
}
/**
* An interface to handle the touch interaction with an AutofillPopup object.
*/
public interface AutofillPopupDelegate {
/**
* Requests the controller to hide AutofillPopup.
*/
public void requestHide();
/**
* Handles the selection of an Autofill suggestion from an AutofillPopup.
* @param listIndex The index of the selected Autofill suggestion.
*/
public void suggestionSelected(int listIndex);
}
/**
* Creates an AutofillWindow with specified parameters.
* @param context Application context.
* @param viewAndroidDelegate View delegate used to add and remove views.
* @param autofillCallback A object that handles the calls to the native AutofillPopupView.
*/
public AutofillPopup(Context context, ViewAndroidDelegate viewAndroidDelegate,
AutofillPopupDelegate autofillCallback) {
super(context, null, 0, R.style.AutofillPopupWindow);
mContext = context;
mViewAndroidDelegate = viewAndroidDelegate;
mAutofillCallback = autofillCallback;
setOnItemClickListener(this);
mAnchorView = mViewAndroidDelegate.acquireAnchorView();
mAnchorView.setId(R.id.autofill_popup_window);
mAnchorView.setTag(this);
mViewAndroidDelegate.setAnchorViewPosition(mAnchorView, mAnchorX, mAnchorY, mAnchorWidth,
mAnchorHeight);
mLayoutChangeListener = new OnLayoutChangeListener() {
@Override
public void onLayoutChange(View v, int left, int top, int right, int bottom,
int oldLeft, int oldTop, int oldRight, int oldBottom) {
if (v == mAnchorView) AutofillPopup.this.show();
}
};
mAnchorView.addOnLayoutChangeListener(mLayoutChangeListener);
setAnchorView(mAnchorView);
}
@Override
public void show() {
// An ugly hack to keep the popup from expanding on top of the keyboard.
setInputMethodMode(INPUT_METHOD_NEEDED);
super.show();
getListView().setDividerHeight(0);
}
/**
* Sets the location and the size of the anchor view that the AutofillPopup will use to attach
* itself.
* @param x X coordinate of the top left corner of the anchor view.
* @param y Y coordinate of the top left corner of the anchor view.
* @param width The width of the anchor view.
* @param height The height of the anchor view.
*/
public void setAnchorRect(float x, float y, float width, float height) {
mAnchorWidth = width;
mAnchorHeight = height;
mAnchorX = x;
mAnchorY = y;
if (mAnchorView != null) {
mViewAndroidDelegate.setAnchorViewPosition(mAnchorView, mAnchorX, mAnchorY,
mAnchorWidth, mAnchorHeight);
}
}
/**
* Sets the Autofill suggestions to display in the popup and shows the popup.
* @param suggestions Autofill suggestion data.
*/
public void show(AutofillSuggestion[] suggestions) {
mSuggestions = new ArrayList<AutofillSuggestion>(Arrays.asList(suggestions));
// Remove the AutofillSuggestions with IDs that are not supported by Android
ArrayList<AutofillSuggestion> cleanedData = new ArrayList<AutofillSuggestion>();
HashSet<Integer> separators = new HashSet<Integer>();
for (int i = 0; i < suggestions.length; i++) {
int itemId = suggestions[i].mUniqueId;
if (itemId > 0 || itemId == ITEM_ID_AUTOCOMPLETE_ENTRY ||
itemId == ITEM_ID_PASSWORD_ENTRY || itemId == ITEM_ID_DATA_LIST_ENTRY) {
cleanedData.add(suggestions[i]);
} else if (itemId == ITEM_ID_SEPARATOR_ENTRY) {
separators.add(cleanedData.size());
}
}
setAdapter(new AutofillListAdapter(mContext, cleanedData, separators));
// Once the mAnchorRect is resized and placed correctly, it will show the Autofill popup.
mAnchorWidth = Math.max(getDesiredWidth(cleanedData), mAnchorWidth);
mViewAndroidDelegate.setAnchorViewPosition(mAnchorView, mAnchorX, mAnchorY, mAnchorWidth,
mAnchorHeight);
}
/**
* Overrides the default dismiss behavior to request the controller to dismiss the view.
*/
@Override
public void dismiss() {
mAutofillCallback.requestHide();
}
/**
* Hides the popup and removes the anchor view from the ContainerView.
*/
public void hide() {
super.dismiss();
mAnchorView.removeOnLayoutChangeListener(mLayoutChangeListener);
mAnchorView.setTag(null);
mViewAndroidDelegate.releaseAnchorView(mAnchorView);
}
/**
* Get desired popup window width by calculating the maximum text length from Autofill data.
* @param data Autofill suggestion data.
* @return The popup window width in DIP.
*/
private float getDesiredWidth(ArrayList<AutofillSuggestion> data) {
if (mLabelViewPaint == null || mSublabelViewPaint == null) {
LayoutInflater inflater =
(LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
View layout = inflater.inflate(R.layout.autofill_text, null);
TextView labelView = (TextView) layout.findViewById(R.id.autofill_label);
mLabelViewPaint = labelView.getPaint();
TextView sublabelView = (TextView) layout.findViewById(R.id.autofill_sublabel);
mSublabelViewPaint = sublabelView.getPaint();
}
float maxTextWidth = 0;
Rect bounds = new Rect();
for (int i = 0; i < data.size(); ++i) {
bounds.setEmpty();
String label = data.get(i).mLabel;
if (!TextUtils.isEmpty(label)) {
mLabelViewPaint.getTextBounds(label, 0, label.length(), bounds);
}
float labelWidth = bounds.width();
bounds.setEmpty();
String sublabel = data.get(i).mSublabel;
if (!TextUtils.isEmpty(sublabel)) {
mSublabelViewPaint.getTextBounds(sublabel, 0, sublabel.length(), bounds);
}
float localMax = Math.max(labelWidth, bounds.width());
maxTextWidth = Math.max(maxTextWidth, localMax);
}
// Scale it down to make it unscaled by screen density.
maxTextWidth = maxTextWidth / mContext.getResources().getDisplayMetrics().density;
// Adding padding.
return maxTextWidth + TEXT_PADDING_DP;
}
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
AutofillListAdapter adapter = (AutofillListAdapter) parent.getAdapter();
AutofillSuggestion selectedSuggestion = adapter.getItem(position);
int listIndex = mSuggestions.indexOf(selectedSuggestion);
assert listIndex > -1;
if (sAutofillLogger != null) {
sAutofillLogger.logSuggestionSelected(selectedSuggestion.mLabel);
}
mAutofillCallback.suggestionSelected(listIndex);
}
}