mobile/android/base/FormAssistPopup.java

Wed, 31 Dec 2014 07:22:50 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Wed, 31 Dec 2014 07:22:50 +0100
branch
TOR_BUG_3246
changeset 4
fc2d59ddac77
permissions
-rw-r--r--

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 }

mercurial