Bug 1463484 - 1. Add SessionFinder; r?snorp r?droeh
Add a SessionFinder API for find-in-page functionality.
MozReview-Commit-ID: KB7dOPUC6s2
--- a/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/GeckoSession.java
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/GeckoSession.java
@@ -11,17 +11,16 @@ import java.lang.annotation.RetentionPol
import java.net.URLConnection;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.UUID;
import org.mozilla.gecko.annotation.WrapForJNI;
import org.mozilla.gecko.EventDispatcher;
import org.mozilla.gecko.gfx.LayerSession;
-import org.mozilla.gecko.GeckoAppShell;
import org.mozilla.gecko.GeckoEditableChild;
import org.mozilla.gecko.GeckoThread;
import org.mozilla.gecko.IGeckoEditableParent;
import org.mozilla.gecko.mozglue.JNIObject;
import org.mozilla.gecko.NativeQueue;
import org.mozilla.gecko.util.BundleEventListener;
import org.mozilla.gecko.util.EventCallback;
import org.mozilla.gecko.util.GeckoBundle;
@@ -29,17 +28,16 @@ import org.mozilla.gecko.util.ThreadUtil
import android.content.ContentResolver;
import android.content.Context;
import android.content.res.Resources;
import android.database.Cursor;
import android.graphics.RectF;
import android.net.Uri;
import android.os.Binder;
-import android.os.Bundle;
import android.os.IBinder;
import android.os.IInterface;
import android.os.Parcel;
import android.os.Parcelable;
import android.os.SystemClock;
import android.support.annotation.IntDef;
import android.support.annotation.Nullable;
import android.support.annotation.NonNull;
@@ -89,26 +87,24 @@ public class GeckoSession extends LayerS
private final NativeQueue mNativeQueue =
new NativeQueue(State.INITIAL, State.READY);
private final EventDispatcher mEventDispatcher =
new EventDispatcher(mNativeQueue);
private final SessionTextInput mTextInput = new SessionTextInput(this, mNativeQueue);
-
- private SessionAccessibility mSessionAccessibility;
+ private SessionAccessibility mAccessibility;
+ private SessionFinder mFinder;
private String mId = UUID.randomUUID().toString().replace("-", "");
/* package */ String getId() { return mId; }
- private static abstract class CallbackResult<T> extends GeckoResult<T> implements EventCallback {
- @Override
- public abstract void sendSuccess(Object response);
-
+ /* package */ static abstract class CallbackResult<T> extends GeckoResult<T>
+ implements EventCallback {
@Override
public void sendError(Object response) {
completeExceptionally(response != null ?
new Exception(response.toString()) :
new UnknownError());
}
}
@@ -866,20 +862,20 @@ public class GeckoSession extends LayerS
}
/**
* Get the SessionAccessibility instance for this session.
*
* @return SessionAccessibility instance.
*/
public @NonNull SessionAccessibility getAccessibility() {
- if (mSessionAccessibility == null) {
- mSessionAccessibility = new SessionAccessibility(this);
+ if (mAccessibility == null) {
+ mAccessibility = new SessionAccessibility(this);
}
- return mSessionAccessibility;
+ return mAccessibility;
}
@IntDef(flag = true,
value = { LOAD_FLAGS_NONE, LOAD_FLAGS_BYPASS_CACHE, LOAD_FLAGS_BYPASS_PROXY,
LOAD_FLAGS_EXTERNAL, LOAD_FLAGS_ALLOW_POPUPS })
public @interface LoadFlags {}
// These flags follow similarly named ones in Gecko's nsIWebNavigation.idl
@@ -1086,16 +1082,99 @@ public class GeckoSession extends LayerS
/**
* Go forward in history.
*/
public void goForward() {
mEventDispatcher.dispatch("GeckoView:GoForward", null);
}
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(flag = true,
+ value = {FINDER_FIND_BACKWARDS, FINDER_FIND_LINKS_ONLY,
+ FINDER_FIND_MATCH_CASE, FINDER_FIND_WHOLE_WORD})
+ /* package */ @interface FinderFindFlags {}
+
+ /** Go backwards when finding the next match. */
+ public static final int FINDER_FIND_BACKWARDS = 1;
+ /** Perform case-sensitive match; default is to perform a case-insensitive match. */
+ public static final int FINDER_FIND_MATCH_CASE = 1 << 1;
+ /** Must match entire words; default is to allow matching partial words. */
+ public static final int FINDER_FIND_WHOLE_WORD = 1 << 2;
+ /** Limit matches to links on the page. */
+ public static final int FINDER_FIND_LINKS_ONLY = 1 << 3;
+
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(flag = true,
+ value = {FINDER_DISPLAY_HIGHLIGHT_ALL, FINDER_DISPLAY_DIM_PAGE,
+ FINDER_DISPLAY_DRAW_LINK_OUTLINE})
+ /* package */ @interface FinderDisplayFlags {}
+
+ /** Highlight all find-in-page matches. */
+ public static final int FINDER_DISPLAY_HIGHLIGHT_ALL = 1;
+ /** Dim the rest of the page when showing a find-in-page match. */
+ public static final int FINDER_DISPLAY_DIM_PAGE = 1 << 1;
+ /** Draw outlines around matching links. */
+ public static final int FINDER_DISPLAY_DRAW_LINK_OUTLINE = 1 << 2;
+
+ /**
+ * Represent the result of a find-in-page operation.
+ */
+ public static final class FinderResult {
+ /** Whether a match was found. */
+ public final boolean found;
+ /** Whether the search wrapped around the top or bottom of the page. */
+ public final boolean wrapped;
+ /** Ordinal number of the current match starting from 1, or 0 if no match. */
+ public final int current;
+ /** Total number of matches found so far, or -1 if unknown. */
+ public final int total;
+ /** Search string. */
+ @NonNull public final String searchString;
+ /** Flags used for the search; either 0 or a combination of {@link #FINDER_FIND_BACKWARDS
+ * FINDER_FIND_*} flags. */
+ @FinderFindFlags public final int flags;
+ /** URI of the link, if the current match is a link, or null otherwise. */
+ @Nullable public final String linkUri;
+ /** Bounds of the current match in client coordinates, or null if unknown. */
+ @Nullable public final RectF clientRect;
+
+ /* package */ FinderResult(@NonNull final GeckoBundle bundle) {
+ found = bundle.getBoolean("found");
+ wrapped = bundle.getBoolean("wrapped");
+ current = bundle.getInt("current", 0);
+ total = bundle.getInt("total", -1);
+ searchString = bundle.getString("searchString");
+ flags = SessionFinder.getFlagsFromBundle(bundle.getBundle("flags"));
+ linkUri = bundle.getString("linkURL");
+
+ final GeckoBundle rectBundle = bundle.getBundle("clientRect");
+ if (rectBundle == null) {
+ clientRect = null;
+ } else {
+ clientRect = new RectF((float) rectBundle.getDouble("left"),
+ (float) rectBundle.getDouble("top"),
+ (float) rectBundle.getDouble("right"),
+ (float) rectBundle.getDouble("bottom"));
+ }
+ }
+ }
+
+ /**
+ * Get the SessionFinder instance for this session, to perform find-in-page operations.
+ *
+ * @return SessionFinder instance.
+ */
+ public SessionFinder getFinder() {
+ if (mFinder == null) {
+ mFinder = new SessionFinder(getEventDispatcher());
+ }
+ return mFinder;
+ }
+
/**
* Set this GeckoSession as active or inactive. Setting a GeckoSession to inactive will
* significantly reduce its memory footprint, but should only be done if the
* GeckoSession is not currently visible.
* @param active A boolean determining whether the GeckoSession is active
*/
public void setActive(boolean active) {
final GeckoBundle msg = new GeckoBundle();
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/SessionFinder.java
@@ -0,0 +1,139 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.geckoview;
+
+import org.mozilla.gecko.EventDispatcher;
+import org.mozilla.gecko.util.GeckoBundle;
+import org.mozilla.geckoview.GeckoSession.FinderFindFlags;
+import org.mozilla.geckoview.GeckoSession.FinderDisplayFlags;
+import org.mozilla.geckoview.GeckoSession.FinderResult;
+
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.util.Pair;
+
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * {@code SessionFinder} instances returned by {@link GeckoSession#getFinder()} performs
+ * find-in-page operations.
+ */
+public final class SessionFinder {
+ private static final String LOGTAG = "GeckoSessionFinder";
+
+ private static final List<Pair<Integer, String>> sFlagNames = Arrays.asList(
+ new Pair<>(GeckoSession.FINDER_FIND_BACKWARDS, "backwards"),
+ new Pair<>(GeckoSession.FINDER_FIND_LINKS_ONLY, "linksOnly"),
+ new Pair<>(GeckoSession.FINDER_FIND_MATCH_CASE, "matchCase"),
+ new Pair<>(GeckoSession.FINDER_FIND_WHOLE_WORD, "wholeWord")
+ );
+
+ private static void addFlagsToBundle(@FinderFindFlags final int flags,
+ @NonNull final GeckoBundle bundle) {
+ for (final Pair<Integer, String> name : sFlagNames) {
+ if ((flags & name.first) != 0) {
+ bundle.putBoolean(name.second, true);
+ }
+ }
+ }
+
+ /* package */ static int getFlagsFromBundle(@Nullable final GeckoBundle bundle) {
+ if (bundle == null) {
+ return 0;
+ }
+
+ int flags = 0;
+ for (final Pair<Integer, String> name : sFlagNames) {
+ if (bundle.getBoolean(name.second)) {
+ flags |= name.first;
+ }
+ }
+ return flags;
+ }
+
+ private final EventDispatcher mDispatcher;
+ @FinderDisplayFlags private int mDisplayFlags;
+
+ /* package */ SessionFinder(@NonNull final EventDispatcher dispatcher) {
+ mDispatcher = dispatcher;
+ setDisplayFlags(0);
+ }
+
+ /**
+ * Find and select a string on the current page, starting from the current selection or the
+ * start of the page if there is no selection. Optionally return results related to the
+ * search in a {@link FinderResult} object. If {@code searchString} is null, search
+ * is performed using the previous search string.
+ *
+ * @param searchString String to search, or null to find again using the previous string.
+ * @param flags Flags for performing the search; either 0 or a combination of {@link
+ * GeckoSession#FINDER_FIND_BACKWARDS FINDER_FIND_*} constants.
+ * @return Result of the search operation as a {@link GeckoResult} object.
+ * @see #clear
+ * @see #setDisplayFlags
+ */
+ @NonNull
+ public GeckoResult<FinderResult> find(@Nullable final String searchString,
+ @FinderFindFlags final int flags) {
+ final GeckoBundle bundle = new GeckoBundle(sFlagNames.size() + 1);
+ bundle.putString("searchString", searchString);
+ addFlagsToBundle(flags, bundle);
+
+ final GeckoSession.CallbackResult<FinderResult> result =
+ new GeckoSession.CallbackResult<FinderResult>() {
+ @Override
+ public void sendSuccess(Object response) {
+ complete(new FinderResult((GeckoBundle) response));
+ }
+ };
+ mDispatcher.dispatch("GeckoView:FindInPage", bundle, result);
+ return result;
+ }
+
+ /**
+ * Clear any highlighted find-in-page matches.
+ *
+ * @see #find
+ * @see #setDisplayFlags
+ */
+ public void clear() {
+ mDispatcher.dispatch("GeckoView:ClearMatches", null);
+ }
+
+ /**
+ * Return flags for displaying find-in-page matches.
+ *
+ * @return Display flags as a combination of {@link GeckoSession#FINDER_DISPLAY_HIGHLIGHT_ALL
+ * FINDER_DISPLAY_*} constants.
+ * @see #setDisplayFlags
+ * @see #find
+ */
+ @FinderDisplayFlags public int getDisplayFlags() {
+ return mDisplayFlags;
+ }
+
+ /**
+ * Set flags for displaying find-in-page matches.
+ *
+ * @param flags Display flags as a combination of {@link
+ * GeckoSession#FINDER_DISPLAY_HIGHLIGHT_ALL FINDER_DISPLAY_*} constants.
+ * @see #getDisplayFlags
+ * @see #find
+ */
+ public void setDisplayFlags(@FinderDisplayFlags final int flags) {
+ mDisplayFlags = flags;
+
+ final GeckoBundle bundle = new GeckoBundle(3);
+ bundle.putBoolean("highlightAll",
+ (flags & GeckoSession.FINDER_DISPLAY_HIGHLIGHT_ALL) != 0);
+ bundle.putBoolean("dimPage",
+ (flags & GeckoSession.FINDER_DISPLAY_DIM_PAGE) != 0);
+ bundle.putBoolean("drawOutline",
+ (flags & GeckoSession.FINDER_DISPLAY_DRAW_LINK_OUTLINE) != 0);
+ mDispatcher.dispatch("GeckoView:DisplayMatches", bundle);
+ }
+}
--- a/mobile/android/modules/geckoview/GeckoViewContent.jsm
+++ b/mobile/android/modules/geckoview/GeckoViewContent.jsm
@@ -12,16 +12,19 @@ ChromeUtils.import("resource://gre/modul
XPCOMUtils.defineLazyModuleGetters(this, {
Services: "resource://gre/modules/Services.jsm",
});
class GeckoViewContent extends GeckoViewModule {
onInit() {
this.registerListener([
"GeckoViewContent:ExitFullScreen",
+ "GeckoView:ClearMatches",
+ "GeckoView:DisplayMatches",
+ "GeckoView:FindInPage",
"GeckoView:RestoreState",
"GeckoView:SaveState",
"GeckoView:SetActive",
"GeckoView:ZoomToInput",
]);
this.messageManager.addMessageListener("GeckoView:SaveStateFinish", this);
}
@@ -53,16 +56,28 @@ class GeckoViewContent extends GeckoView
// Bundle event handler.
onEvent(aEvent, aData, aCallback) {
debug `onEvent: event=${aEvent}, data=${aData}`;
switch (aEvent) {
case "GeckoViewContent:ExitFullScreen":
this.messageManager.sendAsyncMessage("GeckoView:DOMFullscreenExited");
break;
+ case "GeckoView:ClearMatches": {
+ this._clearMatches();
+ break;
+ }
+ case "GeckoView:DisplayMatches": {
+ this._displayMatches(aData);
+ break;
+ }
+ case "GeckoView:FindInPage": {
+ this._findInPage(aData, aCallback);
+ break;
+ }
case "GeckoView:ZoomToInput":
this.messageManager.sendAsyncMessage(aEvent);
break;
case "GeckoView:SetActive":
if (aData.active) {
this.browser.setAttribute("primary", "true");
this.browser.focus();
this.browser.docShellIsActive = true;
@@ -143,9 +158,160 @@ class GeckoViewContent extends GeckoView
this.eventDispatcher.sendRequest({
type: "GeckoView:ContentCrash"
});
}
break;
}
}
+
+ _findInPage(aData, aCallback) {
+ debug `findInPage: data=${aData} callback=${aCallback && "non-null"}`;
+
+ let finder;
+ try {
+ finder = this.browser.finder;
+ } catch (e) {
+ if (aCallback) {
+ aCallback.onError(`No finder: ${e}`);
+ }
+ return;
+ }
+
+ if (this._finderListener) {
+ finder.removeResultListener(this._finderListener);
+ }
+
+ this._finderListener = {
+ response: {
+ found: false,
+ wrapped: false,
+ current: 0,
+ total: -1,
+ searchString: aData.searchString || finder.searchString,
+ linkURL: null,
+ clientRect: null,
+ flags: {
+ backwards: !!aData.backwards,
+ linksOnly: !!aData.linksOnly,
+ matchCase: !!aData.matchCase,
+ wholeWord: !!aData.wholeWord,
+ },
+ },
+
+ onFindResult(aOptions) {
+ if (!aCallback || aOptions.searchString !== aData.searchString) {
+ // Result from a previous search.
+ return;
+ }
+
+ Object.assign(this.response, {
+ found: aOptions.result !== Ci.nsITypeAheadFind.FIND_NOTFOUND,
+ wrapped: aOptions.result !== Ci.nsITypeAheadFind.FIND_FOUND,
+ linkURL: aOptions.linkURL,
+ clientRect: aOptions.rect && {
+ left: aOptions.rect.left,
+ top: aOptions.rect.top,
+ right: aOptions.rect.right,
+ bottom: aOptions.rect.bottom,
+ },
+ flags: {
+ backwards: aOptions.findBackwards,
+ linksOnly: aOptions.linksOnly,
+ matchCase: this.response.flags.matchCase,
+ wholeWord: this.response.flags.wholeWord,
+ },
+ });
+
+ if (!this.response.found) {
+ this.response.current = 0;
+ this.response.total = 0;
+ }
+
+ // Only send response if we have a count.
+ if (!this.response.found || this.response.current !== 0) {
+ debug `onFindResult: ${this.response}`;
+ aCallback.onSuccess(this.response);
+ aCallback = undefined;
+ }
+ },
+
+ onMatchesCountResult(aResult) {
+ if (!aCallback || finder.searchString !== aData.searchString) {
+ // Result from a previous search.
+ return;
+ }
+
+ Object.assign(this.response, {
+ current: aResult.current,
+ total: aResult.total,
+ });
+
+ // Only send response if we have a result. `found` and `wrapped` are
+ // both false only when we haven't received a result yet.
+ if (this.response.found || this.response.wrapped) {
+ debug `onMatchesCountResult: ${this.response}`;
+ aCallback.onSuccess(this.response);
+ aCallback = undefined;
+ }
+ },
+ };
+
+ finder.caseSensitive = !!aData.matchCase;
+ finder.entireWord = !!aData.wholeWord;
+
+ if (aCallback) {
+ finder.addResultListener(this._finderListener);
+ }
+
+ const drawOutline = this._matchDisplayOptions &&
+ !!this._matchDisplayOptions.drawOutline;
+
+ if (!aData.searchString || aData.searchString === finder.searchString) {
+ // Search again.
+ aData.searchString = finder.searchString;
+ finder.findAgain(!!aData.backwards,
+ !!aData.linksOnly,
+ drawOutline);
+ } else {
+ finder.fastFind(aData.searchString,
+ !!aData.linksOnly,
+ drawOutline);
+ }
+ }
+
+ _clearMatches() {
+ try {
+ this.browser.finder.removeSelection();
+ } catch (e) {
+ }
+ }
+
+ _displayMatches(aData) {
+ debug `displayMatches: data=${aData}`;
+
+ let finder;
+ try {
+ finder = this.browser.finder;
+ } catch (e) {
+ return;
+ }
+
+ this._matchDisplayOptions = aData;
+ finder.onHighlightAllChange(!!aData.highlightAll);
+ finder.onModalHighlightChange(!!aData.dimPage);
+
+ if (!finder.searchString) {
+ return;
+ }
+ if (!aData.highlightAll && !aData.dimPage && !aData.drawOutline) {
+ finder.highlighter.highlight(false);
+ return;
+ }
+ const linksOnly = this._finderListener &&
+ this._finderListener.response.linksOnly;
+ finder.highlighter.highlight(true,
+ finder.searchString,
+ linksOnly,
+ !!aData.drawOutline);
+ }
}