Wed, 31 Dec 2014 07:22:50 +0100
Correct previous dual key logic pending first delivery installment.
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/. */
6 package org.mozilla.gecko;
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;
13 import org.json.JSONArray;
14 import org.json.JSONException;
15 import org.json.JSONObject;
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;
37 import java.util.Arrays;
38 import java.util.Collection;
40 public class FormAssistPopup extends RelativeLayout implements GeckoEventListener {
41 private Context mContext;
42 private Animation mAnimation;
44 private ListView mAutoCompleteList;
45 private RelativeLayout mValidationMessage;
46 private TextView mValidationMessageText;
47 private ImageView mValidationMessageArrow;
48 private ImageView mValidationMessageArrowInverted;
50 private double mX;
51 private double mY;
52 private double mW;
53 private double mH;
55 private enum PopupType {
56 AUTOCOMPLETE,
57 VALIDATIONMESSAGE;
58 }
59 private PopupType mPopupType;
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;
68 private static final String LOGTAG = "GeckoFormAssistPopup";
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 });
79 public FormAssistPopup(Context context, AttributeSet attrs) {
80 super(context, attrs);
81 mContext = context;
83 mAnimation = AnimationUtils.loadAnimation(context, R.anim.grow_fade_in);
84 mAnimation.setDuration(75);
86 setFocusable(false);
88 registerEventListener("FormAssist:AutoComplete");
89 registerEventListener("FormAssist:ValidationMessage");
90 registerEventListener("FormAssist:Hide");
91 }
93 void destroy() {
94 unregisterEventListener("FormAssist:AutoComplete");
95 unregisterEventListener("FormAssist:ValidationMessage");
96 unregisterEventListener("FormAssist:Hide");
97 }
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 }
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 }
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 }
136 private void handleHideMessage(JSONObject message) {
137 ThreadUtils.postToUiThread(new Runnable() {
138 @Override
139 public void run() {
140 hide();
141 }
142 });
143 }
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);
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 });
162 addView(mAutoCompleteList);
163 }
165 AutoCompleteListAdapter adapter = new AutoCompleteListAdapter(mContext, R.layout.autocomplete_list_item);
166 adapter.populateSuggestionsList(suggestions);
167 mAutoCompleteList.setAdapter(adapter);
169 if (setGeckoPositionData(rect, true)) {
170 positionAndShowPopup();
171 }
172 }
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);
179 addView(mValidationMessage);
180 mValidationMessageText = (TextView) mValidationMessage.findViewById(R.id.validation_message_text);
182 sValidationTextMarginTop = (int) (mContext.getResources().getDimension(R.dimen.validation_message_margin_top));
184 sValidationTextLayoutNormal = new RelativeLayout.LayoutParams(mValidationMessageText.getLayoutParams());
185 sValidationTextLayoutNormal.setMargins(0, sValidationTextMarginTop, 0, 0);
187 sValidationTextLayoutInverted = new RelativeLayout.LayoutParams(sValidationTextLayoutNormal);
188 sValidationTextLayoutInverted.setMargins(0, 0, 0, 0);
190 mValidationMessageArrow = (ImageView) mValidationMessage.findViewById(R.id.validation_message_arrow);
191 mValidationMessageArrowInverted = (ImageView) mValidationMessage.findViewById(R.id.validation_message_arrow_inverted);
192 }
194 mValidationMessageText.setText(validationMessage);
196 // We need to set the text as selected for the marquee text to work.
197 mValidationMessageText.setSelected(true);
199 if (setGeckoPositionData(rect, false)) {
200 positionAndShowPopup();
201 }
202 }
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 }
216 mPopupType = (isAutoComplete ?
217 PopupType.AUTOCOMPLETE : PopupType.VALIDATIONMESSAGE);
218 return true;
219 }
221 private void positionAndShowPopup() {
222 positionAndShowPopup(GeckoAppShell.getLayerView().getViewportMetrics());
223 }
225 private void positionAndShowPopup(ImmutableViewportMetrics aMetrics) {
226 ThreadUtils.assertOnUiThread();
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;
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);
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 }
247 float zoom = aMetrics.zoomFactor;
248 PointF offset = aMetrics.getMarginOffset();
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);
257 int popupWidth = RelativeLayout.LayoutParams.FILL_PARENT;
258 int popupLeft = left < 0 ? 0 : left;
260 FloatSize viewport = aMetrics.getSize();
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;
267 // Ensure the popup has a minimum width.
268 if (popupWidth < sAutoCompleteMinWidth) {
269 popupWidth = sAutoCompleteMinWidth;
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 }
277 int popupHeight;
278 if (mPopupType == PopupType.AUTOCOMPLETE)
279 popupHeight = sAutoCompleteRowHeight * mAutoCompleteList.getAdapter().getCount();
280 else
281 popupHeight = sValidationMessageHeight;
283 int popupTop = top + height;
285 if (mPopupType == PopupType.VALIDATIONMESSAGE) {
286 mValidationMessageText.setLayoutParams(sValidationTextLayoutNormal);
287 mValidationMessageArrow.setVisibility(VISIBLE);
288 mValidationMessageArrowInverted.setVisibility(GONE);
289 }
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 }
308 if (mPopupType == PopupType.VALIDATIONMESSAGE) {
309 mValidationMessageText.setLayoutParams(sValidationTextLayoutInverted);
310 mValidationMessageArrow.setVisibility(GONE);
311 mValidationMessageArrowInverted.setVisibility(VISIBLE);
312 }
313 }
314 }
316 RelativeLayout.LayoutParams layoutParams =
317 new RelativeLayout.LayoutParams(popupWidth, popupHeight);
318 layoutParams.setMargins(popupLeft, popupTop, 0, 0);
319 setLayoutParams(layoutParams);
320 requestLayout();
322 if (!isShown()) {
323 setVisibility(VISIBLE);
324 startAnimation(mAnimation);
325 }
326 }
328 public void hide() {
329 if (isShown()) {
330 setVisibility(GONE);
331 broadcastGeckoEvent("FormAssist:Hidden", null);
332 }
333 }
335 void onInputMethodChanged(String newInputMethod) {
336 boolean blocklisted = sInputMethodBlocklist.contains(newInputMethod);
337 broadcastGeckoEvent("FormAssist:Blocklisted", String.valueOf(blocklisted));
338 }
340 void onMetricsChanged(final ImmutableViewportMetrics aMetrics) {
341 if (!isShown()) {
342 return;
343 }
345 ThreadUtils.postToUiThread(new Runnable() {
346 @Override
347 public void run() {
348 positionAndShowPopup(aMetrics);
349 }
350 });
351 }
353 private static void broadcastGeckoEvent(String eventName, String eventData) {
354 GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent(eventName, eventData));
355 }
357 private class AutoCompleteListAdapter extends ArrayAdapter<Pair<String, String>> {
358 private LayoutInflater mInflater;
359 private int mTextViewResourceId;
361 public AutoCompleteListAdapter(Context context, int textViewResourceId) {
362 super(context, textViewResourceId);
364 mInflater = (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
365 mTextViewResourceId = textViewResourceId;
366 }
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 }
383 @Override
384 public View getView(int position, View convertView, ViewGroup parent) {
385 if (convertView == null)
386 convertView = mInflater.inflate(mTextViewResourceId, null);
388 Pair<String, String> item = getItem(position);
389 TextView itemView = (TextView) convertView;
391 // Set the text with the suggestion label
392 itemView.setText(item.first);
394 // Set a tag with the suggestion value
395 itemView.setTag(item.second);
397 return convertView;
398 }
399 }
401 private void registerEventListener(String event) {
402 GeckoAppShell.getEventDispatcher().registerEventListener(event, this);
403 }
405 private void unregisterEventListener(String event) {
406 GeckoAppShell.getEventDispatcher().unregisterEventListener(event, this);
407 }
408 }