| |
1 /* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- |
| |
2 * This Source Code Form is subject to the terms of the Mozilla Public |
| |
3 * License, v. 2.0. If a copy of the MPL was not distributed with this |
| |
4 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ |
| |
5 |
| |
6 package org.mozilla.gecko; |
| |
7 |
| |
8 import org.mozilla.gecko.gfx.FloatSize; |
| |
9 import org.mozilla.gecko.gfx.ImmutableViewportMetrics; |
| |
10 import org.mozilla.gecko.util.GeckoEventListener; |
| |
11 import org.mozilla.gecko.util.ThreadUtils; |
| |
12 |
| |
13 import org.json.JSONArray; |
| |
14 import org.json.JSONException; |
| |
15 import org.json.JSONObject; |
| |
16 |
| |
17 import android.content.Context; |
| |
18 import android.content.res.Resources; |
| |
19 import android.graphics.PointF; |
| |
20 import android.util.AttributeSet; |
| |
21 import android.util.Log; |
| |
22 import android.util.Pair; |
| |
23 import android.view.LayoutInflater; |
| |
24 import android.view.View; |
| |
25 import android.view.ViewGroup; |
| |
26 import android.view.animation.Animation; |
| |
27 import android.view.animation.AnimationUtils; |
| |
28 import android.view.inputmethod.InputMethodManager; |
| |
29 import android.widget.AdapterView; |
| |
30 import android.widget.AdapterView.OnItemClickListener; |
| |
31 import android.widget.ArrayAdapter; |
| |
32 import android.widget.ImageView; |
| |
33 import android.widget.ListView; |
| |
34 import android.widget.RelativeLayout; |
| |
35 import android.widget.TextView; |
| |
36 |
| |
37 import java.util.Arrays; |
| |
38 import java.util.Collection; |
| |
39 |
| |
40 public class FormAssistPopup extends RelativeLayout implements GeckoEventListener { |
| |
41 private Context mContext; |
| |
42 private Animation mAnimation; |
| |
43 |
| |
44 private ListView mAutoCompleteList; |
| |
45 private RelativeLayout mValidationMessage; |
| |
46 private TextView mValidationMessageText; |
| |
47 private ImageView mValidationMessageArrow; |
| |
48 private ImageView mValidationMessageArrowInverted; |
| |
49 |
| |
50 private double mX; |
| |
51 private double mY; |
| |
52 private double mW; |
| |
53 private double mH; |
| |
54 |
| |
55 private enum PopupType { |
| |
56 AUTOCOMPLETE, |
| |
57 VALIDATIONMESSAGE; |
| |
58 } |
| |
59 private PopupType mPopupType; |
| |
60 |
| |
61 private static int sAutoCompleteMinWidth = 0; |
| |
62 private static int sAutoCompleteRowHeight = 0; |
| |
63 private static int sValidationMessageHeight = 0; |
| |
64 private static int sValidationTextMarginTop = 0; |
| |
65 private static RelativeLayout.LayoutParams sValidationTextLayoutNormal; |
| |
66 private static RelativeLayout.LayoutParams sValidationTextLayoutInverted; |
| |
67 |
| |
68 private static final String LOGTAG = "GeckoFormAssistPopup"; |
| |
69 |
| |
70 // The blocklist is so short that ArrayList is probably cheaper than HashSet. |
| |
71 private static final Collection<String> sInputMethodBlocklist = Arrays.asList(new String[] { |
| |
72 InputMethods.METHOD_GOOGLE_JAPANESE_INPUT, // bug 775850 |
| |
73 InputMethods.METHOD_OPENWNN_PLUS, // bug 768108 |
| |
74 InputMethods.METHOD_SIMEJI, // bug 768108 |
| |
75 InputMethods.METHOD_SWYPE, // bug 755909 |
| |
76 InputMethods.METHOD_SWYPE_BETA, // bug 755909 |
| |
77 }); |
| |
78 |
| |
79 public FormAssistPopup(Context context, AttributeSet attrs) { |
| |
80 super(context, attrs); |
| |
81 mContext = context; |
| |
82 |
| |
83 mAnimation = AnimationUtils.loadAnimation(context, R.anim.grow_fade_in); |
| |
84 mAnimation.setDuration(75); |
| |
85 |
| |
86 setFocusable(false); |
| |
87 |
| |
88 registerEventListener("FormAssist:AutoComplete"); |
| |
89 registerEventListener("FormAssist:ValidationMessage"); |
| |
90 registerEventListener("FormAssist:Hide"); |
| |
91 } |
| |
92 |
| |
93 void destroy() { |
| |
94 unregisterEventListener("FormAssist:AutoComplete"); |
| |
95 unregisterEventListener("FormAssist:ValidationMessage"); |
| |
96 unregisterEventListener("FormAssist:Hide"); |
| |
97 } |
| |
98 |
| |
99 @Override |
| |
100 public void handleMessage(String event, JSONObject message) { |
| |
101 try { |
| |
102 if (event.equals("FormAssist:AutoComplete")) { |
| |
103 handleAutoCompleteMessage(message); |
| |
104 } else if (event.equals("FormAssist:ValidationMessage")) { |
| |
105 handleValidationMessage(message); |
| |
106 } else if (event.equals("FormAssist:Hide")) { |
| |
107 handleHideMessage(message); |
| |
108 } |
| |
109 } catch (Exception e) { |
| |
110 Log.e(LOGTAG, "Exception handling message \"" + event + "\":", e); |
| |
111 } |
| |
112 } |
| |
113 |
| |
114 private void handleAutoCompleteMessage(JSONObject message) throws JSONException { |
| |
115 final JSONArray suggestions = message.getJSONArray("suggestions"); |
| |
116 final JSONObject rect = message.getJSONObject("rect"); |
| |
117 ThreadUtils.postToUiThread(new Runnable() { |
| |
118 @Override |
| |
119 public void run() { |
| |
120 showAutoCompleteSuggestions(suggestions, rect); |
| |
121 } |
| |
122 }); |
| |
123 } |
| |
124 |
| |
125 private void handleValidationMessage(JSONObject message) throws JSONException { |
| |
126 final String validationMessage = message.getString("validationMessage"); |
| |
127 final JSONObject rect = message.getJSONObject("rect"); |
| |
128 ThreadUtils.postToUiThread(new Runnable() { |
| |
129 @Override |
| |
130 public void run() { |
| |
131 showValidationMessage(validationMessage, rect); |
| |
132 } |
| |
133 }); |
| |
134 } |
| |
135 |
| |
136 private void handleHideMessage(JSONObject message) { |
| |
137 ThreadUtils.postToUiThread(new Runnable() { |
| |
138 @Override |
| |
139 public void run() { |
| |
140 hide(); |
| |
141 } |
| |
142 }); |
| |
143 } |
| |
144 |
| |
145 private void showAutoCompleteSuggestions(JSONArray suggestions, JSONObject rect) { |
| |
146 if (mAutoCompleteList == null) { |
| |
147 LayoutInflater inflater = LayoutInflater.from(mContext); |
| |
148 mAutoCompleteList = (ListView) inflater.inflate(R.layout.autocomplete_list, null); |
| |
149 |
| |
150 mAutoCompleteList.setOnItemClickListener(new OnItemClickListener() { |
| |
151 @Override |
| |
152 public void onItemClick(AdapterView<?> parentView, View view, int position, long id) { |
| |
153 // Use the value stored with the autocomplete view, not the label text, |
| |
154 // since they can be different. |
| |
155 TextView textView = (TextView) view; |
| |
156 String value = (String) textView.getTag(); |
| |
157 broadcastGeckoEvent("FormAssist:AutoComplete", value); |
| |
158 hide(); |
| |
159 } |
| |
160 }); |
| |
161 |
| |
162 addView(mAutoCompleteList); |
| |
163 } |
| |
164 |
| |
165 AutoCompleteListAdapter adapter = new AutoCompleteListAdapter(mContext, R.layout.autocomplete_list_item); |
| |
166 adapter.populateSuggestionsList(suggestions); |
| |
167 mAutoCompleteList.setAdapter(adapter); |
| |
168 |
| |
169 if (setGeckoPositionData(rect, true)) { |
| |
170 positionAndShowPopup(); |
| |
171 } |
| |
172 } |
| |
173 |
| |
174 private void showValidationMessage(String validationMessage, JSONObject rect) { |
| |
175 if (mValidationMessage == null) { |
| |
176 LayoutInflater inflater = LayoutInflater.from(mContext); |
| |
177 mValidationMessage = (RelativeLayout) inflater.inflate(R.layout.validation_message, null); |
| |
178 |
| |
179 addView(mValidationMessage); |
| |
180 mValidationMessageText = (TextView) mValidationMessage.findViewById(R.id.validation_message_text); |
| |
181 |
| |
182 sValidationTextMarginTop = (int) (mContext.getResources().getDimension(R.dimen.validation_message_margin_top)); |
| |
183 |
| |
184 sValidationTextLayoutNormal = new RelativeLayout.LayoutParams(mValidationMessageText.getLayoutParams()); |
| |
185 sValidationTextLayoutNormal.setMargins(0, sValidationTextMarginTop, 0, 0); |
| |
186 |
| |
187 sValidationTextLayoutInverted = new RelativeLayout.LayoutParams(sValidationTextLayoutNormal); |
| |
188 sValidationTextLayoutInverted.setMargins(0, 0, 0, 0); |
| |
189 |
| |
190 mValidationMessageArrow = (ImageView) mValidationMessage.findViewById(R.id.validation_message_arrow); |
| |
191 mValidationMessageArrowInverted = (ImageView) mValidationMessage.findViewById(R.id.validation_message_arrow_inverted); |
| |
192 } |
| |
193 |
| |
194 mValidationMessageText.setText(validationMessage); |
| |
195 |
| |
196 // We need to set the text as selected for the marquee text to work. |
| |
197 mValidationMessageText.setSelected(true); |
| |
198 |
| |
199 if (setGeckoPositionData(rect, false)) { |
| |
200 positionAndShowPopup(); |
| |
201 } |
| |
202 } |
| |
203 |
| |
204 private boolean setGeckoPositionData(JSONObject rect, boolean isAutoComplete) { |
| |
205 try { |
| |
206 mX = rect.getDouble("x"); |
| |
207 mY = rect.getDouble("y"); |
| |
208 mW = rect.getDouble("w"); |
| |
209 mH = rect.getDouble("h"); |
| |
210 } catch (JSONException e) { |
| |
211 // Bail if we can't get the correct dimensions for the popup. |
| |
212 Log.e(LOGTAG, "Error getting FormAssistPopup dimensions", e); |
| |
213 return false; |
| |
214 } |
| |
215 |
| |
216 mPopupType = (isAutoComplete ? |
| |
217 PopupType.AUTOCOMPLETE : PopupType.VALIDATIONMESSAGE); |
| |
218 return true; |
| |
219 } |
| |
220 |
| |
221 private void positionAndShowPopup() { |
| |
222 positionAndShowPopup(GeckoAppShell.getLayerView().getViewportMetrics()); |
| |
223 } |
| |
224 |
| |
225 private void positionAndShowPopup(ImmutableViewportMetrics aMetrics) { |
| |
226 ThreadUtils.assertOnUiThread(); |
| |
227 |
| |
228 // Don't show the form assist popup when using fullscreen VKB |
| |
229 InputMethodManager imm = |
| |
230 (InputMethodManager) GeckoAppShell.getContext().getSystemService(Context.INPUT_METHOD_SERVICE); |
| |
231 if (imm.isFullscreenMode()) |
| |
232 return; |
| |
233 |
| |
234 // Hide/show the appropriate popup contents |
| |
235 if (mAutoCompleteList != null) |
| |
236 mAutoCompleteList.setVisibility((mPopupType == PopupType.AUTOCOMPLETE) ? VISIBLE : GONE); |
| |
237 if (mValidationMessage != null) |
| |
238 mValidationMessage.setVisibility((mPopupType == PopupType.AUTOCOMPLETE) ? GONE : VISIBLE); |
| |
239 |
| |
240 if (sAutoCompleteMinWidth == 0) { |
| |
241 Resources res = mContext.getResources(); |
| |
242 sAutoCompleteMinWidth = (int) (res.getDimension(R.dimen.autocomplete_min_width)); |
| |
243 sAutoCompleteRowHeight = (int) (res.getDimension(R.dimen.autocomplete_row_height)); |
| |
244 sValidationMessageHeight = (int) (res.getDimension(R.dimen.validation_message_height)); |
| |
245 } |
| |
246 |
| |
247 float zoom = aMetrics.zoomFactor; |
| |
248 PointF offset = aMetrics.getMarginOffset(); |
| |
249 |
| |
250 // These values correspond to the input box for which we want to |
| |
251 // display the FormAssistPopup. |
| |
252 int left = (int) (mX * zoom - aMetrics.viewportRectLeft + offset.x); |
| |
253 int top = (int) (mY * zoom - aMetrics.viewportRectTop + offset.y); |
| |
254 int width = (int) (mW * zoom); |
| |
255 int height = (int) (mH * zoom); |
| |
256 |
| |
257 int popupWidth = RelativeLayout.LayoutParams.FILL_PARENT; |
| |
258 int popupLeft = left < 0 ? 0 : left; |
| |
259 |
| |
260 FloatSize viewport = aMetrics.getSize(); |
| |
261 |
| |
262 // For autocomplete suggestions, if the input is smaller than the screen-width, |
| |
263 // shrink the popup's width. Otherwise, keep it as FILL_PARENT. |
| |
264 if ((mPopupType == PopupType.AUTOCOMPLETE) && (left + width) < viewport.width) { |
| |
265 popupWidth = left < 0 ? left + width : width; |
| |
266 |
| |
267 // Ensure the popup has a minimum width. |
| |
268 if (popupWidth < sAutoCompleteMinWidth) { |
| |
269 popupWidth = sAutoCompleteMinWidth; |
| |
270 |
| |
271 // Move the popup to the left if there isn't enough room for it. |
| |
272 if ((popupLeft + popupWidth) > viewport.width) |
| |
273 popupLeft = (int) (viewport.width - popupWidth); |
| |
274 } |
| |
275 } |
| |
276 |
| |
277 int popupHeight; |
| |
278 if (mPopupType == PopupType.AUTOCOMPLETE) |
| |
279 popupHeight = sAutoCompleteRowHeight * mAutoCompleteList.getAdapter().getCount(); |
| |
280 else |
| |
281 popupHeight = sValidationMessageHeight; |
| |
282 |
| |
283 int popupTop = top + height; |
| |
284 |
| |
285 if (mPopupType == PopupType.VALIDATIONMESSAGE) { |
| |
286 mValidationMessageText.setLayoutParams(sValidationTextLayoutNormal); |
| |
287 mValidationMessageArrow.setVisibility(VISIBLE); |
| |
288 mValidationMessageArrowInverted.setVisibility(GONE); |
| |
289 } |
| |
290 |
| |
291 // If the popup doesn't fit below the input box, shrink its height, or |
| |
292 // see if we can place it above the input instead. |
| |
293 if ((popupTop + popupHeight) > viewport.height) { |
| |
294 // Find where the maximum space is, and put the popup there. |
| |
295 if ((viewport.height - popupTop) > top) { |
| |
296 // Shrink the height to fit it below the input box. |
| |
297 popupHeight = (int) (viewport.height - popupTop); |
| |
298 } else { |
| |
299 if (popupHeight < top) { |
| |
300 // No shrinking needed to fit on top. |
| |
301 popupTop = (top - popupHeight); |
| |
302 } else { |
| |
303 // Shrink to available space on top. |
| |
304 popupTop = 0; |
| |
305 popupHeight = top; |
| |
306 } |
| |
307 |
| |
308 if (mPopupType == PopupType.VALIDATIONMESSAGE) { |
| |
309 mValidationMessageText.setLayoutParams(sValidationTextLayoutInverted); |
| |
310 mValidationMessageArrow.setVisibility(GONE); |
| |
311 mValidationMessageArrowInverted.setVisibility(VISIBLE); |
| |
312 } |
| |
313 } |
| |
314 } |
| |
315 |
| |
316 RelativeLayout.LayoutParams layoutParams = |
| |
317 new RelativeLayout.LayoutParams(popupWidth, popupHeight); |
| |
318 layoutParams.setMargins(popupLeft, popupTop, 0, 0); |
| |
319 setLayoutParams(layoutParams); |
| |
320 requestLayout(); |
| |
321 |
| |
322 if (!isShown()) { |
| |
323 setVisibility(VISIBLE); |
| |
324 startAnimation(mAnimation); |
| |
325 } |
| |
326 } |
| |
327 |
| |
328 public void hide() { |
| |
329 if (isShown()) { |
| |
330 setVisibility(GONE); |
| |
331 broadcastGeckoEvent("FormAssist:Hidden", null); |
| |
332 } |
| |
333 } |
| |
334 |
| |
335 void onInputMethodChanged(String newInputMethod) { |
| |
336 boolean blocklisted = sInputMethodBlocklist.contains(newInputMethod); |
| |
337 broadcastGeckoEvent("FormAssist:Blocklisted", String.valueOf(blocklisted)); |
| |
338 } |
| |
339 |
| |
340 void onMetricsChanged(final ImmutableViewportMetrics aMetrics) { |
| |
341 if (!isShown()) { |
| |
342 return; |
| |
343 } |
| |
344 |
| |
345 ThreadUtils.postToUiThread(new Runnable() { |
| |
346 @Override |
| |
347 public void run() { |
| |
348 positionAndShowPopup(aMetrics); |
| |
349 } |
| |
350 }); |
| |
351 } |
| |
352 |
| |
353 private static void broadcastGeckoEvent(String eventName, String eventData) { |
| |
354 GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent(eventName, eventData)); |
| |
355 } |
| |
356 |
| |
357 private class AutoCompleteListAdapter extends ArrayAdapter<Pair<String, String>> { |
| |
358 private LayoutInflater mInflater; |
| |
359 private int mTextViewResourceId; |
| |
360 |
| |
361 public AutoCompleteListAdapter(Context context, int textViewResourceId) { |
| |
362 super(context, textViewResourceId); |
| |
363 |
| |
364 mInflater = (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE); |
| |
365 mTextViewResourceId = textViewResourceId; |
| |
366 } |
| |
367 |
| |
368 // This method takes an array of autocomplete suggestions with label/value properties |
| |
369 // and adds label/value Pair objects to the array that backs the adapter. |
| |
370 public void populateSuggestionsList(JSONArray suggestions) { |
| |
371 try { |
| |
372 for (int i = 0; i < suggestions.length(); i++) { |
| |
373 JSONObject suggestion = suggestions.getJSONObject(i); |
| |
374 String label = suggestion.getString("label"); |
| |
375 String value = suggestion.getString("value"); |
| |
376 add(new Pair<String, String>(label, value)); |
| |
377 } |
| |
378 } catch (JSONException e) { |
| |
379 Log.e(LOGTAG, "JSONException", e); |
| |
380 } |
| |
381 } |
| |
382 |
| |
383 @Override |
| |
384 public View getView(int position, View convertView, ViewGroup parent) { |
| |
385 if (convertView == null) |
| |
386 convertView = mInflater.inflate(mTextViewResourceId, null); |
| |
387 |
| |
388 Pair<String, String> item = getItem(position); |
| |
389 TextView itemView = (TextView) convertView; |
| |
390 |
| |
391 // Set the text with the suggestion label |
| |
392 itemView.setText(item.first); |
| |
393 |
| |
394 // Set a tag with the suggestion value |
| |
395 itemView.setTag(item.second); |
| |
396 |
| |
397 return convertView; |
| |
398 } |
| |
399 } |
| |
400 |
| |
401 private void registerEventListener(String event) { |
| |
402 GeckoAppShell.getEventDispatcher().registerEventListener(event, this); |
| |
403 } |
| |
404 |
| |
405 private void unregisterEventListener(String event) { |
| |
406 GeckoAppShell.getEventDispatcher().unregisterEventListener(event, this); |
| |
407 } |
| |
408 } |