chromium/chrome/browser/touch_to_fill/password_manager/touch_to_fill_controller.cc

// Copyright 2019 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/touch_to_fill/password_manager/touch_to_fill_controller.h"

#include "base/check_op.h"
#include "base/functional/bind.h"
#include "base/ranges/algorithm.h"
#include "chrome/browser/password_manager/android/password_manager_launcher_android.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/touch_to_fill/password_manager/touch_to_fill_controller_delegate.h"
#include "chrome/browser/touch_to_fill/password_manager/touch_to_fill_view.h"
#include "chrome/browser/touch_to_fill/password_manager/touch_to_fill_view_factory.h"
#include "components/password_manager/content/browser/keyboard_replacing_surface_visibility_controller.h"
#include "components/password_manager/core/browser/origin_credential_store.h"
#include "components/password_manager/core/browser/passkey_credential.h"
#include "components/url_formatter/elide_url.h"
#include "components/webauthn/android/webauthn_cred_man_delegate.h"
#include "services/network/public/cpp/is_potentially_trustworthy.h"
#include "ui/android/view_android.h"
#include "url/gurl.h"
#include "url/origin.h"

namespace {
using password_manager::PasskeyCredential;
using password_manager::UiCredential;
using webauthn::WebAuthnCredManDelegate;
using DisplayTarget = TouchToFillController::DisplayTarget;

std::vector<UiCredential> SortCredentials(
    base::span<const UiCredential> credentials) {
  std::vector<UiCredential> result(credentials.begin(), credentials.end());
  // Sort `credentials` according to the following criteria:
  // 1) Prefer exact matches then affiliated, then PSL matches.
  // 2) Prefer credentials that were used recently over others.
  //
  // Note: This ordering matches password_manager_util::FindBestMatches().
  base::ranges::sort(result, std::greater<>{}, [](const UiCredential& cred) {
    return std::make_pair(-static_cast<int>(cred.match_type()),
                          cred.last_used());
  });

  return result;
}
}  // namespace

TouchToFillController::TouchToFillController(
    Profile* profile,
    base::WeakPtr<
        password_manager::KeyboardReplacingSurfaceVisibilityController>
        visibility_controller)
    : profile_(profile), visibility_controller_(visibility_controller) {}
TouchToFillController::~TouchToFillController() = default;

bool TouchToFillController::Show(
    base::span<const UiCredential> credentials,
    base::span<PasskeyCredential> passkey_credentials,
    std::unique_ptr<TouchToFillControllerDelegate> ttf_delegate,
    webauthn::WebAuthnCredManDelegate* cred_man_delegate,
    base::WeakPtr<password_manager::ContentPasswordManagerDriver>
        frame_driver) {
  if (!ttf_delegate->ShouldShowTouchToFill()) {
    return false;
  }

  DCHECK(!ttf_delegate_);
  ttf_delegate_ = std::move(ttf_delegate);

  cred_man_delegate_ = cred_man_delegate;
  visibility_controller_->SetVisible(std::move(frame_driver));

  ttf_delegate_->OnShow(credentials, passkey_credentials);
  GURL url = ttf_delegate_->GetFrameUrl();
  // If the render frame host has been destroyed already, the url will be empty
  // in which case Show() should never be called.
  CHECK(!url.is_empty());

  switch (GetResponsibleDisplayTarget(credentials, passkey_credentials)) {
    case DisplayTarget::kNone:
      // Ideally this should never happen. However, in case we do end up
      // invoking Show() without credentials, we should not show Touch To Fill
      // to the user and treat this case as dismissal, in order to restore the
      // soft keyboard.
      OnDismiss();
      return false;
    case DisplayTarget::kShowNoPasskeysSheet:
      if (!no_passkeys_bridge_) {
        no_passkeys_bridge_ = std::make_unique<NoPasskeysBottomSheetBridge>();
      }
      no_passkeys_bridge_->Show(
          GetNativeView()->GetWindowAndroid(), url::Origin::Create(url).host(),
          base::BindOnce(&TouchToFillController::OnDismiss,
                         weak_ptr_factory_.GetWeakPtr()),
          base::BindOnce(&TouchToFillController::OnHybridSignInSelected,
                         weak_ptr_factory_.GetWeakPtr()));
      return true;
    case DisplayTarget::kDeferToCredMan:
      cred_man_delegate->SetRequestCompletionCallback(
          base::BindRepeating(&TouchToFillController::OnCredManUiClosed,
                              weak_ptr_factory_.GetWeakPtr()));
      OnShowCredManSelected();
      return true;
    case DisplayTarget::kShowTouchToFill:
      if (!view_) {
        view_ = TouchToFillViewFactory::Create(this);
      }

      int flags = TouchToFillView::kNone;

      if (ttf_delegate_->ShouldTriggerSubmission()) {
        flags |= TouchToFillView::kTriggerSubmission;
      }
      if (password_manager_launcher::CanManagePasswordsWhenPasskeysPresent(
              profile_)) {
        flags |= TouchToFillView::kCanManagePasswordsWhenPasskeysPresent;
      }
      if (ttf_delegate_->ShouldShowHybridOption()) {
        flags |= TouchToFillView::kShouldShowHybridOption;
      }
      if (cred_man_delegate &&
          cred_man_delegate->HasPasskeys() ==
              WebAuthnCredManDelegate::State::kHasPasskeys) {
        cred_man_delegate->SetRequestCompletionCallback(
            base::BindRepeating(&TouchToFillController::OnCredManUiClosed,
                                weak_ptr_factory_.GetWeakPtr()));
        flags |= TouchToFillView::kShouldShowCredManEntry;
      }

      return view_->Show(url,
                         TouchToFillView::IsOriginSecure(
                             network::IsOriginPotentiallyTrustworthy(
                                 url::Origin::Create(url))),
                         SortCredentials(credentials), passkey_credentials,
                         flags);
  }
}

void TouchToFillController::OnCredentialSelected(
    const UiCredential& credential) {
  view_.reset();
  // Unretained is safe here because TouchToFillController owns the delegate.
  ttf_delegate_->OnCredentialSelected(
      credential, base::BindOnce(&TouchToFillController::ActionCompleted,
                                 base::Unretained(this)));
}

void TouchToFillController::OnPasskeyCredentialSelected(
    const PasskeyCredential& credential) {
  view_.reset();
  // Unretained is safe here because TouchToFillController owns the delegate.
  ttf_delegate_->OnPasskeyCredentialSelected(
      credential, base::BindOnce(&TouchToFillController::ActionCompleted,
                                 base::Unretained(this)));
}

void TouchToFillController::OnManagePasswordsSelected(bool passkeys_shown) {
  view_.reset();
  // Unretained is safe here because TouchToFillController owns the delegate.
  ttf_delegate_->OnManagePasswordsSelected(
      passkeys_shown, base::BindOnce(&TouchToFillController::ActionCompleted,
                                     base::Unretained(this)));
}

void TouchToFillController::OnHybridSignInSelected() {
  view_.reset();
  ttf_delegate_->OnHybridSignInSelected(base::BindOnce(
      &TouchToFillController::ActionCompleted, base::Unretained(this)));
}

void TouchToFillController::OnShowCredManSelected() {
  view_.reset();
  cred_man_delegate_->TriggerCredManUi(
      WebAuthnCredManDelegate::RequestPasswords(false));
}

void TouchToFillController::OnCredManUiClosed(bool success) {
  if (!ttf_delegate_) {
    return;
  }
  // Unretained is safe here because TouchToFillController owns the delegate.
  ttf_delegate_->OnCredManDismissed(base::BindOnce(
      &TouchToFillController::ActionCompleted, base::Unretained(this)));
}

void TouchToFillController::OnDismiss() {
  view_.reset();
  no_passkeys_bridge_.reset();
  if (!ttf_delegate_) {
    // TODO(crbug.com/40274966): Remove this check when
    // PasswordSuggestionBottomSheetV2 is launched
    return;
  }
  // Unretained is safe here because TouchToFillController owns the delegate.
  ttf_delegate_->OnDismiss(base::BindOnce(
      &TouchToFillController::ActionCompleted, base::Unretained(this)));
}

Profile* TouchToFillController::GetProfile() {
  return profile_;
}

gfx::NativeView TouchToFillController::GetNativeView() {
  return ttf_delegate_->GetNativeView();
}

void TouchToFillController::Close() {
  // TODO(crbug.com/40277147). This is a duplicate of `OnDismiss`. Merge the two
  // functions.
  OnDismiss();
}

void TouchToFillController::Reset() {
  if (!visibility_controller_) {
    return;
  }
  if (visibility_controller_->IsVisible()) {
    Close();
  }
  visibility_controller_->Reset();
}

void TouchToFillController::ActionCompleted() {
  if (visibility_controller_) {
    visibility_controller_->SetShown();
  }
  ttf_delegate_.reset();
}

DisplayTarget TouchToFillController::GetResponsibleDisplayTarget(
    base::span<const password_manager::UiCredential> credentials,
    base::span<password_manager::PasskeyCredential> passkey_credentials) const {
  bool has_passkeys_in_cred_man =
      cred_man_delegate_ && cred_man_delegate_->HasPasskeys() ==
                                WebAuthnCredManDelegate::State::kHasPasskeys;
  if (!credentials.empty() || !passkey_credentials.empty()) {
    return DisplayTarget::kShowTouchToFill;
  }

  if (has_passkeys_in_cred_man) {
    return DisplayTarget::kDeferToCredMan;
  }
  if (ttf_delegate_->ShouldShowNoPasskeysSheetIfRequired()) {
    return DisplayTarget::kShowNoPasskeysSheet;
  }
  return DisplayTarget::kNone;
}