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.

michael@0 1 /* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
michael@0 2 * This Source Code Form is subject to the terms of the Mozilla Public
michael@0 3 * License, v. 2.0. If a copy of the MPL was not distributed with this
michael@0 4 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
michael@0 5
michael@0 6 package org.mozilla.gecko;
michael@0 7
michael@0 8 import org.mozilla.gecko.gfx.FloatSize;
michael@0 9 import org.mozilla.gecko.gfx.ImmutableViewportMetrics;
michael@0 10 import org.mozilla.gecko.util.GeckoEventListener;
michael@0 11 import org.mozilla.gecko.util.ThreadUtils;
michael@0 12
michael@0 13 import org.json.JSONArray;
michael@0 14 import org.json.JSONException;
michael@0 15 import org.json.JSONObject;
michael@0 16
michael@0 17 import android.content.Context;
michael@0 18 import android.content.res.Resources;
michael@0 19 import android.graphics.PointF;
michael@0 20 import android.util.AttributeSet;
michael@0 21 import android.util.Log;
michael@0 22 import android.util.Pair;
michael@0 23 import android.view.LayoutInflater;
michael@0 24 import android.view.View;
michael@0 25 import android.view.ViewGroup;
michael@0 26 import android.view.animation.Animation;
michael@0 27 import android.view.animation.AnimationUtils;
michael@0 28 import android.view.inputmethod.InputMethodManager;
michael@0 29 import android.widget.AdapterView;
michael@0 30 import android.widget.AdapterView.OnItemClickListener;
michael@0 31 import android.widget.ArrayAdapter;
michael@0 32 import android.widget.ImageView;
michael@0 33 import android.widget.ListView;
michael@0 34 import android.widget.RelativeLayout;
michael@0 35 import android.widget.TextView;
michael@0 36
michael@0 37 import java.util.Arrays;
michael@0 38 import java.util.Collection;
michael@0 39
michael@0 40 public class FormAssistPopup extends RelativeLayout implements GeckoEventListener {
michael@0 41 private Context mContext;
michael@0 42 private Animation mAnimation;
michael@0 43
michael@0 44 private ListView mAutoCompleteList;
michael@0 45 private RelativeLayout mValidationMessage;
michael@0 46 private TextView mValidationMessageText;
michael@0 47 private ImageView mValidationMessageArrow;
michael@0 48 private ImageView mValidationMessageArrowInverted;
michael@0 49
michael@0 50 private double mX;
michael@0 51 private double mY;
michael@0 52 private double mW;
michael@0 53 private double mH;
michael@0 54
michael@0 55 private enum PopupType {
michael@0 56 AUTOCOMPLETE,
michael@0 57 VALIDATIONMESSAGE;
michael@0 58 }
michael@0 59 private PopupType mPopupType;
michael@0 60
michael@0 61 private static int sAutoCompleteMinWidth = 0;
michael@0 62 private static int sAutoCompleteRowHeight = 0;
michael@0 63 private static int sValidationMessageHeight = 0;
michael@0 64 private static int sValidationTextMarginTop = 0;
michael@0 65 private static RelativeLayout.LayoutParams sValidationTextLayoutNormal;
michael@0 66 private static RelativeLayout.LayoutParams sValidationTextLayoutInverted;
michael@0 67
michael@0 68 private static final String LOGTAG = "GeckoFormAssistPopup";
michael@0 69
michael@0 70 // The blocklist is so short that ArrayList is probably cheaper than HashSet.
michael@0 71 private static final Collection<String> sInputMethodBlocklist = Arrays.asList(new String[] {
michael@0 72 InputMethods.METHOD_GOOGLE_JAPANESE_INPUT, // bug 775850
michael@0 73 InputMethods.METHOD_OPENWNN_PLUS, // bug 768108
michael@0 74 InputMethods.METHOD_SIMEJI, // bug 768108
michael@0 75 InputMethods.METHOD_SWYPE, // bug 755909
michael@0 76 InputMethods.METHOD_SWYPE_BETA, // bug 755909
michael@0 77 });
michael@0 78
michael@0 79 public FormAssistPopup(Context context, AttributeSet attrs) {
michael@0 80 super(context, attrs);
michael@0 81 mContext = context;
michael@0 82
michael@0 83 mAnimation = AnimationUtils.loadAnimation(context, R.anim.grow_fade_in);
michael@0 84 mAnimation.setDuration(75);
michael@0 85
michael@0 86 setFocusable(false);
michael@0 87
michael@0 88 registerEventListener("FormAssist:AutoComplete");
michael@0 89 registerEventListener("FormAssist:ValidationMessage");
michael@0 90 registerEventListener("FormAssist:Hide");
michael@0 91 }
michael@0 92
michael@0 93 void destroy() {
michael@0 94 unregisterEventListener("FormAssist:AutoComplete");
michael@0 95 unregisterEventListener("FormAssist:ValidationMessage");
michael@0 96 unregisterEventListener("FormAssist:Hide");
michael@0 97 }
michael@0 98
michael@0 99 @Override
michael@0 100 public void handleMessage(String event, JSONObject message) {
michael@0 101 try {
michael@0 102 if (event.equals("FormAssist:AutoComplete")) {
michael@0 103 handleAutoCompleteMessage(message);
michael@0 104 } else if (event.equals("FormAssist:ValidationMessage")) {
michael@0 105 handleValidationMessage(message);
michael@0 106 } else if (event.equals("FormAssist:Hide")) {
michael@0 107 handleHideMessage(message);
michael@0 108 }
michael@0 109 } catch (Exception e) {
michael@0 110 Log.e(LOGTAG, "Exception handling message \"" + event + "\":", e);
michael@0 111 }
michael@0 112 }
michael@0 113
michael@0 114 private void handleAutoCompleteMessage(JSONObject message) throws JSONException {
michael@0 115 final JSONArray suggestions = message.getJSONArray("suggestions");
michael@0 116 final JSONObject rect = message.getJSONObject("rect");
michael@0 117 ThreadUtils.postToUiThread(new Runnable() {
michael@0 118 @Override
michael@0 119 public void run() {
michael@0 120 showAutoCompleteSuggestions(suggestions, rect);
michael@0 121 }
michael@0 122 });
michael@0 123 }
michael@0 124
michael@0 125 private void handleValidationMessage(JSONObject message) throws JSONException {
michael@0 126 final String validationMessage = message.getString("validationMessage");
michael@0 127 final JSONObject rect = message.getJSONObject("rect");
michael@0 128 ThreadUtils.postToUiThread(new Runnable() {
michael@0 129 @Override
michael@0 130 public void run() {
michael@0 131 showValidationMessage(validationMessage, rect);
michael@0 132 }
michael@0 133 });
michael@0 134 }
michael@0 135
michael@0 136 private void handleHideMessage(JSONObject message) {
michael@0 137 ThreadUtils.postToUiThread(new Runnable() {
michael@0 138 @Override
michael@0 139 public void run() {
michael@0 140 hide();
michael@0 141 }
michael@0 142 });
michael@0 143 }
michael@0 144
michael@0 145 private void showAutoCompleteSuggestions(JSONArray suggestions, JSONObject rect) {
michael@0 146 if (mAutoCompleteList == null) {
michael@0 147 LayoutInflater inflater = LayoutInflater.from(mContext);
michael@0 148 mAutoCompleteList = (ListView) inflater.inflate(R.layout.autocomplete_list, null);
michael@0 149
michael@0 150 mAutoCompleteList.setOnItemClickListener(new OnItemClickListener() {
michael@0 151 @Override
michael@0 152 public void onItemClick(AdapterView<?> parentView, View view, int position, long id) {
michael@0 153 // Use the value stored with the autocomplete view, not the label text,
michael@0 154 // since they can be different.
michael@0 155 TextView textView = (TextView) view;
michael@0 156 String value = (String) textView.getTag();
michael@0 157 broadcastGeckoEvent("FormAssist:AutoComplete", value);
michael@0 158 hide();
michael@0 159 }
michael@0 160 });
michael@0 161
michael@0 162 addView(mAutoCompleteList);
michael@0 163 }
michael@0 164
michael@0 165 AutoCompleteListAdapter adapter = new AutoCompleteListAdapter(mContext, R.layout.autocomplete_list_item);
michael@0 166 adapter.populateSuggestionsList(suggestions);
michael@0 167 mAutoCompleteList.setAdapter(adapter);
michael@0 168
michael@0 169 if (setGeckoPositionData(rect, true)) {
michael@0 170 positionAndShowPopup();
michael@0 171 }
michael@0 172 }
michael@0 173
michael@0 174 private void showValidationMessage(String validationMessage, JSONObject rect) {
michael@0 175 if (mValidationMessage == null) {
michael@0 176 LayoutInflater inflater = LayoutInflater.from(mContext);
michael@0 177 mValidationMessage = (RelativeLayout) inflater.inflate(R.layout.validation_message, null);
michael@0 178
michael@0 179 addView(mValidationMessage);
michael@0 180 mValidationMessageText = (TextView) mValidationMessage.findViewById(R.id.validation_message_text);
michael@0 181
michael@0 182 sValidationTextMarginTop = (int) (mContext.getResources().getDimension(R.dimen.validation_message_margin_top));
michael@0 183
michael@0 184 sValidationTextLayoutNormal = new RelativeLayout.LayoutParams(mValidationMessageText.getLayoutParams());
michael@0 185 sValidationTextLayoutNormal.setMargins(0, sValidationTextMarginTop, 0, 0);
michael@0 186
michael@0 187 sValidationTextLayoutInverted = new RelativeLayout.LayoutParams(sValidationTextLayoutNormal);
michael@0 188 sValidationTextLayoutInverted.setMargins(0, 0, 0, 0);
michael@0 189
michael@0 190 mValidationMessageArrow = (ImageView) mValidationMessage.findViewById(R.id.validation_message_arrow);
michael@0 191 mValidationMessageArrowInverted = (ImageView) mValidationMessage.findViewById(R.id.validation_message_arrow_inverted);
michael@0 192 }
michael@0 193
michael@0 194 mValidationMessageText.setText(validationMessage);
michael@0 195
michael@0 196 // We need to set the text as selected for the marquee text to work.
michael@0 197 mValidationMessageText.setSelected(true);
michael@0 198
michael@0 199 if (setGeckoPositionData(rect, false)) {
michael@0 200 positionAndShowPopup();
michael@0 201 }
michael@0 202 }
michael@0 203
michael@0 204 private boolean setGeckoPositionData(JSONObject rect, boolean isAutoComplete) {
michael@0 205 try {
michael@0 206 mX = rect.getDouble("x");
michael@0 207 mY = rect.getDouble("y");
michael@0 208 mW = rect.getDouble("w");
michael@0 209 mH = rect.getDouble("h");
michael@0 210 } catch (JSONException e) {
michael@0 211 // Bail if we can't get the correct dimensions for the popup.
michael@0 212 Log.e(LOGTAG, "Error getting FormAssistPopup dimensions", e);
michael@0 213 return false;
michael@0 214 }
michael@0 215
michael@0 216 mPopupType = (isAutoComplete ?
michael@0 217 PopupType.AUTOCOMPLETE : PopupType.VALIDATIONMESSAGE);
michael@0 218 return true;
michael@0 219 }
michael@0 220
michael@0 221 private void positionAndShowPopup() {
michael@0 222 positionAndShowPopup(GeckoAppShell.getLayerView().getViewportMetrics());
michael@0 223 }
michael@0 224
michael@0 225 private void positionAndShowPopup(ImmutableViewportMetrics aMetrics) {
michael@0 226 ThreadUtils.assertOnUiThread();
michael@0 227
michael@0 228 // Don't show the form assist popup when using fullscreen VKB
michael@0 229 InputMethodManager imm =
michael@0 230 (InputMethodManager) GeckoAppShell.getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
michael@0 231 if (imm.isFullscreenMode())
michael@0 232 return;
michael@0 233
michael@0 234 // Hide/show the appropriate popup contents
michael@0 235 if (mAutoCompleteList != null)
michael@0 236 mAutoCompleteList.setVisibility((mPopupType == PopupType.AUTOCOMPLETE) ? VISIBLE : GONE);
michael@0 237 if (mValidationMessage != null)
michael@0 238 mValidationMessage.setVisibility((mPopupType == PopupType.AUTOCOMPLETE) ? GONE : VISIBLE);
michael@0 239
michael@0 240 if (sAutoCompleteMinWidth == 0) {
michael@0 241 Resources res = mContext.getResources();
michael@0 242 sAutoCompleteMinWidth = (int) (res.getDimension(R.dimen.autocomplete_min_width));
michael@0 243 sAutoCompleteRowHeight = (int) (res.getDimension(R.dimen.autocomplete_row_height));
michael@0 244 sValidationMessageHeight = (int) (res.getDimension(R.dimen.validation_message_height));
michael@0 245 }
michael@0 246
michael@0 247 float zoom = aMetrics.zoomFactor;
michael@0 248 PointF offset = aMetrics.getMarginOffset();
michael@0 249
michael@0 250 // These values correspond to the input box for which we want to
michael@0 251 // display the FormAssistPopup.
michael@0 252 int left = (int) (mX * zoom - aMetrics.viewportRectLeft + offset.x);
michael@0 253 int top = (int) (mY * zoom - aMetrics.viewportRectTop + offset.y);
michael@0 254 int width = (int) (mW * zoom);
michael@0 255 int height = (int) (mH * zoom);
michael@0 256
michael@0 257 int popupWidth = RelativeLayout.LayoutParams.FILL_PARENT;
michael@0 258 int popupLeft = left < 0 ? 0 : left;
michael@0 259
michael@0 260 FloatSize viewport = aMetrics.getSize();
michael@0 261
michael@0 262 // For autocomplete suggestions, if the input is smaller than the screen-width,
michael@0 263 // shrink the popup's width. Otherwise, keep it as FILL_PARENT.
michael@0 264 if ((mPopupType == PopupType.AUTOCOMPLETE) && (left + width) < viewport.width) {
michael@0 265 popupWidth = left < 0 ? left + width : width;
michael@0 266
michael@0 267 // Ensure the popup has a minimum width.
michael@0 268 if (popupWidth < sAutoCompleteMinWidth) {
michael@0 269 popupWidth = sAutoCompleteMinWidth;
michael@0 270
michael@0 271 // Move the popup to the left if there isn't enough room for it.
michael@0 272 if ((popupLeft + popupWidth) > viewport.width)
michael@0 273 popupLeft = (int) (viewport.width - popupWidth);
michael@0 274 }
michael@0 275 }
michael@0 276
michael@0 277 int popupHeight;
michael@0 278 if (mPopupType == PopupType.AUTOCOMPLETE)
michael@0 279 popupHeight = sAutoCompleteRowHeight * mAutoCompleteList.getAdapter().getCount();
michael@0 280 else
michael@0 281 popupHeight = sValidationMessageHeight;
michael@0 282
michael@0 283 int popupTop = top + height;
michael@0 284
michael@0 285 if (mPopupType == PopupType.VALIDATIONMESSAGE) {
michael@0 286 mValidationMessageText.setLayoutParams(sValidationTextLayoutNormal);
michael@0 287 mValidationMessageArrow.setVisibility(VISIBLE);
michael@0 288 mValidationMessageArrowInverted.setVisibility(GONE);
michael@0 289 }
michael@0 290
michael@0 291 // If the popup doesn't fit below the input box, shrink its height, or
michael@0 292 // see if we can place it above the input instead.
michael@0 293 if ((popupTop + popupHeight) > viewport.height) {
michael@0 294 // Find where the maximum space is, and put the popup there.
michael@0 295 if ((viewport.height - popupTop) > top) {
michael@0 296 // Shrink the height to fit it below the input box.
michael@0 297 popupHeight = (int) (viewport.height - popupTop);
michael@0 298 } else {
michael@0 299 if (popupHeight < top) {
michael@0 300 // No shrinking needed to fit on top.
michael@0 301 popupTop = (top - popupHeight);
michael@0 302 } else {
michael@0 303 // Shrink to available space on top.
michael@0 304 popupTop = 0;
michael@0 305 popupHeight = top;
michael@0 306 }
michael@0 307
michael@0 308 if (mPopupType == PopupType.VALIDATIONMESSAGE) {
michael@0 309 mValidationMessageText.setLayoutParams(sValidationTextLayoutInverted);
michael@0 310 mValidationMessageArrow.setVisibility(GONE);
michael@0 311 mValidationMessageArrowInverted.setVisibility(VISIBLE);
michael@0 312 }
michael@0 313 }
michael@0 314 }
michael@0 315
michael@0 316 RelativeLayout.LayoutParams layoutParams =
michael@0 317 new RelativeLayout.LayoutParams(popupWidth, popupHeight);
michael@0 318 layoutParams.setMargins(popupLeft, popupTop, 0, 0);
michael@0 319 setLayoutParams(layoutParams);
michael@0 320 requestLayout();
michael@0 321
michael@0 322 if (!isShown()) {
michael@0 323 setVisibility(VISIBLE);
michael@0 324 startAnimation(mAnimation);
michael@0 325 }
michael@0 326 }
michael@0 327
michael@0 328 public void hide() {
michael@0 329 if (isShown()) {
michael@0 330 setVisibility(GONE);
michael@0 331 broadcastGeckoEvent("FormAssist:Hidden", null);
michael@0 332 }
michael@0 333 }
michael@0 334
michael@0 335 void onInputMethodChanged(String newInputMethod) {
michael@0 336 boolean blocklisted = sInputMethodBlocklist.contains(newInputMethod);
michael@0 337 broadcastGeckoEvent("FormAssist:Blocklisted", String.valueOf(blocklisted));
michael@0 338 }
michael@0 339
michael@0 340 void onMetricsChanged(final ImmutableViewportMetrics aMetrics) {
michael@0 341 if (!isShown()) {
michael@0 342 return;
michael@0 343 }
michael@0 344
michael@0 345 ThreadUtils.postToUiThread(new Runnable() {
michael@0 346 @Override
michael@0 347 public void run() {
michael@0 348 positionAndShowPopup(aMetrics);
michael@0 349 }
michael@0 350 });
michael@0 351 }
michael@0 352
michael@0 353 private static void broadcastGeckoEvent(String eventName, String eventData) {
michael@0 354 GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent(eventName, eventData));
michael@0 355 }
michael@0 356
michael@0 357 private class AutoCompleteListAdapter extends ArrayAdapter<Pair<String, String>> {
michael@0 358 private LayoutInflater mInflater;
michael@0 359 private int mTextViewResourceId;
michael@0 360
michael@0 361 public AutoCompleteListAdapter(Context context, int textViewResourceId) {
michael@0 362 super(context, textViewResourceId);
michael@0 363
michael@0 364 mInflater = (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
michael@0 365 mTextViewResourceId = textViewResourceId;
michael@0 366 }
michael@0 367
michael@0 368 // This method takes an array of autocomplete suggestions with label/value properties
michael@0 369 // and adds label/value Pair objects to the array that backs the adapter.
michael@0 370 public void populateSuggestionsList(JSONArray suggestions) {
michael@0 371 try {
michael@0 372 for (int i = 0; i < suggestions.length(); i++) {
michael@0 373 JSONObject suggestion = suggestions.getJSONObject(i);
michael@0 374 String label = suggestion.getString("label");
michael@0 375 String value = suggestion.getString("value");
michael@0 376 add(new Pair<String, String>(label, value));
michael@0 377 }
michael@0 378 } catch (JSONException e) {
michael@0 379 Log.e(LOGTAG, "JSONException", e);
michael@0 380 }
michael@0 381 }
michael@0 382
michael@0 383 @Override
michael@0 384 public View getView(int position, View convertView, ViewGroup parent) {
michael@0 385 if (convertView == null)
michael@0 386 convertView = mInflater.inflate(mTextViewResourceId, null);
michael@0 387
michael@0 388 Pair<String, String> item = getItem(position);
michael@0 389 TextView itemView = (TextView) convertView;
michael@0 390
michael@0 391 // Set the text with the suggestion label
michael@0 392 itemView.setText(item.first);
michael@0 393
michael@0 394 // Set a tag with the suggestion value
michael@0 395 itemView.setTag(item.second);
michael@0 396
michael@0 397 return convertView;
michael@0 398 }
michael@0 399 }
michael@0 400
michael@0 401 private void registerEventListener(String event) {
michael@0 402 GeckoAppShell.getEventDispatcher().registerEventListener(event, this);
michael@0 403 }
michael@0 404
michael@0 405 private void unregisterEventListener(String event) {
michael@0 406 GeckoAppShell.getEventDispatcher().unregisterEventListener(event, this);
michael@0 407 }
michael@0 408 }

mercurial