Bug 1577003 - Move autofill stuff from TextInputDelegate into AutofillDelegate r=geckoview-reviewers,agi,esawin

This also moves `autofill()` and `provideAutofillVirtualStructure()`
into `GeckoSession`.

Differential Revision: https://phabricator.services.mozilla.com/D47481

--HG--
extra : moz-landing-system : lando
This commit is contained in:
James Willcox 2019-10-04 17:54:36 +00:00
Родитель 8d375d387b
Коммит 05c1575597
6 изменённых файлов: 520 добавлений и 504 удалений

Просмотреть файл

@ -254,11 +254,11 @@ class ContentDelegateTest : BaseSessionTest() {
// SessionAccessibility for a11y auto-fill support.
mainSession.loadTestPath(FORMS_HTML_PATH)
// Wait for the auto-fill nodes to populate.
sessionRule.waitUntilCalled(object : Callbacks.TextInputDelegate {
sessionRule.waitUntilCalled(object : Callbacks.AutofillDelegate {
// For the root document and the iframe document, each has a form group and
// a group for inputs outside of forms, so the total count is 4.
@AssertCalled(count = 4)
override fun notifyAutoFill(session: GeckoSession, notification: Int, virtualId: Int) {
override fun onAutofill(session: GeckoSession, notification: Int, virtualId: Int) {
}
})
@ -361,10 +361,10 @@ class ContentDelegateTest : BaseSessionTest() {
val rootStructure = MockViewNode()
mainSession.textInput.onProvideAutofillVirtualStructure(rootStructure, 0)
mainSession.provideAutofillVirtualStructure(null, rootStructure, 0)
checkAutoFillChild(rootStructure)
mainSession.textInput.autofill(autoFillValues)
mainSession.autofill(autoFillValues)
// Wait on the promises and check for correct values.
for ((key, actual, expected, eventInterface) in promises.map { it.value.asJSList<String>() }) {
@ -390,7 +390,7 @@ class ContentDelegateTest : BaseSessionTest() {
{ it.className == "android.widget.EditText" },
root: MockViewNode? = null): Int {
val node = if (root !== null) root else MockViewNode().also {
mainSession.textInput.onProvideAutofillVirtualStructure(it, 0)
mainSession.provideAutofillVirtualStructure(null, it, 0)
}
return (if (cond(node)) 1 else 0) +
@ -400,12 +400,12 @@ class ContentDelegateTest : BaseSessionTest() {
// Wait for the accessibility nodes to populate.
mainSession.loadTestPath(FORMS_HTML_PATH)
sessionRule.waitUntilCalled(object : Callbacks.TextInputDelegate {
sessionRule.waitUntilCalled(object : Callbacks.AutofillDelegate {
@AssertCalled(count = 4)
override fun notifyAutoFill(session: GeckoSession, notification: Int, virtualId: Int) {
override fun onAutofill(session: GeckoSession, notification: Int, virtualId: Int) {
assertThat("Should be starting auto-fill", notification, equalTo(forEachCall(
GeckoSession.TextInputDelegate.AUTO_FILL_NOTIFY_STARTED,
GeckoSession.TextInputDelegate.AUTO_FILL_NOTIFY_VIEW_ADDED)))
GeckoSession.AutofillDelegate.AUTO_FILL_NOTIFY_STARTED,
GeckoSession.AutofillDelegate.AUTO_FILL_NOTIFY_VIEW_ADDED)))
assertThat("ID should be valid", virtualId, not(equalTo(View.NO_ID)))
}
})
@ -414,12 +414,12 @@ class ContentDelegateTest : BaseSessionTest() {
// Now wait for the nodes to clear.
mainSession.loadTestPath(HELLO_HTML_PATH)
sessionRule.waitUntilCalled(object : Callbacks.TextInputDelegate {
sessionRule.waitUntilCalled(object : Callbacks.AutofillDelegate {
@AssertCalled(count = 1)
override fun notifyAutoFill(session: GeckoSession, notification: Int, virtualId: Int) {
override fun onAutofill(session: GeckoSession, notification: Int, virtualId: Int) {
assertThat("Should be canceling auto-fill",
notification,
equalTo(GeckoSession.TextInputDelegate.AUTO_FILL_NOTIFY_CANCELED))
equalTo(GeckoSession.AutofillDelegate.AUTO_FILL_NOTIFY_CANCELED))
assertThat("ID should be valid", virtualId, equalTo(View.NO_ID))
}
})
@ -429,12 +429,12 @@ class ContentDelegateTest : BaseSessionTest() {
// Now wait for the nodes to reappear.
mainSession.waitForPageStop()
mainSession.goBack()
sessionRule.waitUntilCalled(object : Callbacks.TextInputDelegate {
sessionRule.waitUntilCalled(object : Callbacks.AutofillDelegate {
@AssertCalled(count = 4)
override fun notifyAutoFill(session: GeckoSession, notification: Int, virtualId: Int) {
override fun onAutofill(session: GeckoSession, notification: Int, virtualId: Int) {
assertThat("Should be starting auto-fill", notification, equalTo(forEachCall(
GeckoSession.TextInputDelegate.AUTO_FILL_NOTIFY_STARTED,
GeckoSession.TextInputDelegate.AUTO_FILL_NOTIFY_VIEW_ADDED)))
GeckoSession.AutofillDelegate.AUTO_FILL_NOTIFY_STARTED,
GeckoSession.AutofillDelegate.AUTO_FILL_NOTIFY_VIEW_ADDED)))
assertThat("ID should be valid", virtualId, not(equalTo(View.NO_ID)))
}
})
@ -444,12 +444,12 @@ class ContentDelegateTest : BaseSessionTest() {
countAutoFillNodes({ it.isFocused }), equalTo(0))
mainSession.evaluateJS("document.querySelector('#pass2').focus()")
sessionRule.waitUntilCalled(object : Callbacks.TextInputDelegate {
sessionRule.waitUntilCalled(object : Callbacks.AutofillDelegate {
@AssertCalled(count = 1)
override fun notifyAutoFill(session: GeckoSession, notification: Int, virtualId: Int) {
override fun onAutofill(session: GeckoSession, notification: Int, virtualId: Int) {
assertThat("Should be entering auto-fill view",
notification,
equalTo(GeckoSession.TextInputDelegate.AUTO_FILL_NOTIFY_VIEW_ENTERED))
equalTo(GeckoSession.AutofillDelegate.AUTO_FILL_NOTIFY_VIEW_ENTERED))
assertThat("ID should be valid", virtualId, not(equalTo(View.NO_ID)))
}
})
@ -461,12 +461,12 @@ class ContentDelegateTest : BaseSessionTest() {
equalTo(7))
mainSession.evaluateJS("document.querySelector('#pass2').blur()")
sessionRule.waitUntilCalled(object : Callbacks.TextInputDelegate {
sessionRule.waitUntilCalled(object : Callbacks.AutofillDelegate {
@AssertCalled(count = 1)
override fun notifyAutoFill(session: GeckoSession, notification: Int, virtualId: Int) {
override fun onAutofill(session: GeckoSession, notification: Int, virtualId: Int) {
assertThat("Should be exiting auto-fill view",
notification,
equalTo(GeckoSession.TextInputDelegate.AUTO_FILL_NOTIFY_VIEW_EXITED))
equalTo(GeckoSession.AutofillDelegate.AUTO_FILL_NOTIFY_VIEW_EXITED))
assertThat("ID should be valid", virtualId, not(equalTo(View.NO_ID)))
}
})
@ -482,9 +482,9 @@ class ContentDelegateTest : BaseSessionTest() {
mainSession.loadTestPath(FORMS2_HTML_PATH)
// Wait for the auto-fill nodes to populate.
sessionRule.waitUntilCalled(object : Callbacks.TextInputDelegate {
sessionRule.waitUntilCalled(object : Callbacks.AutofillDelegate {
@AssertCalled(count = 1)
override fun notifyAutoFill(session: GeckoSession, notification: Int, virtualId: Int) {
override fun onAutofill(session: GeckoSession, notification: Int, virtualId: Int) {
}
})
@ -524,7 +524,7 @@ class ContentDelegateTest : BaseSessionTest() {
val rootStructure = MockViewNode()
mainSession.textInput.onProvideAutofillVirtualStructure(rootStructure, 0)
mainSession.provideAutofillVirtualStructure(null, rootStructure, 0)
// form and iframe have each 2 hints.
assertThat("autofill hint count",
checkAutoFillChild(rootStructure), equalTo(4))

Просмотреть файл

@ -23,12 +23,13 @@ import org.json.JSONObject
class Callbacks private constructor() {
object Default : All
interface All : ContentBlockingDelegate, ContentDelegate,
interface All : AutofillDelegate, ContentBlockingDelegate, ContentDelegate,
HistoryDelegate, MediaDelegate,
NavigationDelegate, PermissionDelegate, ProgressDelegate,
PromptDelegate, ScrollDelegate, SelectionActionDelegate,
TextInputDelegate
interface AutofillDelegate : GeckoSession.AutofillDelegate {}
interface ContentDelegate : GeckoSession.ContentDelegate {}
interface NavigationDelegate : GeckoSession.NavigationDelegate {}
interface PermissionDelegate : GeckoSession.PermissionDelegate {}
@ -58,8 +59,5 @@ class Callbacks private constructor() {
override fun updateCursorAnchorInfo(session: GeckoSession, info: CursorAnchorInfo) {
}
override fun notifyAutoFill(session: GeckoSession, notification: Int, virtualId: Int) {
}
}
}

Просмотреть файл

@ -0,0 +1,383 @@
package org.mozilla.geckoview;
import android.annotation.TargetApi;
import android.graphics.Rect;
import android.os.Build;
import android.support.annotation.AnyThread;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.annotation.UiThread;
import android.text.InputType;
import android.util.Log;
import android.util.SparseArray;
import android.view.View;
import android.view.ViewStructure;
import org.mozilla.gecko.util.BundleEventListener;
import org.mozilla.gecko.util.EventCallback;
import org.mozilla.gecko.util.GeckoBundle;
import java.util.Locale;
/* 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<GeckoBundle> mAutoFillNodes;
private SparseArray<EventCallback> 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<CharSequence> 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, "performAutoFill(" + 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 GeckoSession.AutofillDelegate delegate) {
mDelegate = delegate;
}
public GeckoSession.AutofillDelegate getDelegate() {
return mDelegate;
}
/* 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.AUTO_FILL_NOTIFY_STARTED, id);
} else {
mDelegate.onAutofill(
mSession, GeckoSession.AutofillDelegate.AUTO_FILL_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.AUTO_FILL_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.AUTO_FILL_NOTIFY_VIEW_EXITED,
mAutoFillFocusedId);
}
mAutoFillFocusedId = id;
mAutoFillFocusedRoot = root;
if (mDelegate != null && mAutoFillFocusedId != View.NO_ID) {
mDelegate.onAutofill(
mSession, GeckoSession.AutofillDelegate.AUTO_FILL_NOTIFY_VIEW_ENTERED,
mAutoFillFocusedId);
}
}
/**
* Fill the specified {@link ViewStructure} with auto-fill fields from the current page.
*
* @param structure Structure to be filled.
* @param flags Zero or a combination of {@link View#AUTOFILL_FLAG_INCLUDE_NOT_IMPORTANT_VIEWS
* AUTOFILL_FLAG_*} constants.
*/
@TargetApi(23)
@UiThread
public void provideAutofillVirtualStructure(@Nullable final View view,
@NonNull final ViewStructure structure,
final int flags) {
if (view != null) {
structure.setClassName(view.getClass().getName());
}
structure.setEnabled(true);
structure.setVisibility(View.VISIBLE);
final Rect rect = getDummyAutoFillRect(mSession, false, null);
structure.setDimens(rect.left, rect.top, 0, 0, rect.width(), rect.height());
if (mAutoFillRoots == null) {
structure.setChildCount(0);
return;
}
final int size = mAutoFillRoots.size();
structure.setChildCount(size);
for (int i = 0; i < size; i++) {
final int id = mAutoFillRoots.keyAt(i);
final GeckoBundle root = mAutoFillNodes.get(id);
fillAutoFillStructure(view, id, root, structure.newChild(i), rect);
}
}
@TargetApi(23)
private void fillAutoFillStructure(@Nullable final View view, final int id,
@NonNull final GeckoBundle bundle,
@NonNull final ViewStructure structure,
@NonNull final Rect rect) {
if (mAutoFillRoots == null) {
return;
}
if (DEBUG) {
Log.d(LOGTAG, "fillAutoFillStructure(" + id + ')');
}
if (Build.VERSION.SDK_INT >= 26) {
if (view != null) {
structure.setAutofillId(view.getAutofillId(), id);
}
structure.setWebDomain(bundle.getString("origin"));
}
structure.setId(id, null, null, null);
if (mAutoFillFocusedRoot != View.NO_ID &&
mAutoFillFocusedRoot == bundle.getInt("root", View.NO_ID)) {
structure.setDimens(0, 0, 0, 0, rect.width(), rect.height());
}
final GeckoBundle[] children = bundle.getBundleArray("children");
if (children != null) {
structure.setChildCount(children.length);
for (int i = 0; i < children.length; i++) {
final GeckoBundle childBundle = children[i];
final int childId = childBundle.getInt("id");
final ViewStructure childStructure = structure.newChild(i);
fillAutoFillStructure(view, childId, childBundle, childStructure, rect);
mAutoFillNodes.append(childId, childBundle);
}
}
String tag = bundle.getString("tag", "");
final String type = bundle.getString("type", "text");
if (Build.VERSION.SDK_INT >= 26) {
final GeckoBundle attrs = bundle.getBundle("attributes");
final ViewStructure.HtmlInfo.Builder builder =
structure.newHtmlInfoBuilder(tag.toLowerCase(Locale.US));
for (final String key : attrs.keys()) {
builder.addAttribute(key, String.valueOf(attrs.get(key)));
}
structure.setHtmlInfo(builder.build());
}
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");
structure.setClassName("android.widget.EditText");
structure.setEnabled(!disabled);
structure.setFocusable(!disabled);
structure.setFocused(id == mAutoFillFocusedId);
structure.setVisibility(View.VISIBLE);
if (Build.VERSION.SDK_INT >= 26) {
structure.setAutofillType(View.AUTOFILL_TYPE_TEXT);
}
break;
}
default:
if (children != null) {
structure.setClassName("android.view.ViewGroup");
} else {
structure.setClassName("android.view.View");
}
break;
}
if (Build.VERSION.SDK_INT >= 26 && "INPUT".equals(tag)) {
// LastPass will fill password to the feild that setAutofillHints is unset and setInputType is set.
switch (type) {
case "email":
structure.setAutofillHints(new String[] { View.AUTOFILL_HINT_EMAIL_ADDRESS });
structure.setInputType(InputType.TYPE_CLASS_TEXT |
InputType.TYPE_TEXT_VARIATION_WEB_EMAIL_ADDRESS);
break;
case "number":
structure.setInputType(InputType.TYPE_CLASS_NUMBER);
break;
case "password":
structure.setAutofillHints(new String[] { View.AUTOFILL_HINT_PASSWORD });
structure.setInputType(InputType.TYPE_CLASS_TEXT |
InputType.TYPE_TEXT_VARIATION_WEB_PASSWORD);
break;
case "tel":
structure.setAutofillHints(new String[] { View.AUTOFILL_HINT_PHONE });
structure.setInputType(InputType.TYPE_CLASS_PHONE);
break;
case "url":
structure.setInputType(InputType.TYPE_CLASS_TEXT |
InputType.TYPE_TEXT_VARIATION_URI);
break;
case "text":
final String autofillhint = bundle.getString("autofillhint", "");
if (autofillhint.equals("username")) {
structure.setAutofillHints(new String[] { View.AUTOFILL_HINT_USERNAME });
structure.setInputType(InputType.TYPE_CLASS_TEXT |
InputType.TYPE_TEXT_VARIATION_WEB_EDIT_TEXT);
}
break;
}
}
}
/* 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 onScreenMetricsUpdated() {
if (mDelegate != null && mAutoFillFocusedId != View.NO_ID) {
getDelegate().onAutofill(
mSession, GeckoSession.AutofillDelegate.AUTO_FILL_NOTIFY_VIEW_ENTERED, mAutoFillFocusedId);
}
}
}

Просмотреть файл

@ -34,6 +34,7 @@ import org.mozilla.gecko.util.GeckoBundle;
import org.mozilla.gecko.util.IntentUtils;
import org.mozilla.gecko.util.ThreadUtils;
import android.annotation.TargetApi;
import android.content.ContentResolver;
import android.content.Context;
import android.database.Cursor;
@ -58,10 +59,14 @@ import android.support.annotation.UiThread;
import android.util.Base64;
import android.util.Log;
import android.util.LongSparseArray;
import android.util.SparseArray;
import android.view.Surface;
import android.view.inputmethod.CursorAnchorInfo;
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";
@ -121,6 +126,7 @@ public class GeckoSession implements Parcelable {
private OverscrollEdgeEffect mOverscroll;
private DynamicToolbarAnimator mToolbar;
private CompositorController mController;
private AutofillSupport mAutofillSupport;
private boolean mAttachedCompositor;
private boolean mCompositorReady;
@ -1523,7 +1529,7 @@ public class GeckoSession implements Parcelable {
mTextInput.onWindowChanged(mWindow);
}
if ((change == WINDOW_CLOSE || change == WINDOW_TRANSFER_OUT) && !inProgress) {
mTextInput.clearAutoFill();
getAutofillSupport().clearAutoFill();
}
}
@ -5103,41 +5109,6 @@ public class GeckoSession implements Parcelable {
@UiThread
default void updateCursorAnchorInfo(@NonNull GeckoSession session,
@NonNull CursorAnchorInfo info) {}
/** An auto-fill session has started, usually as a result of loading a page. */
int AUTO_FILL_NOTIFY_STARTED = 0;
/** An auto-fill session has been committed, usually as a result of submitting a form. */
int AUTO_FILL_NOTIFY_COMMITTED = 1;
/** An auto-fill session has been canceled, usually as a result of unloading a page. */
int AUTO_FILL_NOTIFY_CANCELED = 2;
/** A view within the auto-fill session has been added. */
int AUTO_FILL_NOTIFY_VIEW_ADDED = 3;
/** A view within the auto-fill session has been removed. */
int AUTO_FILL_NOTIFY_VIEW_REMOVED = 4;
/** A view within the auto-fill session has been updated (e.g. change in state). */
int AUTO_FILL_NOTIFY_VIEW_UPDATED = 5;
/** A view within the auto-fill session has gained focus. */
int AUTO_FILL_NOTIFY_VIEW_ENTERED = 6;
/** A view within the auto-fill session has lost focus. */
int AUTO_FILL_NOTIFY_VIEW_EXITED = 7;
/**
* Notify that an auto-fill event has occurred. The default implementation forwards the
* notification to the system {@link android.view.autofill.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 #AUTO_FILL_NOTIFY_STARTED
* AUTO_FILL_NOTIFY_*} constants.
* @param virtualId Virtual ID of the target, or {@link android.view.View#NO_ID} if not
* applicable. The ID matches one of the virtual IDs provided by {@link
* SessionTextInput#onProvideAutofillVirtualStructure} and can be used
* with {@link SessionTextInput#autofill}.
*/
@UiThread
default void notifyAutoFill(@NonNull GeckoSession session,
@AutoFillNotification int notification,
int virtualId) {}
}
@Retention(RetentionPolicy.SOURCE)
@ -5145,18 +5116,6 @@ public class GeckoSession implements Parcelable {
TextInputDelegate.RESTART_REASON_CONTENT_CHANGE})
/* package */ @interface RestartReason {}
@Retention(RetentionPolicy.SOURCE)
@IntDef({
TextInputDelegate.AUTO_FILL_NOTIFY_STARTED,
TextInputDelegate.AUTO_FILL_NOTIFY_COMMITTED,
TextInputDelegate.AUTO_FILL_NOTIFY_CANCELED,
TextInputDelegate.AUTO_FILL_NOTIFY_VIEW_ADDED,
TextInputDelegate.AUTO_FILL_NOTIFY_VIEW_REMOVED,
TextInputDelegate.AUTO_FILL_NOTIFY_VIEW_UPDATED,
TextInputDelegate.AUTO_FILL_NOTIFY_VIEW_ENTERED,
TextInputDelegate.AUTO_FILL_NOTIFY_VIEW_EXITED})
/* package */ @interface AutoFillNotification {}
/* package */ void onSurfaceChanged(final Surface surface, final int x, final int y, final int width,
final int height) {
ThreadUtils.assertOnUiThread();
@ -5375,7 +5334,7 @@ public class GeckoSession implements Parcelable {
mViewportTop = scrollY;
mViewportZoom = zoom;
mTextInput.onScreenMetricsUpdated();
getAutofillSupport().onScreenMetricsUpdated();
}
/* protected */ void onWindowBoundsChanged() {
@ -5628,4 +5587,98 @@ public class GeckoSession implements Parcelable {
HistoryDelegate.VISIT_UNRECOVERABLE_ERROR
})
/* package */ @interface VisitFlags {}
@Retention(RetentionPolicy.SOURCE)
@IntDef({
AutofillDelegate.AUTO_FILL_NOTIFY_STARTED,
AutofillDelegate.AUTO_FILL_NOTIFY_COMMITTED,
AutofillDelegate.AUTO_FILL_NOTIFY_CANCELED,
AutofillDelegate.AUTO_FILL_NOTIFY_VIEW_ADDED,
AutofillDelegate.AUTO_FILL_NOTIFY_VIEW_REMOVED,
AutofillDelegate.AUTO_FILL_NOTIFY_VIEW_UPDATED,
AutofillDelegate.AUTO_FILL_NOTIFY_VIEW_ENTERED,
AutofillDelegate.AUTO_FILL_NOTIFY_VIEW_EXITED})
/* package */ @interface AutofillNotification {}
public interface AutofillDelegate {
/** An auto-fill session has started, usually as a result of loading a page. */
int AUTO_FILL_NOTIFY_STARTED = 0;
/** An auto-fill session has been committed, usually as a result of submitting a form. */
int AUTO_FILL_NOTIFY_COMMITTED = 1;
/** An auto-fill session has been canceled, usually as a result of unloading a page. */
int AUTO_FILL_NOTIFY_CANCELED = 2;
/** A view within the auto-fill session has been added. */
int AUTO_FILL_NOTIFY_VIEW_ADDED = 3;
/** A view within the auto-fill session has been removed. */
int AUTO_FILL_NOTIFY_VIEW_REMOVED = 4;
/** A view within the auto-fill session has been updated (e.g. change in state). */
int AUTO_FILL_NOTIFY_VIEW_UPDATED = 5;
/** A view within the auto-fill session has gained focus. */
int AUTO_FILL_NOTIFY_VIEW_ENTERED = 6;
/** A view within the auto-fill session has lost focus. */
int AUTO_FILL_NOTIFY_VIEW_EXITED = 7;
/**
* Notify that an auto-fill 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 #AUTO_FILL_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() {
if (mAutofillSupport == null) {
mAutofillSupport = new AutofillSupport(this);
}
return mAutofillSupport;
}
/**
* Sets the autofill delegate for this session.
*
* @param delegate An instance of {@link AutofillDelegate}.
*/
@UiThread
public void setAutofillDelegate(final @Nullable AutofillDelegate delegate) {
getAutofillSupport().setDelegate(delegate);
}
/**
* @return The current {@link AutofillDelegate} for this session, if any.
*/
@UiThread
public @Nullable AutofillDelegate getAutofillDelegate() {
return getAutofillSupport().getDelegate();
}
/**
* Perform auto-fill using the specified values.
*
* @param values Map of auto-fill IDs to values.
*/
@UiThread
public void autofill(final @NonNull SparseArray<CharSequence> values) {
getAutofillSupport().autofill(values);
}
@TargetApi(23)
@UiThread
public void provideAutofillVirtualStructure(@Nullable final View view,
@NonNull final ViewStructure structure,
final int flags) {
getAutofillSupport().provideAutofillVirtualStructure(view, structure, flags);
}
}

Просмотреть файл

@ -714,7 +714,7 @@ public class GeckoView extends FrameLayout {
super.onProvideAutofillVirtualStructure(structure, flags);
if (mSession != null) {
mSession.getTextInput().onProvideAutofillVirtualStructure(structure, flags);
mSession.provideAutofillVirtualStructure(this, structure, flags);
}
}
@ -734,7 +734,7 @@ public class GeckoView extends FrameLayout {
strValues.put(values.keyAt(i), value.getTextValue());
}
}
mSession.getTextInput().autofill(strValues);
mSession.autofill(strValues);
}
/**

Просмотреть файл

@ -5,37 +5,19 @@
package org.mozilla.geckoview;
import org.mozilla.gecko.GeckoAppShell;
import org.mozilla.gecko.InputMethods;
import org.mozilla.gecko.annotation.WrapForJNI;
import org.mozilla.gecko.IGeckoEditableParent;
import org.mozilla.gecko.NativeQueue;
import org.mozilla.gecko.util.ActivityUtils;
import org.mozilla.gecko.util.BundleEventListener;
import org.mozilla.gecko.util.EventCallback;
import org.mozilla.gecko.util.GeckoBundle;
import org.mozilla.gecko.util.ThreadUtils;
import android.annotation.TargetApi;
import android.app.Activity;
import android.content.Context;
import android.graphics.Matrix;
import android.graphics.Rect;
import android.graphics.RectF;
import android.os.Build;
import android.os.Handler;
import android.support.annotation.AnyThread;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.annotation.UiThread;
import android.text.Editable;
import android.text.InputType;
import android.util.Log;
import android.util.SparseArray;
import android.view.KeyEvent;
import android.view.View;
import android.view.ViewStructure;
import android.view.autofill.AutofillManager;
import android.view.inputmethod.CursorAnchorInfo;
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.ExtractedText;
@ -43,7 +25,12 @@ import android.view.inputmethod.ExtractedTextRequest;
import android.view.inputmethod.InputConnection;
import android.view.inputmethod.InputMethodManager;
import java.util.Locale;
import org.mozilla.gecko.IGeckoEditableParent;
import org.mozilla.gecko.InputMethods;
import org.mozilla.gecko.NativeQueue;
import org.mozilla.gecko.annotation.WrapForJNI;
import org.mozilla.gecko.util.ActivityUtils;
import org.mozilla.gecko.util.ThreadUtils;
/**
* {@code SessionTextInput} handles text input for {@code GeckoSession} through key events or input
@ -238,77 +225,6 @@ public final class SessionTextInput {
imm.updateCursorAnchorInfo(view, info);
}
}
private Rect displayRectForId(@NonNull final GeckoSession session,
@NonNull final int virtualId,
@Nullable final View view) {
final SessionTextInput textInput = session.getTextInput();
Rect contentRect;
if (textInput.mAutoFillNodes.indexOfKey(virtualId) >= 0) {
GeckoBundle element = textInput.mAutoFillNodes
.get(virtualId);
GeckoBundle bounds = element.getBundle("bounds");
contentRect = new Rect(bounds.getInt("left"),
bounds.getInt("top"),
bounds.getInt("right"),
bounds.getInt("bottom"));
final Matrix matrix = new Matrix();
final RectF rectF = new RectF(contentRect);
if (GeckoAppShell.isFennec()) {
session.getClientToScreenMatrix(matrix);
} else {
session.getPageToScreenMatrix(matrix);
}
matrix.mapRect(rectF);
rectF.roundOut(contentRect);
if (DEBUG) {
Log.d(LOGTAG, "Displaying autofill rect at (" + contentRect.left + ", " +
contentRect.top + "), (" + contentRect.right + ", " +
contentRect.bottom + ")");
}
} else {
contentRect = getDummyAutoFillRect(session, true, view);
}
return contentRect;
}
@Override
public void notifyAutoFill(@NonNull final GeckoSession session,
@GeckoSession.AutoFillNotification final int notification,
final int virtualId) {
ThreadUtils.assertOnUiThread();
final View view = session.getTextInput().getView();
if (Build.VERSION.SDK_INT < 26 || view == null) {
return;
}
final AutofillManager manager =
view.getContext().getSystemService(AutofillManager.class);
if (manager == null) {
return;
}
switch (notification) {
case AUTO_FILL_NOTIFY_STARTED:
// This line seems necessary for auto-fill to work on the initial page.
manager.cancel();
break;
case AUTO_FILL_NOTIFY_COMMITTED:
manager.commit();
break;
case AUTO_FILL_NOTIFY_CANCELED:
manager.cancel();
break;
case AUTO_FILL_NOTIFY_VIEW_ENTERED:
manager.notifyViewEntered(view, virtualId, displayRectForId(session, virtualId, view));
break;
case AUTO_FILL_NOTIFY_VIEW_EXITED:
manager.notifyViewExited(view, virtualId);
break;
}
}
}
private final GeckoSession mSession;
@ -316,36 +232,12 @@ public final class SessionTextInput {
private final GeckoEditable mEditable;
private InputConnectionClient mInputConnection;
private GeckoSession.TextInputDelegate mDelegate;
// Auto-fill nodes.
private SparseArray<GeckoBundle> mAutoFillNodes;
private SparseArray<EventCallback> mAutoFillRoots;
private int mAutoFillFocusedId = View.NO_ID;
private int mAutoFillFocusedRoot = View.NO_ID;
/* package */ SessionTextInput(final @NonNull GeckoSession session,
final @NonNull NativeQueue queue) {
mSession = session;
mQueue = queue;
mEditable = new GeckoEditable(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);
}
/* package */ void onWindowChanged(final GeckoSession.Window window) {
@ -529,314 +421,4 @@ public final class SessionTextInput {
}
return mDelegate;
}
/*package*/ void onScreenMetricsUpdated() {
if (mAutoFillFocusedId != View.NO_ID) {
getDelegate().notifyAutoFill(
mSession, GeckoSession.TextInputDelegate.AUTO_FILL_NOTIFY_VIEW_ENTERED, mAutoFillFocusedId);
}
}
/**
* Fill the specified {@link ViewStructure} with auto-fill fields from the current page.
*
* @param structure Structure to be filled.
* @param flags Zero or a combination of {@link View#AUTOFILL_FLAG_INCLUDE_NOT_IMPORTANT_VIEWS
* AUTOFILL_FLAG_*} constants.
*/
@TargetApi(23)
@UiThread
public void onProvideAutofillVirtualStructure(@NonNull final ViewStructure structure,
final int flags) {
final View view = getView();
if (view != null) {
structure.setClassName(view.getClass().getName());
}
structure.setEnabled(true);
structure.setVisibility(View.VISIBLE);
final Rect rect = getDummyAutoFillRect(mSession, false, null);
structure.setDimens(rect.left, rect.top, 0, 0, rect.width(), rect.height());
if (mAutoFillRoots == null) {
structure.setChildCount(0);
return;
}
final int size = mAutoFillRoots.size();
structure.setChildCount(size);
for (int i = 0; i < size; i++) {
final int id = mAutoFillRoots.keyAt(i);
final GeckoBundle root = mAutoFillNodes.get(id);
fillAutoFillStructure(view, id, root, structure.newChild(i), rect);
}
}
/**
* Perform auto-fill using the specified values.
*
* @param values Map of auto-fill IDs to values.
*/
@UiThread
public void autofill(final @NonNull SparseArray<CharSequence> 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, "performAutoFill(" + 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);
}
}
@TargetApi(23)
private void fillAutoFillStructure(@Nullable final View view, final int id,
@NonNull final GeckoBundle bundle,
@NonNull final ViewStructure structure,
@NonNull final Rect rect) {
if (mAutoFillRoots == null) {
return;
}
if (DEBUG) {
Log.d(LOGTAG, "fillAutoFillStructure(" + id + ')');
}
if (Build.VERSION.SDK_INT >= 26) {
if (view != null) {
structure.setAutofillId(view.getAutofillId(), id);
}
structure.setWebDomain(bundle.getString("origin"));
}
structure.setId(id, null, null, null);
if (mAutoFillFocusedRoot != View.NO_ID &&
mAutoFillFocusedRoot == bundle.getInt("root", View.NO_ID)) {
structure.setDimens(0, 0, 0, 0, rect.width(), rect.height());
}
final GeckoBundle[] children = bundle.getBundleArray("children");
if (children != null) {
structure.setChildCount(children.length);
for (int i = 0; i < children.length; i++) {
final GeckoBundle childBundle = children[i];
final int childId = childBundle.getInt("id");
final ViewStructure childStructure = structure.newChild(i);
fillAutoFillStructure(view, childId, childBundle, childStructure, rect);
mAutoFillNodes.append(childId, childBundle);
}
}
String tag = bundle.getString("tag", "");
final String type = bundle.getString("type", "text");
if (Build.VERSION.SDK_INT >= 26) {
final GeckoBundle attrs = bundle.getBundle("attributes");
final ViewStructure.HtmlInfo.Builder builder =
structure.newHtmlInfoBuilder(tag.toLowerCase(Locale.US));
for (final String key : attrs.keys()) {
builder.addAttribute(key, String.valueOf(attrs.get(key)));
}
structure.setHtmlInfo(builder.build());
}
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");
structure.setClassName("android.widget.EditText");
structure.setEnabled(!disabled);
structure.setFocusable(!disabled);
structure.setFocused(id == mAutoFillFocusedId);
structure.setVisibility(View.VISIBLE);
if (Build.VERSION.SDK_INT >= 26) {
structure.setAutofillType(View.AUTOFILL_TYPE_TEXT);
}
break;
}
default:
if (children != null) {
structure.setClassName("android.view.ViewGroup");
} else {
structure.setClassName("android.view.View");
}
break;
}
if (Build.VERSION.SDK_INT >= 26 && "INPUT".equals(tag)) {
// LastPass will fill password to the feild that setAutofillHints is unset and setInputType is set.
switch (type) {
case "email":
structure.setAutofillHints(new String[] { View.AUTOFILL_HINT_EMAIL_ADDRESS });
structure.setInputType(InputType.TYPE_CLASS_TEXT |
InputType.TYPE_TEXT_VARIATION_WEB_EMAIL_ADDRESS);
break;
case "number":
structure.setInputType(InputType.TYPE_CLASS_NUMBER);
break;
case "password":
structure.setAutofillHints(new String[] { View.AUTOFILL_HINT_PASSWORD });
structure.setInputType(InputType.TYPE_CLASS_TEXT |
InputType.TYPE_TEXT_VARIATION_WEB_PASSWORD);
break;
case "tel":
structure.setAutofillHints(new String[] { View.AUTOFILL_HINT_PHONE });
structure.setInputType(InputType.TYPE_CLASS_PHONE);
break;
case "url":
structure.setInputType(InputType.TYPE_CLASS_TEXT |
InputType.TYPE_TEXT_VARIATION_URI);
break;
case "text":
final String autofillhint = bundle.getString("autofillhint", "");
if (autofillhint.equals("username")) {
structure.setAutofillHints(new String[] { View.AUTOFILL_HINT_USERNAME });
structure.setInputType(InputType.TYPE_CLASS_TEXT |
InputType.TYPE_TEXT_VARIATION_WEB_EDIT_TEXT);
}
break;
}
}
}
/* package */ void addAutoFill(@NonNull final GeckoBundle message,
@NonNull final EventCallback callback) {
if (Build.VERSION.SDK_INT < 23) {
return;
}
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 (initializing) {
getDelegate().notifyAutoFill(
mSession, GeckoSession.TextInputDelegate.AUTO_FILL_NOTIFY_STARTED, id);
} else {
getDelegate().notifyAutoFill(
mSession, GeckoSession.TextInputDelegate.AUTO_FILL_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;
getDelegate().notifyAutoFill(
mSession, GeckoSession.TextInputDelegate.AUTO_FILL_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 (mAutoFillFocusedId != View.NO_ID) {
getDelegate().notifyAutoFill(
mSession, GeckoSession.TextInputDelegate.AUTO_FILL_NOTIFY_VIEW_EXITED,
mAutoFillFocusedId);
}
mAutoFillFocusedId = id;
mAutoFillFocusedRoot = root;
}
/* 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;
}
}