// 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); } }