chromium/chrome/browser/keyboard_accessory/android/manual_filling_controller_impl.cc

// Copyright 2018 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "chrome/browser/keyboard_accessory/android/manual_filling_controller_impl.h"

#include <numeric>
#include <optional>
#include <utility>

#include "base/containers/fixed_flat_set.h"
#include "base/functional/callback.h"
#include "base/memory/ptr_util.h"
#include "base/metrics/histogram_macros.h"
#include "base/strings/stringprintf.h"
#include "base/task/single_thread_task_runner.h"
#include "base/trace_event/memory_allocator_dump.h"
#include "base/trace_event/memory_dump_manager.h"
#include "base/trace_event/memory_usage_estimator.h"
#include "base/trace_event/process_memory_dump.h"
#include "chrome/browser/autofill/manual_filling_view_interface.h"
#include "chrome/browser/keyboard_accessory/android/accessory_sheet_data.h"
#include "chrome/browser/keyboard_accessory/android/accessory_sheet_enums.h"
#include "chrome/browser/keyboard_accessory/android/address_accessory_controller.h"
#include "chrome/browser/keyboard_accessory/android/affiliated_plus_profiles_cache.h"
#include "chrome/browser/keyboard_accessory/android/password_accessory_controller.h"
#include "chrome/browser/keyboard_accessory/android/payment_method_accessory_controller.h"
#include "chrome/browser/password_manager/chrome_password_manager_client.h"
#include "chrome/browser/plus_addresses/plus_address_service_factory.h"
#include "components/autofill/content/browser/content_autofill_client.h"
#include "components/plus_addresses/features.h"
#include "content/public/browser/web_contents.h"

using autofill::AccessoryAction;
using autofill::AccessorySheetData;
using autofill::AccessoryTabType;
using autofill::AddressAccessoryController;
using autofill::PaymentMethodAccessoryController;
using autofill::mojom::FocusedFieldType;

using FillingSource = ManualFillingController::FillingSource;

namespace {

constexpr auto kAllowedFillingSources = base::MakeFixedFlatSet<FillingSource>(
    {FillingSource::PASSWORD_FALLBACKS, FillingSource::CREDIT_CARD_FALLBACKS,
     FillingSource::ADDRESS_FALLBACKS});

}  // namespace

ManualFillingControllerImpl::~ManualFillingControllerImpl() {
  base::trace_event::MemoryDumpManager::GetInstance()->UnregisterDumpProvider(
      this);
}
// static
base::WeakPtr<ManualFillingController> ManualFillingController::GetOrCreate(
    content::WebContents* contents) {
  ManualFillingControllerImpl* mf_controller =
      ManualFillingControllerImpl::FromWebContents(contents);
  if (!mf_controller) {
    ManualFillingControllerImpl::CreateForWebContents(contents);
    mf_controller = ManualFillingControllerImpl::FromWebContents(contents);
    mf_controller->Initialize();
  }
  return mf_controller->AsWeakPtr();
}

// static
base::WeakPtr<ManualFillingController> ManualFillingController::Get(
    content::WebContents* contents) {
  ManualFillingControllerImpl* mf_controller =
      ManualFillingControllerImpl::FromWebContents(contents);
  return mf_controller ? mf_controller->AsWeakPtr() : nullptr;
}

// static
void ManualFillingControllerImpl::CreateForWebContentsForTesting(
    content::WebContents* web_contents,
    base::WeakPtr<PasswordAccessoryController> pwd_controller,
    base::WeakPtr<AddressAccessoryController> address_controller,
    base::WeakPtr<PaymentMethodAccessoryController> payment_method_controller,
    std::unique_ptr<ManualFillingViewInterface> view) {
  DCHECK(web_contents) << "Need valid WebContents to attach controller to!";
  DCHECK(!FromWebContents(web_contents)) << "Controller already attached!";
  DCHECK(pwd_controller);
  DCHECK(address_controller);
  DCHECK(payment_method_controller);
  DCHECK(view);

  web_contents->SetUserData(
      UserDataKey(),
      // Using `new` to access a non-public constructor.
      base::WrapUnique(new ManualFillingControllerImpl(
          web_contents, std::move(pwd_controller),
          std::move(address_controller), std::move(payment_method_controller),
          std::move(view))));

  FromWebContents(web_contents)->Initialize();
}

void ManualFillingControllerImpl::OnAccessoryActionAvailabilityChanged(
    ShouldShowAction shouldShowAction,
    autofill::AccessoryAction action) {
  DCHECK(view_);
  view_->OnAccessoryActionAvailabilityChanged(shouldShowAction, action);
}

void ManualFillingControllerImpl::NotifyFocusedInputChanged(
    autofill::FieldRendererId focused_field_id,
    autofill::mojom::FocusedFieldType focused_field_type) {
  TRACE_EVENT0("passwords",
               "ManualFillingControllerImpl::NotifyFocusedInputChanged");
  autofill::LocalFrameToken frame_token;
  if (content::RenderFrameHost* rfh = GetWebContents().GetFocusedFrame()) {
    frame_token = autofill::LocalFrameToken(rfh->GetFrameToken().value());
  }
  last_focused_field_id_ = {frame_token, focused_field_id};
  last_focused_field_type_ = focused_field_type;

  // Ensure warnings and filling state is updated according to focused field.
  if (payment_method_controller_) {
    payment_method_controller_->RefreshSuggestions();
  }

  // Whenever the focus changes, reset the accessory.
  if (ShouldShowAccessory())
    view_->SwapSheetWithKeyboard();
  else
    view_->CloseAccessorySheet();

  UpdateVisibility();
}

autofill::FieldGlobalId ManualFillingControllerImpl::GetLastFocusedFieldId()
    const {
  return last_focused_field_id_;
}

void ManualFillingControllerImpl::ShowAccessorySheetTab(
    const autofill::AccessoryTabType& tab_type) {
  if (tab_type == autofill::AccessoryTabType::CREDIT_CARDS) {
    payment_method_controller_->RefreshSuggestions();
  } else {
    NOTIMPLEMENTED()
        << "ShowAccessorySheetTab does not support the given TabType yet "
        << tab_type;
  }
  view_->ShowAccessorySheetTab(tab_type);
}

void ManualFillingControllerImpl::UpdateSourceAvailability(
    FillingSource source,
    bool has_suggestions) {
  if (has_suggestions == available_sources_.contains(source))
    return;

  if (has_suggestions) {
    available_sources_.insert(source);
    UpdateVisibility();
    return;
  }

  available_sources_.erase(source);
  if (!ShouldShowAccessory())
    UpdateVisibility();
}

void ManualFillingControllerImpl::Hide() {
  view_->Hide();
}

void ManualFillingControllerImpl::OnFillingTriggered(
    AccessoryTabType type,
    const autofill::AccessorySheetField& selection) {
  AccessoryController* controller = GetControllerForTabType(type);
  if (!controller)
    return;  // Controller not available anymore.
  controller->OnFillingTriggered(last_focused_field_id_, selection);
  view_->SwapSheetWithKeyboard();  // Soft-close the keyboard.
}

void ManualFillingControllerImpl::OnPasskeySelected(
    AccessoryTabType type,
    const std::vector<uint8_t>& passkey_id) {
  AccessoryController* controller = GetControllerForTabType(type);
  if (!controller) {
    return;  // Controller not available anymore.
  }
  controller->OnPasskeySelected(passkey_id);
  view_->Hide();  // Close the sheet since the passkey sheet will be triggered.
}

void ManualFillingControllerImpl::OnOptionSelected(
    AccessoryAction selected_action) const {
  UMA_HISTOGRAM_ENUMERATION("KeyboardAccessory.AccessoryActionSelected",
                            selected_action, AccessoryAction::COUNT);
  AccessoryController* controller = GetControllerForAction(selected_action);
  if (!controller)
    return;  // Controller not available anymore.
  controller->OnOptionSelected(selected_action);
}

void ManualFillingControllerImpl::OnToggleChanged(
    AccessoryAction toggled_action,
    bool enabled) const {
  AccessoryController* controller = GetControllerForAction(toggled_action);
  if (!controller)
    return;  // Controller not available anymore.
  controller->OnToggleChanged(toggled_action, enabled);
}

void ManualFillingControllerImpl::RequestAccessorySheet(
    autofill::AccessoryTabType tab_type,
    base::OnceCallback<void(autofill::AccessorySheetData)> callback) {
  // TODO(crbug.com/40165275): Consider to execute this async to reduce jank.
  std::optional<AccessorySheetData> sheet =
      GetControllerForTabType(tab_type)->GetSheetData();
  // After they were loaded, all currently existing sheet types always return a
  // value and will always result in a called callback.
  // The only case where they are not available is before their first load (so
  // if a user entered a tab but didn't focus any fields yet). In that case, the
  // update is unnecessary since the first focus will push the correct sheet.
  // TODO(crbug.com/40165275): Consider sending a null or default sheet to cover
  // future cases where we can't rely on a sheet always being available.
  if (sheet.has_value()) {
    std::move(callback).Run(sheet.value());
  }
}

gfx::NativeView ManualFillingControllerImpl::container_view() const {
  // While a const_cast is not ideal. The Autofill API uses const in various
  // spots and the content public API doesn't have const accessors. So the const
  // cast is the lesser of two evils.
  return const_cast<content::WebContents&>(GetWebContents()).GetNativeView();
}

// Returns a weak pointer for this object.
base::WeakPtr<ManualFillingController>
ManualFillingControllerImpl::AsWeakPtr() {
  return weak_factory_.GetWeakPtr();
}

void ManualFillingControllerImpl::Initialize() {
  DCHECK(FromWebContents(&GetWebContents())) << "Don't call from constructor!";
  RegisterObserverForAllowedSources();
  if (address_controller_)
    address_controller_->RefreshSuggestions();
}

ManualFillingControllerImpl::ManualFillingControllerImpl(
    content::WebContents* web_contents)
    : content::WebContentsUserData<ManualFillingControllerImpl>(*web_contents) {
  pwd_controller_ = ChromePasswordManagerClient::FromWebContents(web_contents)
                        ->GetOrCreatePasswordAccessory()
                        ->AsWeakPtr();
  DCHECK(pwd_controller_);

  address_controller_ =
      AddressAccessoryController::GetOrCreate(web_contents)->AsWeakPtr();
  DCHECK(address_controller_);

  payment_method_controller_ =
      PaymentMethodAccessoryController::GetOrCreate(web_contents)->AsWeakPtr();
  DCHECK(payment_method_controller_);

  InitializePlusProfilesCache();

  base::trace_event::MemoryDumpManager::GetInstance()->RegisterDumpProvider(
      this, "ManualFillingCache",
      base::SingleThreadTaskRunner::GetCurrentDefault());
}

ManualFillingControllerImpl::ManualFillingControllerImpl(
    content::WebContents* web_contents,
    base::WeakPtr<PasswordAccessoryController> pwd_controller,
    base::WeakPtr<AddressAccessoryController> address_controller,
    base::WeakPtr<PaymentMethodAccessoryController> payment_method_controller,
    std::unique_ptr<ManualFillingViewInterface> view)
    : content::WebContentsUserData<ManualFillingControllerImpl>(*web_contents),
      pwd_controller_(std::move(pwd_controller)),
      address_controller_(std::move(address_controller)),
      payment_method_controller_(std::move(payment_method_controller)),
      view_(std::move(view)) {
  InitializePlusProfilesCache();

  base::trace_event::MemoryDumpManager::GetInstance()->RegisterDumpProvider(
      this, "ManualFillingCache",
      base::SingleThreadTaskRunner::GetCurrentDefault());
}

void ManualFillingControllerImpl::InitializePlusProfilesCache() {
  if (!base::FeatureList::IsEnabled(
          plus_addresses::features::kPlusAddressAndroidManualFallbackEnabled)) {
    return;
  }
  auto* client =
      autofill::ContentAutofillClient::FromWebContents(&GetWebContents());
  auto* service = PlusAddressServiceFactory::GetForBrowserContext(
      GetWebContents().GetBrowserContext());
  if (client && service) {
    plus_profiles_cache_ =
        std::make_unique<AffiliatedPlusProfilesCache>(client, service);
    pwd_controller_->RegisterPlusProfilesProvider(
        plus_profiles_cache_->GetWeakPtr());
    address_controller_->RegisterPlusProfilesProvider(
        plus_profiles_cache_->GetWeakPtr());
  }
}

bool ManualFillingControllerImpl::OnMemoryDump(
    const base::trace_event::MemoryDumpArgs& args,
    base::trace_event::ProcessMemoryDump* process_memory_dump) {
  auto* dump = process_memory_dump->CreateAllocatorDump(
      base::StringPrintf("passwords/manual_filling_controller/0x%" PRIXPTR,
                         reinterpret_cast<uintptr_t>(this)));
  // TODO: crbug.com/40165275 - Clean up memory usage logging.
  dump->AddScalar(base::trace_event::MemoryAllocatorDump::kNameSize,
                  base::trace_event::MemoryAllocatorDump::kUnitsBytes,
                  /*value=*/0);
  return true;
}

bool ManualFillingControllerImpl::ShouldShowAccessory() const {
  switch (last_focused_field_type_) {
    // If there are suggestions, show on usual form fields.
    case FocusedFieldType::kFillablePasswordField:
    case FocusedFieldType::kFillableUsernameField:
    case FocusedFieldType::kFillableWebauthnTaggedField:
    case FocusedFieldType::kFillableNonSearchField:
      return !available_sources_.empty();

    // Fallbacks aren't really useful on search fields but autocomplete entries
    // justify showing the accessory.
    case FocusedFieldType::kFillableSearchField:
      return available_sources_.contains(FillingSource::AUTOFILL);

    // Even if there are suggestions, don't show on textareas.
    case FocusedFieldType::kFillableTextArea:
      return false;  // TODO(crbug.com/40628376): true on long-press.

    // Sometimes autocomplete entries may be set when the focus is on an unknown
    // or unfillable field.
    case FocusedFieldType::kUnfillableElement:
    case FocusedFieldType::kUnknown:
      return available_sources_.contains(FillingSource::AUTOFILL);
  }
}

void ManualFillingControllerImpl::UpdateVisibility() {
  TRACE_EVENT0("passwords", "ManualFillingControllerImpl::UpdateVisibility");
  if (ShouldShowAccessory()) {
    for (const FillingSource& source : available_sources_) {
      if (source == FillingSource::AUTOFILL)
        continue;  // Autofill suggestions have no sheet.
      AccessoryController* controller = GetControllerForFillingSource(source);
      if (!controller) {
        continue;  // Most-likely, the controller was cleaned up already.
      }
      std::optional<AccessorySheetData> sheet = controller->GetSheetData();
      if (sheet.has_value())
        view_->OnItemsAvailable(std::move(sheet.value()));
    }
    if (plus_profiles_cache_) {
      plus_profiles_cache_->FetchAffiliatedPlusProfiles();
    }
    view_->Show(ManualFillingViewInterface::WaitForKeyboard(
        last_focused_field_type_ != FocusedFieldType::kUnfillableElement &&
        last_focused_field_type_ != FocusedFieldType::kUnknown));

  } else {
    if (plus_profiles_cache_) {
      plus_profiles_cache_->ClearCachedPlusProfiles();
    }
    view_->Hide();
  }
}

void ManualFillingControllerImpl::RegisterObserverForAllowedSources() {
  for (FillingSource source : kAllowedFillingSources) {
    AccessoryController* sheet_controller =
        GetControllerForFillingSource(source);
    if (!sheet_controller)
      continue;  // Ignore disallowed sheets.
    sheet_controller->RegisterFillingSourceObserver(base::BindRepeating(
        &ManualFillingControllerImpl::OnSourceAvailabilityChanged,
        weak_factory_.GetWeakPtr(), source));
  }
}

void ManualFillingControllerImpl::OnSourceAvailabilityChanged(
    FillingSource source,
    AccessoryController* source_controller,
    AccessoryController::IsFillingSourceAvailable is_source_available) {
  TRACE_EVENT0("passwords",
               "ManualFillingControllerImpl::OnSourceAvailabilityChanged");
  std::optional<AccessorySheetData> sheet = source_controller->GetSheetData();
  bool show_filling_source = sheet.has_value() && is_source_available;
  // TODO(crbug.com/40165275): Remove once all sheets pull this information
  // instead of waiting to get it pushed.
  view_->OnItemsAvailable(std::move(sheet.value()));
  UpdateSourceAvailability(source, show_filling_source);
}

AccessoryController* ManualFillingControllerImpl::GetControllerForTabType(
    AccessoryTabType type) const {
  switch (type) {
    case AccessoryTabType::ADDRESSES:
      return address_controller_.get();
    case AccessoryTabType::PASSWORDS:
      return pwd_controller_.get();
    case AccessoryTabType::CREDIT_CARDS:
      return payment_method_controller_.get();
    case AccessoryTabType::OBSOLETE_TOUCH_TO_FILL:
    case AccessoryTabType::ALL:
    case AccessoryTabType::COUNT:
      NOTREACHED_IN_MIGRATION()
          << "Controller not defined for tab: " << static_cast<int>(type);
      return nullptr;
  }
}

AccessoryController* ManualFillingControllerImpl::GetControllerForAction(
    AccessoryAction action) const {
  switch (action) {
    case AccessoryAction::GENERATE_PASSWORD_MANUAL:
    case AccessoryAction::MANAGE_PASSWORDS:
    case AccessoryAction::USE_OTHER_PASSWORD:
    case AccessoryAction::GENERATE_PASSWORD_AUTOMATIC:
    case AccessoryAction::TOGGLE_SAVE_PASSWORDS:
    case AccessoryAction::CREDMAN_CONDITIONAL_UI_REENTRY:
    case AccessoryAction::CROSS_DEVICE_PASSKEY:
    case AccessoryAction::CREATE_PLUS_ADDRESS_FROM_PASSWORD_SHEET:
    case AccessoryAction::SELECT_PLUS_ADDRESS_FROM_PASSWORD_SHEET:
    case AccessoryAction::MANAGE_PLUS_ADDRESS_FROM_PASSWORD_SHEET:
      return pwd_controller_.get();
    case AccessoryAction::MANAGE_ADDRESSES:
    case AccessoryAction::CREATE_PLUS_ADDRESS_FROM_ADDRESS_SHEET:
    case AccessoryAction::SELECT_PLUS_ADDRESS_FROM_ADDRESS_SHEET:
    case AccessoryAction::MANAGE_PLUS_ADDRESS_FROM_ADDRESS_SHEET:
      return address_controller_.get();
    case AccessoryAction::MANAGE_CREDIT_CARDS:
      return payment_method_controller_.get();
    case AccessoryAction::AUTOFILL_SUGGESTION:
    case AccessoryAction::COUNT:
      NOTREACHED_IN_MIGRATION()
          << "Controller not defined for action: " << static_cast<int>(action);
      return nullptr;
  }
}

AccessoryController* ManualFillingControllerImpl::GetControllerForFillingSource(
    const FillingSource& filling_source) const {
  switch (filling_source) {
    case FillingSource::PASSWORD_FALLBACKS:
      return pwd_controller_.get();
    case FillingSource::CREDIT_CARD_FALLBACKS:
      return payment_method_controller_.get();
    case FillingSource::ADDRESS_FALLBACKS:
      return address_controller_.get();
    case FillingSource::AUTOFILL:
      NOTREACHED_IN_MIGRATION() << "Controller not defined for filling source: "
                                << static_cast<int>(filling_source);
      return nullptr;
  }
}

WEB_CONTENTS_USER_DATA_KEY_IMPL(ManualFillingControllerImpl);