diff options
author | Allan Sandfeld Jensen <allan.jensen@qt.io> | 2020-10-12 14:27:29 +0200 |
---|---|---|
committer | Allan Sandfeld Jensen <allan.jensen@qt.io> | 2020-10-13 09:35:20 +0000 |
commit | c30a6232df03e1efbd9f3b226777b07e087a1122 (patch) | |
tree | e992f45784689f373bcc38d1b79a239ebe17ee23 /chromium/components/autofill/android/provider | |
parent | 7b5b123ac58f58ffde0f4f6e488bcd09aa4decd3 (diff) | |
download | qtwebengine-chromium-85-based.tar.gz |
BASELINE: Update Chromium to 85.0.4183.14085-based
Change-Id: Iaa42f4680837c57725b1344f108c0196741f6057
Reviewed-by: Allan Sandfeld Jensen <allan.jensen@qt.io>
Diffstat (limited to 'chromium/components/autofill/android/provider')
16 files changed, 2583 insertions, 0 deletions
diff --git a/chromium/components/autofill/android/provider/BUILD.gn b/chromium/components/autofill/android/provider/BUILD.gn new file mode 100644 index 00000000000..263b88afd5e --- /dev/null +++ b/chromium/components/autofill/android/provider/BUILD.gn @@ -0,0 +1,53 @@ +# Copyright 2020 The Chromium Authors. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +import("//build/config/android/rules.gni") +import("//build/config/locales.gni") + +android_library("java") { + deps = [ + "//base:base_java", + "//base:jni_java", + "//components/autofill/android:autofill_java", + "//components/autofill/core/common/mojom:mojo_types_java", + "//components/version_info/android:version_constants_java", + "//content/public/android:content_java", + "//third_party/android_deps:androidx_annotation_annotation_java", + "//ui/android:ui_java", + ] + annotation_processor_deps = [ "//base/android/jni_generator:jni_processor" ] + sources = [ + "java/src/org/chromium/components/autofill/AutofillActionModeCallback.java", + "java/src/org/chromium/components/autofill/AutofillManagerWrapper.java", + "java/src/org/chromium/components/autofill/AutofillProvider.java", + "java/src/org/chromium/components/autofill/AutofillProviderUMA.java", + "java/src/org/chromium/components/autofill/FormData.java", + "java/src/org/chromium/components/autofill/FormFieldData.java", + ] +} + +generate_jni("jni_headers") { + sources = [ + "java/src/org/chromium/components/autofill/AutofillProvider.java", + "java/src/org/chromium/components/autofill/FormData.java", + "java/src/org/chromium/components/autofill/FormFieldData.java", + ] +} + +static_library("provider") { + sources = [ + "autofill_provider_android.cc", + "autofill_provider_android.h", + "form_data_android.cc", + "form_data_android.h", + "form_field_data_android.cc", + "form_field_data_android.h", + ] + deps = [ + ":jni_headers", + "//components/autofill/core/browser:browser", + "//content/public/browser", + "//ui/android", + ] +} diff --git a/chromium/components/autofill/android/provider/OWNERS b/chromium/components/autofill/android/provider/OWNERS new file mode 100644 index 00000000000..44a22b15980 --- /dev/null +++ b/chromium/components/autofill/android/provider/OWNERS @@ -0,0 +1,2 @@ +michaelbai@chromium.org + diff --git a/chromium/components/autofill/android/provider/autofill_provider_android.cc b/chromium/components/autofill/android/provider/autofill_provider_android.cc new file mode 100644 index 00000000000..408ff96c130 --- /dev/null +++ b/chromium/components/autofill/android/provider/autofill_provider_android.cc @@ -0,0 +1,405 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "components/autofill/android/provider/autofill_provider_android.h" + +#include <memory> + +#include "base/android/jni_android.h" +#include "base/android/jni_array.h" +#include "base/android/jni_string.h" +#include "components/autofill/android/provider/form_data_android.h" +#include "components/autofill/android/provider/jni_headers/AutofillProvider_jni.h" +#include "components/autofill/core/browser/autofill_driver.h" +#include "components/autofill/core/browser/autofill_handler_proxy.h" +#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; +using base::android::ConvertJavaStringToUTF16; +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; + +namespace autofill { + +using mojom::SubmissionSource; + +AutofillProviderAndroid::AutofillProviderAndroid( + const JavaRef<jobject>& jcaller, + content::WebContents* web_contents) + : id_(kNoQueryId), web_contents_(web_contents), check_submission_(false) { + OnJavaAutofillProviderChanged(AttachCurrentThread(), jcaller); +} + +void AutofillProviderAndroid::OnJavaAutofillProviderChanged( + JNIEnv* env, + const JavaRef<jobject>& jcaller) { + // If the current Java object isn't null (e.g., because it hasn't been + // garbage-collected yet), clear its reference to this object. + ScopedJavaLocalRef<jobject> obj = java_ref_.get(env); + if (!obj.is_null()) { + Java_AutofillProvider_setNativeAutofillProvider(env, obj, 0); + } + + java_ref_ = JavaObjectWeakGlobalRef(env, jcaller); + + // If the new Java object isn't null, set its native object to |this|. + obj = java_ref_.get(env); + if (!obj.is_null()) { + Java_AutofillProvider_setNativeAutofillProvider( + env, obj, reinterpret_cast<jlong>(this)); + } +} + +AutofillProviderAndroid::~AutofillProviderAndroid() { + JNIEnv* env = AttachCurrentThread(); + ScopedJavaLocalRef<jobject> obj = java_ref_.get(env); + if (obj.is_null()) + return; + + // Remove the reference to this object on the Java side. + Java_AutofillProvider_setNativeAutofillProvider(env, obj, 0); +} + +void AutofillProviderAndroid::OnQueryFormFieldAutofill( + AutofillHandlerProxy* handler, + int32_t id, + const FormData& form, + const FormFieldData& field, + const gfx::RectF& bounding_box, + bool /*unused_autoselect_first_suggestion*/) { + // The id isn't passed to Java side because Android API guarantees the + // response is always for current session, so we just use the current id + // in response, see OnAutofillAvailable. + DCHECK_CURRENTLY_ON(BrowserThread::UI); + id_ = id; + + // Focus or field value change will also trigger the query, so it should be + // 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( + AutofillHandlerProxy* handler, + const FormData& form) { + // Only start a new session when form or handler is changed, the change of + // handler indicates query from other frame and a new session is needed. + return !IsCurrentlyLinkedForm(form) || !IsCurrentlyLinkedHandler(handler); +} + +void AutofillProviderAndroid::StartNewSession(AutofillHandlerProxy* handler, + const FormData& form, + const FormFieldData& field, + const gfx::RectF& bounding_box) { + JNIEnv* env = AttachCurrentThread(); + ScopedJavaLocalRef<jobject> obj = java_ref_.get(env); + if (obj.is_null()) + return; + + form_ = std::make_unique<FormDataAndroid>( + form, base::BindRepeating( + &AutofillDriver::TransformBoundingBoxToViewportCoordinates, + base::Unretained(handler->driver()))); + + size_t index; + if (!form_->GetFieldIndex(field, &index)) { + form_.reset(); + return; + } + + FormStructure* form_structure = nullptr; + AutofillField* autofill_field = nullptr; + if (!handler->GetCachedFormAndField(form, field, &form_structure, + &autofill_field)) { + form_structure = nullptr; + } + gfx::RectF transformed_bounding = ToClientAreaBound(bounding_box); + + ScopedJavaLocalRef<jobject> form_obj = form_->GetJavaPeer(form_structure); + handler_ = handler->GetWeakPtr(); + Java_AutofillProvider_startAutofillSession( + env, obj, form_obj, index, transformed_bounding.x(), + transformed_bounding.y(), transformed_bounding.width(), + transformed_bounding.height()); +} + +void AutofillProviderAndroid::OnAutofillAvailable(JNIEnv* env, + jobject jcaller, + jobject formData) { + DCHECK_CURRENTLY_ON(BrowserThread::UI); + if (handler_) { + const FormData& form = form_->GetAutofillValues(); + SendFormDataToRenderer(handler_.get(), id_, form); + } +} + +void AutofillProviderAndroid::OnAcceptDataListSuggestion(JNIEnv* env, + jobject jcaller, + jstring value) { + DCHECK_CURRENTLY_ON(BrowserThread::UI); + if (auto* handler = handler_.get()) { + RendererShouldAcceptDataListSuggestion( + handler, ConvertJavaStringToUTF16(env, value)); + } +} + +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, + const FormFieldData& field, + const gfx::RectF& bounding_box, + const base::TimeTicks timestamp) { + FireFormFieldDidChanged(handler, form, field, bounding_box); +} + +void AutofillProviderAndroid::OnTextFieldDidScroll( + AutofillHandlerProxy* handler, + const FormData& form, + const FormFieldData& field, + const gfx::RectF& bounding_box) { + DCHECK_CURRENTLY_ON(BrowserThread::UI); + size_t index; + if (!IsCurrentlyLinkedHandler(handler) || !IsCurrentlyLinkedForm(form) || + !form_->GetSimilarFieldIndex(field, &index)) + return; + + form_->OnFormFieldDidChange(index, field.value); + JNIEnv* env = AttachCurrentThread(); + ScopedJavaLocalRef<jobject> obj = java_ref_.get(env); + if (obj.is_null()) + return; + + gfx::RectF transformed_bounding = ToClientAreaBound(bounding_box); + Java_AutofillProvider_onTextFieldDidScroll( + env, obj, index, transformed_bounding.x(), transformed_bounding.y(), + transformed_bounding.width(), transformed_bounding.height()); +} + +void AutofillProviderAndroid::OnSelectControlDidChange( + AutofillHandlerProxy* handler, + const FormData& form, + const FormFieldData& field, + const gfx::RectF& bounding_box) { + if (ShouldStartNewSession(handler, form)) + StartNewSession(handler, form, field, bounding_box); + FireFormFieldDidChanged(handler, form, field, bounding_box); +} + +void AutofillProviderAndroid::FireSuccessfulSubmission( + SubmissionSource source) { + JNIEnv* env = AttachCurrentThread(); + ScopedJavaLocalRef<jobject> obj = java_ref_.get(env); + if (obj.is_null()) + return; + + Java_AutofillProvider_onFormSubmitted(env, obj, (int)source); + Reset(); +} + +void AutofillProviderAndroid::OnFormSubmitted(AutofillHandlerProxy* handler, + const FormData& form, + bool known_success, + SubmissionSource source) { + DCHECK_CURRENTLY_ON(BrowserThread::UI); + if (!IsCurrentlyLinkedHandler(handler) || !IsCurrentlyLinkedForm(form)) + return; + + if (known_success || source == SubmissionSource::FORM_SUBMISSION) { + FireSuccessfulSubmission(source); + return; + } + + check_submission_ = true; + pending_submission_source_ = source; +} + +void AutofillProviderAndroid::OnFocusNoLongerOnForm( + AutofillHandlerProxy* handler) { + DCHECK_CURRENTLY_ON(BrowserThread::UI); + if (!IsCurrentlyLinkedHandler(handler)) + return; + + OnFocusChanged(false, 0, RectF()); +} + +void AutofillProviderAndroid::OnFocusOnFormField( + AutofillHandlerProxy* handler, + const FormData& form, + const FormFieldData& field, + const gfx::RectF& bounding_box) { + DCHECK_CURRENTLY_ON(BrowserThread::UI); + + size_t index; + if (!IsCurrentlyLinkedHandler(handler) || !IsCurrentlyLinkedForm(form) || + !form_->GetSimilarFieldIndex(field, &index)) + return; + + // Because this will trigger a suggestion query, set request id to browser + // initiated request. + id_ = kNoQueryId; + + OnFocusChanged(true, index, ToClientAreaBound(bounding_box)); +} + +void AutofillProviderAndroid::OnFocusChanged(bool focus_on_form, + size_t index, + const gfx::RectF& bounding_box) { + DCHECK_CURRENTLY_ON(BrowserThread::UI); + JNIEnv* env = AttachCurrentThread(); + ScopedJavaLocalRef<jobject> obj = java_ref_.get(env); + if (obj.is_null()) + return; + + Java_AutofillProvider_onFocusChanged( + env, obj, focus_on_form, index, bounding_box.x(), bounding_box.y(), + bounding_box.width(), bounding_box.height()); +} + +void AutofillProviderAndroid::FireFormFieldDidChanged( + AutofillHandlerProxy* handler, + const FormData& form, + const FormFieldData& field, + const gfx::RectF& bounding_box) { + DCHECK_CURRENTLY_ON(BrowserThread::UI); + size_t index; + if (!IsCurrentlyLinkedHandler(handler) || !IsCurrentlyLinkedForm(form) || + !form_->GetSimilarFieldIndex(field, &index)) + return; + + form_->OnFormFieldDidChange(index, field.value); + JNIEnv* env = AttachCurrentThread(); + ScopedJavaLocalRef<jobject> obj = java_ref_.get(env); + if (obj.is_null()) + return; + + gfx::RectF transformed_bounding = ToClientAreaBound(bounding_box); + Java_AutofillProvider_onFormFieldDidChange( + env, obj, index, transformed_bounding.x(), transformed_bounding.y(), + transformed_bounding.width(), transformed_bounding.height()); +} + +void AutofillProviderAndroid::OnDidFillAutofillFormData( + AutofillHandlerProxy* handler, + const FormData& form, + base::TimeTicks timestamp) { + DCHECK_CURRENTLY_ON(BrowserThread::UI); + if (handler != handler_.get() || !IsCurrentlyLinkedForm(form)) + return; + + JNIEnv* env = AttachCurrentThread(); + ScopedJavaLocalRef<jobject> obj = java_ref_.get(env); + if (obj.is_null()) + return; + + Java_AutofillProvider_onDidFillAutofillFormData(env, obj); +} + +void AutofillProviderAndroid::OnFormsSeen(AutofillHandlerProxy* handler, + const std::vector<FormData>& forms, + const base::TimeTicks) { + handler_for_testing_ = handler->GetWeakPtr(); + if (!check_submission_) + return; + + if (handler != handler_.get()) + return; + + if (form_.get() == nullptr) + return; + + for (auto const& form : forms) { + if (form_->SimilarFormAs(form)) + return; + } + // The form_ disappeared after it was submitted, we consider the submission + // succeeded. + FireSuccessfulSubmission(pending_submission_source_); +} + +void AutofillProviderAndroid::OnHidePopup(AutofillHandlerProxy* handler) { + DCHECK_CURRENTLY_ON(BrowserThread::UI); + if (handler == handler_.get()) { + JNIEnv* env = AttachCurrentThread(); + ScopedJavaLocalRef<jobject> obj = java_ref_.get(env); + if (obj.is_null()) + return; + + Java_AutofillProvider_hidePopup(env, obj); + } +} + +void AutofillProviderAndroid::Reset(AutofillHandlerProxy* handler) { + DCHECK_CURRENTLY_ON(BrowserThread::UI); + if (handler == handler_.get()) { + JNIEnv* env = AttachCurrentThread(); + ScopedJavaLocalRef<jobject> obj = java_ref_.get(env); + if (obj.is_null()) + return; + + Java_AutofillProvider_reset(env, obj); + } +} + +bool AutofillProviderAndroid::IsCurrentlyLinkedHandler( + AutofillHandlerProxy* handler) { + return handler == handler_.get(); +} + +bool AutofillProviderAndroid::IsCurrentlyLinkedForm(const FormData& form) { + return form_ && form_->SimilarFormAs(form); +} + +gfx::RectF AutofillProviderAndroid::ToClientAreaBound( + const gfx::RectF& bounding_box) { + gfx::Rect client_area = web_contents_->GetContainerBounds(); + return bounding_box + client_area.OffsetFromOrigin(); +} + +void AutofillProviderAndroid::Reset() { + form_.reset(nullptr); + id_ = kNoQueryId; + check_submission_ = false; +} + +} // namespace autofill diff --git a/chromium/components/autofill/android/provider/autofill_provider_android.h b/chromium/components/autofill/android/provider/autofill_provider_android.h new file mode 100644 index 00000000000..b0830e2338e --- /dev/null +++ b/chromium/components/autofill/android/provider/autofill_provider_android.h @@ -0,0 +1,127 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef COMPONENTS_AUTOFILL_ANDROID_PROVIDER_AUTOFILL_PROVIDER_ANDROID_H_ +#define COMPONENTS_AUTOFILL_ANDROID_PROVIDER_AUTOFILL_PROVIDER_ANDROID_H_ + +#include "base/android/jni_weak_ref.h" +#include "base/memory/weak_ptr.h" +#include "components/autofill/core/browser/autofill_provider.h" + +namespace content { +class WebContents; +} + +namespace autofill { + +class FormDataAndroid; + +// Android implementation of AutofillProvider, it has one instance per +// WebContents, this class is native peer of AutofillProvider.java. +class AutofillProviderAndroid : public AutofillProvider { + public: + AutofillProviderAndroid(const base::android::JavaRef<jobject>& jcaller, + content::WebContents* web_contents); + // Invoked when the Java-side AutofillProvider counterpart of this object + // has been changed (either to null or to a new object). + void OnJavaAutofillProviderChanged( + JNIEnv* env, + const base::android::JavaRef<jobject>& jcaller); + + ~AutofillProviderAndroid() override; + + // AutofillProvider: + void OnQueryFormFieldAutofill( + AutofillHandlerProxy* handler, + int32_t id, + const FormData& form, + const FormFieldData& field, + const gfx::RectF& bounding_box, + bool /*unused_autoselect_first_suggestion*/) override; + void OnTextFieldDidChange(AutofillHandlerProxy* handler, + const FormData& form, + const FormFieldData& field, + const gfx::RectF& bounding_box, + const base::TimeTicks timestamp) override; + void OnTextFieldDidScroll(AutofillHandlerProxy* handler, + const FormData& form, + const FormFieldData& field, + const gfx::RectF& bounding_box) override; + void OnSelectControlDidChange(AutofillHandlerProxy* handler, + const FormData& form, + const FormFieldData& field, + const gfx::RectF& bounding_box) override; + void OnFormSubmitted(AutofillHandlerProxy* handler, + const FormData& form, + bool known_success, + mojom::SubmissionSource source) override; + void OnFocusNoLongerOnForm(AutofillHandlerProxy* handler) override; + void OnFocusOnFormField(AutofillHandlerProxy* handler, + const FormData& form, + const FormFieldData& field, + const gfx::RectF& bounding_box) override; + void OnDidFillAutofillFormData(AutofillHandlerProxy* handler, + const FormData& form, + base::TimeTicks timestamp) override; + void OnFormsSeen(AutofillHandlerProxy* handler, + const std::vector<FormData>& forms, + const base::TimeTicks timestamp) override; + void OnHidePopup(AutofillHandlerProxy* handler) override; + + void Reset(AutofillHandlerProxy* handler) override; + + // Methods called by Java. + 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, + size_t index, + const gfx::RectF& bounding_box); + void FireFormFieldDidChanged(AutofillHandlerProxy* handler, + const FormData& form, + const FormFieldData& field, + const gfx::RectF& bounding_box); + + bool IsCurrentlyLinkedHandler(AutofillHandlerProxy* handler); + + bool IsCurrentlyLinkedForm(const FormData& form); + + gfx::RectF ToClientAreaBound(const gfx::RectF& bounding_box); + + bool ShouldStartNewSession(AutofillHandlerProxy* handler, + const FormData& form); + + void StartNewSession(AutofillHandlerProxy* handler, + const FormData& form, + const FormFieldData& field, + const gfx::RectF& bounding_box); + + void Reset(); + + int32_t id_; + std::unique_ptr<FormDataAndroid> form_; + base::WeakPtr<AutofillHandlerProxy> handler_; + JavaObjectWeakGlobalRef java_ref_; + content::WebContents* web_contents_; + bool check_submission_; + // Valid only if check_submission_ is true. + mojom::SubmissionSource pending_submission_source_; + + base::WeakPtr<AutofillHandlerProxy> handler_for_testing_; + + DISALLOW_COPY_AND_ASSIGN(AutofillProviderAndroid); +}; +} // namespace autofill + +#endif // COMPONENTS_AUTOFILL_ANDROID_PROVIDER_AUTOFILL_PROVIDER_ANDROID_H_ diff --git a/chromium/components/autofill/android/provider/form_data_android.cc b/chromium/components/autofill/android/provider/form_data_android.cc new file mode 100644 index 00000000000..7f24f8967c5 --- /dev/null +++ b/chromium/components/autofill/android/provider/form_data_android.cc @@ -0,0 +1,112 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "components/autofill/android/provider/form_data_android.h" + +#include "base/android/jni_string.h" +#include "components/autofill/android/provider/form_field_data_android.h" +#include "components/autofill/android/provider/jni_headers/FormData_jni.h" +#include "components/autofill/core/browser/form_structure.h" + +using base::android::AttachCurrentThread; +using base::android::ConvertJavaStringToUTF16; +using base::android::ConvertUTF16ToJavaString; +using base::android::ConvertUTF8ToJavaString; +using base::android::JavaParamRef; +using base::android::ScopedJavaGlobalRef; +using base::android::ScopedJavaLocalRef; + +namespace autofill { + +FormDataAndroid::FormDataAndroid(const FormData& form, + const TransformCallback& callback) + : form_(form), index_(0) { + for (FormFieldData& field : form_.fields) + field.bounds = callback.Run(field.bounds); +} + +FormDataAndroid::~FormDataAndroid() = default; + +ScopedJavaLocalRef<jobject> FormDataAndroid::GetJavaPeer( + const FormStructure* form_structure) { + // |form_structure| is ephemeral and shouldn't be used outside this call + // stack. + JNIEnv* env = AttachCurrentThread(); + ScopedJavaLocalRef<jobject> obj = java_ref_.get(env); + if (obj.is_null()) { + for (size_t i = 0; i < form_.fields.size(); ++i) { + fields_.push_back(std::unique_ptr<FormFieldDataAndroid>( + new FormFieldDataAndroid(&form_.fields[i]))); + } + if (form_structure) + ApplyHeuristicFieldType(*form_structure); + ScopedJavaLocalRef<jstring> jname = + ConvertUTF16ToJavaString(env, form_.name); + ScopedJavaLocalRef<jstring> jhost = + ConvertUTF8ToJavaString(env, form_.url.GetOrigin().spec()); + obj = Java_FormData_createFormData(env, reinterpret_cast<intptr_t>(this), + jname, jhost, form_.fields.size()); + java_ref_ = JavaObjectWeakGlobalRef(env, obj); + } + return obj; +} + +const FormData& FormDataAndroid::GetAutofillValues() { + for (std::unique_ptr<FormFieldDataAndroid>& field : fields_) + field->GetValue(); + return form_; +} + +ScopedJavaLocalRef<jobject> FormDataAndroid::GetNextFormFieldData(JNIEnv* env) { + DCHECK(index_ <= fields_.size()); + if (index_ == fields_.size()) + return ScopedJavaLocalRef<jobject>(); + return fields_[index_++]->GetJavaPeer(); +} + +void FormDataAndroid::OnFormFieldDidChange(size_t index, + const base::string16& value) { + form_.fields[index].value = value; + fields_[index]->OnFormFieldDidChange(value); +} + +bool FormDataAndroid::GetFieldIndex(const FormFieldData& field, size_t* index) { + for (size_t i = 0; i < form_.fields.size(); ++i) { + if (form_.fields[i].SameFieldAs(field)) { + *index = i; + return true; + } + } + return false; +} + +bool FormDataAndroid::GetSimilarFieldIndex(const FormFieldData& field, + size_t* index) { + for (size_t i = 0; i < form_.fields.size(); ++i) { + if (form_.fields[i].SimilarFieldAs(field)) { + *index = i; + return true; + } + } + return false; +} + +bool FormDataAndroid::SimilarFormAs(const FormData& form) { + return form_.SimilarFormAs(form); +} + +void FormDataAndroid::ApplyHeuristicFieldType( + const FormStructure& form_structure) { + DCHECK(form_structure.field_count() == fields_.size()); + auto form_field_data_android = fields_.begin(); + for (const auto& autofill_field : form_structure) { + DCHECK(form_field_data_android->get()->SimilarFieldAs(*autofill_field)); + form_field_data_android->get()->set_heuristic_type( + AutofillType(autofill_field->heuristic_type())); + if (++form_field_data_android == fields_.end()) + break; + } +} + +} // namespace autofill diff --git a/chromium/components/autofill/android/provider/form_data_android.h b/chromium/components/autofill/android/provider/form_data_android.h new file mode 100644 index 00000000000..8882a788673 --- /dev/null +++ b/chromium/components/autofill/android/provider/form_data_android.h @@ -0,0 +1,73 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef COMPONENTS_AUTOFILL_ANDROID_PROVIDER_FORM_DATA_ANDROID_H_ +#define COMPONENTS_AUTOFILL_ANDROID_PROVIDER_FORM_DATA_ANDROID_H_ + +#include "base/android/jni_weak_ref.h" +#include "base/android/scoped_java_ref.h" +#include "components/autofill/core/common/form_data.h" + +namespace autofill { + +class FormFieldDataAndroid; +class FormStructure; + +// This class is native peer of FormData.java, to make autofill::FormData +// available in Java. +class FormDataAndroid { + public: + // The callback func to transform FormFieldData's bounds to viewport's + // coordinates, it is only used in FormDataAndroid constructor and transforms + // bounds in place to avoids an extra copy of FormData. + using TransformCallback = + base::RepeatingCallback<gfx::RectF(const gfx::RectF&)>; + + FormDataAndroid(const FormData& form, const TransformCallback& callback); + virtual ~FormDataAndroid(); + + base::android::ScopedJavaLocalRef<jobject> GetJavaPeer( + const FormStructure* form_structure); + + // Get autofill values from Java side and return FormData. + const FormData& GetAutofillValues(); + + base::android::ScopedJavaLocalRef<jobject> GetNextFormFieldData(JNIEnv* env); + + // Get index of given field, return True and index of focus field if found. + bool GetFieldIndex(const FormFieldData& field, size_t* index); + + // Get index of given field, return True and index of focus field if + // similar field is found. This method compares less attributes than + // GetFieldIndex() does, and should be used when field could be changed + // dynamically, but the changed has no impact on autofill purpose, e.g. css + // style change, see FormFieldData::SimilarFieldAs() for details. + bool GetSimilarFieldIndex(const FormFieldData& field, size_t* index); + + // Return true if this form is similar to the given form. + bool SimilarFormAs(const FormData& form); + + // Invoked when form field which specified by |index| is charged to new + // |value|. + void OnFormFieldDidChange(size_t index, const base::string16& value); + + void ApplyHeuristicFieldType(const FormStructure& form); + + const FormData& form_for_testing() { return form_; } + + private: + // Same as the form passed in from constructor, but FormFieldData's bounds is + // transformed to viewport coordinates. + FormData form_; + std::vector<std::unique_ptr<FormFieldDataAndroid>> fields_; + JavaObjectWeakGlobalRef java_ref_; + // keep track of index when popping up fields to Java. + size_t index_; + + DISALLOW_COPY_AND_ASSIGN(FormDataAndroid); +}; + +} // namespace autofill + +#endif // COMPONENTS_AUTOFILL_ANDROID_PROVIDER_FORM_DATA_ANDROID_H_ diff --git a/chromium/components/autofill/android/provider/form_field_data_android.cc b/chromium/components/autofill/android/provider/form_field_data_android.cc new file mode 100644 index 00000000000..ee5471f5239 --- /dev/null +++ b/chromium/components/autofill/android/provider/form_field_data_android.cc @@ -0,0 +1,106 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "components/autofill/android/provider/form_field_data_android.h" + +#include "base/android/jni_array.h" +#include "base/android/jni_string.h" +#include "components/autofill/android/provider/jni_headers/FormFieldData_jni.h" +#include "components/autofill/core/common/autofill_util.h" + +using base::android::AttachCurrentThread; +using base::android::ConvertJavaStringToUTF16; +using base::android::ConvertUTF16ToJavaString; +using base::android::ConvertUTF8ToJavaString; +using base::android::JavaParamRef; +using base::android::JavaRef; +using base::android::ScopedJavaGlobalRef; +using base::android::ScopedJavaLocalRef; +using base::android::ToJavaArrayOfStrings; + +namespace autofill { + +FormFieldDataAndroid::FormFieldDataAndroid(FormFieldData* field) + : heuristic_type_(AutofillType(UNKNOWN_TYPE)), field_ptr_(field) {} + +ScopedJavaLocalRef<jobject> FormFieldDataAndroid::GetJavaPeer() { + JNIEnv* env = AttachCurrentThread(); + ScopedJavaLocalRef<jobject> obj = java_ref_.get(env); + if (obj.is_null()) { + ScopedJavaLocalRef<jstring> jname = + ConvertUTF16ToJavaString(env, field_ptr_->name); + ScopedJavaLocalRef<jstring> jlabel = + ConvertUTF16ToJavaString(env, field_ptr_->label); + ScopedJavaLocalRef<jstring> jvalue = + ConvertUTF16ToJavaString(env, field_ptr_->value); + ScopedJavaLocalRef<jstring> jautocomplete_attr = + ConvertUTF8ToJavaString(env, field_ptr_->autocomplete_attribute); + ScopedJavaLocalRef<jstring> jplaceholder = + ConvertUTF16ToJavaString(env, field_ptr_->placeholder); + ScopedJavaLocalRef<jstring> jid = + ConvertUTF16ToJavaString(env, field_ptr_->id_attribute); + ScopedJavaLocalRef<jstring> jtype = + ConvertUTF8ToJavaString(env, field_ptr_->form_control_type); + ScopedJavaLocalRef<jobjectArray> joption_values = + ToJavaArrayOfStrings(env, field_ptr_->option_values); + ScopedJavaLocalRef<jobjectArray> joption_contents = + ToJavaArrayOfStrings(env, field_ptr_->option_contents); + ScopedJavaLocalRef<jstring> jheuristic_type; + if (!heuristic_type_.IsUnknown()) + jheuristic_type = + ConvertUTF8ToJavaString(env, heuristic_type_.ToString()); + ScopedJavaLocalRef<jobjectArray> jdatalist_values = + ToJavaArrayOfStrings(env, field_ptr_->datalist_values); + ScopedJavaLocalRef<jobjectArray> jdatalist_labels = + ToJavaArrayOfStrings(env, field_ptr_->datalist_labels); + + obj = Java_FormFieldData_createFormFieldData( + env, jname, jlabel, jvalue, jautocomplete_attr, + field_ptr_->should_autocomplete, jplaceholder, jtype, jid, + joption_values, joption_contents, IsCheckable(field_ptr_->check_status), + IsChecked(field_ptr_->check_status), field_ptr_->max_length, + jheuristic_type, field_ptr_->bounds.x(), field_ptr_->bounds.y(), + field_ptr_->bounds.right(), field_ptr_->bounds.bottom(), + jdatalist_values, jdatalist_labels); + java_ref_ = JavaObjectWeakGlobalRef(env, obj); + } + return obj; +} + +void FormFieldDataAndroid::GetValue() { + JNIEnv* env = AttachCurrentThread(); + + ScopedJavaLocalRef<jobject> obj = java_ref_.get(env); + if (obj.is_null()) + return; + + if (IsCheckable(field_ptr_->check_status)) { + bool checked = Java_FormFieldData_isChecked(env, obj); + SetCheckStatus(field_ptr_, true, checked); + } else { + ScopedJavaLocalRef<jstring> jvalue = Java_FormFieldData_getValue(env, obj); + if (jvalue.is_null()) + return; + field_ptr_->value = ConvertJavaStringToUTF16(env, jvalue); + } + field_ptr_->is_autofilled = true; +} + +void FormFieldDataAndroid::OnFormFieldDidChange(const base::string16& value) { + field_ptr_->value = value; + field_ptr_->is_autofilled = false; + JNIEnv* env = AttachCurrentThread(); + ScopedJavaLocalRef<jobject> obj = java_ref_.get(env); + if (obj.is_null()) + return; + + Java_FormFieldData_updateValue(env, obj, + ConvertUTF16ToJavaString(env, value)); +} + +bool FormFieldDataAndroid::SimilarFieldAs(const FormFieldData& field) const { + return field_ptr_->SimilarFieldAs(field); +} + +} // namespace autofill diff --git a/chromium/components/autofill/android/provider/form_field_data_android.h b/chromium/components/autofill/android/provider/form_field_data_android.h new file mode 100644 index 00000000000..a12ad979bfe --- /dev/null +++ b/chromium/components/autofill/android/provider/form_field_data_android.h @@ -0,0 +1,42 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef COMPONENTS_AUTOFILL_ANDROID_PROVIDER_FORM_FIELD_DATA_ANDROID_H_ +#define COMPONENTS_AUTOFILL_ANDROID_PROVIDER_FORM_FIELD_DATA_ANDROID_H_ + +#include "base/android/jni_weak_ref.h" +#include "base/android/scoped_java_ref.h" +#include "components/autofill/core/browser/autofill_type.h" +#include "components/autofill/core/common/form_field_data.h" + +namespace autofill { + +// This class is native peer of FormFieldData.java, makes +// autofill::FormFieldData available in Java. +class FormFieldDataAndroid { + public: + FormFieldDataAndroid(FormFieldData* field); + virtual ~FormFieldDataAndroid() {} + + base::android::ScopedJavaLocalRef<jobject> GetJavaPeer(); + void GetValue(); + void OnFormFieldDidChange(const base::string16& value); + bool SimilarFieldAs(const FormFieldData& field) const; + + void set_heuristic_type(const AutofillType& heuristic_type) { + heuristic_type_ = heuristic_type; + } + + private: + AutofillType heuristic_type_; + // Not owned. + FormFieldData* field_ptr_; + JavaObjectWeakGlobalRef java_ref_; + + DISALLOW_COPY_AND_ASSIGN(FormFieldDataAndroid); +}; + +} // namespace autofill + +#endif // COMPONENTS_AUTOFILL_ANDROID_PROVIDER_FORM_FIELD_DATA_ANDROID_H_ diff --git a/chromium/components/autofill/android/provider/java/src/org/chromium/components/autofill/AutofillActionModeCallback.java b/chromium/components/autofill/android/provider/java/src/org/chromium/components/autofill/AutofillActionModeCallback.java new file mode 100644 index 00000000000..6921fcab52c --- /dev/null +++ b/chromium/components/autofill/android/provider/java/src/org/chromium/components/autofill/AutofillActionModeCallback.java @@ -0,0 +1,60 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package org.chromium.components.autofill; + +import android.content.Context; +import android.view.ActionMode; +import android.view.Menu; +import android.view.MenuItem; + +/** + * The class to implement autofill context menu. To match the Android native view behavior, the + * autofill context menu only appears when there is no text selected. + */ +public class AutofillActionModeCallback implements ActionMode.Callback { + private final Context mContext; + private final AutofillProvider mAutofillProvider; + private final int mAutofillMenuItemTitle; + private final int mAutofillMenuItem; + + public AutofillActionModeCallback(Context context, AutofillProvider autofillProvider) { + mContext = context; + mAutofillProvider = autofillProvider; + // TODO(michaelbai): Uses the resource directly after sdk roll to Android O MR1. + // crbug.com/740628 + mAutofillMenuItemTitle = + mContext.getResources().getIdentifier("autofill", "string", "android"); + mAutofillMenuItem = mContext.getResources().getIdentifier("autofill", "id", "android"); + } + + @Override + public boolean onCreateActionMode(ActionMode mode, Menu menu) { + return mAutofillMenuItemTitle != 0 && mAutofillMenuItem != 0; + } + + @Override + public boolean onPrepareActionMode(ActionMode mode, Menu menu) { + if (mAutofillMenuItemTitle != 0 && mAutofillProvider.shouldQueryAutofillSuggestion()) { + MenuItem item = menu.add( + Menu.NONE, mAutofillMenuItem, Menu.CATEGORY_SECONDARY, mAutofillMenuItemTitle); + item.setShowAsActionFlags( + MenuItem.SHOW_AS_ACTION_NEVER | MenuItem.SHOW_AS_ACTION_WITH_TEXT); + } + return true; + } + + @Override + public boolean onActionItemClicked(ActionMode mode, MenuItem item) { + if (item.getItemId() == mAutofillMenuItem) { + mAutofillProvider.queryAutofillSuggestion(); + mode.finish(); + return true; + } + return false; + } + + @Override + public void onDestroyActionMode(ActionMode mode) {} +} diff --git a/chromium/components/autofill/android/provider/java/src/org/chromium/components/autofill/AutofillManagerWrapper.java b/chromium/components/autofill/android/provider/java/src/org/chromium/components/autofill/AutofillManagerWrapper.java new file mode 100644 index 00000000000..0287ac38f97 --- /dev/null +++ b/chromium/components/autofill/android/provider/java/src/org/chromium/components/autofill/AutofillManagerWrapper.java @@ -0,0 +1,206 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package org.chromium.components.autofill; + +import android.annotation.TargetApi; +import android.content.Context; +import android.graphics.Rect; +import android.os.Build; +import android.view.View; +import android.view.autofill.AutofillManager; +import android.view.autofill.AutofillValue; + +import androidx.annotation.VisibleForTesting; + +import org.chromium.base.Log; + +import java.lang.ref.WeakReference; +import java.util.ArrayList; +import java.util.Iterator; + +/** + * The class to call Android's AutofillManager. + */ +@TargetApi(Build.VERSION_CODES.O) +public class AutofillManagerWrapper { + // Don't change TAG, it is used for runtime log. + // NOTE: As a result of the above, the tag below still references the name of this class from + // when it was originally developed specifically for Android WebView. + public static final String TAG = "AwAutofillManager"; + + /** + * The observer of suggestion window. + */ + public static interface InputUIObserver { void onInputUIShown(); } + + private static class AutofillInputUIMonitor extends AutofillManager.AutofillCallback { + private WeakReference<AutofillManagerWrapper> mManager; + + public AutofillInputUIMonitor(AutofillManagerWrapper manager) { + mManager = new WeakReference<AutofillManagerWrapper>(manager); + } + + @Override + public void onAutofillEvent(View view, int virtualId, int event) { + AutofillManagerWrapper manager = mManager.get(); + if (manager == null) return; + manager.mIsAutofillInputUIShowing = (event == EVENT_INPUT_SHOWN); + if (event == EVENT_INPUT_SHOWN) manager.notifyInputUIChange(); + } + } + + private static boolean sIsLoggable; + private AutofillManager mAutofillManager; + private boolean mIsAutofillInputUIShowing; + private AutofillInputUIMonitor mMonitor; + private boolean mDestroyed; + private boolean mDisabled; + private ArrayList<WeakReference<InputUIObserver>> mInputUIObservers; + + public AutofillManagerWrapper(Context context) { + updateLogStat(); + if (isLoggable()) log("constructor"); + mAutofillManager = context.getSystemService(AutofillManager.class); + mDisabled = mAutofillManager == null || !mAutofillManager.isEnabled(); + if (mDisabled) { + if (isLoggable()) log("disabled"); + return; + } + + mMonitor = new AutofillInputUIMonitor(this); + mAutofillManager.registerCallback(mMonitor); + } + + public void notifyVirtualValueChanged(View parent, int childId, AutofillValue value) { + if (mDisabled || checkAndWarnIfDestroyed()) return; + if (isLoggable()) log("notifyVirtualValueChanged"); + mAutofillManager.notifyValueChanged(parent, childId, value); + } + + public void commit(int submissionSource) { + if (mDisabled || checkAndWarnIfDestroyed()) return; + if (isLoggable()) log("commit source:" + submissionSource); + mAutofillManager.commit(); + } + + public void cancel() { + if (mDisabled || checkAndWarnIfDestroyed()) return; + if (isLoggable()) log("cancel"); + mAutofillManager.cancel(); + } + + public void notifyVirtualViewEntered(View parent, int childId, Rect absBounds) { + // Log warning only when the autofill is triggered. + if (mDisabled) { + Log.w(TAG, "Autofill is disabled: AutofillManager isn't available in given Context."); + return; + } + if (checkAndWarnIfDestroyed()) return; + if (isLoggable()) log("notifyVirtualViewEntered"); + mAutofillManager.notifyViewEntered(parent, childId, absBounds); + } + + public void notifyVirtualViewExited(View parent, int childId) { + if (mDisabled || checkAndWarnIfDestroyed()) return; + if (isLoggable()) log("notifyVirtualViewExited"); + mAutofillManager.notifyViewExited(parent, childId); + } + + public void requestAutofill(View parent, int virtualId, Rect absBounds) { + if (mDisabled || checkAndWarnIfDestroyed()) return; + if (isLoggable()) log("requestAutofill"); + mAutofillManager.requestAutofill(parent, virtualId, absBounds); + } + + public boolean isAutofillInputUIShowing() { + if (mDisabled || checkAndWarnIfDestroyed()) return false; + if (isLoggable()) log("isAutofillInputUIShowing: " + mIsAutofillInputUIShowing); + return mIsAutofillInputUIShowing; + } + + public void destroy() { + if (mDisabled || checkAndWarnIfDestroyed()) return; + if (isLoggable()) log("destroy"); + try { + // The binder in the autofill service side might already be dropped, + // unregisterCallback() will cause various exceptions in this + // scenario (see crbug.com/1078337), catching RuntimeException here prevents crash. + mAutofillManager.unregisterCallback(mMonitor); + } catch (RuntimeException e) { + // We are not logging anything here since some of the exceptions are raised as 'generic' + // RuntimeException which makes it difficult to catch and ignore separately; and the + // RuntimeException seemed only happen in Android O, therefore, isn't actionable. + } finally { + mAutofillManager = null; + mDestroyed = true; + } + } + + public boolean isDisabled() { + return mDisabled; + } + + private boolean checkAndWarnIfDestroyed() { + if (mDestroyed) { + Log.w(TAG, "Application attempted to call on a destroyed AutofillManagerWrapper", + new Throwable()); + } + return mDestroyed; + } + + public void addInputUIObserver(InputUIObserver observer) { + if (observer == null) return; + if (mInputUIObservers == null) { + mInputUIObservers = new ArrayList<WeakReference<InputUIObserver>>(); + } + mInputUIObservers.add(new WeakReference<InputUIObserver>(observer)); + } + + public void removeInputUIObserver(InputUIObserver observer) { + if (observer == null) return; + for (Iterator<WeakReference<InputUIObserver>> i = mInputUIObservers.listIterator(); + i.hasNext();) { + WeakReference<InputUIObserver> o = i.next(); + if (o.get() == null || o.get() == observer) i.remove(); + } + } + + @VisibleForTesting + public void notifyInputUIChange() { + for (Iterator<WeakReference<InputUIObserver>> i = mInputUIObservers.listIterator(); + i.hasNext();) { + WeakReference<InputUIObserver> o = i.next(); + InputUIObserver observer = o.get(); + if (observer == null) { + i.remove(); + continue; + } + observer.onInputUIShown(); + } + } + + public void notifyNewSessionStarted() { + updateLogStat(); + if (isLoggable()) log("Session starts"); + } + + /** + * Always check isLoggable() before call this method. + */ + public static void log(String log) { + // Log.i() instead of Log.d() is used here because log.d() is stripped out in release build. + Log.i(TAG, log); + } + + public static boolean isLoggable() { + return sIsLoggable; + } + + private static void updateLogStat() { + // Use 'setprop log.tag.AwAutofillManager DEBUG' to enable the log at runtime. + // NOTE: See the comment on TAG above for why this is still AwAutofillManager. + sIsLoggable = Log.isLoggable(TAG, Log.DEBUG); + } +} diff --git a/chromium/components/autofill/android/provider/java/src/org/chromium/components/autofill/AutofillProvider.java b/chromium/components/autofill/android/provider/java/src/org/chromium/components/autofill/AutofillProvider.java new file mode 100644 index 00000000000..acbf7a61a22 --- /dev/null +++ b/chromium/components/autofill/android/provider/java/src/org/chromium/components/autofill/AutofillProvider.java @@ -0,0 +1,786 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package org.chromium.components.autofill; + +import android.annotation.TargetApi; +import android.content.Context; +import android.graphics.Matrix; +import android.graphics.Rect; +import android.graphics.RectF; +import android.os.Build; +import android.os.Bundle; +import android.util.SparseArray; +import android.view.View; +import android.view.ViewGroup; +import android.view.ViewStructure; +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.CalledByNative; +import org.chromium.base.annotations.JNINamespace; +import org.chromium.base.annotations.NativeMethods; +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; + +/** + * This class works with Android autofill service to fill web form, it doesn't use chrome's + * autofill service or suggestion UI. All methods are supposed to be called in UI thread. + * + * AutofillProvider handles one autofill session at time, each call of + * queryFormFieldAutofill cancels previous session and starts a new one, the + * calling of other methods shall associate with current session. + * + * This class doesn't have 1:1 mapping to native AutofillProviderAndroid; the + * normal ownership model is that this object is owned by the embedder-specific + * Java WebContents wrapper (e.g., AwContents.java in //android_webview), and + * AutofillProviderAndroid is owned by the embedder-specific C++ WebContents + * wrapper (e.g., native AwContents in //android_webview). + * + * VerifiesOnO since it causes class verification errors, see crbug.com/991851. + */ +@VerifiesOnO +@TargetApi(Build.VERSION_CODES.O) +@JNINamespace("autofill") +public class AutofillProvider { + private static final String TAG = "AutofillProvider"; + private static class FocusField { + public final short fieldIndex; + public final Rect absBound; + + public FocusField(short fieldIndex, Rect absBound) { + this.fieldIndex = fieldIndex; + this.absBound = absBound; + } + } + /** + * The class to wrap the request to framework. + * + * Though framework guarantees always giving us the autofill value of current + * session, we still want to verify this by using unique virtual id which is + * composed of sessionId and form field index, we don't use the request id + * which comes from renderer as session id because it is not unique. + */ + private static class AutofillRequest { + private static final int INIT_ID = 1; // ID can't be 0 in Android. + private static int sSessionId = INIT_ID; + public final int sessionId; + private FormData mFormData; + private FocusField mFocusField; + + public AutofillRequest(FormData formData, FocusField focus) { + sessionId = getNextClientId(); + mFormData = formData; + mFocusField = focus; + } + + public void fillViewStructure(ViewStructure structure) { + structure.setWebDomain(mFormData.mHost); + structure.setHtmlInfo(structure.newHtmlInfoBuilder("form") + .addAttribute("name", mFormData.mName) + .build()); + int index = structure.addChildCount(mFormData.mFields.size()); + short fieldIndex = 0; + for (FormFieldData field : mFormData.mFields) { + ViewStructure child = structure.newChild(index++); + int virtualId = toVirtualId(sessionId, fieldIndex++); + child.setAutofillId(structure.getAutofillId(), virtualId); + if (field.mAutocompleteAttr != null && !field.mAutocompleteAttr.isEmpty()) { + child.setAutofillHints(field.mAutocompleteAttr.split(" +")); + } + child.setHint(field.mPlaceholder); + + RectF bounds = field.getBoundsInContainerViewCoordinates(); + // Field has no scroll. + child.setDimens((int) bounds.left, (int) bounds.top, 0 /* scrollX*/, + 0 /* scrollY */, (int) bounds.width(), (int) bounds.height()); + + ViewStructure.HtmlInfo.Builder builder = + child.newHtmlInfoBuilder("input") + .addAttribute("name", field.mName) + .addAttribute("type", field.mType) + .addAttribute("label", field.mLabel) + .addAttribute("ua-autofill-hints", field.mHeuristicType) + .addAttribute("id", field.mId); + + switch (field.getControlType()) { + case FormFieldData.ControlType.LIST: + child.setAutofillType(View.AUTOFILL_TYPE_LIST); + child.setAutofillOptions(field.mOptionContents); + int i = findIndex(field.mOptionValues, field.getValue()); + if (i != -1) { + child.setAutofillValue(AutofillValue.forList(i)); + } + break; + case FormFieldData.ControlType.TOGGLE: + child.setAutofillType(View.AUTOFILL_TYPE_TOGGLE); + child.setAutofillValue(AutofillValue.forToggle(field.isChecked())); + break; + case FormFieldData.ControlType.TEXT: + case FormFieldData.ControlType.DATALIST: + child.setAutofillType(View.AUTOFILL_TYPE_TEXT); + child.setAutofillValue(AutofillValue.forText(field.getValue())); + if (field.mMaxLength != 0) { + builder.addAttribute("maxlength", String.valueOf(field.mMaxLength)); + } + if (field.getControlType() == FormFieldData.ControlType.DATALIST) { + child.setAutofillOptions(field.mDatalistValues); + } + break; + default: + break; + } + child.setHtmlInfo(builder.build()); + } + } + + public boolean autofill(final SparseArray<AutofillValue> values) { + for (int i = 0; i < values.size(); ++i) { + int id = values.keyAt(i); + if (toSessionId(id) != sessionId) return false; + AutofillValue value = values.get(id); + if (value == null) continue; + short index = toIndex(id); + if (index < 0 || index >= mFormData.mFields.size()) return false; + FormFieldData field = mFormData.mFields.get(index); + if (field == null) return false; + try { + switch (field.getControlType()) { + case FormFieldData.ControlType.LIST: + int j = value.getListValue(); + if (j < 0 && j >= field.mOptionValues.length) continue; + field.setAutofillValue(field.mOptionValues[j]); + break; + case FormFieldData.ControlType.TOGGLE: + field.setChecked(value.getToggleValue()); + break; + case FormFieldData.ControlType.TEXT: + case FormFieldData.ControlType.DATALIST: + field.setAutofillValue((String) value.getTextValue()); + break; + default: + break; + } + } catch (IllegalStateException e) { + // Refer to crbug.com/1080580 . + Log.e(TAG, "The given AutofillValue wasn't expected, abort autofill.", e); + return false; + } + } + return true; + } + + public void setFocusField(FocusField focusField) { + mFocusField = focusField; + } + + public FocusField getFocusField() { + return mFocusField; + } + + public int getFieldCount() { + return mFormData.mFields.size(); + } + + public AutofillValue getFieldNewValue(int index) { + FormFieldData field = mFormData.mFields.get(index); + if (field == null) return null; + switch (field.getControlType()) { + case FormFieldData.ControlType.LIST: + int i = findIndex(field.mOptionValues, field.getValue()); + if (i == -1) return null; + return AutofillValue.forList(i); + case FormFieldData.ControlType.TOGGLE: + return AutofillValue.forToggle(field.isChecked()); + case FormFieldData.ControlType.TEXT: + case FormFieldData.ControlType.DATALIST: + return AutofillValue.forText(field.getValue()); + default: + return null; + } + } + + public int getVirtualId(short index) { + return toVirtualId(sessionId, index); + } + + public FormFieldData getField(short index) { + return mFormData.mFields.get(index); + } + + private static int findIndex(String[] values, String value) { + if (values != null && value != null) { + for (int i = 0; i < values.length; i++) { + if (value.equals(values[i])) return i; + } + } + return -1; + } + + private static int getNextClientId() { + ThreadUtils.assertOnUiThread(); + if (sSessionId == 0xffff) sSessionId = INIT_ID; + return sSessionId++; + } + + private static int toSessionId(int virtualId) { + return (virtualId & 0xffff0000) >> 16; + } + + private static short toIndex(int virtualId) { + return (short) (virtualId & 0xffff); + } + + private static int toVirtualId(int clientId, short index) { + return (clientId << 16) | index; + } + } + + private final String mProviderName; + private AutofillManagerWrapper mAutofillManager; + private ViewGroup mContainerView; + private WebContents mWebContents; + + private AutofillRequest mRequest; + private long mNativeAutofillProvider; + private AutofillProviderUMA mAutofillUMA; + private AutofillManagerWrapper.InputUIObserver mInputUIObserver; + private long mAutofillTriggeredTimeMillis; + private Context mContext; + private AutofillPopup mDatalistPopup; + private WebContentsAccessibility mWebContentsAccessibility; + private View mAnchorView; + + public AutofillProvider(Context context, ViewGroup containerView, String providerName) { + this(containerView, new AutofillManagerWrapper(context), context, providerName); + } + + @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) + public AutofillProvider(ViewGroup containerView, AutofillManagerWrapper manager, + Context context, String providerName) { + mProviderName = providerName; + try (ScopedSysTraceEvent e = ScopedSysTraceEvent.scoped("AutofillProvider.constructor")) { + assert Build.VERSION.SDK_INT >= Build.VERSION_CODES.O; + mAutofillManager = manager; + mContainerView = containerView; + mAutofillUMA = new AutofillProviderUMA(context); + mInputUIObserver = new AutofillManagerWrapper.InputUIObserver() { + @Override + public void onInputUIShown() { + // Not need to report suggestion window displayed if there is no live autofill + // session. + if (mRequest == null) return; + mAutofillUMA.onSuggestionDisplayed( + System.currentTimeMillis() - mAutofillTriggeredTimeMillis); + } + }; + mAutofillManager.addInputUIObserver(mInputUIObserver); + mContext = context; + } + } + + /** + * Invoked when container view is changed. + * + * @param containerView new container view. + */ + public void onContainerViewChanged(ViewGroup containerView) { + mContainerView = containerView; + } + + /** + * Invoked when autofill service needs the form structure. + * + * @param structure see View.onProvideAutofillVirtualStructure() + * @param flags see View.onProvideAutofillVirtualStructure() + */ + public void onProvideAutoFillVirtualStructure(ViewStructure structure, int flags) { + // This method could be called for the session started by the native + // control outside of the scope of autofill, e.g. the URL bar, in this case, we simply + // return. + if (mRequest == null) return; + + Bundle bundle = structure.getExtras(); + if (bundle != null) { + bundle.putCharSequence("VIRTUAL_STRUCTURE_PROVIDER_NAME", mProviderName); + bundle.putCharSequence( + "VIRTUAL_STRUCTURE_PROVIDER_VERSION", VersionConstants.PRODUCT_VERSION); + } + mRequest.fillViewStructure(structure); + if (AutofillManagerWrapper.isLoggable()) { + AutofillManagerWrapper.log( + "onProvideAutoFillVirtualStructure fields:" + structure.getChildCount()); + } + mAutofillUMA.onVirtualStructureProvided(); + } + + /** + * Invoked when autofill value is available, AutofillProvider shall fill the + * form with the provided values. + * + * @param values the array of autofill values, the key is virtual id of form + * field. + */ + public void autofill(final SparseArray<AutofillValue> values) { + if (mNativeAutofillProvider != 0 && mRequest != null && mRequest.autofill((values))) { + autofill(mNativeAutofillProvider, mRequest.mFormData); + if (AutofillManagerWrapper.isLoggable()) { + AutofillManagerWrapper.log("autofill values:" + values.size()); + } + mAutofillUMA.onAutofill(); + } + } + + /** + * @return whether query autofill suggestion. + */ + public boolean shouldQueryAutofillSuggestion() { + return mRequest != null && mRequest.getFocusField() != null + && !mAutofillManager.isAutofillInputUIShowing(); + } + + public void queryAutofillSuggestion() { + if (shouldQueryAutofillSuggestion()) { + FocusField focusField = mRequest.getFocusField(); + mAutofillManager.requestAutofill(mContainerView, + mRequest.getVirtualId(focusField.fieldIndex), focusField.absBound); + } + } + + /** + * Invoked when filling form is need. AutofillProvider shall ask autofill + * service for the values with which to fill the form. + * + * @param formData the form needs to fill. + * @param focus the index of focus field in formData + * @param x the boundary of focus field. + * @param y the boundary of focus field. + * @param width the boundary of focus field. + * @param height the boundary of focus field. + */ + @CalledByNative + public void startAutofillSession( + FormData formData, int focus, float x, float y, float width, float height) { + // Check focusField inside short value? + // Autofill Manager might have session that wasn't started by AutofillProvider, + // we just always cancel existing session here. + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) { + mAutofillManager.cancel(); + } + + Rect absBound = transformToWindowBounds(new RectF(x, y, x + width, y + height)); + if (mRequest != null) notifyViewExitBeforeDestroyRequest(); + transformFormFieldToContainViewCoordinates(formData); + mRequest = new AutofillRequest(formData, new FocusField((short) focus, absBound)); + int virtualId = mRequest.getVirtualId((short) focus); + notifyVirtualViewEntered(mContainerView, virtualId, absBound); + mAutofillUMA.onSessionStarted(mAutofillManager.isDisabled()); + mAutofillTriggeredTimeMillis = System.currentTimeMillis(); + + mAutofillManager.notifyNewSessionStarted(); + } + + /** + * Invoked when form field's value is changed. + * + * @param index index of field in current form. + * @param x the boundary of focus field. + * @param y the boundary of focus field. + * @param width the boundary of focus field. + * @param height the boundary of focus field. + * + */ + @CalledByNative + public void onFormFieldDidChange(int index, float x, float y, float width, float height) { + // Check index inside short value? + if (mRequest == null) return; + + short sIndex = (short) index; + FocusField focusField = mRequest.getFocusField(); + if (focusField == null || sIndex != focusField.fieldIndex) { + onFocusChangedImpl(true, index, x, y, width, height, true /*causedByValueChange*/); + } else { + // Currently there is no api to notify both value and position + // change, before the API is available, we still need to call + // notifyVirtualViewEntered() to tell current coordinates because + // the position could be changed. + int virtualId = mRequest.getVirtualId(sIndex); + Rect absBound = transformToWindowBounds(new RectF(x, y, x + width, y + height)); + if (!focusField.absBound.equals(absBound)) { + notifyVirtualViewExited(mContainerView, virtualId); + notifyVirtualViewEntered(mContainerView, virtualId, absBound); + // Update focus field position. + mRequest.setFocusField(new FocusField(focusField.fieldIndex, absBound)); + } + } + notifyVirtualValueChanged(index, /* forceNotify = */ false); + mAutofillUMA.onUserChangeFieldValue(mRequest.getField(sIndex).hasPreviouslyAutofilled()); + } + + /** + * Invoked when text field is scrolled. + * + * @param index index of field in current form. + * @param x the boundary of focus field. + * @param y the boundary of focus field. + * @param width the boundary of focus field. + * @param height the boundary of focus field. + * + */ + @CalledByNative + public void onTextFieldDidScroll(int index, float x, float y, float width, float height) { + // crbug.com/730764 - from P and above, Android framework listens to the onScrollChanged() + // and repositions the autofill UI automatically. + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) return; + if (mRequest == null) return; + + short sIndex = (short) index; + FocusField focusField = mRequest.getFocusField(); + if (focusField == null || sIndex != focusField.fieldIndex) return; + + int virtualId = mRequest.getVirtualId(sIndex); + Rect absBound = transformToWindowBounds(new RectF(x, y, x + width, y + height)); + // Notify the new position to the Android framework. Note that we do not call + // notifyVirtualViewExited() here intentionally to avoid flickering. + notifyVirtualViewEntered(mContainerView, virtualId, absBound); + + // Update focus field position. + mRequest.setFocusField(new FocusField(focusField.fieldIndex, absBound)); + } + + private boolean isDatalistField(int childId) { + FormFieldData field = mRequest.getField((short) childId); + return field.mControlType == FormFieldData.ControlType.DATALIST; + } + + private void notifyVirtualValueChanged(int index, boolean forceNotify) { + // The ValueChanged, ViewEntered and ViewExited aren't notified to the autofill service for + // the focused datalist to avoid the potential UI conflict. + // The datalist support was added later and the option list is displayed by WebView, the + // autofill service might also show its suggestions when the datalist (associated the input + // field) is focused, the two UI overlap, the solution is to completely hide the fact that + // the datalist is being focused to the autofill service to prevent it from displaying the + // suggestion. + // The ValueChange will still be sent to autofill service when the form + // submitted or autofilled. + if (!forceNotify && isDatalistField(index)) return; + AutofillValue autofillValue = mRequest.getFieldNewValue(index); + if (autofillValue == null) return; + mAutofillManager.notifyVirtualValueChanged( + mContainerView, mRequest.getVirtualId((short) index), autofillValue); + } + + private void notifyVirtualViewEntered(View parent, int childId, Rect absBounds) { + // Refer to notifyVirtualValueChanged() for the reason of the datalist's special handling. + if (isDatalistField(childId)) return; + mAutofillManager.notifyVirtualViewEntered(parent, childId, absBounds); + } + + private void notifyVirtualViewExited(View parent, int childId) { + // Refer to notifyVirtualValueChanged() for the reason of the datalist's special handling. + if (isDatalistField(childId)) return; + mAutofillManager.notifyVirtualViewExited(parent, childId); + } + + /** + * Invoked when current form will be submitted. + * @param submissionSource the submission source, could be any member defined in + * SubmissionSource.java + */ + @CalledByNative + public void onFormSubmitted(int submissionSource) { + // The changes could be missing, like those made by Javascript, we'd better to notify + // AutofillManager current values. also see crbug.com/353001 and crbug.com/732856. + forceNotifyFormValues(); + mAutofillManager.commit(submissionSource); + mRequest = null; + mAutofillUMA.onFormSubmitted(submissionSource); + } + + /** + * Invoked when focus field changed. + * + * @param focusOnForm whether focus is still on form. + * @param focusItem the index of field has focus + * @param x the boundary of focus field. + * @param y the boundary of focus field. + * @param width the boundary of focus field. + * @param height the boundary of focus field. + */ + @CalledByNative + public void onFocusChanged( + boolean focusOnForm, int focusField, float x, float y, float width, float height) { + onFocusChangedImpl( + focusOnForm, focusField, x, y, width, height, false /*causedByValueChange*/); + } + + @CalledByNative + public void hidePopup() { + if (mDatalistPopup != null) { + mDatalistPopup.dismiss(); + mDatalistPopup = null; + } + if (mWebContentsAccessibility != null) { + mWebContentsAccessibility.onAutofillPopupDismissed(); + } + } + + private void notifyViewExitBeforeDestroyRequest() { + if (mRequest == null) return; + FocusField focusField = mRequest.getFocusField(); + if (focusField == null) return; + notifyVirtualViewExited(mContainerView, mRequest.getVirtualId(focusField.fieldIndex)); + mRequest.setFocusField(null); + } + + private void onFocusChangedImpl(boolean focusOnForm, int focusField, float x, float y, + float width, float height, boolean causedByValueChange) { + // Check focusField inside short value? + // FocusNoLongerOnForm is called after form submitted. + if (mRequest == null) return; + FocusField prev = mRequest.getFocusField(); + if (focusOnForm) { + Rect absBound = transformToWindowBounds(new RectF(x, y, x + width, y + height)); + if (prev != null && prev.fieldIndex == focusField && absBound.equals(prev.absBound)) { + return; + } + + // Notify focus changed. + if (prev != null) { + notifyVirtualViewExited(mContainerView, mRequest.getVirtualId(prev.fieldIndex)); + } + + notifyVirtualViewEntered( + mContainerView, mRequest.getVirtualId((short) focusField), absBound); + + if (!causedByValueChange) { + // The focus field value might not sync with platform's + // AutofillManager, just notify it value changed. + notifyVirtualValueChanged(focusField, /* forceNotify = */ false); + mAutofillTriggeredTimeMillis = System.currentTimeMillis(); + } + mRequest.setFocusField(new FocusField((short) focusField, absBound)); + } else { + if (prev == null) return; + // Notify focus changed. + notifyVirtualViewExited(mContainerView, mRequest.getVirtualId(prev.fieldIndex)); + mRequest.setFocusField(null); + } + } + + @CalledByNative + 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); + } + + /** + * Invoked when current query need to be reset. + */ + @CalledByNative + protected void reset() { + // We don't need to reset anything here, it should be safe to cancel + // current autofill session when new one starts in + // startAutofillSession(). + } + + @CalledByNative + protected void setNativeAutofillProvider(long nativeAutofillProvider) { + if (nativeAutofillProvider == mNativeAutofillProvider) return; + // Setting the mNativeAutofillProvider to 0 may occur as a + // result of WebView.destroy, or because a WebView has been + // gc'ed. In the former case we can go ahead and clean up the + // frameworks autofill manager, but in the latter case the + // binder connection has already been dropped in a framework + // finalizer, and so the methods we call will throw. It's not + // possible to know which case we're in, so just catch the exception + // in AutofillManagerWrapper.destroy(). + if (mNativeAutofillProvider != 0) mRequest = null; + mNativeAutofillProvider = nativeAutofillProvider; + if (nativeAutofillProvider == 0) mAutofillManager.destroy(); + } + + public void setWebContents(WebContents webContents) { + if (webContents == mWebContents) return; + if (mWebContents != null) mRequest = null; + mWebContents = webContents; + } + + @CalledByNative + protected void onDidFillAutofillFormData() { + // The changes were caused by the autofill service autofill form, + // notified it about the result. + forceNotifyFormValues(); + } + + private void forceNotifyFormValues() { + if (mRequest == null) return; + for (int i = 0; i < mRequest.getFieldCount(); ++i) { + notifyVirtualValueChanged(i, /* forceNotify = */ true); + } + } + + @VisibleForTesting + public AutofillPopup getDatalistPopupForTesting() { + return mDatalistPopup; + } + + @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) + public Rect transformToWindowBounds(RectF rect) { + // Convert bounds to device pixel. + WindowAndroid windowAndroid = mWebContents.getTopLevelNativeWindow(); + DisplayAndroid displayAndroid = windowAndroid.getDisplay(); + float dipScale = displayAndroid.getDipScale(); + RectF bounds = new RectF(rect); + Matrix matrix = new Matrix(); + matrix.setScale(dipScale, dipScale); + int[] location = new int[2]; + mContainerView.getLocationOnScreen(location); + matrix.postTranslate(location[0], location[1]); + matrix.mapRect(bounds); + return new Rect( + (int) bounds.left, (int) bounds.top, (int) bounds.right, (int) bounds.bottom); + } + + /** + * Transform FormFieldData's bounds to ContainView's coordinates and update the bounds with the + * transformed one. + * + * @param formData the form need to be transformed. + */ + @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) + public void transformFormFieldToContainViewCoordinates(FormData formData) { + WindowAndroid windowAndroid = mWebContents.getTopLevelNativeWindow(); + DisplayAndroid displayAndroid = windowAndroid.getDisplay(); + float dipScale = displayAndroid.getDipScale(); + Matrix matrix = new Matrix(); + matrix.setScale(dipScale, dipScale); + matrix.postTranslate(mContainerView.getScrollX(), mContainerView.getScrollY()); + + for (FormFieldData field : formData.mFields) { + RectF bounds = new RectF(); + matrix.mapRect(bounds, field.getBounds()); + field.setBoundsInContainerViewCoordinates(bounds); + } + } + + /** + * Send form to renderer for filling. + * + * @param nativeAutofillProvider the native autofill provider. + * @param formData the form to fill. + */ + private void autofill(long nativeAutofillProvider, FormData formData) { + AutofillProviderJni.get().onAutofillAvailable( + nativeAutofillProvider, AutofillProvider.this, formData); + } + + private void acceptDataListSuggestion(long nativeAutofillProvider, String value) { + AutofillProviderJni.get().onAcceptDataListSuggestion( + nativeAutofillProvider, AutofillProvider.this, value); + } + + private void setAnchorViewRect(long nativeAutofillProvider, View anchorView, RectF rect) { + AutofillProviderJni.get().setAnchorViewRect(nativeAutofillProvider, AutofillProvider.this, + anchorView, rect.left, rect.top, rect.width(), rect.height()); + } + + @NativeMethods + interface Natives { + void onAutofillAvailable( + long nativeAutofillProviderAndroid, AutofillProvider caller, FormData formData); + + void onAcceptDataListSuggestion( + long nativeAutofillProviderAndroid, AutofillProvider caller, String value); + + void setAnchorViewRect(long nativeAutofillProviderAndroid, AutofillProvider caller, + View anchorView, float x, float y, float width, float height); + } +} diff --git a/chromium/components/autofill/android/provider/java/src/org/chromium/components/autofill/AutofillProviderUMA.java b/chromium/components/autofill/android/provider/java/src/org/chromium/components/autofill/AutofillProviderUMA.java new file mode 100644 index 00000000000..394cd849c95 --- /dev/null +++ b/chromium/components/autofill/android/provider/java/src/org/chromium/components/autofill/AutofillProviderUMA.java @@ -0,0 +1,255 @@ +// Copyright 2018 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package org.chromium.components.autofill; + +import android.content.Context; + +import org.chromium.autofill.mojom.SubmissionSource; +import org.chromium.base.ContextUtils; +import org.chromium.base.metrics.RecordHistogram; + +import java.util.concurrent.TimeUnit; + +/** + * The class for AutofillProvider-related UMA. Note that most of the concrete histogram + * names include "WebView"; when this class was originally developed it was WebView-specific, + * and when generalizing it we did not change these names to maintain continuity when + * analyzing the histograms. + */ +public class AutofillProviderUMA { + // Records whether the Autofill service is enabled or not. + public static final String UMA_AUTOFILL_ENABLED = "Autofill.WebView.Enabled"; + + // Records whether the Autofill provider is created by activity context or not. + public static final String UMA_AUTOFILL_CREATED_BY_ACTIVITY_CONTEXT = + "Autofill.WebView.CreatedByActivityContext"; + + // Records what happened in an autofill session. + public static final String UMA_AUTOFILL_AUTOFILL_SESSION = "Autofill.WebView.AutofillSession"; + // The possible value of UMA_AUTOFILL_AUTOFILL_SESSION. + public static final int SESSION_UNKNOWN = 0; + public static final int NO_CALLBACK_FORM_FRAMEWORK = 1; + public static final int NO_SUGGESTION_USER_CHANGE_FORM_FORM_SUBMITTED = 2; + public static final int NO_SUGGESTION_USER_CHANGE_FORM_NO_FORM_SUBMITTED = 3; + public static final int NO_SUGGESTION_USER_NOT_CHANGE_FORM_FORM_SUBMITTED = 4; + public static final int NO_SUGGESTION_USER_NOT_CHANGE_FORM_NO_FORM_SUBMITTED = 5; + public static final int USER_SELECT_SUGGESTION_USER_CHANGE_FORM_FORM_SUBMITTED = 6; + public static final int USER_SELECT_SUGGESTION_USER_CHANGE_FORM_NO_FORM_SUBMITTED = 7; + public static final int USER_SELECT_SUGGESTION_USER_NOT_CHANGE_FORM_FORM_SUBMITTED = 8; + public static final int USER_SELECT_SUGGESTION_USER_NOT_CHANGE_FORM_NO_FORM_SUBMITTED = 9; + public static final int USER_NOT_SELECT_SUGGESTION_USER_CHANGE_FORM_FORM_SUBMITTED = 10; + public static final int USER_NOT_SELECT_SUGGESTION_USER_CHANGE_FORM_NO_FORM_SUBMITTED = 11; + public static final int USER_NOT_SELECT_SUGGESTION_USER_NOT_CHANGE_FORM_FORM_SUBMITTED = 12; + public static final int USER_NOT_SELECT_SUGGESTION_USER_NOT_CHANGE_FORM_NO_FORM_SUBMITTED = 13; + public static final int AUTOFILL_SESSION_HISTOGRAM_COUNT = 14; + + // Records whether user changed autofilled field if user ever changed the form. The action isn't + // recorded if user didn't change form at all. + public static final String UMA_AUTOFILL_USER_CHANGED_AUTOFILLED_FIELD = + "Autofill.WebView.UserChangedAutofilledField"; + + public static final String UMA_AUTOFILL_SUBMISSION_SOURCE = "Autofill.WebView.SubmissionSource"; + // The possible value of UMA_AUTOFILL_SUBMISSION_SOURCE. + public static final int SAME_DOCUMENT_NAVIGATION = 0; + public static final int XHR_SUCCEEDED = 1; + public static final int FRAME_DETACHED = 2; + public static final int DOM_MUTATION_AFTER_XHR = 3; + public static final int PROBABLY_FORM_SUBMITTED = 4; + public static final int FORM_SUBMISSION = 5; + public static final int SUBMISSION_SOURCE_HISTOGRAM_COUNT = 6; + + // The million seconds from user touched the field to the autofill session starting. + public static final String UMA_AUTOFILL_TRIGGERING_TIME = "Autofill.WebView.TriggeringTime"; + + // The million seconds from the autofill session starting to the suggestion being displayed. + public static final String UMA_AUTOFILL_SUGGESTION_TIME = "Autofill.WebView.SuggestionTime"; + + // The expected time range of time is from 10ms to 2 seconds, and 50 buckets is sufficient. + private static final long MIN_TIME_MILLIS = 10; + private static final long MAX_TIME_MILLIS = TimeUnit.SECONDS.toMillis(2); + private static final int NUM_OF_BUCKETS = 50; + + private static void recordTimesHistogram(String name, long durationMillis) { + RecordHistogram.recordCustomTimesHistogram( + name, durationMillis, MIN_TIME_MILLIS, MAX_TIME_MILLIS, NUM_OF_BUCKETS); + } + + private static class SessionRecorder { + public static final int EVENT_VIRTUAL_STRUCTURE_PROVIDED = 0x1 << 0; + public static final int EVENT_SUGGESTION_DISPLAYED = 0x1 << 1; + public static final int EVENT_FORM_AUTOFILLED = 0x1 << 2; + public static final int EVENT_USER_CHANGED_FIELD_VALUE = 0x1 << 3; + public static final int EVENT_FORM_SUBMITTED = 0x1 << 4; + public static final int EVENT_USER_CHANGED_AUTOFILLED_FIELD = 0x1 << 5; + + private Long mSuggestionTimeMillis; + + public void record(int event) { + // Not record any event until we get EVENT_VIRTUAL_STRUCTURE_PROVIDED which makes the + // following events meaningful. + if (event != EVENT_VIRTUAL_STRUCTURE_PROVIDED && mState == 0) return; + if (EVENT_USER_CHANGED_FIELD_VALUE == event && mUserChangedAutofilledField == null) { + mUserChangedAutofilledField = Boolean.valueOf(false); + } else if (EVENT_USER_CHANGED_AUTOFILLED_FIELD == event) { + if (mUserChangedAutofilledField == null) { + mUserChangedAutofilledField = Boolean.valueOf(true); + } + mUserChangedAutofilledField = true; + event = EVENT_USER_CHANGED_FIELD_VALUE; + } + mState |= event; + } + + public void setSuggestionTimeMillis(long suggestionTimeMillis) { + // Only record first suggestion. + if (mSuggestionTimeMillis == null) { + mSuggestionTimeMillis = Long.valueOf(suggestionTimeMillis); + } + } + + public void recordHistogram() { + RecordHistogram.recordEnumeratedHistogram(UMA_AUTOFILL_AUTOFILL_SESSION, + toUMAAutofillSessionValue(), AUTOFILL_SESSION_HISTOGRAM_COUNT); + // Only record if user ever changed form. + if (mUserChangedAutofilledField != null) { + RecordHistogram.recordBooleanHistogram( + UMA_AUTOFILL_USER_CHANGED_AUTOFILLED_FIELD, mUserChangedAutofilledField); + } + if (mSuggestionTimeMillis != null) { + recordTimesHistogram(UMA_AUTOFILL_SUGGESTION_TIME, mSuggestionTimeMillis); + } + } + + private int toUMAAutofillSessionValue() { + if (mState == 0) { + return NO_CALLBACK_FORM_FRAMEWORK; + } else if (mState == EVENT_VIRTUAL_STRUCTURE_PROVIDED) { + return NO_SUGGESTION_USER_NOT_CHANGE_FORM_NO_FORM_SUBMITTED; + } else if (mState + == (EVENT_VIRTUAL_STRUCTURE_PROVIDED | EVENT_USER_CHANGED_FIELD_VALUE)) { + return NO_SUGGESTION_USER_CHANGE_FORM_NO_FORM_SUBMITTED; + } else if (mState == (EVENT_VIRTUAL_STRUCTURE_PROVIDED | EVENT_FORM_SUBMITTED)) { + return NO_SUGGESTION_USER_NOT_CHANGE_FORM_FORM_SUBMITTED; + } else if (mState + == (EVENT_VIRTUAL_STRUCTURE_PROVIDED | EVENT_USER_CHANGED_FIELD_VALUE + | EVENT_FORM_SUBMITTED)) { + return NO_SUGGESTION_USER_CHANGE_FORM_FORM_SUBMITTED; + } else if (mState + == (EVENT_VIRTUAL_STRUCTURE_PROVIDED | EVENT_SUGGESTION_DISPLAYED + | EVENT_FORM_AUTOFILLED)) { + return USER_SELECT_SUGGESTION_USER_NOT_CHANGE_FORM_NO_FORM_SUBMITTED; + } else if (mState + == (EVENT_VIRTUAL_STRUCTURE_PROVIDED | EVENT_SUGGESTION_DISPLAYED + | EVENT_FORM_AUTOFILLED | EVENT_FORM_SUBMITTED)) { + return USER_SELECT_SUGGESTION_USER_NOT_CHANGE_FORM_FORM_SUBMITTED; + } else if (mState + == (EVENT_VIRTUAL_STRUCTURE_PROVIDED | EVENT_SUGGESTION_DISPLAYED + | EVENT_FORM_AUTOFILLED | EVENT_USER_CHANGED_FIELD_VALUE + | EVENT_FORM_SUBMITTED)) { + return USER_SELECT_SUGGESTION_USER_CHANGE_FORM_FORM_SUBMITTED; + } else if (mState + == (EVENT_VIRTUAL_STRUCTURE_PROVIDED | EVENT_SUGGESTION_DISPLAYED + | EVENT_FORM_AUTOFILLED | EVENT_USER_CHANGED_FIELD_VALUE)) { + return USER_SELECT_SUGGESTION_USER_CHANGE_FORM_NO_FORM_SUBMITTED; + } else if (mState == (EVENT_VIRTUAL_STRUCTURE_PROVIDED | EVENT_SUGGESTION_DISPLAYED)) { + return USER_NOT_SELECT_SUGGESTION_USER_NOT_CHANGE_FORM_NO_FORM_SUBMITTED; + } else if (mState + == (EVENT_VIRTUAL_STRUCTURE_PROVIDED | EVENT_SUGGESTION_DISPLAYED + | EVENT_FORM_SUBMITTED)) { + return USER_NOT_SELECT_SUGGESTION_USER_NOT_CHANGE_FORM_FORM_SUBMITTED; + } else if (mState + == (EVENT_VIRTUAL_STRUCTURE_PROVIDED | EVENT_SUGGESTION_DISPLAYED + | EVENT_USER_CHANGED_FIELD_VALUE | EVENT_FORM_SUBMITTED)) { + return USER_NOT_SELECT_SUGGESTION_USER_CHANGE_FORM_FORM_SUBMITTED; + } else if (mState + == (EVENT_VIRTUAL_STRUCTURE_PROVIDED | EVENT_SUGGESTION_DISPLAYED + | EVENT_USER_CHANGED_FIELD_VALUE)) { + return USER_NOT_SELECT_SUGGESTION_USER_CHANGE_FORM_NO_FORM_SUBMITTED; + } else { + return SESSION_UNKNOWN; + } + } + + private int mState; + private Boolean mUserChangedAutofilledField; + } + + private SessionRecorder mRecorder; + private Boolean mAutofillDisabled; + + public AutofillProviderUMA(Context context) { + RecordHistogram.recordBooleanHistogram(UMA_AUTOFILL_CREATED_BY_ACTIVITY_CONTEXT, + ContextUtils.activityFromContext(context) != null); + } + + public void onFormSubmitted(int submissionSource) { + if (mRecorder != null) mRecorder.record(SessionRecorder.EVENT_FORM_SUBMITTED); + recordSession(); + // We record this no matter autofill service is disabled or not. + RecordHistogram.recordEnumeratedHistogram(UMA_AUTOFILL_SUBMISSION_SOURCE, + toUMASubmissionSource(submissionSource), SUBMISSION_SOURCE_HISTOGRAM_COUNT); + } + + public void onSessionStarted(boolean autofillDisabled) { + // Record autofill status once per instance and only if user triggers the autofill. + if (mAutofillDisabled == null || mAutofillDisabled.booleanValue() != autofillDisabled) { + RecordHistogram.recordBooleanHistogram(UMA_AUTOFILL_ENABLED, !autofillDisabled); + mAutofillDisabled = Boolean.valueOf(autofillDisabled); + } + + if (mRecorder != null) recordSession(); + mRecorder = new SessionRecorder(); + } + + public void onVirtualStructureProvided() { + if (mRecorder != null) mRecorder.record(SessionRecorder.EVENT_VIRTUAL_STRUCTURE_PROVIDED); + } + + public void onSuggestionDisplayed(long suggestionTimeMillis) { + if (mRecorder != null) { + mRecorder.record(SessionRecorder.EVENT_SUGGESTION_DISPLAYED); + mRecorder.setSuggestionTimeMillis(suggestionTimeMillis); + } + } + + public void onAutofill() { + if (mRecorder != null) mRecorder.record(SessionRecorder.EVENT_FORM_AUTOFILLED); + } + + public void onUserChangeFieldValue(boolean isPreviouslyAutofilled) { + if (mRecorder == null) return; + if (isPreviouslyAutofilled) { + mRecorder.record(SessionRecorder.EVENT_USER_CHANGED_AUTOFILLED_FIELD); + } else { + mRecorder.record(SessionRecorder.EVENT_USER_CHANGED_FIELD_VALUE); + } + } + + private void recordSession() { + if (mAutofillDisabled != null && !mAutofillDisabled.booleanValue() && mRecorder != null) { + mRecorder.recordHistogram(); + } + mRecorder = null; + } + + private int toUMASubmissionSource(int source) { + switch (source) { + case SubmissionSource.SAME_DOCUMENT_NAVIGATION: + return SAME_DOCUMENT_NAVIGATION; + case SubmissionSource.XHR_SUCCEEDED: + return XHR_SUCCEEDED; + case SubmissionSource.FRAME_DETACHED: + return FRAME_DETACHED; + case SubmissionSource.DOM_MUTATION_AFTER_XHR: + return DOM_MUTATION_AFTER_XHR; + case SubmissionSource.PROBABLY_FORM_SUBMITTED: + return PROBABLY_FORM_SUBMITTED; + case SubmissionSource.FORM_SUBMISSION: + return FORM_SUBMISSION; + default: + return SUBMISSION_SOURCE_HISTOGRAM_COUNT; + } + } +} diff --git a/chromium/components/autofill/android/provider/java/src/org/chromium/components/autofill/FormData.java b/chromium/components/autofill/android/provider/java/src/org/chromium/components/autofill/FormData.java new file mode 100644 index 00000000000..8069aa887b6 --- /dev/null +++ b/chromium/components/autofill/android/provider/java/src/org/chromium/components/autofill/FormData.java @@ -0,0 +1,56 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package org.chromium.components.autofill; + +import androidx.annotation.VisibleForTesting; + +import org.chromium.base.annotations.CalledByNative; +import org.chromium.base.annotations.JNINamespace; +import org.chromium.base.annotations.NativeMethods; + +import java.util.ArrayList; + +/** + * The wrap class of native autofill::FormDataAndroid. + */ +@JNINamespace("autofill") +public class FormData { + public final String mName; + public final String mHost; + public final ArrayList<FormFieldData> mFields; + + @CalledByNative + private static FormData createFormData( + long nativeObj, String name, String origin, int fieldCount) { + return new FormData(nativeObj, name, origin, fieldCount); + } + + private static ArrayList<FormFieldData> popupFormFields(long nativeObj, int fieldCount) { + FormFieldData formFieldData = FormDataJni.get().getNextFormFieldData(nativeObj); + ArrayList<FormFieldData> fields = new ArrayList<FormFieldData>(fieldCount); + while (formFieldData != null) { + fields.add(formFieldData); + formFieldData = FormDataJni.get().getNextFormFieldData(nativeObj); + } + assert fields.size() == fieldCount; + return fields; + } + + @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) + public FormData(String name, String host, ArrayList<FormFieldData> fields) { + mName = name; + mHost = host; + mFields = fields; + } + + private FormData(long nativeObj, String name, String host, int fieldCount) { + this(name, host, popupFormFields(nativeObj, fieldCount)); + } + + @NativeMethods + interface Natives { + FormFieldData getNextFormFieldData(long nativeFormDataAndroid); + } +} diff --git a/chromium/components/autofill/android/provider/java/src/org/chromium/components/autofill/FormFieldData.java b/chromium/components/autofill/android/provider/java/src/org/chromium/components/autofill/FormFieldData.java new file mode 100644 index 00000000000..eb0a94a6bce --- /dev/null +++ b/chromium/components/autofill/android/provider/java/src/org/chromium/components/autofill/FormFieldData.java @@ -0,0 +1,162 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package org.chromium.components.autofill; + +import android.graphics.RectF; + +import androidx.annotation.IntDef; +import androidx.annotation.VisibleForTesting; + +import org.chromium.base.annotations.CalledByNative; +import org.chromium.base.annotations.JNINamespace; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * The wrap class of native autofill::FormFieldDataAndroid. + */ +@JNINamespace("autofill") +public class FormFieldData { + /** + * Define the control types supported by android.view.autofill.AutofillValue. + * + * Android doesn't have DATALIST control, it is sent to the Autofill service as + * View.AUTOFILL_TYPE_TEXT with AutofillOptions. + */ + @IntDef({ControlType.TEXT, ControlType.TOGGLE, ControlType.LIST, ControlType.DATALIST}) + @Retention(RetentionPolicy.SOURCE) + public @interface ControlType { + int TEXT = 0; + int TOGGLE = 1; + int LIST = 2; + int DATALIST = 3; + } + + public final String mLabel; + public final String mName; + public final String mAutocompleteAttr; + public final boolean mShouldAutocomplete; + public final String mPlaceholder; + public final String mType; + public final String mId; + public final String[] mOptionValues; + public final String[] mOptionContents; + public final @ControlType int mControlType; + public final int mMaxLength; + public final String mHeuristicType; + public final String[] mDatalistValues; + public final String[] mDatalistLabels; + + // The bounds in the viewport's coordinates + private final RectF mBounds; + // The bounds in the container view's coordinates. + private RectF mBoundsInContainerViewCoordinates; + + private boolean mIsChecked; + private String mValue; + // Indicates whether mValue is autofilled. + private boolean mAutofilled; + // Indicates whether this fields was autofilled, but changed by user. + private boolean mPreviouslyAutofilled; + + private FormFieldData(String name, String label, String value, String autocompleteAttr, + boolean shouldAutocomplete, String placeholder, String type, String id, + String[] optionValues, String[] optionContents, boolean isCheckField, boolean isChecked, + int maxLength, String heuristicType, float left, float top, float right, float bottom, + String[] datalistValues, String[] datalistLabels) { + mName = name; + mLabel = label; + mValue = value; + mAutocompleteAttr = autocompleteAttr; + mShouldAutocomplete = shouldAutocomplete; + mPlaceholder = placeholder; + mType = type; + mId = id; + mOptionValues = optionValues; + mOptionContents = optionContents; + mIsChecked = isChecked; + mDatalistLabels = datalistLabels; + mDatalistValues = datalistValues; + if (mOptionValues != null && mOptionValues.length != 0) { + mControlType = ControlType.LIST; + } else if (mDatalistValues != null && mDatalistValues.length != 0) { + mControlType = ControlType.DATALIST; + } else if (isCheckField) { + mControlType = ControlType.TOGGLE; + } else { + mControlType = ControlType.TEXT; + } + mMaxLength = maxLength; + mHeuristicType = heuristicType; + mBounds = new RectF(left, top, right, bottom); + } + + public @ControlType int getControlType() { + return mControlType; + } + + public RectF getBounds() { + return mBounds; + } + + public void setBoundsInContainerViewCoordinates(RectF bounds) { + mBoundsInContainerViewCoordinates = bounds; + } + + public RectF getBoundsInContainerViewCoordinates() { + return mBoundsInContainerViewCoordinates; + } + + /** + * @return value of field. + */ + @CalledByNative + public String getValue() { + return mValue; + } + + public void setAutofillValue(String value) { + mValue = value; + updateAutofillState(true); + } + + public void setChecked(boolean checked) { + mIsChecked = checked; + updateAutofillState(true); + } + + @CalledByNative + private void updateValue(String value) { + mValue = value; + updateAutofillState(false); + } + + @CalledByNative + public boolean isChecked() { + return mIsChecked; + } + + public boolean hasPreviouslyAutofilled() { + return mPreviouslyAutofilled; + } + + private void updateAutofillState(boolean autofilled) { + if (mAutofilled && !autofilled) mPreviouslyAutofilled = true; + mAutofilled = autofilled; + } + + @CalledByNative + @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) + public static FormFieldData createFormFieldData(String name, String label, String value, + String autocompleteAttr, boolean shouldAutocomplete, String placeholder, String type, + String id, String[] optionValues, String[] optionContents, boolean isCheckField, + boolean isChecked, int maxLength, String heuristicType, float left, float top, + float right, float bottom, String[] datalistValues, String[] datalistLabels) { + return new FormFieldData(name, label, value, autocompleteAttr, shouldAutocomplete, + placeholder, type, id, optionValues, optionContents, isCheckField, isChecked, + maxLength, heuristicType, left, top, right, bottom, datalistValues, datalistLabels); + } +} diff --git a/chromium/components/autofill/android/provider/junit/BUILD.gn b/chromium/components/autofill/android/provider/junit/BUILD.gn new file mode 100644 index 00000000000..95cea50bf90 --- /dev/null +++ b/chromium/components/autofill/android/provider/junit/BUILD.gn @@ -0,0 +1,23 @@ +# Copyright 2020 The Chromium Authors. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +import("//build/config/android/config.gni") +import("//build/config/android/rules.gni") + +java_library("components_autofill_junit_tests") { + # Platform checks are broken for Robolectric. See https://crbug.com/1071638. + bypass_platform_checks = true + testonly = true + sources = [ "src/org/chromium/components/autofill/AutofillProviderTest.java" ] + deps = [ + "//base:base_java_test_support", + "//base:base_junit_test_support", + "//components/autofill/android/provider:java", + "//content/public/android:content_java", + "//third_party/android_deps:robolectric_all_java", + "//third_party/junit", + "//third_party/mockito:mockito_java", + "//ui/android:ui_java", + ] +} diff --git a/chromium/components/autofill/android/provider/junit/src/org/chromium/components/autofill/AutofillProviderTest.java b/chromium/components/autofill/android/provider/junit/src/org/chromium/components/autofill/AutofillProviderTest.java new file mode 100644 index 00000000000..e4b12cec808 --- /dev/null +++ b/chromium/components/autofill/android/provider/junit/src/org/chromium/components/autofill/AutofillProviderTest.java @@ -0,0 +1,115 @@ +// Copyright 2020 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package org.chromium.components.autofill; + +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.when; + +import android.content.Context; +import android.graphics.Rect; +import android.graphics.RectF; +import android.view.ViewGroup; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentMatchers; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; +import org.robolectric.annotation.Config; + +import org.chromium.base.test.BaseRobolectricTestRunner; +import org.chromium.content_public.browser.WebContents; +import org.chromium.ui.base.WindowAndroid; +import org.chromium.ui.display.DisplayAndroid; + +import java.util.ArrayList; + +/** + * The unit tests for AutofillProvider. + */ +@RunWith(BaseRobolectricTestRunner.class) +@Config(manifest = Config.NONE) +public class AutofillProviderTest { + private static final float EXPECTED_DIP_SCALE = 2; + private static final int SCROLL_X = 15; + private static final int SCROLL_Y = 155; + private static final int LOCATION_X = 25; + private static final int LOCATION_Y = 255; + + private Context mContext; + private WindowAndroid mWindowAndroid; + private WebContents mWebContents; + private ViewGroup mContainerView; + private AutofillProvider mAutofillProvider; + private DisplayAndroid mDisplayAndroid; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + mContext = Mockito.mock(Context.class); + mWindowAndroid = Mockito.mock(WindowAndroid.class); + mDisplayAndroid = Mockito.mock(DisplayAndroid.class); + mWebContents = Mockito.mock(WebContents.class); + mContainerView = Mockito.mock(ViewGroup.class); + mAutofillProvider = new AutofillProvider(mContext, mContainerView, "AutofillProviderTest"); + mAutofillProvider.setWebContents(mWebContents); + + when(mWebContents.getTopLevelNativeWindow()).thenReturn(mWindowAndroid); + when(mWindowAndroid.getDisplay()).thenReturn(mDisplayAndroid); + when(mDisplayAndroid.getDipScale()).thenReturn(EXPECTED_DIP_SCALE); + when(mContainerView.getScrollX()).thenReturn(SCROLL_X); + when(mContainerView.getScrollY()).thenReturn(SCROLL_Y); + doAnswer(new Answer<Void>() { + @Override + public Void answer(InvocationOnMock invocation) { + Object[] args = invocation.getArguments(); + int[] location = (int[]) args[0]; + location[0] = LOCATION_X; + location[1] = LOCATION_Y; + return null; + } + }) + .when(mContainerView) + .getLocationOnScreen(ArgumentMatchers.any()); + } + + @Test + public void testTransformFormFieldToContainViewCoordinates() { + ArrayList<FormFieldData> fields = new ArrayList<FormFieldData>(1); + fields.add(FormFieldData.createFormFieldData(null, null, null, null, false, null, null, + null, null, null, false, false, 0, null, 10 /* left */, 20 /* top */, + 300 /* right */, 60 /*bottom*/, null, null)); + fields.add(FormFieldData.createFormFieldData(null, null, null, null, false, null, null, + null, null, null, false, false, 0, null, 20 /* left */, 100 /* top */, + 400 /* right */, 200 /*bottom*/, null, null)); + FormData formData = new FormData(null, null, fields); + mAutofillProvider.transformFormFieldToContainViewCoordinates(formData); + RectF result = formData.mFields.get(0).getBoundsInContainerViewCoordinates(); + assertEquals(10 * EXPECTED_DIP_SCALE + SCROLL_X, result.left, 0); + assertEquals(20 * EXPECTED_DIP_SCALE + SCROLL_Y, result.top, 0); + assertEquals(300 * EXPECTED_DIP_SCALE + SCROLL_X, result.right, 0); + assertEquals(60 * EXPECTED_DIP_SCALE + SCROLL_Y, result.bottom, 0); + + result = formData.mFields.get(1).getBoundsInContainerViewCoordinates(); + assertEquals(20 * EXPECTED_DIP_SCALE + SCROLL_X, result.left, 0); + assertEquals(100 * EXPECTED_DIP_SCALE + SCROLL_Y, result.top, 0); + assertEquals(400 * EXPECTED_DIP_SCALE + SCROLL_X, result.right, 0); + assertEquals(200 * EXPECTED_DIP_SCALE + SCROLL_Y, result.bottom, 0); + } + + @Test + public void testTransformToWindowBounds() { + RectF source = new RectF(10, 20, 300, 400); + Rect result = mAutofillProvider.transformToWindowBounds(source); + assertEquals(10 * EXPECTED_DIP_SCALE + LOCATION_X, result.left, 0); + assertEquals(20 * EXPECTED_DIP_SCALE + LOCATION_Y, result.top, 0); + assertEquals(300 * EXPECTED_DIP_SCALE + LOCATION_X, result.right, 0); + assertEquals(400 * EXPECTED_DIP_SCALE + LOCATION_Y, result.bottom, 0); + } +} |