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