Commit 5be96c97 authored by Tao Bai's avatar Tao Bai Committed by Commit Bot

Android Autofill:Display the AutofillPopup for the datalist

The AutofillPopup is used for displaying the datalist options only.

The most of implementation was copied from AwAutofillClient, we
intentionally used the same UI as AwAutofillClient to avoid any
new UI change for WebView.

The integration test was added to verify the popup work.

The ForTesting method isn't called in production code, per discussion on
https://chromium-review.googlesource.com/c/chromium/src/+/2244459
Using No-Try to bypass the android-binary-size check failure

Bug:949555

hange-Id: Id8537b6232edca09e243e807cf8f20e5a3bfc5f3
Change-Id: Id8537b6232edca09e243e807cf8f20e5a3bfc5f3

No-Try: True
Change-Id: Id8537b6232edca09e243e807cf8f20e5a3bfc5f3
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2242209
Commit-Queue: Tao Bai <michaelbai@chromium.org>
Reviewed-by: default avatarTommy Martino <tmartino@chromium.org>
Reviewed-by: default avatarDominic Battré <battre@chromium.org>
Cr-Commit-Position: refs/heads/master@{#778396}
parent b17b2a5b
......@@ -52,12 +52,14 @@ import org.chromium.base.test.util.FlakyTest;
import org.chromium.base.test.util.MetricsUtils;
import org.chromium.base.test.util.MinAndroidSdkLevel;
import org.chromium.components.autofill.AutofillManagerWrapper;
import org.chromium.components.autofill.AutofillPopup;
import org.chromium.components.autofill.AutofillProvider;
import org.chromium.components.autofill.AutofillProviderImpl;
import org.chromium.components.autofill.AutofillProviderUMA;
import org.chromium.content_public.browser.UiThreadTaskTraits;
import org.chromium.content_public.browser.test.util.DOMUtils;
import org.chromium.content_public.browser.test.util.TestThreadUtils;
import org.chromium.content_public.browser.test.util.TouchCommon;
import org.chromium.net.test.util.TestWebServer;
import java.io.ByteArrayInputStream;
......@@ -756,6 +758,7 @@ public class AwAutofillTest {
private int mSubmissionSource;
private TestAutofillManagerWrapper mTestAutofillManagerWrapper;
private AwAutofillSessionUMATestHelper mUMATestHelper;
private AutofillProviderImpl mAutofillProviderImpl;
@Before
public void setUp() throws Exception {
......@@ -768,8 +771,9 @@ public class AwAutofillTest {
public AutofillProvider createAutofillProvider(
Context context, ViewGroup containerView) {
mTestAutofillManagerWrapper = new TestAutofillManagerWrapper(context);
return new AutofillProviderImpl(containerView, mTestAutofillManagerWrapper,
context, "AwAutofillTest");
mAutofillProviderImpl = new AutofillProviderImpl(containerView,
mTestAutofillManagerWrapper, context, "AwAutofillTest");
return mAutofillProviderImpl;
}
});
mAwContents = mTestContainerView.getAwContents();
......@@ -779,6 +783,7 @@ public class AwAutofillTest {
@After
public void tearDown() {
mWebServer.shutdown();
mAutofillProviderImpl = null;
}
@Test
......@@ -1892,6 +1897,44 @@ public class AwAutofillTest {
cnt, new Integer[] {AUTOFILL_VIEW_ENTERED, AUTOFILL_VALUE_CHANGED});
}
@Test
@SmallTest
@Feature({"AndroidWebView"})
public void testDatalistPopup() throws Throwable {
final String data = "<html><head></head><body><form action='a.html' name='formname'>"
+ "<input type='text' id='text1' name='username'>"
+ "<input list='datalist_id' name='count' id='text2'/><datalist id='datalist_id'>"
+ "<option value='A1'>one</option><option value='A2'>two</option></datalist>"
+ "</form></body></html>";
final String url = mWebServer.setResponse(FILE, data, null);
loadUrlSync(url);
int cnt = 0;
executeJavaScriptAndWaitForResult("document.getElementById('text2').select();");
dispatchDownAndUpKeyEvents(KeyEvent.KEYCODE_A);
pollDatalistPopupShown();
TouchCommon.singleClickView(
mAutofillProviderImpl.getDatalistPopupForTesting().getListView().getChildAt(1));
// Verify the selection accepted by renderer.
pollJavascriptResult("document.getElementById('text2').value;", "\"A2\"");
}
private void pollJavascriptResult(String script, String expectedResult) throws Throwable {
AwActivityTestRule.pollInstrumentationThread(() -> {
try {
return expectedResult.equals(executeJavaScriptAndWaitForResult(script));
} catch (Throwable e) {
return false;
}
});
}
private void pollDatalistPopupShown() {
AwActivityTestRule.pollInstrumentationThread(() -> {
AutofillPopup popup = mAutofillProviderImpl.getDatalistPopupForTesting();
return popup != null && popup.getListView().getChildCount() > 0;
});
}
private void scrollToBottom() {
TestThreadUtils.runOnUiThreadBlocking(
() -> { mTestContainerView.scrollTo(0, mTestContainerView.getHeight()); });
......
......@@ -170,6 +170,7 @@ instrumentation_test_apk("webview_instrumentation_test_apk") {
"//android_webview/test/embedded_test_server:aw_net_java_test_support",
"//base:base_java",
"//base:base_java_test_support",
"//components/autofill/android:autofill_java",
"//components/autofill/android:provider_java",
"//components/autofill/core/common/mojom:mojo_types_java",
"//components/background_task_scheduler:background_task_scheduler_task_ids_java",
......
......@@ -58,6 +58,7 @@ android_library("autofill_java") {
android_library("provider_java") {
deps = [
":autofill_java",
"//base:base_java",
"//base:jni_java",
"//components/autofill/core/common/mojom:mojo_types_java",
......@@ -99,5 +100,6 @@ static_library("provider") {
":jni_headers",
"//components/autofill/core/browser:browser",
"//content/public/browser",
"//ui/android",
]
}
......@@ -16,6 +16,7 @@
#include "components/autofill/core/common/autofill_constants.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/web_contents.h"
#include "ui/android/window_android.h"
#include "ui/gfx/geometry/rect_f.h"
using base::android::AttachCurrentThread;
......@@ -24,6 +25,7 @@ using base::android::ConvertUTF16ToJavaString;
using base::android::ConvertUTF8ToJavaString;
using base::android::JavaRef;
using base::android::ScopedJavaLocalRef;
using base::android::ToJavaArrayOfStrings;
using content::BrowserThread;
using content::WebContents;
using gfx::RectF;
......@@ -86,6 +88,21 @@ void AutofillProviderAndroid::OnQueryFormFieldAutofill(
// ignored if the form is same.
if (ShouldStartNewSession(handler, form))
StartNewSession(handler, form, field, bounding_box);
JNIEnv* env = AttachCurrentThread();
ScopedJavaLocalRef<jobject> obj = java_ref_.get(env);
if (obj.is_null())
return;
if (!field.datalist_values.empty()) {
ScopedJavaLocalRef<jobjectArray> jdatalist_values =
ToJavaArrayOfStrings(env, field.datalist_values);
ScopedJavaLocalRef<jobjectArray> jdatalist_labels =
ToJavaArrayOfStrings(env, field.datalist_labels);
Java_AutofillProvider_showDatalistPopup(
env, obj, jdatalist_values, jdatalist_labels,
field.text_direction == base::i18n::RIGHT_TO_LEFT);
}
}
bool AutofillProviderAndroid::ShouldStartNewSession(
......@@ -152,6 +169,21 @@ void AutofillProviderAndroid::OnAcceptDataListSuggestion(JNIEnv* env,
}
}
void AutofillProviderAndroid::SetAnchorViewRect(JNIEnv* env,
jobject jcaller,
jobject anchor_view,
jfloat x,
jfloat y,
jfloat width,
jfloat height) {
ui::ViewAndroid* view_android = web_contents_->GetNativeView();
if (!view_android)
return;
view_android->SetAnchorRect(ScopedJavaLocalRef<jobject>(env, anchor_view),
gfx::RectF(x, y, width, height));
}
void AutofillProviderAndroid::OnTextFieldDidChange(
AutofillHandlerProxy* handler,
const FormData& form,
......
......@@ -75,6 +75,14 @@ class AutofillProviderAndroid : public AutofillProvider {
void OnAutofillAvailable(JNIEnv* env, jobject jcaller, jobject form_data);
void OnAcceptDataListSuggestion(JNIEnv* env, jobject jcaller, jstring value);
void SetAnchorViewRect(JNIEnv* env,
jobject jcaller,
jobject anchor_view,
jfloat x,
jfloat y,
jfloat width,
jfloat height);
private:
void FireSuccessfulSubmission(mojom::SubmissionSource source);
void OnFocusChanged(bool focus_on_form,
......
......@@ -4,7 +4,9 @@
package org.chromium.components.autofill;
import android.graphics.RectF;
import android.util.SparseArray;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewStructure;
import android.view.autofill.AutofillValue;
......@@ -142,6 +144,11 @@ public abstract class AutofillProvider {
nativeAutofillProvider, AutofillProvider.this, value);
}
protected void setAnchorViewRect(long nativeAutofillProvider, View anchorView, RectF rect) {
AutofillProviderJni.get().setAnchorViewRect(nativeAutofillProvider, AutofillProvider.this,
anchorView, rect.left, rect.top, rect.width(), rect.height());
}
/**
* Invoked when current query need to be reset.
*/
......@@ -157,6 +164,10 @@ public abstract class AutofillProvider {
@CalledByNative
protected abstract void hidePopup();
@CalledByNative
protected abstract void showDatalistPopup(
String[] datalistValues, String[] datalistLabels, boolean isRtl);
@NativeMethods
interface Natives {
void onAutofillAvailable(
......@@ -164,5 +175,8 @@ public abstract class AutofillProvider {
void onAcceptDataListSuggestion(
long nativeAutofillProviderAndroid, AutofillProvider caller, String value);
void setAnchorViewRect(long nativeAutofillProviderAndroid, AutofillProvider caller,
View anchorView, float x, float y, float width, float height);
}
}
......@@ -19,12 +19,17 @@ import android.view.autofill.AutofillValue;
import androidx.annotation.VisibleForTesting;
import org.chromium.base.ContextUtils;
import org.chromium.base.Log;
import org.chromium.base.StrictModeContext;
import org.chromium.base.ThreadUtils;
import org.chromium.base.annotations.VerifiesOnO;
import org.chromium.base.metrics.ScopedSysTraceEvent;
import org.chromium.components.version_info.VersionConstants;
import org.chromium.content_public.browser.WebContents;
import org.chromium.content_public.browser.WebContentsAccessibility;
import org.chromium.ui.DropdownItem;
import org.chromium.ui.base.ViewAndroidDelegate;
import org.chromium.ui.base.WindowAndroid;
import org.chromium.ui.display.DisplayAndroid;
......@@ -246,6 +251,10 @@ public class AutofillProviderImpl extends AutofillProvider {
private AutofillProviderUMA mAutofillUMA;
private AutofillManagerWrapper.InputUIObserver mInputUIObserver;
private long mAutofillTriggeredTimeMillis;
private Context mContext;
private AutofillPopup mDatalistPopup;
private WebContentsAccessibility mWebContentsAccessibility;
private View mAnchorView;
public AutofillProviderImpl(Context context, ViewGroup containerView, String providerName) {
this(containerView, new AutofillManagerWrapper(context), context, providerName);
......@@ -272,6 +281,7 @@ public class AutofillProviderImpl extends AutofillProvider {
}
};
mAutofillManager.addInputUIObserver(mInputUIObserver);
mContext = context;
}
}
......@@ -448,7 +458,15 @@ public class AutofillProviderImpl extends AutofillProvider {
}
@Override
protected void hidePopup() {}
protected void hidePopup() {
if (mDatalistPopup != null) {
mDatalistPopup.dismiss();
mDatalistPopup = null;
}
if (mWebContentsAccessibility != null) {
mWebContentsAccessibility.onAutofillPopupDismissed();
}
}
private void notifyViewExitBeforeDestroyRequest() {
if (mRequest == null) return;
......@@ -493,6 +511,86 @@ public class AutofillProviderImpl extends AutofillProvider {
}
}
@Override
protected void showDatalistPopup(
String[] datalistValues, String[] datalistLabels, boolean isRtl) {
if (mRequest == null) return;
FocusField focusField = mRequest.getFocusField();
if (focusField != null) {
showDatalistPopup(datalistValues, datalistLabels,
mRequest.getField(focusField.fieldIndex).getBounds(), isRtl);
}
}
/**
* Display the simplest popup for the datalist. This is same as WebView's datalist popup in
* Android pre-o. No suggestion from the autofill service will be presented, No advance
* features of AutofillPopup are used.
*/
private void showDatalistPopup(
String[] datalistValues, String[] datalistLabels, RectF bounds, boolean isRtl) {
final AutofillSuggestion[] suggestions = new AutofillSuggestion[datalistValues.length];
for (int i = 0; i < suggestions.length; i++) {
suggestions[i] = new AutofillSuggestion(datalistValues[i], datalistLabels[i],
DropdownItem.NO_ICON, false /* isIconAtLeft */, i, false /* isDeletable */,
false /* isMultilineLabel */, false /* isBoldLabel */);
}
if (mWebContentsAccessibility == null) {
mWebContentsAccessibility = WebContentsAccessibility.fromWebContents(mWebContents);
}
if (mDatalistPopup == null) {
if (ContextUtils.activityFromContext(mContext) == null) return;
ViewAndroidDelegate delegate = mWebContents.getViewAndroidDelegate();
if (mAnchorView == null) mAnchorView = delegate.acquireView();
setAnchorViewRect(bounds);
try (StrictModeContext ignored = StrictModeContext.allowDiskReads()) {
mDatalistPopup = new AutofillPopup(mContext, mAnchorView, new AutofillDelegate() {
@Override
public void dismissed() {
onDatalistPopupDismissed();
}
@Override
public void suggestionSelected(int listIndex) {
onSuggestionSelected(suggestions[listIndex].getLabel());
}
@Override
public void deleteSuggestion(int listIndex) {}
@Override
public void accessibilityFocusCleared() {
mWebContentsAccessibility.onAutofillPopupAccessibilityFocusCleared();
}
});
} catch (RuntimeException e) {
// Deliberately swallowing exception because bad framework implementation can
// throw exceptions in ListPopupWindow constructor.
onDatalistPopupDismissed();
return;
}
}
mDatalistPopup.filterAndShow(suggestions, isRtl, false);
if (mWebContentsAccessibility != null) {
mWebContentsAccessibility.onAutofillPopupDisplayed(mDatalistPopup.getListView());
}
}
private void onDatalistPopupDismissed() {
ViewAndroidDelegate delegate = mWebContents.getViewAndroidDelegate();
delegate.removeView(mAnchorView);
mAnchorView = null;
}
private void onSuggestionSelected(String value) {
acceptDataListSuggestion(mNativeAutofillProvider, value);
hidePopup();
}
private void setAnchorViewRect(RectF rect) {
setAnchorViewRect(mNativeAutofillProvider, mAnchorView, rect);
}
@Override
protected void reset() {
// We don't need to reset anything here, it should be safe to cancel
......@@ -537,6 +635,11 @@ public class AutofillProviderImpl extends AutofillProvider {
}
}
@VisibleForTesting
public AutofillPopup getDatalistPopupForTesting() {
return mDatalistPopup;
}
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
public Rect transformToWindowBounds(RectF rect) {
// Convert bounds to device pixel.
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment