|
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 } |