Bug 1398409 - 2. Use event callback to communicate FormAssistPopup actions; r=sebastian
Use event callbacks instead of separate events to deliver
FormAssistPopup replies back to FormAssistant. This lets us better
handle having multiple FormAssistPopup instances across Fennec, custom
tabs, and PWAs.
FormAssistant._currentInputElement is removed because it does not allow
us to have multiple concurrent popups. Instead, we track the current
element through the event callback closure.
FormAssistant._currentInputValue is also removed for similar reasons,
and I don't think it was really necessary.
MozReview-Commit-ID: DdeMBGCxDou
--- a/mobile/android/base/java/org/mozilla/gecko/BrowserApp.java
+++ b/mobile/android/base/java/org/mozilla/gecko/BrowserApp.java
@@ -1635,24 +1635,16 @@ public class BrowserApp extends GeckoApp
}
for (final BrowserAppDelegate delegate : delegates) {
delegate.onDestroy(this);
}
deleteTempFiles(getApplicationContext());
- if (mDoorHangerPopup != null) {
- mDoorHangerPopup.destroy();
- mDoorHangerPopup = null;
- }
- if (mFormAssistPopup != null)
- mFormAssistPopup.destroy();
- if (mTextSelection != null)
- mTextSelection.destroy();
NotificationHelper.destroy();
GeckoNetworkManager.destroy();
super.onDestroy();
}
@Override
protected void initializeChrome() {
--- a/mobile/android/base/java/org/mozilla/gecko/FormAssistPopup.java
+++ b/mobile/android/base/java/org/mozilla/gecko/FormAssistPopup.java
@@ -43,17 +43,19 @@ import java.util.Collection;
public class FormAssistPopup extends RelativeLayout implements BundleEventListener {
private final Animation mAnimation;
private ListView mAutoCompleteList;
private RelativeLayout mValidationMessage;
private TextView mValidationMessageText;
private ImageView mValidationMessageArrow;
private ImageView mValidationMessageArrowInverted;
- private final GeckoApp geckoApp;
+
+ private GeckoView mGeckoView;
+ private EventCallback mAutoCompleteCallback;
private double mX;
private double mY;
private double mW;
private double mH;
private enum PopupType {
AUTOCOMPLETE,
@@ -83,47 +85,39 @@ public class FormAssistPopup extends Rel
public FormAssistPopup(Context context, AttributeSet attrs) {
super(context, attrs);
mAnimation = AnimationUtils.loadAnimation(context, R.anim.grow_fade_in);
mAnimation.setDuration(75);
setFocusable(false);
-
- geckoApp = (GeckoApp) ActivityUtils.getActivityFromContext(context);
}
- @Override
- public void onAttachedToWindow() {
- super.onAttachedToWindow();
-
- geckoApp.getAppEventDispatcher().registerUiThreadListener(this,
+ public void create(final GeckoView view) {
+ mGeckoView = view;
+ mGeckoView.getEventDispatcher().registerUiThreadListener(this,
"FormAssist:AutoCompleteResult",
"FormAssist:ValidationMessage",
"FormAssist:Hide");
}
- void destroy() {
- }
-
- @Override
- public void onDetachedFromWindow() {
- geckoApp.getAppEventDispatcher().unregisterUiThreadListener(this,
+ public void destroy() {
+ mGeckoView.getEventDispatcher().unregisterUiThreadListener(this,
"FormAssist:AutoCompleteResult",
"FormAssist:ValidationMessage",
"FormAssist:Hide");
-
- super.onDetachedFromWindow();
+ mGeckoView = null;
}
@Override // BundleEventListener
public void handleMessage(final String event, final GeckoBundle message,
final EventCallback callback) {
if ("FormAssist:AutoCompleteResult".equals(event)) {
+ mAutoCompleteCallback = callback;
showAutoCompleteSuggestions(message.getBundleArray("suggestions"),
message.getBundle("rect"),
message.getBoolean("isEmpty"));
} else if ("FormAssist:ValidationMessage".equals(event)) {
showValidationMessage(message.getString("validationMessage"),
message.getBundle("rect"));
@@ -144,42 +138,54 @@ public class FormAssistPopup extends Rel
}
if (mAutoCompleteList == null) {
LayoutInflater inflater = LayoutInflater.from(getContext());
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) {
+ public void onItemClick(final AdapterView<?> parentView, final View view,
+ final int position, final long id) {
+ if (mAutoCompleteCallback == null) {
+ hide();
+ return;
+ }
+
// Use the value stored with the autocomplete view, not the label text,
// since they can be different.
final TextView textView = (TextView) view;
- final GeckoBundle message = new GeckoBundle(1);
+ final GeckoBundle message = new GeckoBundle(2);
+ message.putString("action", "autocomplete");
message.putString("value", (String) textView.getTag());
- geckoApp.getAppEventDispatcher().dispatch("FormAssist:AutoComplete", message);
+ mAutoCompleteCallback.sendSuccess(message);
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) {
+ if (mAutoCompleteCallback == null) {
+ return;
+ }
+
// 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.
- final GeckoBundle message = new GeckoBundle(1);
+ final GeckoBundle message = new GeckoBundle(2);
+ message.putString("action", "remove");
message.putString("value", item.second);
- geckoApp.getAppEventDispatcher().dispatch("FormAssist:Remove", message);
+ mAutoCompleteCallback.sendSuccess(message);
// Update the list
adapter.remove(item);
adapter.notifyDataSetChanged();
positionAndShowPopup();
}
});
mAutoCompleteList.setOnTouchListener(touchListener);
@@ -243,17 +249,17 @@ public class FormAssistPopup extends Rel
mW = rect.getDouble("w");
mH = rect.getDouble("h");
mPopupType = (isAutoComplete ?
PopupType.AUTOCOMPLETE : PopupType.VALIDATIONMESSAGE);
return true;
}
private void positionAndShowPopup() {
- positionAndShowPopup(GeckoAppShell.getLayerView().getViewportMetrics());
+ positionAndShowPopup(mGeckoView.getViewportMetrics());
}
private void positionAndShowPopup(ImmutableViewportMetrics aMetrics) {
ThreadUtils.assertOnUiThread();
// Don't show the form assist popup when using fullscreen VKB
InputMethodManager imm = (InputMethodManager)
getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
@@ -276,17 +282,18 @@ public class FormAssistPopup extends Rel
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().getCurrentToolbarHeight());
+ int top = (int) (mY * zoom - aMetrics.viewportRectTop + mGeckoView.getTop() +
+ mGeckoView.getCurrentToolbarHeight());
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();
@@ -324,17 +331,17 @@ public class FormAssistPopup extends Rel
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) {
+ if ((popupTop + popupHeight) > (mGeckoView.getTop() + 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);
@@ -361,18 +368,18 @@ public class FormAssistPopup extends Rel
setVisibility(VISIBLE);
startAnimation(mAnimation);
}
}
public void hide() {
if (isShown()) {
setVisibility(GONE);
- geckoApp.getAppEventDispatcher().dispatch("FormAssist:Hidden", null);
}
+ mAutoCompleteCallback = null;
}
void onTranslationChanged() {
ThreadUtils.assertOnUiThread();
if (!isShown()) {
return;
}
positionAndShowPopup();
--- a/mobile/android/base/java/org/mozilla/gecko/GeckoApp.java
+++ b/mobile/android/base/java/org/mozilla/gecko/GeckoApp.java
@@ -1478,16 +1478,17 @@ public abstract class GeckoApp extends G
// cause a loop! See Bug 1011008, Comment 12.
super.onConfigurationChanged(getResources().getConfiguration());
}
protected void initializeChrome() {
mDoorHangerPopup = new DoorHangerPopup(this, getAppEventDispatcher());
mDoorHangerPopup.setOnVisibilityChangeListener(this);
mFormAssistPopup = (FormAssistPopup) findViewById(R.id.form_assist_popup);
+ mFormAssistPopup.create(mLayerView);
}
@Override
public void onDoorHangerShow() {
final View overlay = getDoorhangerOverlay();
if (overlay != null) {
final Animator alphaAnimator = ObjectAnimator.ofFloat(overlay, "alpha", 1);
alphaAnimator.setDuration(250);
@@ -2181,16 +2182,31 @@ public abstract class GeckoApp extends G
public void onDestroy() {
if (mIsAbortingAppLaunch) {
// This build does not support the Android version of the device:
// We did not initialize anything, so skip cleaning up.
super.onDestroy();
return;
}
+ if (mFormAssistPopup != null) {
+ mFormAssistPopup.destroy();
+ mFormAssistPopup = null;
+ }
+
+ if (mDoorHangerPopup != null) {
+ mDoorHangerPopup.destroy();
+ mDoorHangerPopup = null;
+ }
+
+ if (mTextSelection != null) {
+ mTextSelection.destroy();
+ mTextSelection = null;
+ }
+
EventDispatcher.getInstance().unregisterGeckoThreadListener(this,
"Accessibility:Ready",
"Gecko:Ready",
null);
EventDispatcher.getInstance().unregisterUiThreadListener(this,
"Update:Check",
"Update:Download",
--- a/mobile/android/chrome/content/FormAssistant.js
+++ b/mobile/android/chrome/content/FormAssistant.js
@@ -5,80 +5,51 @@
"use strict";
XPCOMUtils.defineLazyModuleGetter(this, "FormHistory", "resource://gre/modules/FormHistory.jsm");
var FormAssistant = {
// Weak-ref used to keep track of the currently focused element.
_currentFocusedElement: null,
- // Used to keep track of the element that corresponds to the current
- // autocomplete suggestions.
- _currentInputElement: null,
-
- // The value of the currently focused input.
- _currentInputValue: null,
-
// Whether we're in the middle of an autocomplete.
_doingAutocomplete: false,
init: function() {
Services.obs.addObserver(this, "PanZoom:StateChange");
},
- register: function(aWindow) {
- GeckoViewUtils.getDispatcherForWindow(aWindow).registerListener(this, [
- "FormAssist:AutoComplete",
- "FormAssist:Hidden",
- "FormAssist:Remove",
- ]);
- },
-
- onEvent: function(event, message, callback) {
- switch (event) {
- case "FormAssist:AutoComplete": {
- if (!this._currentInputElement) {
- break;
- }
- let editableElement = this._currentInputElement.QueryInterface(
- Ci.nsIDOMNSEditableElement);
+ _onPopupResponse: function(currentElement, message) {
+ switch (message.action) {
+ case "autocomplete": {
+ let editableElement = currentElement.QueryInterface(Ci.nsIDOMNSEditableElement);
this._doingAutocomplete = true;
// If we have an active composition string, commit it before sending
// the autocomplete event with the text that will replace it.
try {
if (editableElement.editor.composing) {
editableElement.editor.forceCompositionEnd();
}
} catch (e) {}
editableElement.setUserInput(message.value);
- this._currentInputValue = message.value;
- let event = this._currentInputElement.ownerDocument.createEvent("Events");
+ let event = currentElement.ownerDocument.createEvent("Events");
event.initEvent("DOMAutoComplete", true, true);
- this._currentInputElement.dispatchEvent(event);
+ currentElement.dispatchEvent(event);
this._doingAutocomplete = false;
break;
}
- case "FormAssist:Hidden": {
- this._currentInputElement = null;
- break;
- }
-
- case "FormAssist:Remove": {
- if (!this._currentInputElement) {
- break;
- }
-
+ case "remove": {
FormHistory.update({
op: "remove",
- fieldname: this._currentInputElement.name,
+ fieldname: currentElement.name,
value: message.value,
});
break;
}
}
},
observe: function(aSubject, aTopic, aData) {
@@ -131,17 +102,16 @@ var FormAssistant = {
if (this._showValidationMessage(currentElement) ||
this._isAutoComplete(currentElement)) {
this._currentFocusedElement = Cu.getWeakReference(currentElement);
}
break;
}
case "blur": {
- this._currentInputValue = null;
this._currentFocusedElement = null;
break;
}
case "click": {
let currentElement = aEvent.target;
// Prioritize a form validation message over autocomplete suggestions
@@ -163,24 +133,20 @@ var FormAssistant = {
case "input": {
let currentElement = aEvent.target;
let focused = this._currentFocusedElement && this._currentFocusedElement.get();
// If this element isn't focused, we're already in middle of an
// autocomplete, or its value hasn't changed, don't show the
// autocomplete popup.
- if (currentElement !== focused ||
- this._doingAutocomplete ||
- currentElement.value === this._currentInputValue) {
+ if (currentElement !== focused || this._doingAutocomplete) {
break;
}
- this._currentInputValue = currentElement.value;
-
// Since we can only show one popup at a time, prioritze autocomplete
// suggestions over a form validation message
let checkResultsInput = hasResults => {
if (hasResults || this._showValidationMessage(currentElement)) {
return;
}
// If we're not showing autocomplete suggestions, hide the form assist popup
this._hideFormAssistPopup(currentElement);
@@ -290,21 +256,21 @@ var FormAssistant = {
return;
}
GeckoViewUtils.getDispatcherForWindow(aElement.ownerGlobal).sendRequest({
type: "FormAssist:AutoCompleteResult",
suggestions: suggestions,
rect: this._getBoundingContentRect(aElement),
isEmpty: isEmpty,
+ }, {
+ onSuccess: response => this._onPopupResponse(aElement, response),
+ onError: error => Cu.reportError(error),
});
- // Keep track of input element so we can fill it in if the user
- // selects an autocomplete suggestion
- this._currentInputElement = aElement;
aCallback(true);
};
this._getAutoCompleteSuggestions(aElement.value, aElement, resultsAvailable);
},
// Only show a validation message if the user submitted an invalid form,
// there's a non-empty message string, and the element is the correct type
--- a/mobile/android/components/BrowserCLH.js
+++ b/mobile/android/components/BrowserCLH.js
@@ -137,17 +137,16 @@ BrowserCLH.prototype = {
"focus", "blur", "click", "input",
], {
handler: event => {
if (event.target instanceof Ci.nsIDOMHTMLInputElement ||
event.target instanceof Ci.nsIDOMHTMLTextAreaElement ||
event.target instanceof Ci.nsIDOMHTMLSelectElement ||
event.target instanceof Ci.nsIDOMHTMLButtonElement) {
// Only load FormAssistant when the event target is what we care about.
- this.FormAssistant.register(win);
return this.FormAssistant;
}
return null;
},
options: {
capture: true,
mozSystemGroup: true,
},
--- a/mobile/android/modules/geckoview/Messaging.jsm
+++ b/mobile/android/modules/geckoview/Messaging.jsm
@@ -108,20 +108,20 @@ DispatcherDelegate.prototype = {
* Implementations of Messaging APIs for backwards compatibility.
*/
/**
* Sends a request to Java.
*
* @param msg Message to send; must be an object with a "type" property
*/
- sendRequest: function(msg) {
+ sendRequest: function(msg, callback) {
let type = msg.type;
msg.type = undefined;
- this.dispatch(type, msg);
+ this.dispatch(type, msg, callback);
},
/**
* Sends a request to Java, returning a Promise that resolves to the response.
*
* @param msg Message to send; must be an object with a "type" property
* @returns A Promise resolving to the response
*/