blob: 0e822c31ab2dca14ed2da15a148a860d828fd5e5 [file] [log] [blame]
// Copyright 2025 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/ui/autofill/bubble_manager_impl.h"
#include <algorithm>
#include "base/auto_reset.h"
#include "base/check.h"
#include "base/metrics/histogram_functions.h"
#include "base/notreached.h"
#include "base/strings/strcat.h"
#include "base/time/time.h"
#include "chrome/browser/ui/autofill/bubble_controller_base.h"
#include "components/tabs/public/tab_interface.h"
namespace autofill {
namespace {
// The timeout after which the bubble can be replaced if now shown.
constexpr base::TimeDelta kPendingRequestTimeout = base::Seconds(3600);
// Helper to get the priority for a given bubble `type`. Higher number
// indicates higher priority.
int GetPriorityForBubbleType(BubbleType type) {
switch (type) {
case BubbleType::kFilledCardInformation:
return 10;
case BubbleType::kPassword:
return 9;
case BubbleType::kSaveUpdateAutofillAi:
return 8;
case BubbleType::kSaveUpdateCard:
return 7;
case BubbleType::kVirtualCardEnrollConfirmation:
return 6;
case BubbleType::kSaveIban:
return 5;
case BubbleType::kMandatoryReauth:
return 4;
case BubbleType::kSaveUpdateAddress:
return 3;
case BubbleType::kOfferNotification:
return 2;
case BubbleType::kWalletablePassConsent:
return 1;
}
NOTREACHED();
}
// Returns true if a new bubble of this type should always replace an
// existing pending bubble of the same type in the queue.
bool ShouldAlwaysPreemptSameType(BubbleType bubble_type) {
switch (bubble_type) {
case BubbleType::kFilledCardInformation:
case BubbleType::kPassword:
return true;
case BubbleType::kSaveUpdateAutofillAi:
case BubbleType::kSaveUpdateCard:
case BubbleType::kVirtualCardEnrollConfirmation:
case BubbleType::kSaveIban:
case BubbleType::kMandatoryReauth:
case BubbleType::kSaveUpdateAddress:
case BubbleType::kOfferNotification:
case BubbleType::kWalletablePassConsent:
return false;
}
NOTREACHED();
}
// LINT.IfChange(BubbleTypeToMetricSuffix)
std::string_view BubbleTypeToMetricSuffix(BubbleType bubble_type) {
switch (bubble_type) {
case BubbleType::kSaveUpdateAddress:
return "SaveUpdateAddress";
case BubbleType::kSaveIban:
return "SaveIban";
case BubbleType::kSaveUpdateCard:
return "SaveUpdateCard";
case BubbleType::kSaveUpdateAutofillAi:
return "SaveUpdateAutofillAi";
case BubbleType::kVirtualCardEnrollConfirmation:
return "VirtualCardEnrollConfirmation";
case BubbleType::kMandatoryReauth:
return "MandatoryReauth";
case BubbleType::kOfferNotification:
return "OfferNotification";
case BubbleType::kFilledCardInformation:
return "FilledCardInformation";
case BubbleType::kPassword:
return "Password";
case BubbleType::kWalletablePassConsent:
return "WalletablePassConsent";
}
NOTREACHED();
}
// LINT.ThenChange(//tools/metrics/histograms/metadata/autofill/histograms.xml:Autofill.Bubble.Queue.TimeInQueue.BubbleType)
} // namespace
BubbleManagerImpl::PendingRequest::PendingRequest(
base::WeakPtr<BubbleControllerBase> controller,
base::TimeTicks time_added,
int priority)
: controller(std::move(controller)),
time_added(time_added),
priority(priority) {}
BubbleManagerImpl::PendingRequest::~PendingRequest() = default;
BubbleManagerImpl::PendingRequest::PendingRequest(const PendingRequest& other) =
default;
BubbleManagerImpl::PendingRequest& BubbleManagerImpl::PendingRequest::operator=(
const PendingRequest& other) = default;
bool BubbleManagerImpl::PendingRequest::operator<(
const BubbleManagerImpl::PendingRequest& other) const {
if (priority != other.priority) {
return priority > other.priority;
}
// If priorities are equal, the one that arrived first should be shown first.
return time_added < other.time_added;
}
BubbleManagerImpl::BubbleManagerImpl(tabs::TabInterface* tab) {
tab_subscriptions_.push_back(tab->RegisterWillDeactivate(base::BindRepeating(
&BubbleManagerImpl::TabWillEnterBackground, base::Unretained(this))));
tab_subscriptions_.push_back(tab->RegisterDidActivate(base::BindRepeating(
&BubbleManagerImpl::TabDidEnterForeground, base::Unretained(this))));
}
BubbleManagerImpl::~BubbleManagerImpl() = default;
void BubbleManagerImpl::RequestShowController(
BubbleControllerBase& controller_to_show,
bool force_show) {
if (force_show) {
base::UmaHistogramEnumeration("Autofill.Bubble.RequestShow.ForceShow",
controller_to_show.GetBubbleType());
}
base::UmaHistogramEnumeration("Autofill.Bubble.RequestShow",
controller_to_show.GetBubbleType());
base::WeakPtr<BubbleControllerBase> controller_weak_ptr =
controller_to_show.GetBubbleControllerBaseWeakPtr();
base::AutoReset<bool> show_request_guard(&handling_show_request_, true);
if (!active_bubble_controller_) {
// No active bubble, so this one can be shown immediately.
base::UmaHistogramEnumeration("Autofill.Bubble.Show.NoActiveBubble",
controller_to_show.GetBubbleType());
ShowAndSetCurrentActive(controller_weak_ptr);
return;
}
if (force_show ||
ShouldReplaceExistingBubble(controller_to_show.GetBubbleType())) {
base::UmaHistogramEnumeration("Autofill.Bubble.Show.Preemption",
controller_to_show.GetBubbleType());
HideActiveBubbleForPreemption();
ShowAndSetCurrentActive(controller_weak_ptr);
return;
}
// Queue the bubble. Log the reason for queuing.
if (active_bubble_controller_->IsMouseHovered()) {
base::UmaHistogramEnumeration("Autofill.Bubble.Queue.AddedDueToHover",
controller_to_show.GetBubbleType());
} else {
base::UmaHistogramEnumeration(
"Autofill.Bubble.Queue.AddedDueToActiveBubble",
controller_to_show.GetBubbleType());
}
AddToPendingQueue(controller_weak_ptr);
}
void BubbleManagerImpl::ShowAndSetCurrentActive(
base::WeakPtr<BubbleControllerBase> controller_to_show) {
CHECK(controller_to_show);
active_bubble_controller_ = controller_to_show;
for (auto it = pending_bubbles_queue_.begin();
it != pending_bubbles_queue_.end(); ++it) {
if (it->controller.get() == controller_to_show.get()) {
// Remove from the pending queue if it was there.
pending_bubbles_queue_.erase(it);
break;
}
}
active_bubble_controller_->ShowBubble();
}
void BubbleManagerImpl::HideActiveBubbleForPreemption() {
CHECK(active_bubble_controller_ &&
active_bubble_controller_->IsShowingBubble());
CHECK(handling_show_request_);
base::UmaHistogramEnumeration("Autofill.Bubble.WasPreempted",
active_bubble_controller_->GetBubbleType());
// Queue the old bubble. It will be hidden, and its OnBubbleHiddenByController
// call will be a no-op for starting the next bubble because we are inside a
// show request (`handling_show_request_` is true).
AddToPendingQueue(active_bubble_controller_);
active_bubble_controller_->HideBubble();
}
void BubbleManagerImpl::AddToPendingQueue(
base::WeakPtr<BubbleControllerBase> controller) {
CHECK(controller);
const BubbleType new_bubble_type = controller->GetBubbleType();
const base::TimeTicks now = base::TimeTicks::Now();
int priority = GetPriorityForBubbleType(new_bubble_type);
auto it = std::ranges::find_if(
pending_bubbles_queue_,
[&new_bubble_type](const PendingRequest& request) {
return request.controller &&
request.controller->GetBubbleType() == new_bubble_type;
});
if (it != pending_bubbles_queue_.end()) {
// If a bubble of the same type exists, erase it before inserting the new
// one if the controller says so or if it has timed out.
const bool bubble_has_timed_out =
(now - it->time_added) > kPendingRequestTimeout;
if (ShouldAlwaysPreemptSameType(new_bubble_type) || bubble_has_timed_out) {
if (bubble_has_timed_out) {
if (it->controller) {
base::UmaHistogramEnumeration("Autofill.Bubble.Queue.TimedOut",
it->controller->GetBubbleType());
}
} else {
base::UmaHistogramEnumeration("Autofill.Bubble.Queue.Replaced",
new_bubble_type);
}
pending_bubbles_queue_.erase(it);
pending_bubbles_queue_.insert(PendingRequest(controller, now, priority));
} else {
base::UmaHistogramEnumeration("Autofill.Bubble.Queue.Discarded",
new_bubble_type);
}
} else {
// No bubble of this type exists, just insert it.
pending_bubbles_queue_.insert(PendingRequest(controller, now, priority));
}
}
void BubbleManagerImpl::ProcessPendingBubbles() {
if (handling_show_request_ || handling_tab_will_enter_background_request_ ||
(active_bubble_controller_ &&
active_bubble_controller_->IsShowingBubble())) {
// The bubble is hidden due to preemption and added to the queue. Or the tab
// is about to hide. Therefore, do not show any new bubbles.
return;
}
// Clean up any stale pointers and timed out bubbles.
const base::TimeTicks now = base::TimeTicks::Now();
std::ranges::for_each(pending_bubbles_queue_, [&now](const auto& request) {
if (request.controller &&
(now - request.time_added) > kPendingRequestTimeout) {
// Log timed-out bubbles.
base::UmaHistogramEnumeration("Autofill.Bubble.Queue.TimedOut",
request.controller->GetBubbleType());
}
});
std::erase_if(pending_bubbles_queue_, [&now](const auto& request) {
return !request.controller ||
(now - request.time_added) > kPendingRequestTimeout;
});
if (pending_bubbles_queue_.empty()) {
active_bubble_controller_ = nullptr;
return;
}
auto it = pending_bubbles_queue_.begin();
base::WeakPtr<BubbleControllerBase> next_controller_to_show = it->controller;
base::TimeDelta time_in_queue = now - it->time_added;
pending_bubbles_queue_.erase(it);
base::UmaHistogramEnumeration("Autofill.Bubble.Queue.ShownFromQueue",
next_controller_to_show->GetBubbleType());
base::UmaHistogramTimes(
base::StrCat(
{"Autofill.Bubble.Queue.TimeInQueue.",
BubbleTypeToMetricSuffix(next_controller_to_show->GetBubbleType())}),
time_in_queue);
// Show the next bubble from the queue.
ShowAndSetCurrentActive(next_controller_to_show);
}
void BubbleManagerImpl::OnBubbleHiddenByController(
BubbleControllerBase& controller_to_hide) {
base::WeakPtr<BubbleControllerBase> controller_weak_ptr =
controller_to_hide.GetBubbleControllerBaseWeakPtr();
if (active_bubble_controller_.get() == controller_weak_ptr.get()) {
active_bubble_controller_ = nullptr;
ProcessPendingBubbles();
} else {
// The hidden bubble was not the active one, so remove it from the queue.
for (auto it = pending_bubbles_queue_.begin();
it != pending_bubbles_queue_.end(); ++it) {
if (it->controller.get() == controller_weak_ptr.get()) {
pending_bubbles_queue_.erase(it);
break;
}
}
}
}
bool BubbleManagerImpl::HasPendingBubbleOfSameType(
const BubbleType bubble_type) const {
const base::TimeTicks now = base::TimeTicks::Now();
auto it = std::ranges::find_if(
pending_bubbles_queue_, [bubble_type](const PendingRequest& request) {
// Check if the controller is still valid and if its type
// matches.
return request.controller &&
request.controller->GetBubbleType() == bubble_type;
});
// If no bubble of the specified type is found in the queue.
if (it == pending_bubbles_queue_.end()) {
return false;
}
return !ShouldAlwaysPreemptSameType(bubble_type) &&
(now - it->time_added) < kPendingRequestTimeout;
}
bool BubbleManagerImpl::ShouldReplaceExistingBubble(
const BubbleType new_bubble_type) const {
if (active_bubble_controller_->IsMouseHovered()) {
return false;
}
const BubbleType active_bubble_type =
active_bubble_controller_->GetBubbleType();
// If the bubbles have same type, preempt based on the controller type.
if (new_bubble_type == active_bubble_type) {
return ShouldAlwaysPreemptSameType(new_bubble_type);
}
// Otherwise, preempt based on priority.
return GetPriorityForBubbleType(new_bubble_type) >
GetPriorityForBubbleType(active_bubble_type);
}
void BubbleManagerImpl::TabWillEnterBackground(
tabs::TabInterface* tab_interface) {
base::AutoReset<bool> hide_request_guard(
&handling_tab_will_enter_background_request_, true);
if (active_bubble_controller_) {
base::UmaHistogramEnumeration("Autofill.Bubble.HideDueToTabHide",
active_bubble_controller_->GetBubbleType());
AddToPendingQueue(active_bubble_controller_);
active_bubble_controller_->HideBubble();
active_bubble_controller_ = nullptr;
}
}
void BubbleManagerImpl::TabDidEnterForeground(
tabs::TabInterface* tab_interface) {
if (!active_bubble_controller_) {
ProcessPendingBubbles();
} else if (!active_bubble_controller_->IsShowingBubble()) {
// This can happen if a tab created in background becomes visible.
active_bubble_controller_->ShowBubble();
}
}
} // namespace autofill