diff --git a/mobile/android/geckoview/api.txt b/mobile/android/geckoview/api.txt index 1c2e0b8f0e0a..6c4575e04e71 100644 --- a/mobile/android/geckoview/api.txt +++ b/mobile/android/geckoview/api.txt @@ -59,7 +59,7 @@ import java.util.List; import java.util.Map; import org.json.JSONObject; import org.mozilla.geckoview.AllowOrDeny; -import org.mozilla.geckoview.AutofillElement; +import org.mozilla.geckoview.Autofill; import org.mozilla.geckoview.CompositorController; import org.mozilla.geckoview.ContentBlocking; import org.mozilla.geckoview.ContentBlockingController; @@ -105,28 +105,65 @@ package org.mozilla.geckoview { enum_constant public static final AllowOrDeny DENY; } - public class AutofillElement { - ctor protected AutofillElement(); - field public static final int HINT_EMAIL_ADDRESS = 0; - field public static final int HINT_NONE = -1; - field public static final int HINT_PASSWORD = 1; - field public static final int HINT_URL = 2; - field public static final int HINT_USERNAME = 3; - field public static final int INPUT_TYPE_NONE = -1; - field public static final int INPUT_TYPE_NUMBER = 1; - field public static final int INPUT_TYPE_PHONE = 2; - field public static final int INPUT_TYPE_TEXT = 0; - field @NonNull public final Map attributes; - field @NonNull public final Collection children; - field @NonNull public final Rect dimensions; - field @NonNull public final String domain; - field public final boolean enabled; - field public final boolean focusable; - field public final boolean focused; - field public final int hint; - field public final int id; - field public final int inputType; - field @NonNull public final String tag; + public class Autofill { + ctor public Autofill(); + } + + public static interface Autofill.Delegate { + method @UiThread default public void onAutofill(@NonNull GeckoSession, int, @Nullable Autofill.Node); + } + + public static final class Autofill.Hint { + method @AnyThread @Nullable public static String toString(int); + field public static final int EMAIL_ADDRESS = 0; + field public static final int NONE = -1; + field public static final int PASSWORD = 1; + field public static final int URI = 2; + field public static final int USERNAME = 3; + } + + public static final class Autofill.InputType { + method @AnyThread @Nullable public static String toString(int); + field public static final int NONE = -1; + field public static final int NUMBER = 1; + field public static final int PHONE = 2; + field public static final int TEXT = 0; + } + + public static final class Autofill.Node { + method @UiThread public void fillViewStructure(@NonNull View, @NonNull ViewStructure, int); + method @AnyThread @Nullable public String getAttribute(@NonNull String); + method @AnyThread @NonNull public Map getAttributes(); + method @AnyThread @NonNull public Collection getChildren(); + method @AnyThread @NonNull public Rect getDimensions(); + method @AnyThread @NonNull public String getDomain(); + method @AnyThread public boolean getEnabled(); + method @AnyThread public boolean getFocusable(); + method @AnyThread public boolean getFocused(); + method @AnyThread public int getHint(); + method @AnyThread public int getId(); + method @AnyThread public int getInputType(); + method @AnyThread @NonNull public String getTag(); + method @AnyThread @NonNull public String getValue(); + method @AnyThread public boolean getVisible(); + } + + public static final class Autofill.Notify { + method @AnyThread @Nullable public static String toString(int); + field public static final int NODE_ADDED = 3; + field public static final int NODE_BLURRED = 7; + field public static final int NODE_FOCUSED = 6; + field public static final int NODE_REMOVED = 4; + field public static final int NODE_UPDATED = 5; + field public static final int SESSION_CANCELED = 2; + field public static final int SESSION_COMMITTED = 1; + field public static final int SESSION_STARTED = 0; + } + + public static final class Autofill.Session { + method @UiThread public void fillViewStructure(@NonNull View, @NonNull ViewStructure, int); + method @AnyThread @NonNull public Rect getDefaultDimensions(); + method @AnyThread @NonNull public Autofill.Node getRoot(); } @UiThread public class BasicSelectionActionDelegate implements ActionMode.Callback GeckoSession.SelectionActionDelegate { @@ -541,8 +578,8 @@ package org.mozilla.geckoview { method @AnyThread @NonNull public static String createDataUri(@NonNull String, @Nullable String); method @AnyThread public void exitFullScreen(); method @UiThread @NonNull public SessionAccessibility getAccessibility(); - method @UiThread @Nullable public GeckoSession.AutofillDelegate getAutofillDelegate(); - method @UiThread @NonNull public AutofillElement getAutofillElements(); + method @UiThread @Nullable public Autofill.Delegate getAutofillDelegate(); + method @UiThread @NonNull public Autofill.Session getAutofillSession(); method @UiThread public void getClientBounds(@NonNull RectF); method @UiThread public void getClientToScreenMatrix(@NonNull Matrix); method @UiThread public void getClientToSurfaceMatrix(@NonNull Matrix); @@ -594,7 +631,7 @@ package org.mozilla.geckoview { method @AnyThread public void reload(); method @AnyThread public void restoreState(@NonNull GeckoSession.SessionState); method @AnyThread public void setActive(boolean); - method @UiThread public void setAutofillDelegate(@Nullable GeckoSession.AutofillDelegate); + method @UiThread public void setAutofillDelegate(@Nullable Autofill.Delegate); method @AnyThread public void setContentBlockingDelegate(@Nullable ContentBlocking.Delegate); method @UiThread public void setContentDelegate(@Nullable GeckoSession.ContentDelegate); method @AnyThread public void setFocused(boolean); @@ -628,18 +665,6 @@ package org.mozilla.geckoview { field @Nullable protected GeckoSession.Window mWindow; } - public static interface GeckoSession.AutofillDelegate { - method @UiThread default public void onAutofill(@NonNull GeckoSession, int, int); - field public static final int AUTOFILL_NOTIFY_CANCELED = 2; - field public static final int AUTOFILL_NOTIFY_COMMITTED = 1; - field public static final int AUTOFILL_NOTIFY_STARTED = 0; - field public static final int AUTOFILL_NOTIFY_VIEW_ADDED = 3; - field public static final int AUTOFILL_NOTIFY_VIEW_ENTERED = 6; - field public static final int AUTOFILL_NOTIFY_VIEW_EXITED = 7; - field public static final int AUTOFILL_NOTIFY_VIEW_REMOVED = 4; - field public static final int AUTOFILL_NOTIFY_VIEW_UPDATED = 5; - } - public static interface GeckoSession.ContentDelegate { method @UiThread default public void onCloseRequest(@NonNull GeckoSession); method @UiThread default public void onContextMenu(@NonNull GeckoSession, int, int, @NonNull GeckoSession.ContentDelegate.ContextElement); diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/Autofill.java b/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/Autofill.java new file mode 100644 index 000000000000..02dcf6597785 --- /dev/null +++ b/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/Autofill.java @@ -0,0 +1,1167 @@ +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- + * vim: ts=4 sw=4 expandtab: + * 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 java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.Collection; +import java.util.LinkedList; +import java.util.Map; + +import android.annotation.TargetApi; +import android.graphics.Rect; +import android.os.Build; +import android.support.annotation.AnyThread; +import android.support.annotation.IntDef; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.annotation.UiThread; +import android.support.v4.util.ArrayMap; +import android.util.Log; +import android.util.SparseArray; +import android.view.View; +import android.view.ViewStructure; +import android.view.autofill.AutofillManager; + +import org.mozilla.gecko.util.BundleEventListener; +import org.mozilla.gecko.util.EventCallback; +import org.mozilla.gecko.util.GeckoBundle; + +public class Autofill { + private static final boolean DEBUG = true; + + public static final class Notify { + private Notify() {} + + /** + * An autofill session has started. + * Usually triggered by page load. + */ + public static final int SESSION_STARTED = 0; + + /** + * An autofill session has been committed. + * Triggered by form submission or navigation. + */ + public static final int SESSION_COMMITTED = 1; + + /** + * An autofill session has been canceled. + * Triggered by page unload. + */ + public static final int SESSION_CANCELED = 2; + + /** + * A node within the autofill session has been added. + */ + public static final int NODE_ADDED = 3; + + /** + * A node within the autofill session has been removed. + */ + public static final int NODE_REMOVED = 4; + + /** + * A node within the autofill session has been updated. + */ + public static final int NODE_UPDATED = 5; + + /** + * A node within the autofill session has gained focus. + */ + public static final int NODE_FOCUSED = 6; + + /** + * A node within the autofill session has lost focus. + */ + public static final int NODE_BLURRED = 7; + + @AnyThread + public static @Nullable String toString( + final @AutofillNotify int notification) { + final String[] map = new String[] { + "SESSION_STARTED", "SESSION_COMMITTED", "SESSION_CANCELED", + "NODE_ADDED", "NODE_REMOVED", "NODE_UPDATED", "NODE_FOCUSED", + "NODE_BLURRED" }; + if (notification < 0 || notification >= map.length) { + return null; + } + return map[notification]; + } + } + + @Retention(RetentionPolicy.SOURCE) + @IntDef({ + Notify.SESSION_STARTED, + Notify.SESSION_COMMITTED, + Notify.SESSION_CANCELED, + Notify.NODE_ADDED, + Notify.NODE_REMOVED, + Notify.NODE_UPDATED, + Notify.NODE_FOCUSED, + Notify.NODE_BLURRED}) + /* package */ @interface AutofillNotify {} + + public static final class Hint { + private Hint() {} + + /** + * Hint indicating that no special handling is required. + */ + public static final int NONE = -1; + + /** + * Hint indicating that a node represents an email address. + */ + public static final int EMAIL_ADDRESS = 0; + + /** + * Hint indicating that a node represents a password. + */ + public static final int PASSWORD = 1; + + /** + * Hint indicating that a node represents an URI. + */ + public static final int URI = 2; + + /** + * Hint indicating that a node represents a username. + */ + public static final int USERNAME = 3; + + @AnyThread + public static @Nullable String toString(final @AutofillHint int hint) { + final int idx = hint + 1; + final String[] map = new String[] { + "NONE", "EMAIL", "PASSWORD", "URI", "USERNAME" }; + + if (idx < 0 || idx >= map.length) { + return null; + } + return map[idx]; + } + } + + @Retention(RetentionPolicy.SOURCE) + @IntDef({ Hint.NONE, Hint.EMAIL_ADDRESS, Hint.PASSWORD, Hint.URI, + Hint.USERNAME }) + /* package */ @interface AutofillHint {} + + public static final class InputType { + private InputType() {} + + /** + * Indicates that a node is not a known input type. + */ + public static final int NONE = -1; + + /** + * Indicates that a node is a text input type. + * Example: {@code } + */ + public static final int TEXT = 0; + + /** + * Indicates that a node is a number input type. + * Example: {@code } + */ + public static final int NUMBER = 1; + + /** + * Indicates that a node is a phone input type. + * Example: {@code } + */ + public static final int PHONE = 2; + + @AnyThread + public static @Nullable String toString( + final @AutofillInputType int type) { + final int idx = type + 1; + final String[] map = new String[] { + "NONE", "TEXT", "NUMBER", "PHONE" }; + + if (idx < 0 || idx >= map.length) { + return null; + } + return map[idx]; + } + } + + @Retention(RetentionPolicy.SOURCE) + @IntDef({ InputType.NONE, InputType.TEXT, InputType.NUMBER, + InputType.PHONE }) + /* package */ @interface AutofillInputType {} + + /** + * Represents an autofill session. + * A session holds the autofill nodes and state of a page. + */ + public static final class Session { + private static final String LOGTAG = "AutofillSession"; + + private @NonNull final GeckoSession mGeckoSession; + private Node mRoot; + private SparseArray mNodes; + // TODO: support session id? + private int mId = View.NO_ID; + private int mFocusedId = View.NO_ID; + private int mFocusedRootId = View.NO_ID; + + /* package */ Session(@NonNull final GeckoSession geckoSession) { + mGeckoSession = geckoSession; + clear(); + } + + @AnyThread + public @NonNull Rect getDefaultDimensions() { + return Support.getDummyAutofillRect(mGeckoSession, false, null); + } + + /* package */ void clear() { + mId = View.NO_ID; + mFocusedId = View.NO_ID; + mFocusedRootId = View.NO_ID; + mRoot = new Node.Builder(this) + .dimensions(getDefaultDimensions()) + .build(); + mNodes = new SparseArray<>(); + } + + /* package */ boolean isEmpty() { + return mNodes.size() == 0; + } + + /* package */ void addNode(@NonNull final Node node) { + if (DEBUG) { + Log.d(LOGTAG, "addNode: " + node); + } + node.setAutofillSession(this); + mNodes.put(node.getId(), node); + + if (node.getParentId() == View.NO_ID) { + mRoot.addChild(node); + } + } + + /* package */ void setFocus(final int id, final int rootId) { + mFocusedId = id; + mFocusedRootId = rootId; + } + + /* package */ int getFocusedId() { + return mFocusedId; + } + + /* package */ int getFocusedRootId() { + return mFocusedRootId; + } + + /* package */ @Nullable Node getNode(final int id) { + return mNodes.get(id); + } + + + /** + * Get the root node of the session tree. + * Each session is managed in a tree with a virtual root node for the + * document. + * + * @return The root {@link Node} for this session. + */ + @AnyThread + public @NonNull Node getRoot() { + return mRoot; + } + + @Override + @AnyThread + public String toString() { + StringBuilder builder = new StringBuilder("Session {"); + builder + .append("id=").append(mId) + .append(", focusedId=").append(mFocusedId) + .append(", focusedRootId=").append(mFocusedRootId) + .append(", root=").append(getRoot()) + .append("}"); + return builder.toString(); + } + + @TargetApi(23) + @UiThread + public void fillViewStructure( + @NonNull final View view, + @NonNull final ViewStructure structure, + final int flags) { + getRoot().fillViewStructure(view, structure, flags); + } + } + + /** + * Represents an autofill node. + * A node is an input element and may contain child nodes forming a tree. + */ + public static final class Node { + private static final String LOGTAG = "AutofillNode"; + + private int mId; + private int mRootId; + private int mParentId; + private Session mAutofillSession; + private @NonNull Rect mDimens; + private @NonNull Collection mChildren; + private @NonNull Map mAttributes; + private boolean mEnabled; + private boolean mFocusable; + private @AutofillHint int mHint; + private @AutofillInputType int mInputType; + private @NonNull String mTag; + private @NonNull String mDomain; + private @NonNull String mValue; + private @Nullable EventCallback mCallback; + + /** + * Get the unique (within this page) ID for this node. + * + * @return The unique ID of this node. + */ + @AnyThread + public int getId() { + return mId; + } + + /* package */ @NonNull Node setId(final int id) { + mId = id; + return this; + } + + /* package */ @Nullable Node getRoot() { + return getAutofillSession().getNode(mRootId); + } + + /* package */ @NonNull Node setRootId(final int rootId) { + mRootId = rootId; + return this; + } + + /* package */ @Nullable Node getParent() { + return getAutofillSession().getNode(mParentId); + } + + /* package */ int getParentId() { + return mParentId; + } + + /* package */ @NonNull Node setParentId(final int parentId) { + mParentId = parentId; + return this; + } + + /* package */ @NonNull Session getAutofillSession() { + return mAutofillSession; + } + + /* package */ @NonNull Node setAutofillSession( + @Nullable final Session session) { + mAutofillSession = session; + return this; + } + + + /** + * Get whether this node is visible. + * Nodes are visible, when they are part of a focused branch. + * A focused branch includes the focused node, its siblings, its parent + * and the session root node. + * + * @return True if this node is visible, false otherwise. + */ + @AnyThread + public boolean getVisible() { + final int focusedId = getAutofillSession().getFocusedId(); + final int focusedRootId = getAutofillSession().getFocusedRootId(); + + if (focusedId == View.NO_ID) { + return false; + } + + final int focusedParentId = + getAutofillSession().getNode(focusedId).getParentId(); + + return mId == View.NO_ID || // The session root node. + mParentId == focusedParentId || + mRootId == focusedRootId; + } + + /** + * Get the dimensions of this node in CSS coordinates. + * Note: Invisible nodes will report their proper dimensions, see + * {@link #getVisible} for details. + * + * @return The dimensions of this node. + */ + @AnyThread + public @NonNull Rect getDimensions() { + return mDimens; + } + + /* package */ @NonNull Node setDimensions(final Rect rect) { + mDimens = rect; + return this; + } + + /** + * Get the child nodes for this node. + * + * @return The collection of child nodes for this node. + */ + @AnyThread + public @NonNull Collection getChildren() { + return mChildren; + } + + /* package */ @NonNull Node addChild(@NonNull final Node child) { + mChildren.add(child); + return this; + } + + /** + * Get HTML attributes for this node. + * + * @return The HTML attributes for this node. + */ + @AnyThread + public @NonNull Map getAttributes() { + return mAttributes; + } + + @AnyThread + public @Nullable String getAttribute(@NonNull final String key) { + return mAttributes.get(key); + } + + /* package */ @NonNull Node setAttributes( + final Map attributes) { + mAttributes = attributes; + return this; + } + + /* package */ @NonNull Node setAttribute( + final String key, final String value) { + mAttributes.put(key, value); + return this; + } + + /** + * Get whether or not this node is enabled. + * + * @return True if the node is enabled, false otherwise. + */ + @AnyThread + public boolean getEnabled() { + return mEnabled; + } + + /* package */ @NonNull Node setEnabled(final boolean enabled) { + mEnabled = enabled; + return this; + } + + /** + * Get whether or not this node is focusable. + * + * @return True if the node is focusable, false otherwise. + */ + @AnyThread + public boolean getFocusable() { + return mFocusable; + } + + /* package */ @NonNull Node setFocusable(final boolean focusable) { + mFocusable = focusable; + return this; + } + + /** + * Get whether or not this node is focused. + * + * @return True if this node is focused, false otherwise. + */ + @AnyThread + public boolean getFocused() { + return getId() != View.NO_ID && + getAutofillSession().getFocusedId() == getId(); + } + + /** + * Get the hint for the type of data contained in this node. + * + * @return The input data hint for this node, one of {@link Hint}. + */ + @AnyThread + public @AutofillHint int getHint() { + return mHint; + } + + /* package */ @NonNull Node setHint(final @AutofillHint int hint) { + mHint = hint; + return this; + } + + /** + * Get the input type of this node. + * + * @return The input type of this node, one of {@link InputType}. + */ + @AnyThread + public @AutofillInputType int getInputType() { + return mInputType; + } + + /* package */ @NonNull Node setInputType( + final @AutofillInputType int inputType) { + mInputType = inputType; + return this; + } + + /** + * Get the HTML tag of this node. + * + * @return The HTML tag of this node. + */ + @AnyThread + public @NonNull String getTag() { + return mTag; + } + + /* package */ @NonNull Node setTag(final String tag) { + mTag = tag; + return this; + } + + /** + * Get web domain of this node. + * + * @return The domain of this node. + */ + @AnyThread + public @NonNull String getDomain() { + return mDomain; + } + + /* package */ @NonNull Node setDomain(final String domain) { + mDomain = domain; + return this; + } + + /** + * Get the value assigned to this node. + * + * @return The value of this node. + */ + @AnyThread + public @NonNull String getValue() { + return mValue; + } + + /* package */ @NonNull Node setValue(final String value) { + mValue = value; + return this; + } + + /* package */ @Nullable EventCallback getCallback() { + return mCallback; + } + + /* package */ @NonNull Node setCallback(final EventCallback callback) { + mCallback = callback; + return this; + } + + /* package */ Node(@NonNull final Session session) { + mAutofillSession = session; + mId = View.NO_ID; + mDimens = new Rect(0, 0, 0, 0); + mAttributes = new ArrayMap<>(); + mEnabled = false; + mFocusable = false; + mHint = Hint.NONE; + mInputType = InputType.NONE; + mTag = ""; + mDomain = ""; + mValue = ""; + mChildren = new LinkedList<>(); + } + + @Override + @AnyThread + public String toString() { + StringBuilder builder = new StringBuilder("Node {"); + builder + .append("id=").append(mId) + .append(", parent=").append(mParentId) + .append(", root=").append(mRootId) + .append(", dims=").append(getDimensions().toShortString()) + .append(", children=["); + + for (final Node child: mChildren) { + builder.append(child.getId()).append(", "); + } + + builder + .append("]") + .append(", attrs=").append(mAttributes) + .append(", enabled=").append(mEnabled) + .append(", focusable=").append(mFocusable) + .append(", focused=").append(getFocused()) + .append(", visible=").append(getVisible()) + .append(", hint=").append(Hint.toString(mHint)) + .append(", type=").append(InputType.toString(mInputType)) + .append(", tag=").append(mTag) + .append(", domain=").append(mDomain) + .append(", value=").append(mValue) + .append(", callback=").append(mCallback != null) + .append("}"); + + return builder.toString(); + } + + @TargetApi(23) + @UiThread + public void fillViewStructure( + @NonNull final View view, + @NonNull final ViewStructure structure, + final int flags) { + Log.d(LOGTAG, "fillViewStructure"); + + final Node root = getRoot(); + + if (Build.VERSION.SDK_INT >= 26) { + structure.setAutofillId(view.getAutofillId(), getId()); + structure.setWebDomain(getDomain()); + } + + structure.setId(getId(), null, null, null); + structure.setDimens(0, 0, 0, 0, + getDimensions().width(), + getDimensions().height()); + + if (Build.VERSION.SDK_INT >= 26) { + final ViewStructure.HtmlInfo.Builder htmlBuilder = + structure.newHtmlInfoBuilder(getTag()); + for (final String key : getAttributes().keySet()) { + htmlBuilder.addAttribute(key, + String.valueOf(getAttribute(key))); + } + + structure.setHtmlInfo(htmlBuilder.build()); + } + + structure.setChildCount(getChildren().size()); + int childCount = 0; + + for (final Node child : getChildren()) { + final ViewStructure childStructure = + structure.newChild(childCount); + child.fillViewStructure(view, childStructure, flags); + childCount++; + } + + switch (getTag()) { + case "input": + case "textarea": + structure.setClassName("android.widget.EditText"); + structure.setEnabled(getEnabled()); + structure.setFocusable(getFocusable()); + structure.setFocused(getFocused()); + structure.setVisibility(View.VISIBLE); + + if (Build.VERSION.SDK_INT >= 26) { + structure.setAutofillType(View.AUTOFILL_TYPE_TEXT); + } + break; + default: + if (childCount > 0) { + structure.setClassName("android.view.ViewGroup"); + } else { + structure.setClassName("android.view.View"); + } + break; + } + + if (Build.VERSION.SDK_INT < 26 || !"input".equals(getTag())) { + return; + } + // LastPass will fill password to the field where setAutofillHints + // is unset and setInputType is set. + switch (getHint()) { + case Hint.EMAIL_ADDRESS: { + structure.setAutofillHints( + new String[]{ View.AUTOFILL_HINT_EMAIL_ADDRESS }); + structure.setInputType( + android.text.InputType.TYPE_CLASS_TEXT | + android.text.InputType + .TYPE_TEXT_VARIATION_EMAIL_ADDRESS); + break; + } + case Hint.PASSWORD: { + structure.setAutofillHints( + new String[]{ View.AUTOFILL_HINT_PASSWORD }); + structure.setInputType( + android.text.InputType.TYPE_CLASS_TEXT | + android.text.InputType + .TYPE_TEXT_VARIATION_WEB_PASSWORD); + break; + } + case Hint.URI: { + structure.setInputType( + android.text.InputType.TYPE_CLASS_TEXT | + android.text.InputType.TYPE_TEXT_VARIATION_URI); + break; + } + case Hint.USERNAME: { + structure.setAutofillHints( + new String[]{ View.AUTOFILL_HINT_USERNAME }); + structure.setInputType( + android.text.InputType.TYPE_CLASS_TEXT | + android.text.InputType + .TYPE_TEXT_VARIATION_WEB_EDIT_TEXT); + break; + } + } + + switch (getInputType()) { + case InputType.NUMBER: { + structure.setInputType( + android.text.InputType.TYPE_CLASS_NUMBER); + break; + } + case InputType.PHONE: { + structure.setAutofillHints( + new String[]{ View.AUTOFILL_HINT_PHONE }); + structure.setInputType( + android.text.InputType.TYPE_CLASS_PHONE); + break; + } + default: + break; + } + } + + /* package */ static class Builder { + private Node mNode; + + /* package */ Builder(@NonNull final Session session) { + mNode = new Node(session); + } + + public Builder( + @NonNull final Session autofillSession, + @NonNull final GeckoBundle bundle) { + this(autofillSession); + + final GeckoBundle bounds = bundle.getBundle("bounds"); + mNode + .setAutofillSession(autofillSession) + .setId(bundle.getInt("id")) + .setParentId(bundle.getInt("parent", View.NO_ID)) + .setRootId(bundle.getInt("root", View.NO_ID)) + .setDomain(bundle.getString("origin")) + .setValue(bundle.getString("value")) + .setDimensions( + new Rect(bounds.getInt("left"), + bounds.getInt("top"), + bounds.getInt("right"), + bounds.getInt("bottom"))); + + if (mNode.getDimensions().isEmpty()) { + // Some nodes like will have null-dimensions, + // we need to set them to the virtual documents dimensions. + mNode.setDimensions(autofillSession.getDefaultDimensions()); + } + + final GeckoBundle[] children = bundle.getBundleArray("children"); + if (children != null) { + for (final GeckoBundle childBundle: children) { + final Node child = + new Builder(autofillSession, childBundle).build(); + mNode.addChild(child); + autofillSession.addNode(child); + } + } + + String tag = bundle.getString("tag", "").toLowerCase(); + mNode.setTag(tag); + + final GeckoBundle attrs = bundle.getBundle("attributes"); + + for (final String key : attrs.keys()) { + mNode.setAttribute(key, String.valueOf(attrs.get(key))); + } + + if ("input".equals(tag) && + !bundle.getBoolean("editable", false)) { + // Don't process non-editable inputs (e.g., type="button"). + tag = ""; + } + + switch (tag) { + case "input": + case "textarea": { + final boolean disabled = bundle.getBoolean("disabled"); + mNode + .setEnabled(!disabled) + .setFocusable(!disabled); + break; + } + default: + break; + } + + final String type = + bundle.getString("type", "text").toLowerCase(); + + switch (type) { + case "email": { + mNode + .setHint(Hint.EMAIL_ADDRESS) + .setInputType(InputType.TEXT); + break; + } + case "number": { + mNode.setInputType(InputType.NUMBER); + break; + } + case "password": { + mNode + .setHint(Hint.PASSWORD) + .setInputType(InputType.TEXT); + break; + } + case "tel": { + mNode.setInputType(InputType.PHONE); + break; + } + case "url": { + mNode + .setHint(Hint.URI) + .setInputType(InputType.TEXT); + break; + } + case "text": { + final String autofillHint = + bundle.getString("autofillhint", "").toLowerCase(); + if (autofillHint.equals("username")) { + mNode + .setHint(Hint.USERNAME) + .setInputType(InputType.TEXT); + } + break; + } + } + } + + public @NonNull Builder dimensions(final Rect rect) { + mNode.setDimensions(rect); + return this; + } + + public @NonNull Node build() { + return mNode; + } + + public @NonNull Builder id(final int id) { + mNode.setId(id); + return this; + } + + public @NonNull Builder child(@NonNull final Node child) { + mNode.addChild(child); + return this; + } + + public @NonNull Builder attribute( + final String key, final String value) { + mNode.setAttribute(key, value); + return this; + } + + public @NonNull Builder enabled(final boolean enabled) { + mNode.setEnabled(enabled); + return this; + } + + public @NonNull Builder focusable(final boolean focusable) { + mNode.setFocusable(focusable); + return this; + } + + public @NonNull Builder hint(final int hint) { + mNode.setHint(hint); + return this; + } + + public @NonNull Builder inputType(final int inputType) { + mNode.setInputType(inputType); + return this; + } + + public @NonNull Builder tag(final String tag) { + mNode.setTag(tag); + return this; + } + + public @NonNull Builder domain(final String domain) { + mNode.setDomain(domain); + return this; + } + + public @NonNull Builder value(final String value) { + mNode.setValue(value); + return this; + } + } + } + + public interface Delegate { + /** + * Notify that an autofill event has occurred. + * + * The default implementation in {@link GeckoView} forwards the + * notification to the system {@link AutofillManager}. + * This method is only called on Android 6.0 and above and it is called + * in viewless mode as well. + * + * @param session The {@link GeckoSession} instance. + * @param notification Notification type, one of {@link Notify}. + * @param node The target node for this event, or null for + * {@link Notify#SESSION_CANCELED}. + */ + @UiThread + default void onAutofill(@NonNull GeckoSession session, + @AutofillNotify int notification, + @Nullable Node node) {} + } + + /* package */ static final class Support { + private static final String LOGTAG = "AutofillSupport"; + + private @NonNull final GeckoSession mGeckoSession; + private @NonNull final Session mAutofillSession; + private Delegate mDelegate; + + public Support(final GeckoSession geckoSession) { + mGeckoSession = geckoSession; + + geckoSession.getEventDispatcher().registerUiThreadListener( + new BundleEventListener() { + @Override + public void handleMessage( + final String event, + final GeckoBundle message, + final EventCallback callback) { + if ("GeckoView:AddAutofill".equals(event)) { + addNode(message, callback); + } else if ("GeckoView:ClearAutofill".equals(event)) { + clear(); + } else if ("GeckoView:OnAutofillFocus".equals(event)) { + onFocusChanged(message); + } + } + }, + "GeckoView:AddAutofill", + "GeckoView:ClearAutofill", + "GeckoView:OnAutofillFocus", + null); + + mAutofillSession = new Session(geckoSession); + } + + /** + * Perform auto-fill using the specified values. + * + * @param values Map of auto-fill IDs to values. + */ + public void autofill(final SparseArray values) { + if (getAutofillSession().isEmpty()) { + return; + } + + GeckoBundle response = null; + EventCallback callback = null; + + for (int i = 0; i < values.size(); i++) { + final int id = values.keyAt(i); + final CharSequence value = values.valueAt(i); + + if (DEBUG) { + Log.d(LOGTAG, "autofill(" + id + ')'); + } + int rootId = id; + for (int currentId = id; currentId != View.NO_ID; ) { + final Node elem = getAutofillSession().getNode(currentId); + if (elem == null) { + return; + } + rootId = currentId; + currentId = elem.getParentId(); + } + + final Node root = getAutofillSession().getNode(rootId); + final EventCallback newCallback = + root != null + ? root.getCallback() + : null; + if (callback == null || newCallback != callback) { + if (callback != null) { + callback.sendSuccess(response); + } + response = new GeckoBundle(values.size() - i); + callback = newCallback; + } + response.putString(String.valueOf(id), String.valueOf(value)); + } + + if (callback != null) { + callback.sendSuccess(response); + } + } + + public void setDelegate(final @Nullable Delegate delegate) { + mDelegate = delegate; + } + + public @Nullable Delegate getDelegate() { + return mDelegate; + } + + public @NonNull Session getAutofillSession() { + return mAutofillSession; + } + + /* package */ void addNode( + @NonNull final GeckoBundle message, + @NonNull final EventCallback callback) { + final boolean initializing = getAutofillSession().isEmpty(); + final int id = message.getInt("id"); + + if (DEBUG) { + Log.d(LOGTAG, "addNode(" + id + ')'); + } + + if (initializing) { + // TODO: We need this to set the dimensions on the root node. + // We should find a better way of handling this. + getAutofillSession().clear(); + } + + final Node node = new Node.Builder( + getAutofillSession(), message).build(); + node.setCallback(callback); + getAutofillSession().addNode(node); + maybeDispatch( + initializing + ? Notify.SESSION_STARTED + : Notify.NODE_ADDED, + node); + } + + private void maybeDispatch( + final @AutofillNotify int notification, final Node node) { + if (mDelegate == null) { + return; + } + + mDelegate.onAutofill(mGeckoSession, notification, node); + } + + /* package */ void clear() { + if (getAutofillSession().isEmpty()) { + return; + } + + if (DEBUG) { + Log.d(LOGTAG, "clear()"); + } + + getAutofillSession().clear(); + maybeDispatch(Notify.SESSION_CANCELED, null); + } + + /* package */ void onFocusChanged( + @Nullable final GeckoBundle message) { + if (getAutofillSession().isEmpty()) { + return; + } + + final int prevId = getAutofillSession().getFocusedId(); + final int id; + final int root; + + if (message != null) { + id = message.getInt("id"); + root = message.getInt("root"); + } else { + id = root = View.NO_ID; + } + + if (DEBUG) { + Log.d(LOGTAG, "onFocusChanged(" + prevId + " -> " + id + ')'); + } + + if (prevId == id) { + return; + } + + getAutofillSession().setFocus(id, root); + + if (prevId != View.NO_ID) { + maybeDispatch( + Notify.NODE_BLURRED, + getAutofillSession().getNode(prevId)); + } + + if (id != View.NO_ID) { + maybeDispatch( + Notify.NODE_FOCUSED, + getAutofillSession().getNode(id)); + } + } + + /* package */ static Rect getDummyAutofillRect( + @NonNull final GeckoSession geckoSession, + final boolean screen, + @Nullable final View view) { + final Rect rect = new Rect(); + geckoSession.getSurfaceBounds(rect); + + if (screen) { + if (view == null) { + throw new IllegalArgumentException(); + } + final int[] offset = new int[2]; + view.getLocationOnScreen(offset); + rect.offset(offset[0], offset[1]); + } + return rect; + } + + public void onActiveChanged(final boolean active) { + final int focusedId = getAutofillSession().getFocusedId(); + + if (focusedId == View.NO_ID) { + return; + } + + maybeDispatch( + active + ? Notify.NODE_FOCUSED + : Notify.NODE_BLURRED, + getAutofillSession().getNode(focusedId)); + } + } +} diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/AutofillElement.java b/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/AutofillElement.java deleted file mode 100644 index 3833affcc11f..000000000000 --- a/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/AutofillElement.java +++ /dev/null @@ -1,242 +0,0 @@ -/* -*- 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 android.graphics.Rect; -import android.support.annotation.IntDef; -import android.support.annotation.NonNull; -import android.support.v4.util.ArrayMap; -import android.view.View; - -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.util.Collection; -import java.util.Collections; -import java.util.LinkedList; -import java.util.Map; - -/** - * Represents a single autofill element. - */ -public class AutofillElement { - - @Retention(RetentionPolicy.SOURCE) - @IntDef({HINT_NONE, HINT_EMAIL_ADDRESS, HINT_PASSWORD, HINT_URL, HINT_USERNAME}) - /* package */ @interface AutofillHint {} - - /** - * Hint indicating that no special handling is required. - */ - public static final int HINT_NONE = -1; - - /** - * Hint indicating that an element represents an email address. - */ - public static final int HINT_EMAIL_ADDRESS = 0; - - /** - * Hint indicating that an element represents a password. - */ - public static final int HINT_PASSWORD = 1; - - /** - * Hint indicating that an element represents an URL. - */ - public static final int HINT_URL = 2; - - /** - * Hint indicating that an element represents a username. - */ - public static final int HINT_USERNAME = 3; - - @Retention(RetentionPolicy.SOURCE) - @IntDef({INPUT_TYPE_NONE, INPUT_TYPE_TEXT, INPUT_TYPE_NUMBER, INPUT_TYPE_PHONE}) - /* package */ @interface AutofillInputType {} - - /** - * Indicates that an element is not a known input type. - */ - public static final int INPUT_TYPE_NONE = -1; - - /** - * Indicates that an element is a text input type (e.g., {@code }) - */ - public static final int INPUT_TYPE_TEXT = 0; - - /** - * Indicates that an element is a number input type (e.g., {@code }) - */ - public static final int INPUT_TYPE_NUMBER = 1; - - /** - * Indicates that an element is a phone input type (e.g., {@code }) - */ - public static final int INPUT_TYPE_PHONE = 2; - - /** - * A unique (within this page) id for this element. - */ - public final int id; - - /** - * The dimensions of this element in CSS coordinates. - */ - public final @NonNull Rect dimensions; - - /** - * The collection of child elements for this element. - */ - public final @NonNull Collection children; - - /** - * The HTML attributes for this element. - */ - public final @NonNull Map attributes; - - /** - * Whether or not this element is enabled. - */ - public final boolean enabled; - - /** - * Whether or not this element is focusable. - */ - public final boolean focusable; - - /** - * Whether or not this element is focused. - */ - public final boolean focused; - - /** - * A hint for the type of data contained in this element, if any. - */ - public final @AutofillHint int hint; - - /** - * The input type of this element, if any. - */ - public final @AutofillInputType int inputType; - - /** - * The HTML tag for this element. - */ - public final @NonNull String tag; - - /** - * The web domain for this element. - */ - public final @NonNull String domain; - - private AutofillElement(final Builder builder) { - id = builder.mId; - dimensions = builder.mDimensions != null ? builder.mDimensions : new Rect(0, 0, 0, 0); - attributes = Collections.unmodifiableMap(builder.mAttributes != null ? builder.mAttributes : new ArrayMap<>()); - enabled = builder.mEnabled; - focusable = builder.mFocusable; - focused = builder.mFocused; - hint = builder.mHint; - inputType = builder.mInputType; - tag = builder.mTag; - domain = builder.mDomain; - - if (builder.mChildren != null) { - LinkedList children = new LinkedList<>(); - for (Builder child : builder.mChildren) { - children.add(child.build()); - } - this.children = children; - } else { - this.children = new LinkedList<>(); - } - } - - protected AutofillElement() { - id = View.NO_ID; - dimensions = new Rect(0, 0, 0, 0); - attributes = Collections.unmodifiableMap(new ArrayMap<>()); - enabled = false; - focusable = false; - focused = false; - hint = HINT_NONE; - inputType = INPUT_TYPE_NONE; - tag = ""; - domain = ""; - children = new LinkedList<>(); - } - - /* package */ static class Builder { - private int mId = View.NO_ID; - private Rect mDimensions; - private LinkedList mChildren; - private ArrayMap mAttributes; - private boolean mEnabled; - private boolean mFocusable; - private boolean mFocused; - private int mHint = HINT_NONE; - private int mInputType = INPUT_TYPE_NONE; - private String mTag = ""; - private String mDomain = ""; - - public void dimensions(final Rect rect) { - mDimensions = rect; - } - - public AutofillElement build() { - return new AutofillElement(this); - } - - public void id(final int id) { - mId = id; - } - - public Builder child() { - if (mChildren == null) { - mChildren = new LinkedList<>(); - } - - final Builder child = new Builder(); - mChildren.add(child); - return child; - } - - public void attribute(final String key, final String value) { - if (mAttributes == null) { - mAttributes = new ArrayMap<>(); - } - - mAttributes.put(key, value); - } - - public void enabled(final boolean enabled) { - mEnabled = enabled; - } - - public void focusable(final boolean focusable) { - mFocusable = focusable; - } - - public void focused(final boolean focused) { - mFocused = focused; - } - - public void hint(final int hint) { - mHint = hint; - } - - public void inputType(final int inputType) { - mInputType = inputType; - } - - public void tag(final String tag) { - mTag = tag; - } - - public void domain(final String domain) { - mDomain = domain; - } - } -} diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/AutofillSupport.java b/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/AutofillSupport.java deleted file mode 100644 index 85b5f18b6601..000000000000 --- a/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/AutofillSupport.java +++ /dev/null @@ -1,321 +0,0 @@ -package org.mozilla.geckoview; - -import android.graphics.Rect; -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; -import android.util.Log; -import android.util.SparseArray; -import android.view.View; - -import org.mozilla.gecko.util.BundleEventListener; -import org.mozilla.gecko.util.EventCallback; -import org.mozilla.gecko.util.GeckoBundle; - -/* package */ class AutofillSupport { - private static final String LOGTAG = "AutofillSupport"; - private static final boolean DEBUG = false; - - private final GeckoSession mSession; - private GeckoSession.AutofillDelegate mDelegate; - private SparseArray mAutofillNodes; - private SparseArray mAutofillRoots; - private int mAutofillFocusedId = View.NO_ID; - private int mAutofillFocusedRoot = View.NO_ID; - - public AutofillSupport(final GeckoSession session) { - mSession = session; - - session.getEventDispatcher().registerUiThreadListener( - new BundleEventListener() { - @Override - public void handleMessage(final String event, final GeckoBundle message, - final EventCallback callback) { - if ("GeckoView:AddAutofill".equals(event)) { - addAutofill(message, callback); - } else if ("GeckoView:ClearAutofill".equals(event)) { - clearAutofill(); - } else if ("GeckoView:OnAutofillFocus".equals(event)) { - onAutofillFocus(message); - } - } - }, - "GeckoView:AddAutofill", - "GeckoView:ClearAutofill", - "GeckoView:OnAutofillFocus", - null); - } - - /** - * Perform auto-fill using the specified values. - * - * @param values Map of auto-fill IDs to values. - */ - public void autofill(final SparseArray values) { - if (mAutofillRoots == null) { - return; - } - - GeckoBundle response = null; - EventCallback callback = null; - - for (int i = 0; i < values.size(); i++) { - final int id = values.keyAt(i); - final CharSequence value = values.valueAt(i); - - if (DEBUG) { - Log.d(LOGTAG, "autofill(" + id + ')'); - } - int rootId = id; - for (int currentId = id; currentId != View.NO_ID; ) { - final GeckoBundle bundle = mAutofillNodes.get(currentId); - if (bundle == null) { - return; - } - rootId = currentId; - currentId = bundle.getInt("parent", View.NO_ID); - } - - final EventCallback newCallback = mAutofillRoots.get(rootId); - if (callback == null || newCallback != callback) { - if (callback != null) { - callback.sendSuccess(response); - } - response = new GeckoBundle(values.size() - i); - callback = newCallback; - } - response.putString(String.valueOf(id), String.valueOf(value)); - } - - if (callback != null) { - callback.sendSuccess(response); - } - } - - public void setDelegate(final @Nullable GeckoSession.AutofillDelegate delegate) { - mDelegate = delegate; - } - - public @Nullable GeckoSession.AutofillDelegate getDelegate() { - return mDelegate; - } - - public @NonNull AutofillElement getAutofillElements() { - final AutofillElement.Builder builder = new AutofillElement.Builder(); - - final Rect rect = getDummyAutofillRect(mSession, false, null); - builder.dimensions(rect); - - if (mAutofillRoots != null) { - final int size = mAutofillRoots.size(); - for (int i = 0; i < size; i++) { - final int id = mAutofillRoots.keyAt(i); - final GeckoBundle root = mAutofillNodes.get(id); - fillAutofillElement(id, root, rect, builder.child()); - } - } - - return builder.build(); - } - - private void fillAutofillElement(final int id, final GeckoBundle bundle, final Rect rect, final AutofillElement.Builder builder) { - builder.id(id); - builder.domain(bundle.getString("origin")); - - if (mAutofillFocusedRoot != View.NO_ID && mAutofillFocusedRoot == bundle.getInt("root", View.NO_ID)) { - builder.dimensions(rect); - } - - final GeckoBundle[] children = bundle.getBundleArray("children"); - if (children != null) { - for (final GeckoBundle childBundle : children) { - final int childId = childBundle.getInt("id"); - fillAutofillElement(childId, childBundle, rect, builder.child()); - mAutofillNodes.append(childId, childBundle); - } - } - - String tag = bundle.getString("tag", ""); - builder.tag(tag.toLowerCase()); - - final String type = bundle.getString("type", "text"); - - final GeckoBundle attrs = bundle.getBundle("attributes"); - for (final String key : attrs.keys()) { - builder.attribute(key, String.valueOf(attrs.get(key))); - } - - if ("INPUT".equals(tag) && !bundle.getBoolean("editable", false)) { - tag = ""; // Don't process non-editable inputs (e.g. type="button"). - } - - switch (tag) { - case "INPUT": - case "TEXTAREA": { - final boolean disabled = bundle.getBoolean("disabled"); - - builder.enabled(!disabled); - builder.focusable(!disabled); - builder.focused(id == mAutofillFocusedId); - break; - } - default: - break; - } - - switch (type) { - case "email": - builder.hint(AutofillElement.HINT_EMAIL_ADDRESS); - builder.inputType(AutofillElement.INPUT_TYPE_TEXT); - break; - case "number": - builder.inputType(AutofillElement.INPUT_TYPE_NUMBER); - break; - case "password": - builder.hint(AutofillElement.HINT_PASSWORD); - builder.inputType(AutofillElement.INPUT_TYPE_TEXT); - break; - case "tel": - builder.inputType(AutofillElement.INPUT_TYPE_PHONE); - break; - case "url": - builder.hint(AutofillElement.HINT_URL); - builder.inputType(AutofillElement.INPUT_TYPE_TEXT); - break; - case "text": - final String autofillhint = bundle.getString("autofillhint", ""); - if (autofillhint.equals("username")) { - builder.hint(AutofillElement.HINT_USERNAME); - builder.inputType(AutofillElement.INPUT_TYPE_TEXT); - } - break; - } - } - - /* package */ void addAutofill(@NonNull final GeckoBundle message, - @NonNull final EventCallback callback) { - final boolean initializing; - if (mAutofillRoots == null) { - mAutofillRoots = new SparseArray<>(); - mAutofillNodes = new SparseArray<>(); - initializing = true; - } else { - initializing = false; - } - - final int id = message.getInt("id"); - if (DEBUG) { - Log.d(LOGTAG, "addAutofill(" + id + ')'); - } - mAutofillRoots.append(id, callback); - populateAutofillNodes(message); - - if (mDelegate == null) { - return; - } - - if (initializing) { - mDelegate.onAutofill( - mSession, GeckoSession.AutofillDelegate.AUTOFILL_NOTIFY_STARTED, id); - } else { - mDelegate.onAutofill( - mSession, GeckoSession.AutofillDelegate.AUTOFILL_NOTIFY_VIEW_ADDED, id); - } - } - - private void populateAutofillNodes(final GeckoBundle bundle) { - final int id = bundle.getInt("id"); - - mAutofillNodes.append(id, bundle); - - final GeckoBundle[] children = bundle.getBundleArray("children"); - if (children != null) { - for (GeckoBundle child : children) { - populateAutofillNodes(child); - } - } - } - - /* package */ void clearAutofill() { - if (mAutofillRoots == null) { - return; - } - if (DEBUG) { - Log.d(LOGTAG, "clearAutofill()"); - } - mAutofillRoots = null; - mAutofillNodes = null; - mAutofillFocusedId = View.NO_ID; - mAutofillFocusedRoot = View.NO_ID; - - if (mDelegate != null) { - mDelegate.onAutofill( - mSession, GeckoSession.AutofillDelegate.AUTOFILL_NOTIFY_CANCELED, View.NO_ID); - } - } - - /* package */ void onAutofillFocus(@Nullable final GeckoBundle message) { - if (mAutofillRoots == null) { - return; - } - - final int id; - final int root; - if (message != null) { - id = message.getInt("id"); - root = message.getInt("root"); - } else { - id = root = View.NO_ID; - } - - if (DEBUG) { - Log.d(LOGTAG, "onAutofillFocus(" + id + ')'); - } - if (mAutofillFocusedId == id) { - return; - } - - if (mDelegate != null && mAutofillFocusedId != View.NO_ID) { - mDelegate.onAutofill( - mSession, GeckoSession.AutofillDelegate.AUTOFILL_NOTIFY_VIEW_EXITED, - mAutofillFocusedId); - } - - mAutofillFocusedId = id; - mAutofillFocusedRoot = root; - - if (mDelegate != null && mAutofillFocusedId != View.NO_ID) { - mDelegate.onAutofill( - mSession, GeckoSession.AutofillDelegate.AUTOFILL_NOTIFY_VIEW_ENTERED, - mAutofillFocusedId); - } - } - - /* package */ static Rect getDummyAutofillRect(@NonNull final GeckoSession session, - final boolean screen, - @Nullable final View view) { - final Rect rect = new Rect(); - session.getSurfaceBounds(rect); - if (screen) { - if (view == null) { - throw new IllegalArgumentException(); - } - final int[] offset = new int[2]; - view.getLocationOnScreen(offset); - rect.offset(offset[0], offset[1]); - } - return rect; - } - - public void onActiveChanged(final boolean active) { - if (mDelegate == null || mAutofillFocusedId == View.NO_ID) { - return; - } - - // We blur/focus the active element (if we have one) when the document is made inactive/active. - getDelegate().onAutofill( - mSession, - active ? GeckoSession.AutofillDelegate.AUTOFILL_NOTIFY_VIEW_ENTERED - : GeckoSession.AutofillDelegate.AUTOFILL_NOTIFY_VIEW_EXITED, - mAutofillFocusedId); - } -} diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/GeckoSession.java b/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/GeckoSession.java index 1c2867bd5cf7..1afaf7867dc3 100644 --- a/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/GeckoSession.java +++ b/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/GeckoSession.java @@ -64,7 +64,6 @@ import android.view.inputmethod.ExtractedText; import android.view.inputmethod.ExtractedTextRequest; import android.view.View; import android.view.ViewStructure; -import android.view.autofill.AutofillManager; public class GeckoSession implements Parcelable { private static final String LOGTAG = "GeckoSession"; @@ -124,7 +123,7 @@ public class GeckoSession implements Parcelable { private OverscrollEdgeEffect mOverscroll; private DynamicToolbarAnimator mToolbar; private CompositorController mController; - private AutofillSupport mAutofillSupport; + private Autofill.Support mAutofillSupport; private boolean mAttachedCompositor; private boolean mCompositorReady; @@ -1523,7 +1522,7 @@ public class GeckoSession implements Parcelable { mTextInput.onWindowChanged(mWindow); } if ((change == WINDOW_CLOSE || change == WINDOW_TRANSFER_OUT) && !inProgress) { - getAutofillSupport().clearAutofill(); + getAutofillSupport().clear(); } } @@ -5691,59 +5690,10 @@ public class GeckoSession implements Parcelable { }) /* package */ @interface VisitFlags {} - @Retention(RetentionPolicy.SOURCE) - @IntDef({ - AutofillDelegate.AUTOFILL_NOTIFY_STARTED, - AutofillDelegate.AUTOFILL_NOTIFY_COMMITTED, - AutofillDelegate.AUTOFILL_NOTIFY_CANCELED, - AutofillDelegate.AUTOFILL_NOTIFY_VIEW_ADDED, - AutofillDelegate.AUTOFILL_NOTIFY_VIEW_REMOVED, - AutofillDelegate.AUTOFILL_NOTIFY_VIEW_UPDATED, - AutofillDelegate.AUTOFILL_NOTIFY_VIEW_ENTERED, - AutofillDelegate.AUTOFILL_NOTIFY_VIEW_EXITED}) - /* package */ @interface AutofillNotification {} - public interface AutofillDelegate { - - /** An autofill session has started, usually as a result of loading a page. */ - int AUTOFILL_NOTIFY_STARTED = 0; - /** An autofill session has been committed, usually as a result of submitting a form. */ - int AUTOFILL_NOTIFY_COMMITTED = 1; - /** An autofill session has been canceled, usually as a result of unloading a page. */ - int AUTOFILL_NOTIFY_CANCELED = 2; - /** A view within the autofill session has been added. */ - int AUTOFILL_NOTIFY_VIEW_ADDED = 3; - /** A view within the autofill session has been removed. */ - int AUTOFILL_NOTIFY_VIEW_REMOVED = 4; - /** A view within the autofill session has been updated (e.g. change in state). */ - int AUTOFILL_NOTIFY_VIEW_UPDATED = 5; - /** A view within the autofill session has gained focus. */ - int AUTOFILL_NOTIFY_VIEW_ENTERED = 6; - /** A view within the autofill session has lost focus. */ - int AUTOFILL_NOTIFY_VIEW_EXITED = 7; - - /** - * Notify that an autofill event has occurred. The default implementation forwards the - * notification to the system {@link AutofillManager}. This method is - * only called on Android 6.0 and above, and it is called in viewless mode as well. - * - * @param session Session instance. - * @param notification Notification type as one of the {@link #AUTOFILL_NOTIFY_STARTED - * AUTO_FILL_NOTIFY_*} constants. - * @param virtualId Virtual ID of the target, or {@link View#NO_ID} if not - * applicable. The ID matches one of the virtual IDs provided by {@link - * GeckoSession#getAutofillElements()} and can be used - * with {@link GeckoSession#autofill}. - */ - @UiThread - default void onAutofill(@NonNull GeckoSession session, - @GeckoSession.AutofillNotification int notification, - int virtualId) {} - } - - private AutofillSupport getAutofillSupport() { + private Autofill.Support getAutofillSupport() { if (mAutofillSupport == null) { - mAutofillSupport = new AutofillSupport(this); + mAutofillSupport = new Autofill.Support(this); } return mAutofillSupport; @@ -5752,18 +5702,18 @@ public class GeckoSession implements Parcelable { /** * Sets the autofill delegate for this session. * - * @param delegate An instance of {@link AutofillDelegate}. + * @param delegate An instance of {@link Autofill.Delegate}. */ @UiThread - public void setAutofillDelegate(final @Nullable AutofillDelegate delegate) { + public void setAutofillDelegate(final @Nullable Autofill.Delegate delegate) { getAutofillSupport().setDelegate(delegate); } /** - * @return The current {@link AutofillDelegate} for this session, if any. + * @return The current {@link Autofill.Delegate} for this session, if any. */ @UiThread - public @Nullable AutofillDelegate getAutofillDelegate() { + public @Nullable Autofill.Delegate getAutofillDelegate() { return getAutofillSupport().getDelegate(); } @@ -5786,7 +5736,7 @@ public class GeckoSession implements Parcelable { * @return The elements available for autofill. */ @UiThread - public @NonNull AutofillElement getAutofillElements() { - return getAutofillSupport().getAutofillElements(); + public @NonNull Autofill.Session getAutofillSession() { + return getAutofillSupport().getAutofillSession(); } } diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/GeckoView.java b/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/GeckoView.java index 9fc01fcaed23..ec51bd2461aa 100644 --- a/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/GeckoView.java +++ b/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/GeckoView.java @@ -33,7 +33,6 @@ import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.annotation.UiThread; import android.support.v4.view.ViewCompat; -import android.text.InputType; import android.util.AttributeSet; import android.util.DisplayMetrics; import android.util.SparseArray; @@ -68,7 +67,7 @@ public class GeckoView extends FrameLayout { private boolean mAutofillEnabled = true; private GeckoSession.SelectionActionDelegate mSelectionActionDelegate; - private GeckoSession.AutofillDelegate mAutofillDelegate; + private Autofill.Delegate mAutofillDelegate; private static class SavedState extends BaseSavedState { public final GeckoSession session; @@ -734,96 +733,8 @@ public class GeckoView extends FrameLayout { return; } - final AutofillElement root = mSession.getAutofillElements(); - fillViewStructure(root, structure, flags); - } - - @TargetApi(23) - private void fillViewStructure(final AutofillElement element, final ViewStructure structure, final int flags) { - if (Build.VERSION.SDK_INT >= 26) { - structure.setAutofillId(getAutofillId(), element.id); - structure.setWebDomain(element.domain); - } - - structure.setId(element.id, null, null, null); - structure.setDimens(0, 0, 0, 0, element.dimensions.width(), element.dimensions.height()); - - if (Build.VERSION.SDK_INT >= 26) { - final ViewStructure.HtmlInfo.Builder htmlBuilder = structure.newHtmlInfoBuilder(element.tag); - for (final String key : element.attributes.keySet()) { - htmlBuilder.addAttribute(key, String.valueOf(element.attributes.get(key))); - } - - structure.setHtmlInfo(htmlBuilder.build()); - } - - structure.setChildCount(element.children.size()); - int childCount = 0; - - for (final AutofillElement child : element.children) { - final ViewStructure childStructure = structure.newChild(childCount); - fillViewStructure(child, childStructure, flags); - childCount++; - } - - switch (element.tag) { - case "input": - case "textarea": - structure.setClassName("android.widget.EditText"); - structure.setEnabled(element.enabled); - structure.setFocusable(element.focusable); - structure.setFocused(element.focused); - structure.setVisibility(View.VISIBLE); - - if (Build.VERSION.SDK_INT >= 26) { - structure.setAutofillType(View.AUTOFILL_TYPE_TEXT); - } - break; - default: - if (childCount > 0) { - structure.setClassName("android.view.ViewGroup"); - } else { - structure.setClassName("android.view.View"); - } - break; - } - - if (Build.VERSION.SDK_INT >= 26 && "input".equals(element.tag)) { - // LastPass will fill password to the field that setAutofillHints is unset and setInputType is set. - switch (element.hint) { - case AutofillElement.HINT_EMAIL_ADDRESS: - structure.setAutofillHints(new String[] { View.AUTOFILL_HINT_EMAIL_ADDRESS }); - structure.setInputType(InputType.TYPE_CLASS_TEXT | - InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS); - break; - case AutofillElement.HINT_PASSWORD: - structure.setAutofillHints(new String[] { View.AUTOFILL_HINT_PASSWORD }); - structure.setInputType(InputType.TYPE_CLASS_TEXT | - InputType.TYPE_TEXT_VARIATION_WEB_PASSWORD); - break; - case AutofillElement.HINT_URL: - structure.setInputType(InputType.TYPE_CLASS_TEXT | - InputType.TYPE_TEXT_VARIATION_URI); - break; - case AutofillElement.HINT_USERNAME: - structure.setAutofillHints(new String[] { View.AUTOFILL_HINT_USERNAME }); - structure.setInputType(InputType.TYPE_CLASS_TEXT | - InputType.TYPE_TEXT_VARIATION_WEB_EDIT_TEXT); - break; - } - - switch (element.inputType) { - case AutofillElement.INPUT_TYPE_NUMBER: - structure.setInputType(InputType.TYPE_CLASS_NUMBER); - break; - case AutofillElement.INPUT_TYPE_PHONE: - structure.setAutofillHints(new String[] { View.AUTOFILL_HINT_PHONE }); - structure.setInputType(InputType.TYPE_CLASS_PHONE); - break; - default: - break; - } - } + final Autofill.Session autofillSession = mSession.getAutofillSession(); + autofillSession.fillViewStructure(this, structure, flags); } @Override @@ -862,7 +773,7 @@ public class GeckoView extends FrameLayout { /** * Sets whether or not this View participates in Android autofill. * - * When enabled, this will set an {@link GeckoSession.AutofillDelegate} on the + * When enabled, this will set an {@link Autofill.Delegate} on the * {@link GeckoSession} for this instance. * * @param enabled Whether or not Android autofill is enabled for this view. @@ -888,35 +799,16 @@ public class GeckoView extends FrameLayout { return mAutofillEnabled; } - private class AndroidAutofillDelegate implements GeckoSession.AutofillDelegate { - - private AutofillElement findElementWithId(final AutofillElement root, final int id) { - if (root.id == id) { - return root; - } - - for (AutofillElement child : root.children) { - final AutofillElement found = findElementWithId(child, id); - if (found != null) { - return found; - } - } - - return null; - } + private class AndroidAutofillDelegate implements Autofill.Delegate { private Rect displayRectForId(@NonNull final GeckoSession session, - @NonNull final int virtualId, - @Nullable final View view) { - final AutofillElement structure = session.getAutofillElements(); - final AutofillElement element = findElementWithId(structure, virtualId); - - if (element == null) { + @NonNull final Autofill.Node node) { + if (node == null) { return new Rect(0, 0, 0, 0); } final Matrix matrix = new Matrix(); - final RectF rectF = new RectF(element.dimensions); + final RectF rectF = new RectF(node.getDimensions()); session.getPageToScreenMatrix(matrix); matrix.mapRect(rectF); @@ -927,8 +819,8 @@ public class GeckoView extends FrameLayout { @Override public void onAutofill(@NonNull final GeckoSession session, - @GeckoSession.AutofillNotification final int notification, - final int virtualId) { + final int notification, + final Autofill.Node node) { ThreadUtils.assertOnUiThread(); if (Build.VERSION.SDK_INT < 26) { return; @@ -941,21 +833,21 @@ public class GeckoView extends FrameLayout { } switch (notification) { - case AUTOFILL_NOTIFY_STARTED: + case Autofill.Notify.SESSION_STARTED: // This line seems necessary for auto-fill to work on the initial page. + case Autofill.Notify.SESSION_CANCELED: manager.cancel(); break; - case AUTOFILL_NOTIFY_COMMITTED: + case Autofill.Notify.SESSION_COMMITTED: manager.commit(); break; - case AUTOFILL_NOTIFY_CANCELED: - manager.cancel(); + case Autofill.Notify.NODE_FOCUSED: + manager.notifyViewEntered( + GeckoView.this, node.getId(), + displayRectForId(session, node)); break; - case AUTOFILL_NOTIFY_VIEW_ENTERED: - manager.notifyViewEntered(GeckoView.this, virtualId, displayRectForId(session, virtualId, GeckoView.this)); - break; - case AUTOFILL_NOTIFY_VIEW_EXITED: - manager.notifyViewExited(GeckoView.this, virtualId); + case Autofill.Notify.NODE_BLURRED: + manager.notifyViewExited(GeckoView.this, node.getId()); break; } } diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/doc-files/CHANGELOG.md b/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/doc-files/CHANGELOG.md index 2447f15b9591..2bb2fb16180f 100644 --- a/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/doc-files/CHANGELOG.md +++ b/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/doc-files/CHANGELOG.md @@ -69,6 +69,9 @@ exclude: true ([bug 1402369]({{bugzilla}}1402369)) - Added [`GeckoDisplay.screenshot`][71.23] allowing apps finer grain control over screenshots. ([bug 1577192]({{bugzilla}}1577192)) +- ⚠️ Refactored `AutofillElement` and `AutofillSupport` into the + [`Autofill`][71.24] API. + ([bug 1591462]({{bugzilla}}1591462)) [71.1]: {{javadoc_uri}}/RuntimeTelemetry.Delegate.html#onBooleanScalar-org.mozilla.geckoview.RuntimeTelemetry.Metric- [71.2]: {{javadoc_uri}}/RuntimeTelemetry.Delegate.html#onLongScalar-org.mozilla.geckoview.RuntimeTelemetry.Metric- @@ -92,6 +95,7 @@ exclude: true [71.21]: {{javadoc_uri}}/GeckoView.html#setAutofillEnabled-boolean- [71.22]: {{javadoc_uri}}/GeckoSession.PromptDelegate.html#onSharePrompt-org.mozilla.geckoview.GeckoSession-org.mozilla.geckoview.GeckoSession.PromptDelegate.SharePrompt- [71.23]: {{javadoc_uri}}/GeckoDisplay.html#screenshot-- +[71.24]: {{javadoc_uri}}/Autofill.html ## v70 - Added API for session context assignment @@ -414,4 +418,4 @@ exclude: true [65.24]: {{javadoc_uri}}/CrashReporter.html#sendCrashReport-android.content.Context-android.os.Bundle-java.lang.String- [65.25]: {{javadoc_uri}}/GeckoResult.html -[api-version]: c01c3629db0185ce919b026400328804d27985a6 +[api-version]: b8934264da833b5d36110e5d8136a0feef40623d diff --git a/mobile/android/modules/geckoview/GeckoViewAutofill.jsm b/mobile/android/modules/geckoview/GeckoViewAutofill.jsm index ac52a61c054f..140a15a61bf1 100644 --- a/mobile/android/modules/geckoview/GeckoViewAutofill.jsm +++ b/mobile/android/modules/geckoview/GeckoViewAutofill.jsm @@ -94,6 +94,7 @@ class GeckoViewAutofill { root, tag: element.tagName, type: element instanceof window.HTMLInputElement ? element.type : null, + value: element.value, editable: element instanceof window.HTMLInputElement && [