AutofillPopup.java revision a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7
1// Copyright 2013 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5package org.chromium.ui.autofill;
6
7import android.content.Context;
8import android.graphics.Paint;
9import android.graphics.Rect;
10import android.text.TextUtils;
11import android.view.LayoutInflater;
12import android.view.View;
13import android.view.View.OnLayoutChangeListener;
14import android.widget.AdapterView;
15import android.widget.ListPopupWindow;
16import android.widget.TextView;
17
18import org.chromium.ui.R;
19import org.chromium.ui.base.ViewAndroidDelegate;
20
21import java.util.ArrayList;
22import java.util.Arrays;
23import java.util.HashSet;
24import java.util.List;
25
26/**
27 * The Autofill suggestion popup that lists relevant suggestions.
28 */
29public class AutofillPopup extends ListPopupWindow implements AdapterView.OnItemClickListener {
30
31    /**
32     * Constants defining types of Autofill suggestion entries.
33     * Has to be kept in sync with enum in WebAutofillClient.h
34     *
35     * Not supported: MenuItemIDWarningMessage, MenuItemIDClearForm, and
36     * MenuItemIDAutofillOptions.
37     */
38    private static final int ITEM_ID_AUTOCOMPLETE_ENTRY = 0;
39    private static final int ITEM_ID_PASSWORD_ENTRY = -2;
40    private static final int ITEM_ID_SEPARATOR_ENTRY = -3;
41    private static final int ITEM_ID_DATA_LIST_ENTRY = -6;
42
43    private static final int TEXT_PADDING_DP = 30;
44
45    private final AutofillPopupDelegate mAutofillCallback;
46    private final Context mContext;
47    private final ViewAndroidDelegate mViewAndroidDelegate;
48    private View mAnchorView;
49    private float mAnchorWidth;
50    private float mAnchorHeight;
51    private float mAnchorX;
52    private float mAnchorY;
53    private Paint mLabelViewPaint;
54    private Paint mSublabelViewPaint;
55    private OnLayoutChangeListener mLayoutChangeListener;
56    private List<AutofillSuggestion> mSuggestions;
57
58    /**
59     * An interface to handle the touch interaction with an AutofillPopup object.
60     */
61    public interface AutofillPopupDelegate {
62        /**
63         * Requests the controller to hide AutofillPopup.
64         */
65        public void requestHide();
66
67        /**
68         * Handles the selection of an Autofill suggestion from an AutofillPopup.
69         * @param listIndex The index of the selected Autofill suggestion.
70         */
71        public void suggestionSelected(int listIndex);
72    }
73
74    /**
75     * Creates an AutofillWindow with specified parameters.
76     * @param context Application context.
77     * @param viewAndroidDelegate View delegate used to add and remove views.
78     * @param autofillCallback A object that handles the calls to the native AutofillPopupView.
79     */
80    public AutofillPopup(Context context, ViewAndroidDelegate viewAndroidDelegate,
81            AutofillPopupDelegate autofillCallback) {
82        super(context, null, 0, R.style.AutofillPopupWindow);
83        mContext = context;
84        mViewAndroidDelegate = viewAndroidDelegate;
85        mAutofillCallback = autofillCallback;
86
87        setOnItemClickListener(this);
88
89        mAnchorView = mViewAndroidDelegate.acquireAnchorView();
90        mAnchorView.setId(R.id.autofill_popup_window);
91        mAnchorView.setTag(this);
92
93        mViewAndroidDelegate.setAnchorViewPosition(mAnchorView, mAnchorX, mAnchorY, mAnchorWidth,
94                mAnchorHeight);
95
96        mLayoutChangeListener = new OnLayoutChangeListener() {
97            @Override
98            public void onLayoutChange(View v, int left, int top, int right, int bottom,
99                    int oldLeft, int oldTop, int oldRight, int oldBottom) {
100                if (v == mAnchorView) AutofillPopup.this.show();
101            }
102        };
103
104        mAnchorView.addOnLayoutChangeListener(mLayoutChangeListener);
105        setAnchorView(mAnchorView);
106    }
107
108    @Override
109    public void show() {
110        // An ugly hack to keep the popup from expanding on top of the keyboard.
111        setInputMethodMode(INPUT_METHOD_NEEDED);
112        super.show();
113        getListView().setDividerHeight(0);
114    }
115
116    /**
117     * Sets the location and the size of the anchor view that the AutofillPopup will use to attach
118     * itself.
119     * @param x X coordinate of the top left corner of the anchor view.
120     * @param y Y coordinate of the top left corner of the anchor view.
121     * @param width The width of the anchor view.
122     * @param height The height of the anchor view.
123     */
124    public void setAnchorRect(float x, float y, float width, float height) {
125        mAnchorWidth = width;
126        mAnchorHeight = height;
127        mAnchorX = x;
128        mAnchorY = y;
129        if (mAnchorView != null) {
130            mViewAndroidDelegate.setAnchorViewPosition(mAnchorView, mAnchorX, mAnchorY,
131                    mAnchorWidth, mAnchorHeight);
132        }
133    }
134
135    /**
136     * Sets the Autofill suggestions to display in the popup and shows the popup.
137     * @param suggestions Autofill suggestion data.
138     */
139    public void show(AutofillSuggestion[] suggestions) {
140        mSuggestions = new ArrayList<AutofillSuggestion>(Arrays.asList(suggestions));
141        // Remove the AutofillSuggestions with IDs that are not supported by Android
142        ArrayList<AutofillSuggestion> cleanedData = new ArrayList<AutofillSuggestion>();
143        HashSet<Integer> separators = new HashSet<Integer>();
144        for (int i = 0; i < suggestions.length; i++) {
145            int itemId = suggestions[i].mUniqueId;
146            if (itemId > 0 || itemId == ITEM_ID_AUTOCOMPLETE_ENTRY ||
147                    itemId == ITEM_ID_PASSWORD_ENTRY || itemId == ITEM_ID_DATA_LIST_ENTRY) {
148                cleanedData.add(suggestions[i]);
149            } else if (itemId == ITEM_ID_SEPARATOR_ENTRY) {
150                separators.add(cleanedData.size());
151            }
152        }
153        setAdapter(new AutofillListAdapter(mContext, cleanedData, separators));
154        // Once the mAnchorRect is resized and placed correctly, it will show the Autofill popup.
155        mAnchorWidth = Math.max(getDesiredWidth(cleanedData), mAnchorWidth);
156        mViewAndroidDelegate.setAnchorViewPosition(mAnchorView, mAnchorX, mAnchorY, mAnchorWidth,
157                mAnchorHeight);
158    }
159
160    /**
161     * Overrides the default dismiss behavior to request the controller to dismiss the view.
162     */
163    @Override
164    public void dismiss() {
165        mAutofillCallback.requestHide();
166    }
167
168    /**
169     * Hides the popup and removes the anchor view from the ContainerView.
170     */
171    public void hide() {
172        super.dismiss();
173        mAnchorView.removeOnLayoutChangeListener(mLayoutChangeListener);
174        mAnchorView.setTag(null);
175        mViewAndroidDelegate.releaseAnchorView(mAnchorView);
176    }
177
178    /**
179     * Get desired popup window width by calculating the maximum text length from Autofill data.
180     * @param data Autofill suggestion data.
181     * @return The popup window width in DIP.
182     */
183    private float getDesiredWidth(ArrayList<AutofillSuggestion> data) {
184        if (mLabelViewPaint == null || mSublabelViewPaint == null) {
185            LayoutInflater inflater =
186                    (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
187            View layout = inflater.inflate(R.layout.autofill_text, null);
188            TextView labelView = (TextView) layout.findViewById(R.id.autofill_label);
189            mLabelViewPaint = labelView.getPaint();
190            TextView sublabelView = (TextView) layout.findViewById(R.id.autofill_sublabel);
191            mSublabelViewPaint = sublabelView.getPaint();
192        }
193
194        float maxTextWidth = 0;
195        Rect bounds = new Rect();
196        for (int i = 0; i < data.size(); ++i) {
197            bounds.setEmpty();
198            String label = data.get(i).mLabel;
199            if (!TextUtils.isEmpty(label)) {
200                mLabelViewPaint.getTextBounds(label, 0, label.length(), bounds);
201            }
202            float labelWidth = bounds.width();
203
204            bounds.setEmpty();
205            String sublabel = data.get(i).mSublabel;
206            if (!TextUtils.isEmpty(sublabel)) {
207                mSublabelViewPaint.getTextBounds(sublabel, 0, sublabel.length(), bounds);
208            }
209
210            float localMax = Math.max(labelWidth, bounds.width());
211            maxTextWidth = Math.max(maxTextWidth, localMax);
212        }
213        // Scale it down to make it unscaled by screen density.
214        maxTextWidth = maxTextWidth / mContext.getResources().getDisplayMetrics().density;
215        // Adding padding.
216        return maxTextWidth + TEXT_PADDING_DP;
217    }
218
219    @Override
220    public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
221        AutofillListAdapter adapter = (AutofillListAdapter) parent.getAdapter();
222        int listIndex = mSuggestions.indexOf(adapter.getItem(position));
223        assert listIndex > -1;
224        mAutofillCallback.suggestionSelected(listIndex);
225    }
226
227}
228