// Copyright 2024 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "ash/app_list/apps_collections_controller.h"
#include <memory>
#include <optional>
#include <utility>
#include "ash/constants/ash_pref_names.h"
#include "ash/public/cpp/app_list/app_list_client.h"
#include "ash/public/cpp/app_list/app_list_features.h"
#include "ash/public/cpp/app_list/app_list_types.h"
#include "ash/session/session_controller_impl.h"
#include "ash/shell.h"
#include "base/metrics/histogram_functions.h"
#include "components/prefs/pref_service.h"
namespace ash {
namespace {
// The singleton instance owned by `AppListController`.
AppsCollectionsController* g_instance = nullptr;
} // namespace
// AppsCollectionsController ----------------------------------------
AppsCollectionsController::AppsCollectionsController() {
CHECK_EQ(g_instance, nullptr);
g_instance = this;
}
AppsCollectionsController::~AppsCollectionsController() {
CHECK_EQ(g_instance, this);
g_instance = nullptr;
}
// static
AppsCollectionsController* AppsCollectionsController::Get() {
return g_instance;
}
AppsCollectionsController::ExperimentalArm
AppsCollectionsController::GetUserExperimentalArm() {
PrefService* prefs =
Shell::Get()->session_controller()->GetLastActiveUserPrefService();
if (!prefs ||
prefs->FindPreference(prefs::kLauncherAppsCollectionsExperimentArm)
->IsDefaultValue()) {
return ExperimentalArm::kDefaultValue;
}
return static_cast<ExperimentalArm>(
prefs->GetInteger(prefs::kLauncherAppsCollectionsExperimentArm));
}
void AppsCollectionsController::MaybeRecordUserExperimentStatePref(
ExperimentalArm arm) {
PrefService* prefs =
Shell::Get()->session_controller()->GetLastActiveUserPrefService();
if (!prefs) {
return;
}
if (prefs->FindPreference(prefs::kLauncherAppsCollectionsExperimentArm)
->IsDefaultValue()) {
prefs->SetInteger(prefs::kLauncherAppsCollectionsExperimentArm,
static_cast<int>(arm));
}
}
std::string
AppsCollectionsController::GetUserExperimentalArmAsHistogramSuffix() {
switch (GetUserExperimentalArm()) {
case ash::AppsCollectionsController::ExperimentalArm::kDefaultValue:
case ash::AppsCollectionsController::ExperimentalArm::kControl:
return "";
case ash::AppsCollectionsController::ExperimentalArm::kCounterfactual:
return ".Counterfactual";
case ash::AppsCollectionsController::ExperimentalArm::kEnabled:
return ".Enabled";
case ash::AppsCollectionsController::ExperimentalArm::kModifiedOrder:
return ".ModifiedOrder";
}
NOTREACHED();
}
void AppsCollectionsController::CalculateExperimentalArm() {
if (GetUserExperimentalArm() != ExperimentalArm::kDefaultValue) {
return;
}
const auto* const session_controller = Shell::Get()->session_controller();
if (const auto user_type = session_controller->GetUserType();
user_type != user_manager::UserType::kRegular) {
MaybeRecordUserExperimentStatePref(ExperimentalArm::kControl);
return;
}
if (session_controller->IsActiveAccountManaged()) {
MaybeRecordUserExperimentStatePref(ExperimentalArm::kControl);
return;
}
if (!session_controller->IsUserFirstLogin()) {
MaybeRecordUserExperimentStatePref(ExperimentalArm::kControl);
return;
}
// NOTE: Currently only supported for the primary user profile. This is a
// self-imposed restriction.
if (!session_controller->IsUserPrimary()) {
MaybeRecordUserExperimentStatePref(ExperimentalArm::kControl);
return;
}
// If the client was destroyed at this point, (i.e. in tests), return early to
// avoid segmentation fault.
if (!client_) {
MaybeRecordUserExperimentStatePref(ExperimentalArm::kControl);
return;
}
const std::optional<bool>& is_new_user =
client_->IsNewUser(session_controller->GetActiveAccountId());
// If it is not known whether the user is "new" or "existing" when this code
// is reached, the user is treated as "existing" we want to err on the side of
// being conservative.
if (!is_new_user || !is_new_user.value()) {
MaybeRecordUserExperimentStatePref(ExperimentalArm::kControl);
return;
}
if (client_->HasReordered()) {
MaybeRecordUserExperimentStatePref(ExperimentalArm::kControl);
return;
}
// To ensure the population number of the experiment groups (Counterfactual
// and Enabled) are similar sized, query the finch experiment state here.
// The counterfactual arm will serve as control group, so it should not show
// Apps Collections even if it belong to the experiment.
if (!app_list_features::IsAppsCollectionsEnabled()) {
MaybeRecordUserExperimentStatePref(ExperimentalArm::kControl);
return;
}
if (app_list_features::IsAppsCollectionsEnabledCounterfactually()) {
MaybeRecordUserExperimentStatePref(ExperimentalArm::kCounterfactual);
return;
}
if (app_list_features::IsAppsCollectionsEnabledWithModifiedOrder()) {
MaybeRecordUserExperimentStatePref(ExperimentalArm::kModifiedOrder);
return;
}
MaybeRecordUserExperimentStatePref(ExperimentalArm::kEnabled);
return;
}
bool AppsCollectionsController::ShouldShowAppsCollection() {
if (apps_collections_was_dissmissed_) {
return false;
}
if (app_list_was_reordered_) {
return false;
}
if (app_list_features::IsForceShowAppsCollectionsEnabled()) {
return true;
}
if (GetUserExperimentalArm() == ExperimentalArm::kDefaultValue) {
CalculateExperimentalArm();
}
return GetUserExperimentalArm() == ExperimentalArm::kEnabled &&
Shell::Get()->session_controller()->IsUserFirstLogin();
}
void AppsCollectionsController::SetAppsCollectionDismissed(
DismissReason reason) {
apps_collections_was_dissmissed_ = true;
if (reason == DismissReason::kSorting) {
SetAppsReordered();
}
base::UmaHistogramEnumeration("Apps.AppList.AppsCollections.DismissedReason",
reason);
}
void AppsCollectionsController::SetAppsReordered() {
app_list_was_reordered_ = true;
}
void AppsCollectionsController::SetClient(AppListClient* client) {
client_ = client;
}
void AppsCollectionsController::RequestAppReorder(AppListSortOrder order) {
CHECK(reorder_callback_);
reorder_callback_.Run(order);
}
void AppsCollectionsController::SetReorderCallback(ReorderCallback callback) {
CHECK(callback);
reorder_callback_ = std::move(callback);
}
} // namespace ash