blob: 5621e94769911cc034fd7fc7e7224b2ccfd6965f [file] [log] [blame]
// Copyright 2023 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/web_applications/preinstalled_web_app_window_experiment.h"
#include <memory>
#include <ostream>
#include <string>
#include <utility>
#include <vector>
#include "base/barrier_closure.h"
#include "base/check.h"
#include "base/check_op.h"
#include "base/containers/flat_set.h"
#include "base/functional/bind.h"
#include "base/functional/callback_forward.h"
#include "base/functional/callback_helpers.h"
#include "base/location.h"
#include "base/logging.h"
#include "base/scoped_observation.h"
#include "base/time/time.h"
#include "build/build_config.h"
// TODO(crbug.com/1402146): Allow web apps to depend on app service.
#include <optional>
#include "chrome/browser/apps/app_service/app_service_proxy.h" // nogncheck
#include "chrome/browser/apps/app_service/app_service_proxy_factory.h" // nogncheck
#include "chrome/browser/apps/app_service/metrics/app_service_metrics.h" // nogncheck
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/web_applications/locks/app_lock.h"
#include "chrome/browser/web_applications/mojom/user_display_mode.mojom-shared.h"
#include "chrome/browser/web_applications/preinstalled_web_app_window_experiment_utils.h"
#include "chrome/browser/web_applications/web_app.h"
#include "chrome/browser/web_applications/web_app_command_scheduler.h"
#include "chrome/browser/web_applications/web_app_provider.h"
#include "chrome/browser/web_applications/web_app_registrar.h"
#include "chrome/browser/web_applications/web_app_sync_bridge.h"
#include "chrome/common/chrome_features.h"
#include "components/prefs/pref_service.h"
namespace web_app {
using mojom::UserDisplayMode;
using UserGroup = features::PreinstalledWebAppWindowExperimentUserGroup;
namespace {
namespace utils = preinstalled_web_app_window_experiment_utils;
base::flat_set<webapps::AppId> GetLaunchedPreinstalledAppIds(
WebAppRegistrar& registrar) {
std::vector<webapps::AppId> app_ids;
for (const WebApp& web_app : registrar.GetApps()) {
if (web_app.IsPreinstalledApp() && !web_app.last_launch_time().is_null()) {
app_ids.push_back(web_app.app_id());
}
}
return app_ids;
}
std::vector<webapps::AppId> SetSupportedLinksPreferenceForPreinstalledApps(
WebAppRegistrar& registrar,
apps::AppServiceProxy& proxy) {
std::vector<webapps::AppId> apps_affected;
for (const WebApp& web_app : registrar.GetApps()) {
if (web_app.IsPreinstalledApp()) {
proxy.SetSupportedLinksPreference(web_app.app_id());
apps_affected.push_back(web_app.app_id());
}
}
return apps_affected;
}
void SetUserDisplayModeOverridesForPreinstalledAppsOnRegistrar(
WebAppRegistrar& registrar,
PrefService* pref_service,
bool notify_all) {
UserGroup user_group_pref = utils::GetUserGroupPref(pref_service);
auto opt_display_mode = utils::UserGroupToUserDisplayMode(user_group_pref);
if (!opt_display_mode.has_value()) {
// No overrides to apply unless pref maps to `kBrowser` or `kStandalone`.
return;
}
UserDisplayMode display_mode = opt_display_mode.value();
// Exclude any apps for which the user has explicitly set a display mode.
base::flat_set<webapps::AppId> user_set_apps =
utils::GetAppIdsWithUserOverridenDisplayModePref(pref_service);
std::vector<std::pair<webapps::AppId, UserDisplayMode>> overrides;
for (const WebApp& web_app : registrar.GetApps()) {
if (web_app.IsPreinstalledApp() &&
!user_set_apps.contains(web_app.app_id())) {
overrides.emplace_back(web_app.app_id(), display_mode);
}
}
registrar.SetUserDisplayModeOverridesForExperiment(std::move(overrides));
if (!notify_all) {
return;
}
for (const WebApp& web_app : registrar.GetApps()) {
if (web_app.IsPreinstalledApp() &&
!user_set_apps.contains(web_app.app_id())) {
registrar.NotifyWebAppUserDisplayModeChanged(web_app.app_id(),
display_mode);
}
}
}
void PersistStateFromPrefsToWebAppDb(PrefService* pref_service,
WebAppProvider& provider) {
UserGroup user_group = utils::GetUserGroupPref(pref_service);
auto opt_display_mode = utils::UserGroupToUserDisplayMode(user_group);
if (!opt_display_mode.has_value()) {
// Nothing to persist unless pref maps to `kBrowser` or `kStandalone`.
return;
}
// Set all default apps to the experiment display mode, unless the user has
// manually set the display mode for that app.
base::flat_set<webapps::AppId> user_set_apps =
utils::GetAppIdsWithUserOverridenDisplayModePref(pref_service);
std::vector<webapps::AppId> experiment_overrides;
for (const WebApp& web_app : provider.registrar_unsafe().GetApps()) {
if (web_app.IsPreinstalledApp() &&
!user_set_apps.contains(web_app.app_id())) {
experiment_overrides.emplace_back(web_app.app_id());
}
}
for (const webapps::AppId& app_id : experiment_overrides) {
provider.scheduler().SetUserDisplayMode(app_id, *opt_display_mode,
base::DoNothing());
}
}
} // namespace
BASE_FEATURE(kWebAppWindowExperimentCleanup,
"WebAppWindowExperimentCleanup",
base::FEATURE_ENABLED_BY_DEFAULT);
PreinstalledWebAppWindowExperiment::PreinstalledWebAppWindowExperiment(
Profile* profile)
: profile_(profile) {
#if !BUILDFLAG(IS_CHROMEOS)
NOTREACHED() << "PreinstalledWebAppWindowExperiment is CrOS only";
#endif
}
PreinstalledWebAppWindowExperiment::~PreinstalledWebAppWindowExperiment() =
default;
void PreinstalledWebAppWindowExperiment::Start() {
if (!WebAppProvider::GetForWebApps(profile_)) {
// Don't run on ash-chrome when lacros is primary.
return;
}
WebAppProvider::GetForWebApps(profile_)->on_registry_ready().Post(
FROM_HERE,
base::BindOnce(&PreinstalledWebAppWindowExperiment::CheckEligible,
weak_ptr_factory_.GetWeakPtr()));
}
void PreinstalledWebAppWindowExperiment::CheckEligible() {
if (utils::GetUserGroup() == UserGroup::kUnknown) {
// Experiment has been disabled or was never enabled for this user.
CleanUp();
return;
}
// Use eligible pref to know if we need to do first time setup.
std::optional<bool> eligible_pref =
utils::GetEligibilityPref(profile_->GetPrefs());
if (!eligible_pref.has_value()) {
FirstTimeSetup();
return;
}
if (!eligible_pref.value()) {
// Previously determined ineligible.
setup_done_for_testing_.Signal();
return;
}
StartOverridesAndObservations();
}
void PreinstalledWebAppWindowExperiment::NotifyPreinstalledAppsInstalled() {
if (!preinstalled_apps_installed_.is_signaled()) {
preinstalled_apps_installed_.Signal();
}
}
void PreinstalledWebAppWindowExperiment::FirstTimeSetup() {
// Wait for first sync and preinstalled app install before determining
// eligibility and writing it to prefs, then start if eligible.
base::RepeatingClosure barrier = base::BarrierClosure(
/*num_closures=*/2,
base::BindOnce(
&PreinstalledWebAppWindowExperiment::SetFirstTimePrefsThenMaybeStart,
weak_ptr_factory_.GetWeakPtr()));
preinstalled_apps_installed_.Post(FROM_HERE, barrier);
WebAppProvider::GetForWebApps(profile_)
->sync_bridge_unsafe()
.on_sync_connected()
.Post(FROM_HERE, barrier);
}
void PreinstalledWebAppWindowExperiment::SetFirstTimePrefsThenMaybeStart() {
bool eligible = utils::DetermineEligibility(profile_, registrar_unsafe());
utils::SetEligibilityPref(profile_->GetPrefs(), eligible);
if (!eligible) {
setup_done_for_testing_.Signal();
return;
}
// Make the UserGroup setting persist even if the experiment settings change.
utils::SetUserGroupPref(profile_->GetPrefs(), utils::GetUserGroup());
base::flat_set<webapps::AppId> launched_before =
GetLaunchedPreinstalledAppIds(registrar_unsafe());
utils::SetHasLaunchedAppsBeforePref(profile_->GetPrefs(), launched_before);
if (utils::GetUserGroup() == UserGroup::kWindow) {
DCHECK(apps::AppServiceProxyFactory::IsAppServiceAvailableForProfile(
profile_));
auto* proxy = apps::AppServiceProxyFactory::GetForProfile(profile_);
DCHECK(proxy);
apps_that_experiment_setup_set_supported_links_ =
SetSupportedLinksPreferenceForPreinstalledApps(registrar_unsafe(),
*proxy);
}
StartOverridesAndObservations();
}
void PreinstalledWebAppWindowExperiment::StartOverridesAndObservations() {
DCHECK(utils::GetUserGroup() != UserGroup::kUnknown);
DCHECK(utils::GetEligibilityPref(profile_->GetPrefs()).value_or(false));
SetUserDisplayModeOverridesForPreinstalledAppsOnRegistrar(
registrar_unsafe(), profile_->GetPrefs(), /*notify_all=*/true);
// Start listening for `OnWebAppUserDisplayModeChanged`.
registrar_observation_.Observe(&registrar_unsafe());
DCHECK(
apps::AppServiceProxyFactory::IsAppServiceAvailableForProfile(profile_));
auto* proxy = apps::AppServiceProxyFactory::GetForProfile(profile_);
DCHECK(proxy);
// Start listening for `OnPreferredAppChanged`.
preferred_apps_observation_.Observe(&proxy->PreferredAppsList());
setup_done_for_testing_.Signal();
}
void PreinstalledWebAppWindowExperiment::CleanUp() {
// Ensure we aren't listening for changes before making changes.
registrar_observation_.Reset();
preferred_apps_observation_.Reset();
if (base::FeatureList::IsEnabled(kWebAppWindowExperimentCleanup)) {
PersistStateFromPrefsToWebAppDb(profile_->GetPrefs(),
*WebAppProvider::GetForWebApps(profile_));
utils::DeleteExperimentPrefs(profile_->GetPrefs());
}
setup_done_for_testing_.Signal();
}
void PreinstalledWebAppWindowExperiment::OnAppRegistrarDestroyed() {
registrar_observation_.Reset();
}
void PreinstalledWebAppWindowExperiment::OnWebAppUserDisplayModeChanged(
const webapps::AppId& app_id,
UserDisplayMode user_display_mode) {
auto* app = registrar_unsafe().GetAppById(app_id);
if (!app || !app->IsPreinstalledApp()) {
return;
}
// Avoid recursively notifying ourselves when setting the display mode.
if (!utils::GetAppIdsWithUserOverridenDisplayModePref(profile_->GetPrefs())
.contains(app_id)) {
// Record user setting in prefs and remove override from registrar.
utils::SetUserOverridenDisplayModePref(profile_->GetPrefs(), app_id);
SetUserDisplayModeOverridesForPreinstalledAppsOnRegistrar(
registrar_unsafe(), profile_->GetPrefs(), /*notify_all=*/false);
// Update observers again now that the override has been removed, so the
// registrar will return `user_display_mode` if queried directly.
registrar_unsafe().NotifyWebAppUserDisplayModeChanged(app_id,
user_display_mode);
return;
}
std::optional<apps::DefaultAppName> app_name =
apps::PreinstalledWebAppIdToName(app_id);
if (!app_name.has_value()) {
LOG(WARNING) << "Unknown preinstalled app " << app->untranslated_name()
<< " ID " << app_id;
return;
}
utils::RecordDisplayModeChangeHistogram(profile_->GetPrefs(),
user_display_mode, *app_name);
}
void PreinstalledWebAppWindowExperiment::OnPreferredAppChanged(
const std::string& app_id,
bool is_preferred_app) {
auto* app = registrar_unsafe().GetAppById(app_id);
if (!app || !app->IsPreinstalledApp()) {
return;
}
// Ignore the first observation for each app that may result from the
// experiment setup's call to `SetSupportedLinksPreference`.
// Note: this allows for `SetSupportedLinksPreference` to be async (or not)
// and for `OnPreferredAppChanged` to be called only for a subset of apps that
// changed state.
if (apps_that_experiment_setup_set_supported_links_.erase(app_id) &&
is_preferred_app) {
return;
}
std::optional<apps::DefaultAppName> app_name =
apps::PreinstalledWebAppIdToName(app_id);
if (!app_name.has_value()) {
LOG(WARNING) << "Unknown default app " << app->untranslated_name() << " ID "
<< app_id;
return;
}
utils::RecordLinkCapturingChangeHistogram(profile_->GetPrefs(),
is_preferred_app, *app_name);
}
void PreinstalledWebAppWindowExperiment::OnPreferredAppsListWillBeDestroyed(
apps::PreferredAppsListHandle* handle) {
preferred_apps_observation_.Reset();
}
WebAppRegistrar& PreinstalledWebAppWindowExperiment::registrar_unsafe() const {
return WebAppProvider::GetForWebApps(profile_)->registrar_unsafe();
}
} // namespace web_app