summaryrefslogtreecommitdiffstats
path: root/mobile/android/base/java/org/mozilla/gecko/FormAssistPopup.java
diff options
context:
space:
mode:
authorMatt A. Tobin <mattatobin@localhost.localdomain>2018-02-02 04:16:08 -0500
committerMatt A. Tobin <mattatobin@localhost.localdomain>2018-02-02 04:16:08 -0500
commit5f8de423f190bbb79a62f804151bc24824fa32d8 (patch)
tree10027f336435511475e392454359edea8e25895d /mobile/android/base/java/org/mozilla/gecko/FormAssistPopup.java
parent49ee0794b5d912db1f95dce6eb52d781dc210db5 (diff)
downloadUXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar
UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.gz
UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.lz
UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.xz
UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.zip
Add m-esr52 at 52.6.0
Diffstat (limited to 'mobile/android/base/java/org/mozilla/gecko/FormAssistPopup.java')
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/FormAssistPopup.java459
1 files changed, 459 insertions, 0 deletions
diff --git a/mobile/android/base/java/org/mozilla/gecko/FormAssistPopup.java b/mobile/android/base/java/org/mozilla/gecko/FormAssistPopup.java
new file mode 100644
index 000000000..5c7f932c0
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/FormAssistPopup.java
@@ -0,0 +1,459 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko;
+
+import org.mozilla.gecko.animation.ViewHelper;
+import org.mozilla.gecko.gfx.FloatSize;
+import org.mozilla.gecko.gfx.ImmutableViewportMetrics;
+import org.mozilla.gecko.util.GeckoEventListener;
+import org.mozilla.gecko.util.ThreadUtils;
+import org.mozilla.gecko.widget.SwipeDismissListViewTouchListener;
+import org.mozilla.gecko.widget.SwipeDismissListViewTouchListener.OnDismissCallback;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.PointF;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.util.Pair;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.animation.Animation;
+import android.view.animation.AnimationUtils;
+import android.view.inputmethod.InputMethodManager;
+import android.widget.AdapterView;
+import android.widget.AdapterView.OnItemClickListener;
+import android.widget.ArrayAdapter;
+import android.widget.ImageView;
+import android.widget.ListView;
+import android.widget.RelativeLayout;
+import android.widget.RelativeLayout.LayoutParams;
+import android.widget.TextView;
+
+import java.util.Arrays;
+import java.util.Collection;
+
+public class FormAssistPopup extends RelativeLayout implements GeckoEventListener {
+ private final Context mContext;
+ private final Animation mAnimation;
+
+ private ListView mAutoCompleteList;
+ private RelativeLayout mValidationMessage;
+ private TextView mValidationMessageText;
+ private ImageView mValidationMessageArrow;
+ private ImageView mValidationMessageArrowInverted;
+
+ private double mX;
+ private double mY;
+ private double mW;
+ private double mH;
+
+ private enum PopupType {
+ AUTOCOMPLETE,
+ VALIDATIONMESSAGE;
+ }
+ private PopupType mPopupType;
+
+ private static final int MAX_VISIBLE_ROWS = 5;
+
+ private static int sAutoCompleteMinWidth;
+ private static int sAutoCompleteRowHeight;
+ private static int sValidationMessageHeight;
+ private static int sValidationTextMarginTop;
+ private static LayoutParams sValidationTextLayoutNormal;
+ private static LayoutParams sValidationTextLayoutInverted;
+
+ private static final String LOGTAG = "GeckoFormAssistPopup";
+
+ // The blocklist is so short that ArrayList is probably cheaper than HashSet.
+ private static final Collection<String> sInputMethodBlocklist = Arrays.asList(
+ InputMethods.METHOD_GOOGLE_JAPANESE_INPUT, // bug 775850
+ InputMethods.METHOD_OPENWNN_PLUS, // bug 768108
+ InputMethods.METHOD_SIMEJI, // bug 768108
+ InputMethods.METHOD_SWYPE, // bug 755909
+ InputMethods.METHOD_SWYPE_BETA // bug 755909
+ );
+
+ public FormAssistPopup(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ mContext = context;
+
+ mAnimation = AnimationUtils.loadAnimation(context, R.anim.grow_fade_in);
+ mAnimation.setDuration(75);
+
+ setFocusable(false);
+
+ GeckoApp.getEventDispatcher().registerGeckoThreadListener(this,
+ "FormAssist:AutoComplete",
+ "FormAssist:ValidationMessage",
+ "FormAssist:Hide");
+ }
+
+ void destroy() {
+ GeckoApp.getEventDispatcher().unregisterGeckoThreadListener(this,
+ "FormAssist:AutoComplete",
+ "FormAssist:ValidationMessage",
+ "FormAssist:Hide");
+ }
+
+ @Override
+ public void handleMessage(String event, JSONObject message) {
+ try {
+ if (event.equals("FormAssist:AutoComplete")) {
+ handleAutoCompleteMessage(message);
+ } else if (event.equals("FormAssist:ValidationMessage")) {
+ handleValidationMessage(message);
+ } else if (event.equals("FormAssist:Hide")) {
+ handleHideMessage(message);
+ }
+ } catch (Exception e) {
+ Log.e(LOGTAG, "Exception handling message \"" + event + "\":", e);
+ }
+ }
+
+ private void handleAutoCompleteMessage(JSONObject message) throws JSONException {
+ final JSONArray suggestions = message.getJSONArray("suggestions");
+ final JSONObject rect = message.getJSONObject("rect");
+ final boolean isEmpty = message.getBoolean("isEmpty");
+ ThreadUtils.postToUiThread(new Runnable() {
+ @Override
+ public void run() {
+ showAutoCompleteSuggestions(suggestions, rect, isEmpty);
+ }
+ });
+ }
+
+ private void handleValidationMessage(JSONObject message) throws JSONException {
+ final String validationMessage = message.getString("validationMessage");
+ final JSONObject rect = message.getJSONObject("rect");
+ ThreadUtils.postToUiThread(new Runnable() {
+ @Override
+ public void run() {
+ showValidationMessage(validationMessage, rect);
+ }
+ });
+ }
+
+ private void handleHideMessage(JSONObject message) {
+ ThreadUtils.postToUiThread(new Runnable() {
+ @Override
+ public void run() {
+ hide();
+ }
+ });
+ }
+
+ private void showAutoCompleteSuggestions(JSONArray suggestions, JSONObject rect, boolean isEmpty) {
+ final String inputMethod = InputMethods.getCurrentInputMethod(mContext);
+ if (!isEmpty && sInputMethodBlocklist.contains(inputMethod)) {
+ // Don't display the form auto-complete popup after the user starts typing
+ // to avoid confusing somes IME. See bug 758820 and bug 632744.
+ hide();
+ return;
+ }
+
+ if (mAutoCompleteList == null) {
+ LayoutInflater inflater = LayoutInflater.from(mContext);
+ mAutoCompleteList = (ListView) inflater.inflate(R.layout.autocomplete_list, null);
+
+ mAutoCompleteList.setOnItemClickListener(new OnItemClickListener() {
+ @Override
+ public void onItemClick(AdapterView<?> parentView, View view, int position, long id) {
+ // Use the value stored with the autocomplete view, not the label text,
+ // since they can be different.
+ TextView textView = (TextView) view;
+ String value = (String) textView.getTag();
+ broadcastGeckoEvent("FormAssist:AutoComplete", value);
+ hide();
+ }
+ });
+
+ // Create a ListView-specific touch listener. ListViews are given special treatment because
+ // by default they handle touches for their list items... i.e. they're in charge of drawing
+ // the pressed state (the list selector), handling list item clicks, etc.
+ final SwipeDismissListViewTouchListener touchListener = new SwipeDismissListViewTouchListener(mAutoCompleteList, new OnDismissCallback() {
+ @Override
+ public void onDismiss(ListView listView, final int position) {
+ // Use the value stored with the autocomplete view, not the label text,
+ // since they can be different.
+ AutoCompleteListAdapter adapter = (AutoCompleteListAdapter) listView.getAdapter();
+ Pair<String, String> item = adapter.getItem(position);
+
+ // Remove the item from form history.
+ broadcastGeckoEvent("FormAssist:Remove", item.second);
+
+ // Update the list
+ adapter.remove(item);
+ adapter.notifyDataSetChanged();
+ positionAndShowPopup();
+ }
+ });
+ mAutoCompleteList.setOnTouchListener(touchListener);
+
+ // Setting this scroll listener is required to ensure that during ListView scrolling,
+ // we don't look for swipes.
+ mAutoCompleteList.setOnScrollListener(touchListener.makeScrollListener());
+
+ // Setting this recycler listener is required to make sure animated views are reset.
+ mAutoCompleteList.setRecyclerListener(touchListener.makeRecyclerListener());
+
+ addView(mAutoCompleteList);
+ }
+
+ AutoCompleteListAdapter adapter = new AutoCompleteListAdapter(mContext, R.layout.autocomplete_list_item);
+ adapter.populateSuggestionsList(suggestions);
+ mAutoCompleteList.setAdapter(adapter);
+
+ if (setGeckoPositionData(rect, true)) {
+ positionAndShowPopup();
+ }
+ }
+
+ private void showValidationMessage(String validationMessage, JSONObject rect) {
+ if (mValidationMessage == null) {
+ LayoutInflater inflater = LayoutInflater.from(mContext);
+ mValidationMessage = (RelativeLayout) inflater.inflate(R.layout.validation_message, null);
+
+ addView(mValidationMessage);
+ mValidationMessageText = (TextView) mValidationMessage.findViewById(R.id.validation_message_text);
+
+ sValidationTextMarginTop = (int) (mContext.getResources().getDimension(R.dimen.validation_message_margin_top));
+
+ sValidationTextLayoutNormal = new LayoutParams(mValidationMessageText.getLayoutParams());
+ sValidationTextLayoutNormal.setMargins(0, sValidationTextMarginTop, 0, 0);
+
+ sValidationTextLayoutInverted = new LayoutParams((ViewGroup.MarginLayoutParams) sValidationTextLayoutNormal);
+ sValidationTextLayoutInverted.setMargins(0, 0, 0, 0);
+
+ mValidationMessageArrow = (ImageView) mValidationMessage.findViewById(R.id.validation_message_arrow);
+ mValidationMessageArrowInverted = (ImageView) mValidationMessage.findViewById(R.id.validation_message_arrow_inverted);
+ }
+
+ mValidationMessageText.setText(validationMessage);
+
+ // We need to set the text as selected for the marquee text to work.
+ mValidationMessageText.setSelected(true);
+
+ if (setGeckoPositionData(rect, false)) {
+ positionAndShowPopup();
+ }
+ }
+
+ private boolean setGeckoPositionData(JSONObject rect, boolean isAutoComplete) {
+ try {
+ mX = rect.getDouble("x");
+ mY = rect.getDouble("y");
+ mW = rect.getDouble("w");
+ mH = rect.getDouble("h");
+ } catch (JSONException e) {
+ // Bail if we can't get the correct dimensions for the popup.
+ Log.e(LOGTAG, "Error getting FormAssistPopup dimensions", e);
+ return false;
+ }
+
+ mPopupType = (isAutoComplete ?
+ PopupType.AUTOCOMPLETE : PopupType.VALIDATIONMESSAGE);
+ return true;
+ }
+
+ private void positionAndShowPopup() {
+ positionAndShowPopup(GeckoAppShell.getLayerView().getViewportMetrics());
+ }
+
+ private void positionAndShowPopup(ImmutableViewportMetrics aMetrics) {
+ ThreadUtils.assertOnUiThread();
+
+ // Don't show the form assist popup when using fullscreen VKB
+ InputMethodManager imm =
+ (InputMethodManager) mContext.getSystemService(Context.INPUT_METHOD_SERVICE);
+ if (imm.isFullscreenMode()) {
+ return;
+ }
+
+ // Hide/show the appropriate popup contents
+ if (mAutoCompleteList != null) {
+ mAutoCompleteList.setVisibility((mPopupType == PopupType.AUTOCOMPLETE) ? VISIBLE : GONE);
+ }
+ if (mValidationMessage != null) {
+ mValidationMessage.setVisibility((mPopupType == PopupType.AUTOCOMPLETE) ? GONE : VISIBLE);
+ }
+
+ if (sAutoCompleteMinWidth == 0) {
+ Resources res = mContext.getResources();
+ sAutoCompleteMinWidth = (int) (res.getDimension(R.dimen.autocomplete_min_width));
+ sAutoCompleteRowHeight = (int) (res.getDimension(R.dimen.autocomplete_row_height));
+ sValidationMessageHeight = (int) (res.getDimension(R.dimen.validation_message_height));
+ }
+
+ float zoom = aMetrics.zoomFactor;
+
+ // These values correspond to the input box for which we want to
+ // display the FormAssistPopup.
+ int left = (int) (mX * zoom - aMetrics.viewportRectLeft);
+ int top = (int) (mY * zoom - aMetrics.viewportRectTop + GeckoAppShell.getLayerView().getSurfaceTranslation());
+ int width = (int) (mW * zoom);
+ int height = (int) (mH * zoom);
+
+ int popupWidth = LayoutParams.MATCH_PARENT;
+ int popupLeft = left < 0 ? 0 : left;
+
+ FloatSize viewport = aMetrics.getSize();
+
+ // For autocomplete suggestions, if the input is smaller than the screen-width,
+ // shrink the popup's width. Otherwise, keep it as MATCH_PARENT.
+ if ((mPopupType == PopupType.AUTOCOMPLETE) && (left + width) < viewport.width) {
+ popupWidth = left < 0 ? left + width : width;
+
+ // Ensure the popup has a minimum width.
+ if (popupWidth < sAutoCompleteMinWidth) {
+ popupWidth = sAutoCompleteMinWidth;
+
+ // Move the popup to the left if there isn't enough room for it.
+ if ((popupLeft + popupWidth) > viewport.width) {
+ popupLeft = (int) (viewport.width - popupWidth);
+ }
+ }
+ }
+
+ int popupHeight;
+ if (mPopupType == PopupType.AUTOCOMPLETE) {
+ // Limit the amount of visible rows.
+ int rows = mAutoCompleteList.getAdapter().getCount();
+ if (rows > MAX_VISIBLE_ROWS) {
+ rows = MAX_VISIBLE_ROWS;
+ }
+
+ popupHeight = sAutoCompleteRowHeight * rows;
+ } else {
+ popupHeight = sValidationMessageHeight;
+ }
+
+ int popupTop = top + height;
+
+ if (mPopupType == PopupType.VALIDATIONMESSAGE) {
+ mValidationMessageText.setLayoutParams(sValidationTextLayoutNormal);
+ mValidationMessageArrow.setVisibility(VISIBLE);
+ mValidationMessageArrowInverted.setVisibility(GONE);
+ }
+
+ // If the popup doesn't fit below the input box, shrink its height, or
+ // see if we can place it above the input instead.
+ if ((popupTop + popupHeight) > viewport.height) {
+ // Find where the maximum space is, and put the popup there.
+ if ((viewport.height - popupTop) > top) {
+ // Shrink the height to fit it below the input box.
+ popupHeight = (int) (viewport.height - popupTop);
+ } else {
+ if (popupHeight < top) {
+ // No shrinking needed to fit on top.
+ popupTop = (top - popupHeight);
+ } else {
+ // Shrink to available space on top.
+ popupTop = 0;
+ popupHeight = top;
+ }
+
+ if (mPopupType == PopupType.VALIDATIONMESSAGE) {
+ mValidationMessageText.setLayoutParams(sValidationTextLayoutInverted);
+ mValidationMessageArrow.setVisibility(GONE);
+ mValidationMessageArrowInverted.setVisibility(VISIBLE);
+ }
+ }
+ }
+
+ LayoutParams layoutParams = new LayoutParams(popupWidth, popupHeight);
+ layoutParams.setMargins(popupLeft, popupTop, 0, 0);
+ setLayoutParams(layoutParams);
+ requestLayout();
+
+ if (!isShown()) {
+ setVisibility(VISIBLE);
+ startAnimation(mAnimation);
+ }
+ }
+
+ public void hide() {
+ if (isShown()) {
+ setVisibility(GONE);
+ broadcastGeckoEvent("FormAssist:Hidden", null);
+ }
+ }
+
+ void onTranslationChanged() {
+ ThreadUtils.assertOnUiThread();
+ if (!isShown()) {
+ return;
+ }
+ positionAndShowPopup();
+ }
+
+ void onMetricsChanged(final ImmutableViewportMetrics aMetrics) {
+ if (!isShown()) {
+ return;
+ }
+
+ ThreadUtils.postToUiThread(new Runnable() {
+ @Override
+ public void run() {
+ positionAndShowPopup(aMetrics);
+ }
+ });
+ }
+
+ private static void broadcastGeckoEvent(String eventName, String eventData) {
+ GeckoAppShell.notifyObservers(eventName, eventData);
+ }
+
+ private class AutoCompleteListAdapter extends ArrayAdapter<Pair<String, String>> {
+ private final LayoutInflater mInflater;
+ private final int mTextViewResourceId;
+
+ public AutoCompleteListAdapter(Context context, int textViewResourceId) {
+ super(context, textViewResourceId);
+
+ mInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ mTextViewResourceId = textViewResourceId;
+ }
+
+ // This method takes an array of autocomplete suggestions with label/value properties
+ // and adds label/value Pair objects to the array that backs the adapter.
+ public void populateSuggestionsList(JSONArray suggestions) {
+ try {
+ for (int i = 0; i < suggestions.length(); i++) {
+ JSONObject suggestion = suggestions.getJSONObject(i);
+ String label = suggestion.getString("label");
+ String value = suggestion.getString("value");
+ add(new Pair<String, String>(label, value));
+ }
+ } catch (JSONException e) {
+ Log.e(LOGTAG, "JSONException", e);
+ }
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ if (convertView == null) {
+ convertView = mInflater.inflate(mTextViewResourceId, null);
+ }
+
+ Pair<String, String> item = getItem(position);
+ TextView itemView = (TextView) convertView;
+
+ // Set the text with the suggestion label
+ itemView.setText(item.first);
+
+ // Set a tag with the suggestion value
+ itemView.setTag(item.second);
+
+ return convertView;
+ }
+ }
+}