1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/mobile/android/base/FormAssistPopup.java Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,408 @@ 1.4 +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- 1.5 + * This Source Code Form is subject to the terms of the Mozilla Public 1.6 + * License, v. 2.0. If a copy of the MPL was not distributed with this 1.7 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 1.8 + 1.9 +package org.mozilla.gecko; 1.10 + 1.11 +import org.mozilla.gecko.gfx.FloatSize; 1.12 +import org.mozilla.gecko.gfx.ImmutableViewportMetrics; 1.13 +import org.mozilla.gecko.util.GeckoEventListener; 1.14 +import org.mozilla.gecko.util.ThreadUtils; 1.15 + 1.16 +import org.json.JSONArray; 1.17 +import org.json.JSONException; 1.18 +import org.json.JSONObject; 1.19 + 1.20 +import android.content.Context; 1.21 +import android.content.res.Resources; 1.22 +import android.graphics.PointF; 1.23 +import android.util.AttributeSet; 1.24 +import android.util.Log; 1.25 +import android.util.Pair; 1.26 +import android.view.LayoutInflater; 1.27 +import android.view.View; 1.28 +import android.view.ViewGroup; 1.29 +import android.view.animation.Animation; 1.30 +import android.view.animation.AnimationUtils; 1.31 +import android.view.inputmethod.InputMethodManager; 1.32 +import android.widget.AdapterView; 1.33 +import android.widget.AdapterView.OnItemClickListener; 1.34 +import android.widget.ArrayAdapter; 1.35 +import android.widget.ImageView; 1.36 +import android.widget.ListView; 1.37 +import android.widget.RelativeLayout; 1.38 +import android.widget.TextView; 1.39 + 1.40 +import java.util.Arrays; 1.41 +import java.util.Collection; 1.42 + 1.43 +public class FormAssistPopup extends RelativeLayout implements GeckoEventListener { 1.44 + private Context mContext; 1.45 + private Animation mAnimation; 1.46 + 1.47 + private ListView mAutoCompleteList; 1.48 + private RelativeLayout mValidationMessage; 1.49 + private TextView mValidationMessageText; 1.50 + private ImageView mValidationMessageArrow; 1.51 + private ImageView mValidationMessageArrowInverted; 1.52 + 1.53 + private double mX; 1.54 + private double mY; 1.55 + private double mW; 1.56 + private double mH; 1.57 + 1.58 + private enum PopupType { 1.59 + AUTOCOMPLETE, 1.60 + VALIDATIONMESSAGE; 1.61 + } 1.62 + private PopupType mPopupType; 1.63 + 1.64 + private static int sAutoCompleteMinWidth = 0; 1.65 + private static int sAutoCompleteRowHeight = 0; 1.66 + private static int sValidationMessageHeight = 0; 1.67 + private static int sValidationTextMarginTop = 0; 1.68 + private static RelativeLayout.LayoutParams sValidationTextLayoutNormal; 1.69 + private static RelativeLayout.LayoutParams sValidationTextLayoutInverted; 1.70 + 1.71 + private static final String LOGTAG = "GeckoFormAssistPopup"; 1.72 + 1.73 + // The blocklist is so short that ArrayList is probably cheaper than HashSet. 1.74 + private static final Collection<String> sInputMethodBlocklist = Arrays.asList(new String[] { 1.75 + InputMethods.METHOD_GOOGLE_JAPANESE_INPUT, // bug 775850 1.76 + InputMethods.METHOD_OPENWNN_PLUS, // bug 768108 1.77 + InputMethods.METHOD_SIMEJI, // bug 768108 1.78 + InputMethods.METHOD_SWYPE, // bug 755909 1.79 + InputMethods.METHOD_SWYPE_BETA, // bug 755909 1.80 + }); 1.81 + 1.82 + public FormAssistPopup(Context context, AttributeSet attrs) { 1.83 + super(context, attrs); 1.84 + mContext = context; 1.85 + 1.86 + mAnimation = AnimationUtils.loadAnimation(context, R.anim.grow_fade_in); 1.87 + mAnimation.setDuration(75); 1.88 + 1.89 + setFocusable(false); 1.90 + 1.91 + registerEventListener("FormAssist:AutoComplete"); 1.92 + registerEventListener("FormAssist:ValidationMessage"); 1.93 + registerEventListener("FormAssist:Hide"); 1.94 + } 1.95 + 1.96 + void destroy() { 1.97 + unregisterEventListener("FormAssist:AutoComplete"); 1.98 + unregisterEventListener("FormAssist:ValidationMessage"); 1.99 + unregisterEventListener("FormAssist:Hide"); 1.100 + } 1.101 + 1.102 + @Override 1.103 + public void handleMessage(String event, JSONObject message) { 1.104 + try { 1.105 + if (event.equals("FormAssist:AutoComplete")) { 1.106 + handleAutoCompleteMessage(message); 1.107 + } else if (event.equals("FormAssist:ValidationMessage")) { 1.108 + handleValidationMessage(message); 1.109 + } else if (event.equals("FormAssist:Hide")) { 1.110 + handleHideMessage(message); 1.111 + } 1.112 + } catch (Exception e) { 1.113 + Log.e(LOGTAG, "Exception handling message \"" + event + "\":", e); 1.114 + } 1.115 + } 1.116 + 1.117 + private void handleAutoCompleteMessage(JSONObject message) throws JSONException { 1.118 + final JSONArray suggestions = message.getJSONArray("suggestions"); 1.119 + final JSONObject rect = message.getJSONObject("rect"); 1.120 + ThreadUtils.postToUiThread(new Runnable() { 1.121 + @Override 1.122 + public void run() { 1.123 + showAutoCompleteSuggestions(suggestions, rect); 1.124 + } 1.125 + }); 1.126 + } 1.127 + 1.128 + private void handleValidationMessage(JSONObject message) throws JSONException { 1.129 + final String validationMessage = message.getString("validationMessage"); 1.130 + final JSONObject rect = message.getJSONObject("rect"); 1.131 + ThreadUtils.postToUiThread(new Runnable() { 1.132 + @Override 1.133 + public void run() { 1.134 + showValidationMessage(validationMessage, rect); 1.135 + } 1.136 + }); 1.137 + } 1.138 + 1.139 + private void handleHideMessage(JSONObject message) { 1.140 + ThreadUtils.postToUiThread(new Runnable() { 1.141 + @Override 1.142 + public void run() { 1.143 + hide(); 1.144 + } 1.145 + }); 1.146 + } 1.147 + 1.148 + private void showAutoCompleteSuggestions(JSONArray suggestions, JSONObject rect) { 1.149 + if (mAutoCompleteList == null) { 1.150 + LayoutInflater inflater = LayoutInflater.from(mContext); 1.151 + mAutoCompleteList = (ListView) inflater.inflate(R.layout.autocomplete_list, null); 1.152 + 1.153 + mAutoCompleteList.setOnItemClickListener(new OnItemClickListener() { 1.154 + @Override 1.155 + public void onItemClick(AdapterView<?> parentView, View view, int position, long id) { 1.156 + // Use the value stored with the autocomplete view, not the label text, 1.157 + // since they can be different. 1.158 + TextView textView = (TextView) view; 1.159 + String value = (String) textView.getTag(); 1.160 + broadcastGeckoEvent("FormAssist:AutoComplete", value); 1.161 + hide(); 1.162 + } 1.163 + }); 1.164 + 1.165 + addView(mAutoCompleteList); 1.166 + } 1.167 + 1.168 + AutoCompleteListAdapter adapter = new AutoCompleteListAdapter(mContext, R.layout.autocomplete_list_item); 1.169 + adapter.populateSuggestionsList(suggestions); 1.170 + mAutoCompleteList.setAdapter(adapter); 1.171 + 1.172 + if (setGeckoPositionData(rect, true)) { 1.173 + positionAndShowPopup(); 1.174 + } 1.175 + } 1.176 + 1.177 + private void showValidationMessage(String validationMessage, JSONObject rect) { 1.178 + if (mValidationMessage == null) { 1.179 + LayoutInflater inflater = LayoutInflater.from(mContext); 1.180 + mValidationMessage = (RelativeLayout) inflater.inflate(R.layout.validation_message, null); 1.181 + 1.182 + addView(mValidationMessage); 1.183 + mValidationMessageText = (TextView) mValidationMessage.findViewById(R.id.validation_message_text); 1.184 + 1.185 + sValidationTextMarginTop = (int) (mContext.getResources().getDimension(R.dimen.validation_message_margin_top)); 1.186 + 1.187 + sValidationTextLayoutNormal = new RelativeLayout.LayoutParams(mValidationMessageText.getLayoutParams()); 1.188 + sValidationTextLayoutNormal.setMargins(0, sValidationTextMarginTop, 0, 0); 1.189 + 1.190 + sValidationTextLayoutInverted = new RelativeLayout.LayoutParams(sValidationTextLayoutNormal); 1.191 + sValidationTextLayoutInverted.setMargins(0, 0, 0, 0); 1.192 + 1.193 + mValidationMessageArrow = (ImageView) mValidationMessage.findViewById(R.id.validation_message_arrow); 1.194 + mValidationMessageArrowInverted = (ImageView) mValidationMessage.findViewById(R.id.validation_message_arrow_inverted); 1.195 + } 1.196 + 1.197 + mValidationMessageText.setText(validationMessage); 1.198 + 1.199 + // We need to set the text as selected for the marquee text to work. 1.200 + mValidationMessageText.setSelected(true); 1.201 + 1.202 + if (setGeckoPositionData(rect, false)) { 1.203 + positionAndShowPopup(); 1.204 + } 1.205 + } 1.206 + 1.207 + private boolean setGeckoPositionData(JSONObject rect, boolean isAutoComplete) { 1.208 + try { 1.209 + mX = rect.getDouble("x"); 1.210 + mY = rect.getDouble("y"); 1.211 + mW = rect.getDouble("w"); 1.212 + mH = rect.getDouble("h"); 1.213 + } catch (JSONException e) { 1.214 + // Bail if we can't get the correct dimensions for the popup. 1.215 + Log.e(LOGTAG, "Error getting FormAssistPopup dimensions", e); 1.216 + return false; 1.217 + } 1.218 + 1.219 + mPopupType = (isAutoComplete ? 1.220 + PopupType.AUTOCOMPLETE : PopupType.VALIDATIONMESSAGE); 1.221 + return true; 1.222 + } 1.223 + 1.224 + private void positionAndShowPopup() { 1.225 + positionAndShowPopup(GeckoAppShell.getLayerView().getViewportMetrics()); 1.226 + } 1.227 + 1.228 + private void positionAndShowPopup(ImmutableViewportMetrics aMetrics) { 1.229 + ThreadUtils.assertOnUiThread(); 1.230 + 1.231 + // Don't show the form assist popup when using fullscreen VKB 1.232 + InputMethodManager imm = 1.233 + (InputMethodManager) GeckoAppShell.getContext().getSystemService(Context.INPUT_METHOD_SERVICE); 1.234 + if (imm.isFullscreenMode()) 1.235 + return; 1.236 + 1.237 + // Hide/show the appropriate popup contents 1.238 + if (mAutoCompleteList != null) 1.239 + mAutoCompleteList.setVisibility((mPopupType == PopupType.AUTOCOMPLETE) ? VISIBLE : GONE); 1.240 + if (mValidationMessage != null) 1.241 + mValidationMessage.setVisibility((mPopupType == PopupType.AUTOCOMPLETE) ? GONE : VISIBLE); 1.242 + 1.243 + if (sAutoCompleteMinWidth == 0) { 1.244 + Resources res = mContext.getResources(); 1.245 + sAutoCompleteMinWidth = (int) (res.getDimension(R.dimen.autocomplete_min_width)); 1.246 + sAutoCompleteRowHeight = (int) (res.getDimension(R.dimen.autocomplete_row_height)); 1.247 + sValidationMessageHeight = (int) (res.getDimension(R.dimen.validation_message_height)); 1.248 + } 1.249 + 1.250 + float zoom = aMetrics.zoomFactor; 1.251 + PointF offset = aMetrics.getMarginOffset(); 1.252 + 1.253 + // These values correspond to the input box for which we want to 1.254 + // display the FormAssistPopup. 1.255 + int left = (int) (mX * zoom - aMetrics.viewportRectLeft + offset.x); 1.256 + int top = (int) (mY * zoom - aMetrics.viewportRectTop + offset.y); 1.257 + int width = (int) (mW * zoom); 1.258 + int height = (int) (mH * zoom); 1.259 + 1.260 + int popupWidth = RelativeLayout.LayoutParams.FILL_PARENT; 1.261 + int popupLeft = left < 0 ? 0 : left; 1.262 + 1.263 + FloatSize viewport = aMetrics.getSize(); 1.264 + 1.265 + // For autocomplete suggestions, if the input is smaller than the screen-width, 1.266 + // shrink the popup's width. Otherwise, keep it as FILL_PARENT. 1.267 + if ((mPopupType == PopupType.AUTOCOMPLETE) && (left + width) < viewport.width) { 1.268 + popupWidth = left < 0 ? left + width : width; 1.269 + 1.270 + // Ensure the popup has a minimum width. 1.271 + if (popupWidth < sAutoCompleteMinWidth) { 1.272 + popupWidth = sAutoCompleteMinWidth; 1.273 + 1.274 + // Move the popup to the left if there isn't enough room for it. 1.275 + if ((popupLeft + popupWidth) > viewport.width) 1.276 + popupLeft = (int) (viewport.width - popupWidth); 1.277 + } 1.278 + } 1.279 + 1.280 + int popupHeight; 1.281 + if (mPopupType == PopupType.AUTOCOMPLETE) 1.282 + popupHeight = sAutoCompleteRowHeight * mAutoCompleteList.getAdapter().getCount(); 1.283 + else 1.284 + popupHeight = sValidationMessageHeight; 1.285 + 1.286 + int popupTop = top + height; 1.287 + 1.288 + if (mPopupType == PopupType.VALIDATIONMESSAGE) { 1.289 + mValidationMessageText.setLayoutParams(sValidationTextLayoutNormal); 1.290 + mValidationMessageArrow.setVisibility(VISIBLE); 1.291 + mValidationMessageArrowInverted.setVisibility(GONE); 1.292 + } 1.293 + 1.294 + // If the popup doesn't fit below the input box, shrink its height, or 1.295 + // see if we can place it above the input instead. 1.296 + if ((popupTop + popupHeight) > viewport.height) { 1.297 + // Find where the maximum space is, and put the popup there. 1.298 + if ((viewport.height - popupTop) > top) { 1.299 + // Shrink the height to fit it below the input box. 1.300 + popupHeight = (int) (viewport.height - popupTop); 1.301 + } else { 1.302 + if (popupHeight < top) { 1.303 + // No shrinking needed to fit on top. 1.304 + popupTop = (top - popupHeight); 1.305 + } else { 1.306 + // Shrink to available space on top. 1.307 + popupTop = 0; 1.308 + popupHeight = top; 1.309 + } 1.310 + 1.311 + if (mPopupType == PopupType.VALIDATIONMESSAGE) { 1.312 + mValidationMessageText.setLayoutParams(sValidationTextLayoutInverted); 1.313 + mValidationMessageArrow.setVisibility(GONE); 1.314 + mValidationMessageArrowInverted.setVisibility(VISIBLE); 1.315 + } 1.316 + } 1.317 + } 1.318 + 1.319 + RelativeLayout.LayoutParams layoutParams = 1.320 + new RelativeLayout.LayoutParams(popupWidth, popupHeight); 1.321 + layoutParams.setMargins(popupLeft, popupTop, 0, 0); 1.322 + setLayoutParams(layoutParams); 1.323 + requestLayout(); 1.324 + 1.325 + if (!isShown()) { 1.326 + setVisibility(VISIBLE); 1.327 + startAnimation(mAnimation); 1.328 + } 1.329 + } 1.330 + 1.331 + public void hide() { 1.332 + if (isShown()) { 1.333 + setVisibility(GONE); 1.334 + broadcastGeckoEvent("FormAssist:Hidden", null); 1.335 + } 1.336 + } 1.337 + 1.338 + void onInputMethodChanged(String newInputMethod) { 1.339 + boolean blocklisted = sInputMethodBlocklist.contains(newInputMethod); 1.340 + broadcastGeckoEvent("FormAssist:Blocklisted", String.valueOf(blocklisted)); 1.341 + } 1.342 + 1.343 + void onMetricsChanged(final ImmutableViewportMetrics aMetrics) { 1.344 + if (!isShown()) { 1.345 + return; 1.346 + } 1.347 + 1.348 + ThreadUtils.postToUiThread(new Runnable() { 1.349 + @Override 1.350 + public void run() { 1.351 + positionAndShowPopup(aMetrics); 1.352 + } 1.353 + }); 1.354 + } 1.355 + 1.356 + private static void broadcastGeckoEvent(String eventName, String eventData) { 1.357 + GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent(eventName, eventData)); 1.358 + } 1.359 + 1.360 + private class AutoCompleteListAdapter extends ArrayAdapter<Pair<String, String>> { 1.361 + private LayoutInflater mInflater; 1.362 + private int mTextViewResourceId; 1.363 + 1.364 + public AutoCompleteListAdapter(Context context, int textViewResourceId) { 1.365 + super(context, textViewResourceId); 1.366 + 1.367 + mInflater = (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE); 1.368 + mTextViewResourceId = textViewResourceId; 1.369 + } 1.370 + 1.371 + // This method takes an array of autocomplete suggestions with label/value properties 1.372 + // and adds label/value Pair objects to the array that backs the adapter. 1.373 + public void populateSuggestionsList(JSONArray suggestions) { 1.374 + try { 1.375 + for (int i = 0; i < suggestions.length(); i++) { 1.376 + JSONObject suggestion = suggestions.getJSONObject(i); 1.377 + String label = suggestion.getString("label"); 1.378 + String value = suggestion.getString("value"); 1.379 + add(new Pair<String, String>(label, value)); 1.380 + } 1.381 + } catch (JSONException e) { 1.382 + Log.e(LOGTAG, "JSONException", e); 1.383 + } 1.384 + } 1.385 + 1.386 + @Override 1.387 + public View getView(int position, View convertView, ViewGroup parent) { 1.388 + if (convertView == null) 1.389 + convertView = mInflater.inflate(mTextViewResourceId, null); 1.390 + 1.391 + Pair<String, String> item = getItem(position); 1.392 + TextView itemView = (TextView) convertView; 1.393 + 1.394 + // Set the text with the suggestion label 1.395 + itemView.setText(item.first); 1.396 + 1.397 + // Set a tag with the suggestion value 1.398 + itemView.setTag(item.second); 1.399 + 1.400 + return convertView; 1.401 + } 1.402 + } 1.403 + 1.404 + private void registerEventListener(String event) { 1.405 + GeckoAppShell.getEventDispatcher().registerEventListener(event, this); 1.406 + } 1.407 + 1.408 + private void unregisterEventListener(String event) { 1.409 + GeckoAppShell.getEventDispatcher().unregisterEventListener(event, this); 1.410 + } 1.411 +}