blob: b7a5baa2ecd5b26e6e648f7818cbef78501a1828 [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 "components/saved_tab_groups/internal/versioning_message_controller_impl.h"
#include "base/functional/bind.h"
#include "components/data_sharing/public/features.h"
#include "components/prefs/pref_service.h"
#include "components/saved_tab_groups/public/pref_names.h"
#include "components/saved_tab_groups/public/tab_group_sync_service.h"
#include "components/saved_tab_groups/public/utils.h"
namespace tab_groups {
namespace {
// Represents various possible version states based on feature flags.
enum class VersionState {
// Version is out of date. Versioning messages to update chrome can be shown.
// Feature flags:
// data_sharing::features::kSharedDataTypesKillSwitch ENABLED,
// data_sharing::features::kDataSharingEnableUpdateChromeUI ENABLED.
kOutOfDate,
// Version is out of date. However, no specific versioning message should be
// shown.
// Feature flags:
// data_sharing::features::kSharedDataTypesKillSwitch ENABLED,
// data_sharing::features::kDataSharingEnableUpdateChromeUI DISABLED.
kNoMessage,
// Version is up-to-date.
// Feature flags:
// data_sharing::features::kSharedDataTypesKillSwitch DISABLED,
// data_sharing::features::kDataSharingEnableUpdateChromeUI DISABLED.
kUpToDate,
// Invalid combination of feature flags. No specific versioning message should
// be shown.
// Feature flags:
// data_sharing::features::kSharedDataTypesKillSwitch DISABLED,
// data_sharing::features::kDataSharingEnableUpdateChromeUI ENABLED.
kInvalidCombination,
// Data sharing main feature flags are disabled. No versioning message should
// be shown and we shouldn't even bother to set or reset pref state. This
// state can be cleaned up after data sharing feature launch.
// Feature flags:
// data_sharing::features::IsDataSharingFunctionalityEnabled() => false
// data_sharing::features::kSharedDataTypesKillSwitch ANY STATE,
// data_sharing::features::kDataSharingEnableUpdateChromeUI ANY STATE.
kDataSharingFunctionalityDisabled,
};
// Returns the current version state based on combination of feature flags.
VersionState GetVersionState() {
if (!data_sharing::features::IsDataSharingFunctionalityEnabled()) {
return VersionState::kDataSharingFunctionalityDisabled;
}
const bool is_shared_data_types_enabled = !base::FeatureList::IsEnabled(
data_sharing::features::kSharedDataTypesKillSwitch);
const bool is_update_chrome_ui_enabled = base::FeatureList::IsEnabled(
data_sharing::features::kDataSharingEnableUpdateChromeUI);
if (is_shared_data_types_enabled) {
return is_update_chrome_ui_enabled ? VersionState::kInvalidCombination
: VersionState::kUpToDate;
} else {
return is_update_chrome_ui_enabled ? VersionState::kOutOfDate
: VersionState::kNoMessage;
}
}
bool HasCurrentSharedTabGroups(TabGroupSyncService* tab_group_sync_service) {
for (const auto* saved_tab_group : tab_group_sync_service->ReadAllGroups()) {
if (saved_tab_group->collaboration_id().has_value()) {
return true;
}
}
return false;
}
} // namespace
VersioningMessageControllerImpl::VersioningMessageControllerImpl(
PrefService* pref_service,
TabGroupSyncService* tab_group_sync_service)
: pref_service_(pref_service),
tab_group_sync_service_(tab_group_sync_service) {
tab_group_sync_service_->AddObserver(this);
}
VersioningMessageControllerImpl::~VersioningMessageControllerImpl() {
tab_group_sync_service_->RemoveObserver(this);
}
void VersioningMessageControllerImpl::ComputePrefsOnStartup() {
// On startup, read and compute the state of the prefs based on the feature
// flag and number of shared tab groups in the previous session.
switch (GetVersionState()) {
case VersionState::kOutOfDate: {
// Version is out-of-date. If there were shared tab groups last session,
// that means that the version just switched. Compute the pref states
// accordingly.
// Determine if previous session had shared tab groups that make
// it eligible for the instant/persistent message.
const bool had_open_shared_tab_groups =
tab_group_sync_service_->HadSharedTabGroupsLastSession(
/*open_shared_tab_groups=*/true);
const bool had_any_shared_tab_groups =
tab_group_sync_service_->HadSharedTabGroupsLastSession(
/*open_shared_tab_groups=*/false);
if (had_open_shared_tab_groups) {
pref_service_->SetBoolean(
prefs::kEligibleForVersionOutOfDateInstantMessage, true);
}
if (had_any_shared_tab_groups) {
pref_service_->SetBoolean(
prefs::kEligibleForVersionOutOfDatePersistentMessage, true);
// For desktop, instant message should be shown if there are any shared
// tab groups in the previous session regardless of whether they were
// open or closed.
if (!AreLocalIdsPersisted()) {
pref_service_->SetBoolean(
prefs::kEligibleForVersionOutOfDateInstantMessage, true);
}
}
// Always reset the 'updated' message eligibility when out of date.
pref_service_->SetBoolean(prefs::kEligibleForVersionUpdatedMessage,
false);
break;
}
case VersionState::kUpToDate: {
// Version is up-to-date. Determine if eligible for the 'version updated'
// message.
const bool had_any_out_of_date_messages_before =
pref_service_->GetBoolean(prefs::kHasShownAnyVersionOutOfDateMessage);
if (had_any_out_of_date_messages_before) {
pref_service_->SetBoolean(prefs::kEligibleForVersionUpdatedMessage,
true);
}
// Always reset the 'out-of-date' message eligibilities when up to date.
pref_service_->SetBoolean(
prefs::kEligibleForVersionOutOfDateInstantMessage, false);
pref_service_->SetBoolean(prefs::kHasShownAnyVersionOutOfDateMessage,
false);
pref_service_->SetBoolean(
prefs::kEligibleForVersionOutOfDatePersistentMessage, false);
break;
}
case VersionState::kNoMessage:
case VersionState::kInvalidCombination:
case VersionState::kDataSharingFunctionalityDisabled: {
// In these states, no specific versioning messages are tied to the
// feature flag combination. The prefs should carry over their previous
// state or default values if not explicitly set elsewhere.
break;
}
}
}
bool VersioningMessageControllerImpl::ShouldShowMessageUi(
MessageType message_type) {
CHECK(is_initialized_);
VersionState current_version_state = GetVersionState();
switch (message_type) {
case MessageType::VERSION_OUT_OF_DATE_INSTANT_MESSAGE:
return current_version_state == VersionState::kOutOfDate &&
pref_service_->GetBoolean(
prefs::kEligibleForVersionOutOfDateInstantMessage);
case MessageType::VERSION_OUT_OF_DATE_PERSISTENT_MESSAGE:
return current_version_state == VersionState::kOutOfDate &&
pref_service_->GetBoolean(
prefs::kEligibleForVersionOutOfDatePersistentMessage);
case MessageType::VERSION_UPDATED_MESSAGE:
return current_version_state == VersionState::kUpToDate &&
pref_service_->GetBoolean(
prefs::kEligibleForVersionUpdatedMessage) &&
HasCurrentSharedTabGroups(tab_group_sync_service_);
default:
return false;
}
}
bool VersioningMessageControllerImpl::IsInitialized() {
return is_initialized_;
}
void VersioningMessageControllerImpl::ShouldShowMessageUiAsync(
MessageType message_type,
base::OnceCallback<void(bool)> callback) {
// The VERSION_UPDATED_MESSAGE requires waiting for tab groups to be
// downloaded. We handle it separately by queueing the callback.
if (message_type == MessageType::VERSION_UPDATED_MESSAGE) {
// If the check has already completed and final state of the eligibility is
// already determined, we can respond immediately with the final state.
if (processed_version_updated_callbacks_) {
std::move(callback).Run(ShouldShowMessageUi(message_type));
} else {
// Otherwise, queue the callback. It will be run later by either
// OnInitialized or OnTabGroupAdded.
pending_version_updated_callbacks_.push_back(std::move(callback));
}
return;
}
// For all other message types, queue callbacks if init isn't complete,
// otherwise invoke them right away.
if (!is_initialized_) {
pending_callbacks_.push_back(base::BindOnce(
&VersioningMessageControllerImpl::ShouldShowMessageUiAsync,
weak_ptr_factory_.GetWeakPtr(), message_type, std::move(callback)));
return;
}
std::move(callback).Run(ShouldShowMessageUi(message_type));
}
void VersioningMessageControllerImpl::OnMessageUiShown(
MessageType message_type) {
switch (message_type) {
case MessageType::VERSION_OUT_OF_DATE_INSTANT_MESSAGE:
pref_service_->SetBoolean(
prefs::kEligibleForVersionOutOfDateInstantMessage, false);
pref_service_->SetBoolean(prefs::kHasShownAnyVersionOutOfDateMessage,
true);
break;
case MessageType::VERSION_OUT_OF_DATE_PERSISTENT_MESSAGE:
pref_service_->SetBoolean(prefs::kHasShownAnyVersionOutOfDateMessage,
true);
break;
case MessageType::VERSION_UPDATED_MESSAGE:
pref_service_->SetBoolean(prefs::kEligibleForVersionUpdatedMessage,
false);
break;
default:
break;
}
}
void VersioningMessageControllerImpl::OnMessageUiDismissed(
MessageType message_type) {
if (message_type == MessageType::VERSION_OUT_OF_DATE_PERSISTENT_MESSAGE) {
pref_service_->SetBoolean(
prefs::kEligibleForVersionOutOfDatePersistentMessage, false);
}
}
void VersioningMessageControllerImpl::OnInitialized() {
is_initialized_ = true;
ComputePrefsOnStartup();
// Attempt to resolve any pending VERSION_UPDATED_MESSAGE callbacks. This
// handles cases where conditions are already met (or definitively not met)
// at startup.
MaybeResolvePendingVersionUpdatedCallbacks();
// Run pending callbacks for other message types that were waiting for init.
for (auto& callback : pending_callbacks_) {
std::move(callback).Run();
}
pending_callbacks_.clear();
}
void VersioningMessageControllerImpl::OnTabGroupAdded(
const SavedTabGroup& group,
TriggerSource source) {
// We only care about the first shared group that appears.
if (processed_version_updated_callbacks_ || !group.is_shared_tab_group()) {
return;
}
// A shared group has been added. Re-evaluate the conditions to see if we
// can resolve the pending callbacks now.
MaybeResolvePendingVersionUpdatedCallbacks();
}
void VersioningMessageControllerImpl::
MaybeResolvePendingVersionUpdatedCallbacks() {
if (processed_version_updated_callbacks_) {
return;
}
// The message can only be shown if the version is up to date and the user is
// eligible (they have seen an out-of-date message before).
const bool can_ever_show =
GetVersionState() == VersionState::kUpToDate &&
pref_service_->GetBoolean(prefs::kEligibleForVersionUpdatedMessage);
bool should_resolve = false;
bool resolution_value = false;
if (!can_ever_show) {
// If base conditions aren't met, we can definitively resolve with `false`.
should_resolve = true;
resolution_value = false;
} else if (HasCurrentSharedTabGroups(tab_group_sync_service_)) {
// If base conditions *are* met and we have shared groups, resolve with
// `true`.
should_resolve = true;
resolution_value = true;
}
// If `should_resolve` is false, it means conditions are plausible but we are
// still waiting for a shared tab group to be added. We'll wait for another
// call from OnTabGroupAdded.
if (!should_resolve) {
return;
}
for (auto& callback : pending_version_updated_callbacks_) {
std::move(callback).Run(resolution_value);
}
pending_version_updated_callbacks_.clear();
processed_version_updated_callbacks_ = true;
}
} // namespace tab_groups