mobile/android/base/FormAssistPopup.java

changeset 0
6474c204b198
     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 +}

mercurial