michael@0: /* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- michael@0: * This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: package org.mozilla.gecko; michael@0: michael@0: import org.mozilla.gecko.gfx.FloatSize; michael@0: import org.mozilla.gecko.gfx.ImmutableViewportMetrics; michael@0: import org.mozilla.gecko.util.GeckoEventListener; michael@0: import org.mozilla.gecko.util.ThreadUtils; michael@0: michael@0: import org.json.JSONArray; michael@0: import org.json.JSONException; michael@0: import org.json.JSONObject; michael@0: michael@0: import android.content.Context; michael@0: import android.content.res.Resources; michael@0: import android.graphics.PointF; michael@0: import android.util.AttributeSet; michael@0: import android.util.Log; michael@0: import android.util.Pair; michael@0: import android.view.LayoutInflater; michael@0: import android.view.View; michael@0: import android.view.ViewGroup; michael@0: import android.view.animation.Animation; michael@0: import android.view.animation.AnimationUtils; michael@0: import android.view.inputmethod.InputMethodManager; michael@0: import android.widget.AdapterView; michael@0: import android.widget.AdapterView.OnItemClickListener; michael@0: import android.widget.ArrayAdapter; michael@0: import android.widget.ImageView; michael@0: import android.widget.ListView; michael@0: import android.widget.RelativeLayout; michael@0: import android.widget.TextView; michael@0: michael@0: import java.util.Arrays; michael@0: import java.util.Collection; michael@0: michael@0: public class FormAssistPopup extends RelativeLayout implements GeckoEventListener { michael@0: private Context mContext; michael@0: private Animation mAnimation; michael@0: michael@0: private ListView mAutoCompleteList; michael@0: private RelativeLayout mValidationMessage; michael@0: private TextView mValidationMessageText; michael@0: private ImageView mValidationMessageArrow; michael@0: private ImageView mValidationMessageArrowInverted; michael@0: michael@0: private double mX; michael@0: private double mY; michael@0: private double mW; michael@0: private double mH; michael@0: michael@0: private enum PopupType { michael@0: AUTOCOMPLETE, michael@0: VALIDATIONMESSAGE; michael@0: } michael@0: private PopupType mPopupType; michael@0: michael@0: private static int sAutoCompleteMinWidth = 0; michael@0: private static int sAutoCompleteRowHeight = 0; michael@0: private static int sValidationMessageHeight = 0; michael@0: private static int sValidationTextMarginTop = 0; michael@0: private static RelativeLayout.LayoutParams sValidationTextLayoutNormal; michael@0: private static RelativeLayout.LayoutParams sValidationTextLayoutInverted; michael@0: michael@0: private static final String LOGTAG = "GeckoFormAssistPopup"; michael@0: michael@0: // The blocklist is so short that ArrayList is probably cheaper than HashSet. michael@0: private static final Collection sInputMethodBlocklist = Arrays.asList(new String[] { michael@0: InputMethods.METHOD_GOOGLE_JAPANESE_INPUT, // bug 775850 michael@0: InputMethods.METHOD_OPENWNN_PLUS, // bug 768108 michael@0: InputMethods.METHOD_SIMEJI, // bug 768108 michael@0: InputMethods.METHOD_SWYPE, // bug 755909 michael@0: InputMethods.METHOD_SWYPE_BETA, // bug 755909 michael@0: }); michael@0: michael@0: public FormAssistPopup(Context context, AttributeSet attrs) { michael@0: super(context, attrs); michael@0: mContext = context; michael@0: michael@0: mAnimation = AnimationUtils.loadAnimation(context, R.anim.grow_fade_in); michael@0: mAnimation.setDuration(75); michael@0: michael@0: setFocusable(false); michael@0: michael@0: registerEventListener("FormAssist:AutoComplete"); michael@0: registerEventListener("FormAssist:ValidationMessage"); michael@0: registerEventListener("FormAssist:Hide"); michael@0: } michael@0: michael@0: void destroy() { michael@0: unregisterEventListener("FormAssist:AutoComplete"); michael@0: unregisterEventListener("FormAssist:ValidationMessage"); michael@0: unregisterEventListener("FormAssist:Hide"); michael@0: } michael@0: michael@0: @Override michael@0: public void handleMessage(String event, JSONObject message) { michael@0: try { michael@0: if (event.equals("FormAssist:AutoComplete")) { michael@0: handleAutoCompleteMessage(message); michael@0: } else if (event.equals("FormAssist:ValidationMessage")) { michael@0: handleValidationMessage(message); michael@0: } else if (event.equals("FormAssist:Hide")) { michael@0: handleHideMessage(message); michael@0: } michael@0: } catch (Exception e) { michael@0: Log.e(LOGTAG, "Exception handling message \"" + event + "\":", e); michael@0: } michael@0: } michael@0: michael@0: private void handleAutoCompleteMessage(JSONObject message) throws JSONException { michael@0: final JSONArray suggestions = message.getJSONArray("suggestions"); michael@0: final JSONObject rect = message.getJSONObject("rect"); michael@0: ThreadUtils.postToUiThread(new Runnable() { michael@0: @Override michael@0: public void run() { michael@0: showAutoCompleteSuggestions(suggestions, rect); michael@0: } michael@0: }); michael@0: } michael@0: michael@0: private void handleValidationMessage(JSONObject message) throws JSONException { michael@0: final String validationMessage = message.getString("validationMessage"); michael@0: final JSONObject rect = message.getJSONObject("rect"); michael@0: ThreadUtils.postToUiThread(new Runnable() { michael@0: @Override michael@0: public void run() { michael@0: showValidationMessage(validationMessage, rect); michael@0: } michael@0: }); michael@0: } michael@0: michael@0: private void handleHideMessage(JSONObject message) { michael@0: ThreadUtils.postToUiThread(new Runnable() { michael@0: @Override michael@0: public void run() { michael@0: hide(); michael@0: } michael@0: }); michael@0: } michael@0: michael@0: private void showAutoCompleteSuggestions(JSONArray suggestions, JSONObject rect) { michael@0: if (mAutoCompleteList == null) { michael@0: LayoutInflater inflater = LayoutInflater.from(mContext); michael@0: mAutoCompleteList = (ListView) inflater.inflate(R.layout.autocomplete_list, null); michael@0: michael@0: mAutoCompleteList.setOnItemClickListener(new OnItemClickListener() { michael@0: @Override michael@0: public void onItemClick(AdapterView parentView, View view, int position, long id) { michael@0: // Use the value stored with the autocomplete view, not the label text, michael@0: // since they can be different. michael@0: TextView textView = (TextView) view; michael@0: String value = (String) textView.getTag(); michael@0: broadcastGeckoEvent("FormAssist:AutoComplete", value); michael@0: hide(); michael@0: } michael@0: }); michael@0: michael@0: addView(mAutoCompleteList); michael@0: } michael@0: michael@0: AutoCompleteListAdapter adapter = new AutoCompleteListAdapter(mContext, R.layout.autocomplete_list_item); michael@0: adapter.populateSuggestionsList(suggestions); michael@0: mAutoCompleteList.setAdapter(adapter); michael@0: michael@0: if (setGeckoPositionData(rect, true)) { michael@0: positionAndShowPopup(); michael@0: } michael@0: } michael@0: michael@0: private void showValidationMessage(String validationMessage, JSONObject rect) { michael@0: if (mValidationMessage == null) { michael@0: LayoutInflater inflater = LayoutInflater.from(mContext); michael@0: mValidationMessage = (RelativeLayout) inflater.inflate(R.layout.validation_message, null); michael@0: michael@0: addView(mValidationMessage); michael@0: mValidationMessageText = (TextView) mValidationMessage.findViewById(R.id.validation_message_text); michael@0: michael@0: sValidationTextMarginTop = (int) (mContext.getResources().getDimension(R.dimen.validation_message_margin_top)); michael@0: michael@0: sValidationTextLayoutNormal = new RelativeLayout.LayoutParams(mValidationMessageText.getLayoutParams()); michael@0: sValidationTextLayoutNormal.setMargins(0, sValidationTextMarginTop, 0, 0); michael@0: michael@0: sValidationTextLayoutInverted = new RelativeLayout.LayoutParams(sValidationTextLayoutNormal); michael@0: sValidationTextLayoutInverted.setMargins(0, 0, 0, 0); michael@0: michael@0: mValidationMessageArrow = (ImageView) mValidationMessage.findViewById(R.id.validation_message_arrow); michael@0: mValidationMessageArrowInverted = (ImageView) mValidationMessage.findViewById(R.id.validation_message_arrow_inverted); michael@0: } michael@0: michael@0: mValidationMessageText.setText(validationMessage); michael@0: michael@0: // We need to set the text as selected for the marquee text to work. michael@0: mValidationMessageText.setSelected(true); michael@0: michael@0: if (setGeckoPositionData(rect, false)) { michael@0: positionAndShowPopup(); michael@0: } michael@0: } michael@0: michael@0: private boolean setGeckoPositionData(JSONObject rect, boolean isAutoComplete) { michael@0: try { michael@0: mX = rect.getDouble("x"); michael@0: mY = rect.getDouble("y"); michael@0: mW = rect.getDouble("w"); michael@0: mH = rect.getDouble("h"); michael@0: } catch (JSONException e) { michael@0: // Bail if we can't get the correct dimensions for the popup. michael@0: Log.e(LOGTAG, "Error getting FormAssistPopup dimensions", e); michael@0: return false; michael@0: } michael@0: michael@0: mPopupType = (isAutoComplete ? michael@0: PopupType.AUTOCOMPLETE : PopupType.VALIDATIONMESSAGE); michael@0: return true; michael@0: } michael@0: michael@0: private void positionAndShowPopup() { michael@0: positionAndShowPopup(GeckoAppShell.getLayerView().getViewportMetrics()); michael@0: } michael@0: michael@0: private void positionAndShowPopup(ImmutableViewportMetrics aMetrics) { michael@0: ThreadUtils.assertOnUiThread(); michael@0: michael@0: // Don't show the form assist popup when using fullscreen VKB michael@0: InputMethodManager imm = michael@0: (InputMethodManager) GeckoAppShell.getContext().getSystemService(Context.INPUT_METHOD_SERVICE); michael@0: if (imm.isFullscreenMode()) michael@0: return; michael@0: michael@0: // Hide/show the appropriate popup contents michael@0: if (mAutoCompleteList != null) michael@0: mAutoCompleteList.setVisibility((mPopupType == PopupType.AUTOCOMPLETE) ? VISIBLE : GONE); michael@0: if (mValidationMessage != null) michael@0: mValidationMessage.setVisibility((mPopupType == PopupType.AUTOCOMPLETE) ? GONE : VISIBLE); michael@0: michael@0: if (sAutoCompleteMinWidth == 0) { michael@0: Resources res = mContext.getResources(); michael@0: sAutoCompleteMinWidth = (int) (res.getDimension(R.dimen.autocomplete_min_width)); michael@0: sAutoCompleteRowHeight = (int) (res.getDimension(R.dimen.autocomplete_row_height)); michael@0: sValidationMessageHeight = (int) (res.getDimension(R.dimen.validation_message_height)); michael@0: } michael@0: michael@0: float zoom = aMetrics.zoomFactor; michael@0: PointF offset = aMetrics.getMarginOffset(); michael@0: michael@0: // These values correspond to the input box for which we want to michael@0: // display the FormAssistPopup. michael@0: int left = (int) (mX * zoom - aMetrics.viewportRectLeft + offset.x); michael@0: int top = (int) (mY * zoom - aMetrics.viewportRectTop + offset.y); michael@0: int width = (int) (mW * zoom); michael@0: int height = (int) (mH * zoom); michael@0: michael@0: int popupWidth = RelativeLayout.LayoutParams.FILL_PARENT; michael@0: int popupLeft = left < 0 ? 0 : left; michael@0: michael@0: FloatSize viewport = aMetrics.getSize(); michael@0: michael@0: // For autocomplete suggestions, if the input is smaller than the screen-width, michael@0: // shrink the popup's width. Otherwise, keep it as FILL_PARENT. michael@0: if ((mPopupType == PopupType.AUTOCOMPLETE) && (left + width) < viewport.width) { michael@0: popupWidth = left < 0 ? left + width : width; michael@0: michael@0: // Ensure the popup has a minimum width. michael@0: if (popupWidth < sAutoCompleteMinWidth) { michael@0: popupWidth = sAutoCompleteMinWidth; michael@0: michael@0: // Move the popup to the left if there isn't enough room for it. michael@0: if ((popupLeft + popupWidth) > viewport.width) michael@0: popupLeft = (int) (viewport.width - popupWidth); michael@0: } michael@0: } michael@0: michael@0: int popupHeight; michael@0: if (mPopupType == PopupType.AUTOCOMPLETE) michael@0: popupHeight = sAutoCompleteRowHeight * mAutoCompleteList.getAdapter().getCount(); michael@0: else michael@0: popupHeight = sValidationMessageHeight; michael@0: michael@0: int popupTop = top + height; michael@0: michael@0: if (mPopupType == PopupType.VALIDATIONMESSAGE) { michael@0: mValidationMessageText.setLayoutParams(sValidationTextLayoutNormal); michael@0: mValidationMessageArrow.setVisibility(VISIBLE); michael@0: mValidationMessageArrowInverted.setVisibility(GONE); michael@0: } michael@0: michael@0: // If the popup doesn't fit below the input box, shrink its height, or michael@0: // see if we can place it above the input instead. michael@0: if ((popupTop + popupHeight) > viewport.height) { michael@0: // Find where the maximum space is, and put the popup there. michael@0: if ((viewport.height - popupTop) > top) { michael@0: // Shrink the height to fit it below the input box. michael@0: popupHeight = (int) (viewport.height - popupTop); michael@0: } else { michael@0: if (popupHeight < top) { michael@0: // No shrinking needed to fit on top. michael@0: popupTop = (top - popupHeight); michael@0: } else { michael@0: // Shrink to available space on top. michael@0: popupTop = 0; michael@0: popupHeight = top; michael@0: } michael@0: michael@0: if (mPopupType == PopupType.VALIDATIONMESSAGE) { michael@0: mValidationMessageText.setLayoutParams(sValidationTextLayoutInverted); michael@0: mValidationMessageArrow.setVisibility(GONE); michael@0: mValidationMessageArrowInverted.setVisibility(VISIBLE); michael@0: } michael@0: } michael@0: } michael@0: michael@0: RelativeLayout.LayoutParams layoutParams = michael@0: new RelativeLayout.LayoutParams(popupWidth, popupHeight); michael@0: layoutParams.setMargins(popupLeft, popupTop, 0, 0); michael@0: setLayoutParams(layoutParams); michael@0: requestLayout(); michael@0: michael@0: if (!isShown()) { michael@0: setVisibility(VISIBLE); michael@0: startAnimation(mAnimation); michael@0: } michael@0: } michael@0: michael@0: public void hide() { michael@0: if (isShown()) { michael@0: setVisibility(GONE); michael@0: broadcastGeckoEvent("FormAssist:Hidden", null); michael@0: } michael@0: } michael@0: michael@0: void onInputMethodChanged(String newInputMethod) { michael@0: boolean blocklisted = sInputMethodBlocklist.contains(newInputMethod); michael@0: broadcastGeckoEvent("FormAssist:Blocklisted", String.valueOf(blocklisted)); michael@0: } michael@0: michael@0: void onMetricsChanged(final ImmutableViewportMetrics aMetrics) { michael@0: if (!isShown()) { michael@0: return; michael@0: } michael@0: michael@0: ThreadUtils.postToUiThread(new Runnable() { michael@0: @Override michael@0: public void run() { michael@0: positionAndShowPopup(aMetrics); michael@0: } michael@0: }); michael@0: } michael@0: michael@0: private static void broadcastGeckoEvent(String eventName, String eventData) { michael@0: GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent(eventName, eventData)); michael@0: } michael@0: michael@0: private class AutoCompleteListAdapter extends ArrayAdapter> { michael@0: private LayoutInflater mInflater; michael@0: private int mTextViewResourceId; michael@0: michael@0: public AutoCompleteListAdapter(Context context, int textViewResourceId) { michael@0: super(context, textViewResourceId); michael@0: michael@0: mInflater = (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE); michael@0: mTextViewResourceId = textViewResourceId; michael@0: } michael@0: michael@0: // This method takes an array of autocomplete suggestions with label/value properties michael@0: // and adds label/value Pair objects to the array that backs the adapter. michael@0: public void populateSuggestionsList(JSONArray suggestions) { michael@0: try { michael@0: for (int i = 0; i < suggestions.length(); i++) { michael@0: JSONObject suggestion = suggestions.getJSONObject(i); michael@0: String label = suggestion.getString("label"); michael@0: String value = suggestion.getString("value"); michael@0: add(new Pair(label, value)); michael@0: } michael@0: } catch (JSONException e) { michael@0: Log.e(LOGTAG, "JSONException", e); michael@0: } michael@0: } michael@0: michael@0: @Override michael@0: public View getView(int position, View convertView, ViewGroup parent) { michael@0: if (convertView == null) michael@0: convertView = mInflater.inflate(mTextViewResourceId, null); michael@0: michael@0: Pair item = getItem(position); michael@0: TextView itemView = (TextView) convertView; michael@0: michael@0: // Set the text with the suggestion label michael@0: itemView.setText(item.first); michael@0: michael@0: // Set a tag with the suggestion value michael@0: itemView.setTag(item.second); michael@0: michael@0: return convertView; michael@0: } michael@0: } michael@0: michael@0: private void registerEventListener(String event) { michael@0: GeckoAppShell.getEventDispatcher().registerEventListener(event, this); michael@0: } michael@0: michael@0: private void unregisterEventListener(String event) { michael@0: GeckoAppShell.getEventDispatcher().unregisterEventListener(event, this); michael@0: } michael@0: }