| // 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. |
| |
| #include "chrome/browser/android/autofill_assistant/client_android.h" |
| |
| #include <utility> |
| #include <vector> |
| |
| #include "base/android/jni_android.h" |
| #include "base/android/jni_array.h" |
| #include "base/android/jni_string.h" |
| #include "base/android/locale_utils.h" |
| #include "base/android/scoped_java_ref.h" |
| #include "base/bind.h" |
| #include "base/command_line.h" |
| #include "base/metrics/field_trial_params.h" |
| #include "base/task/post_task.h" |
| #include "base/time/default_tick_clock.h" |
| #include "chrome/android/features/autofill_assistant/jni_headers/AutofillAssistantClient_jni.h" |
| #include "chrome/browser/android/chrome_feature_list.h" |
| #include "chrome/browser/autofill/android/personal_data_manager_android.h" |
| #include "chrome/browser/autofill/personal_data_manager_factory.h" |
| #include "chrome/browser/password_manager/chrome_password_manager_client.h" |
| #include "chrome/browser/profiles/profile.h" |
| #include "chrome/browser/profiles/profile_manager.h" |
| #include "chrome/browser/signin/identity_manager_factory.h" |
| #include "chrome/common/channel_info.h" |
| #include "components/autofill_assistant/browser/access_token_fetcher.h" |
| #include "components/autofill_assistant/browser/controller.h" |
| #include "components/autofill_assistant/browser/features.h" |
| #include "components/autofill_assistant/browser/website_login_fetcher_impl.h" |
| #include "components/signin/public/identity_manager/account_info.h" |
| #include "components/signin/public/identity_manager/identity_manager.h" |
| #include "components/version_info/channel.h" |
| #include "content/public/browser/browser_task_traits.h" |
| #include "content/public/browser/browser_thread.h" |
| #include "content/public/browser/web_contents.h" |
| #include "google_apis/google_api_keys.h" |
| #include "url/gurl.h" |
| |
| using base::android::AttachCurrentThread; |
| using base::android::JavaParamRef; |
| using base::android::JavaRef; |
| |
| namespace autofill_assistant { |
| namespace switches { |
| const char* const kAutofillAssistantServerKey = "autofill-assistant-key"; |
| const char* const kAutofillAssistantUrl = "autofill-assistant-url"; |
| } // namespace switches |
| |
| namespace { |
| |
| const char* const kDefaultAutofillAssistantServerUrl = |
| "https://automate-pa.googleapis.com"; |
| |
| // A direct action that corresponds to pressing the close or cancel button on |
| // the UI. |
| const char* const kCancelActionName = "cancel"; |
| |
| // Fills a map from two Java arrays of strings of the same length. |
| void FillStringMapFromJava(JNIEnv* env, |
| const JavaRef<jobjectArray>& names, |
| const JavaRef<jobjectArray>& values, |
| std::map<std::string, std::string>* parameters) { |
| std::vector<std::string> names_vector; |
| base::android::AppendJavaStringArrayToStringVector(env, names, &names_vector); |
| std::vector<std::string> values_vector; |
| base::android::AppendJavaStringArrayToStringVector(env, values, |
| &values_vector); |
| DCHECK_EQ(names_vector.size(), values_vector.size()); |
| for (size_t i = 0; i < names_vector.size(); ++i) { |
| parameters->insert(std::make_pair(names_vector[i], values_vector[i])); |
| } |
| } |
| |
| std::unique_ptr<TriggerContextImpl> CreateTriggerContext( |
| JNIEnv* env, |
| const base::android::JavaParamRef<jstring>& jexperiment_ids, |
| const base::android::JavaParamRef<jobjectArray>& jparameter_names, |
| const base::android::JavaParamRef<jobjectArray>& jparameter_values) { |
| std::map<std::string, std::string> parameters; |
| FillStringMapFromJava(env, jparameter_names, jparameter_values, ¶meters); |
| return std::make_unique<TriggerContextImpl>( |
| std::move(parameters), |
| base::android::ConvertJavaStringToUTF8(env, jexperiment_ids)); |
| } |
| |
| } // namespace |
| |
| static base::android::ScopedJavaLocalRef<jobject> |
| JNI_AutofillAssistantClient_FromWebContents( |
| JNIEnv* env, |
| const base::android::JavaParamRef<jobject>& jweb_contents) { |
| auto* web_contents = content::WebContents::FromJavaWebContents(jweb_contents); |
| ClientAndroid::CreateForWebContents(web_contents); |
| return ClientAndroid::FromWebContents(web_contents)->GetJavaObject(); |
| } |
| |
| ClientAndroid::ClientAndroid(content::WebContents* web_contents) |
| : web_contents_(web_contents), |
| java_object_(Java_AutofillAssistantClient_create( |
| AttachCurrentThread(), |
| reinterpret_cast<intptr_t>(this))) { |
| server_url_ = base::CommandLine::ForCurrentProcess()->GetSwitchValueASCII( |
| switches::kAutofillAssistantUrl); |
| if (server_url_.empty()) { |
| server_url_ = kDefaultAutofillAssistantServerUrl; |
| } |
| } |
| |
| ClientAndroid::~ClientAndroid() { |
| if (controller_ != nullptr && started_) { |
| // In the case of an unexpected closing of the activity or tab, controller_ |
| // will not yet have been cleaned up (since that happens when a web |
| // contents object gets destroyed). |
| Metrics::RecordDropOut(Metrics::DropOutReason::CONTENT_DESTROYED); |
| } |
| Java_AutofillAssistantClient_clearNativePtr(AttachCurrentThread(), |
| java_object_); |
| } |
| |
| base::android::ScopedJavaLocalRef<jobject> ClientAndroid::GetJavaObject() { |
| return base::android::ScopedJavaLocalRef<jobject>(java_object_); |
| } |
| |
| bool ClientAndroid::Start(JNIEnv* env, |
| const JavaParamRef<jobject>& jcaller, |
| const JavaParamRef<jstring>& jinitial_url, |
| const JavaParamRef<jstring>& jexperiment_ids, |
| const JavaParamRef<jobjectArray>& parameter_names, |
| const JavaParamRef<jobjectArray>& parameter_values, |
| const JavaParamRef<jobject>& jonboarding_coordinator, |
| jlong jservice) { |
| // When Start() is called, AA_START should have been measured. From now on, |
| // the client is responsible for keeping track of dropouts, so that for each |
| // AA_START there's a corresponding dropout. |
| started_ = true; |
| |
| std::unique_ptr<Service> service = nullptr; |
| if (jservice) { |
| service.reset(static_cast<Service*>(reinterpret_cast<void*>(jservice))); |
| } |
| CreateController(std::move(service)); |
| |
| // If an overlay is already shown, then show the rest of the UI. |
| if (jonboarding_coordinator) { |
| AttachUI(jonboarding_coordinator); |
| } |
| |
| std::unique_ptr<TriggerContextImpl> trigger_context = CreateTriggerContext( |
| env, jexperiment_ids, parameter_names, parameter_values); |
| trigger_context->SetCCT(true); |
| |
| GURL initial_url(base::android::ConvertJavaStringToUTF8(env, jinitial_url)); |
| return controller_->Start(initial_url, std::move(trigger_context)); |
| } |
| |
| void ClientAndroid::DestroyUI( |
| JNIEnv* env, |
| const base::android::JavaParamRef<jobject>& jcaller) { |
| DestroyUI(); |
| } |
| |
| void ClientAndroid::TransferUITo( |
| JNIEnv* env, |
| const base::android::JavaParamRef<jobject>& jcaller, |
| const base::android::JavaParamRef<jobject>& jother_web_contents) { |
| if (!ui_controller_android_) |
| return; |
| |
| auto ui_ptr = std::move(ui_controller_android_); |
| // From this point on, the UIController, in ui_ptr, is either transferred or |
| // deleted. |
| |
| if (!jother_web_contents) |
| return; |
| |
| auto* other_web_contents = |
| content::WebContents::FromJavaWebContents(jother_web_contents); |
| DCHECK_NE(other_web_contents, web_contents_); |
| |
| ClientAndroid* other_client = |
| ClientAndroid::FromWebContents(other_web_contents); |
| if (!other_client || !other_client->NeedsUI()) |
| return; |
| |
| other_client->ui_controller_android_ = std::move(ui_ptr); |
| other_client->AttachUI(); |
| } |
| |
| base::android::ScopedJavaLocalRef<jstring> ClientAndroid::GetPrimaryAccountName( |
| JNIEnv* env, |
| const JavaParamRef<jobject>& jcaller) { |
| CoreAccountInfo account_info = |
| IdentityManagerFactory::GetForProfile( |
| Profile::FromBrowserContext(web_contents_->GetBrowserContext())) |
| ->GetPrimaryAccountInfo(); |
| return base::android::ConvertUTF8ToJavaString(env, account_info.email); |
| } |
| |
| void ClientAndroid::OnAccessToken(JNIEnv* env, |
| const JavaParamRef<jobject>& jcaller, |
| jboolean success, |
| const JavaParamRef<jstring>& access_token) { |
| if (fetch_access_token_callback_) { |
| std::move(fetch_access_token_callback_) |
| .Run(success, base::android::ConvertJavaStringToUTF8(access_token)); |
| } |
| } |
| |
| void ClientAndroid::ListDirectActions( |
| JNIEnv* env, |
| const base::android::JavaParamRef<jobject>& jcaller, |
| const base::android::JavaParamRef<jstring>& jexperiment_ids, |
| const base::android::JavaParamRef<jobjectArray>& jargument_names, |
| const base::android::JavaParamRef<jobjectArray>& jargument_values, |
| const base::android::JavaParamRef<jobject>& jcallback) { |
| if (!controller_) |
| CreateController(nullptr); |
| |
| base::android::ScopedJavaGlobalRef<jobject> scoped_jcallback(env, jcallback); |
| controller_->Track( |
| CreateTriggerContext(env, jexperiment_ids, jargument_names, |
| jargument_values), |
| base::BindOnce(&ClientAndroid::OnListDirectActions, |
| weak_ptr_factory_.GetWeakPtr(), scoped_jcallback)); |
| } |
| |
| void ClientAndroid::OnListDirectActions( |
| const base::android::JavaRef<jobject>& jcallback) { |
| // Using a set here helps remove duplicates. |
| std::set<std::string> names; |
| for (const UserAction& user_action : controller_->GetUserActions()) { |
| if (!user_action.enabled()) |
| continue; |
| |
| for (const std::string& name : user_action.direct_action().names) { |
| names.insert(name); |
| } |
| } |
| |
| // Cancel is always available when the UI is up. |
| if (ui_controller_android_) |
| names.insert(kCancelActionName); |
| |
| JNIEnv* env = AttachCurrentThread(); |
| Java_AutofillAssistantClient_sendDirectActionList( |
| env, java_object_, jcallback, |
| base::android::ToJavaArrayOfStrings( |
| env, std::vector<std::string>(names.begin(), names.end()))); |
| } |
| |
| bool ClientAndroid::PerformDirectAction( |
| JNIEnv* env, |
| const base::android::JavaParamRef<jobject>& jcaller, |
| const base::android::JavaParamRef<jstring>& jaction_name, |
| const base::android::JavaParamRef<jstring>& jexperiment_ids, |
| const base::android::JavaParamRef<jobjectArray>& jargument_names, |
| const base::android::JavaParamRef<jobjectArray>& jargument_values, |
| const base::android::JavaParamRef<jobject>& jonboarding_coordinator) { |
| std::string action_name = |
| base::android::ConvertJavaStringToUTF8(env, jaction_name); |
| |
| int action_index = FindDirectAction(action_name); |
| |
| std::unique_ptr<TriggerContextImpl> trigger_context = CreateTriggerContext( |
| env, jexperiment_ids, jargument_names, jargument_values); |
| trigger_context->SetDirectAction(true); |
| // Cancel through the UI if it is up. This allows the user to undo. This is |
| // always available, even if no action was found and action_index == -1. |
| if (action_name == kCancelActionName && ui_controller_android_) { |
| ui_controller_android_->CloseOrCancel(action_index, |
| std::move(trigger_context)); |
| return true; |
| } |
| |
| if (action_index == -1) |
| return false; |
| |
| // If an overlay is already shown, then show the rest of the UI immediately. |
| if (jonboarding_coordinator) { |
| AttachUI(jonboarding_coordinator); |
| } |
| |
| return controller_->PerformUserActionWithContext(action_index, |
| std::move(trigger_context)); |
| } |
| |
| int ClientAndroid::FindDirectAction(const std::string& action_name) { |
| // It's too late to create a controller. This should have been done in |
| // ListDirectActions. |
| if (!controller_) |
| return -1; |
| |
| const std::vector<UserAction>& user_actions = controller_->GetUserActions(); |
| int user_action_count = user_actions.size(); |
| for (int i = 0; i < user_action_count; i++) { |
| const UserAction& user_action = user_actions[i]; |
| if (!user_action.enabled()) |
| continue; |
| |
| const std::set<std::string>& action_names = |
| user_action.direct_action().names; |
| if (action_names.count(action_name) != 0) |
| return i; |
| } |
| |
| return -1; |
| } |
| |
| void ClientAndroid::AttachUI() { |
| AttachUI(nullptr); |
| } |
| |
| void ClientAndroid::AttachUI( |
| const JavaParamRef<jobject>& jonboarding_coordinator) { |
| if (!ui_controller_android_) { |
| ui_controller_android_ = UiControllerAndroid::CreateFromWebContents( |
| web_contents_, jonboarding_coordinator); |
| if (!ui_controller_android_) { |
| // The activity is not or not yet in a mode where attaching the UI is |
| // possible. |
| return; |
| } |
| } |
| |
| if (!ui_controller_android_->IsAttached()) { |
| if (!controller_) |
| CreateController(nullptr); |
| |
| ui_controller_android_->Attach(web_contents_, this, controller_.get()); |
| } |
| } |
| |
| void ClientAndroid::DestroyUI() { |
| ui_controller_android_.reset(); |
| } |
| |
| std::string ClientAndroid::GetApiKey() { |
| std::string api_key; |
| if (google_apis::IsGoogleChromeAPIKeyUsed()) { |
| api_key = chrome::GetChannel() == version_info::Channel::STABLE |
| ? google_apis::GetAPIKey() |
| : google_apis::GetNonStableAPIKey(); |
| } |
| const auto* command_line = base::CommandLine::ForCurrentProcess(); |
| if (command_line->HasSwitch(switches::kAutofillAssistantServerKey)) { |
| api_key = command_line->GetSwitchValueASCII( |
| switches::kAutofillAssistantServerKey); |
| } |
| return api_key; |
| } |
| |
| std::string ClientAndroid::GetAccountEmailAddress() { |
| JNIEnv* env = AttachCurrentThread(); |
| return base::android::ConvertJavaStringToUTF8( |
| Java_AutofillAssistantClient_getAccountEmailAddress(env, java_object_)); |
| } |
| |
| AccessTokenFetcher* ClientAndroid::GetAccessTokenFetcher() { |
| return this; |
| } |
| |
| autofill::PersonalDataManager* ClientAndroid::GetPersonalDataManager() { |
| return autofill::PersonalDataManagerFactory::GetForProfile( |
| ProfileManager::GetLastUsedProfile()); |
| } |
| |
| WebsiteLoginFetcher* ClientAndroid::GetWebsiteLoginFetcher() { |
| if (!website_login_fetcher_) { |
| website_login_fetcher_ = std::make_unique<WebsiteLoginFetcherImpl>( |
| ChromePasswordManagerClient::FromWebContents(web_contents_)); |
| } |
| return website_login_fetcher_.get(); |
| } |
| |
| std::string ClientAndroid::GetServerUrl() { |
| return server_url_; |
| } |
| |
| std::string ClientAndroid::GetLocale() { |
| return base::android::GetDefaultLocaleString(); |
| } |
| |
| std::string ClientAndroid::GetCountryCode() { |
| return base::android::ConvertJavaStringToUTF8( |
| Java_AutofillAssistantClient_getCountryCode(AttachCurrentThread(), |
| java_object_)); |
| } |
| |
| void ClientAndroid::Shutdown(Metrics::DropOutReason reason) { |
| if (!controller_) |
| return; |
| |
| if (ui_controller_android_ && ui_controller_android_->IsAttached()) |
| DestroyUI(); |
| |
| if (started_) |
| Metrics::RecordDropOut(reason); |
| |
| // Delete the controller in a separate task. This avoids tricky ordering |
| // issues when Shutdown is called from the controller. |
| base::PostTask(FROM_HERE, {content::BrowserThread::UI}, |
| base::BindOnce(&ClientAndroid::DestroyController, |
| weak_ptr_factory_.GetWeakPtr())); |
| } |
| |
| void ClientAndroid::FetchAccessToken( |
| base::OnceCallback<void(bool, const std::string&)> callback) { |
| DCHECK(!fetch_access_token_callback_); |
| |
| fetch_access_token_callback_ = std::move(callback); |
| JNIEnv* env = AttachCurrentThread(); |
| Java_AutofillAssistantClient_fetchAccessToken(env, java_object_); |
| } |
| |
| void ClientAndroid::InvalidateAccessToken(const std::string& access_token) { |
| JNIEnv* env = AttachCurrentThread(); |
| Java_AutofillAssistantClient_invalidateAccessToken( |
| env, java_object_, |
| base::android::ConvertUTF8ToJavaString(env, access_token)); |
| } |
| |
| void ClientAndroid::CreateController(std::unique_ptr<Service> service) { |
| if (controller_) { |
| return; |
| } |
| controller_ = std::make_unique<Controller>( |
| web_contents_, /* client= */ this, base::DefaultTickClock::GetInstance(), |
| std::move(service)); |
| } |
| |
| void ClientAndroid::DestroyController() { |
| controller_.reset(); |
| started_ = false; |
| } |
| |
| bool ClientAndroid::NeedsUI() { |
| return !ui_controller_android_ && controller_ && controller_->NeedsUI(); |
| } |
| |
| WEB_CONTENTS_USER_DATA_KEY_IMPL(ClientAndroid) |
| |
| } // namespace autofill_assistant. |