// Copyright 2022 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/apps/app_preload_service/app_preload_service.h"
#include <memory>
#include <vector>
#include "base/auto_reset.h"
#include "base/barrier_callback.h"
#include "base/check_is_test.h"
#include "base/feature_list.h"
#include "base/functional/bind.h"
#include "base/functional/callback_helpers.h"
#include "base/logging.h"
#include "base/metrics/histogram_functions.h"
#include "base/ranges/algorithm.h"
#include "base/strings/strcat.h"
#include "base/time/time.h"
#include "chrome/browser/apps/almanac_api_client/device_info_manager.h"
#include "chrome/browser/apps/app_preload_service/app_preload_almanac_endpoint.h"
#include "chrome/browser/apps/app_preload_service/app_preload_service_factory.h"
#include "chrome/browser/apps/app_preload_service/preload_app_definition.h"
#include "chrome/browser/apps/app_service/app_install/app_install_service.h"
#include "chrome/browser/apps/app_service/app_service_proxy.h"
#include "chrome/browser/apps/app_service/app_service_proxy_factory.h"
#include "chrome/browser/profiles/profile.h"
#include "components/pref_registry/pref_registry_syncable.h"
#include "components/prefs/pref_service.h"
#include "components/prefs/scoped_user_pref_update.h"
#include "components/services/app_service/public/cpp/app_types.h"
#include "components/services/app_service/public/cpp/types_util.h"
#include "components/user_manager/user_manager.h"
#include "services/network/public/cpp/shared_url_loader_factory.h"
namespace {
// The pref dict is:
// {
// ...
// "apps.app_preload_service.state_manager": {
// "first_login_flow_started": <bool>,
// "first_login_flow_completed": <bool>
// },
// ...
// }
static constexpr char kFirstLoginFlowStartedKey[] = "first_login_flow_started";
static constexpr char kFirstLoginFlowCompletedKey[] =
"first_login_flow_completed";
static constexpr char kFirstLoginFlowHistogramSuccessName[] =
"AppPreloadService.FirstLoginFlowTime.Success";
static constexpr char kFirstLoginFlowHistogramFailureName[] =
"AppPreloadService.FirstLoginFlowTime.Failure";
bool AreTestAppsEnabled() {
return base::FeatureList::IsEnabled(apps::kAppPreloadServiceEnableTestApps);
}
bool g_disable_preloads_on_startup_for_testing_ = false;
} // namespace
namespace apps {
namespace prefs {
static constexpr char kApsStateManager[] =
"apps.app_preload_service.state_manager";
} // namespace prefs
BASE_FEATURE(kAppPreloadServiceForceRun,
"AppPreloadServiceForceRun",
base::FEATURE_DISABLED_BY_DEFAULT);
BASE_FEATURE(kAppPreloadServiceEnableTestApps,
"AppPreloadServiceEnableTestApps",
base::FEATURE_DISABLED_BY_DEFAULT);
BASE_FEATURE(kAppPreloadServiceEnableArcApps,
"AppPreloadServiceEnableArcApps",
base::FEATURE_DISABLED_BY_DEFAULT);
BASE_FEATURE(kAppPreloadServiceEnableShelfPin,
"AppPreloadServiceEnableShelfPin",
base::FEATURE_DISABLED_BY_DEFAULT);
BASE_FEATURE(kAppPreloadServiceEnableLauncherOrder,
"AppPreloadServiceEnableLauncherOrder",
base::FEATURE_DISABLED_BY_DEFAULT);
BASE_FEATURE(kAppPreloadServiceAllUserTypes,
"AppPreloadServiceAllUserTypes",
base::FEATURE_DISABLED_BY_DEFAULT);
AppPreloadService::AppPreloadService(Profile* profile)
: profile_(profile),
device_info_manager_(std::make_unique<DeviceInfoManager>(profile)) {
if (g_disable_preloads_on_startup_for_testing_) {
return;
}
StartFirstLoginFlow();
}
AppPreloadService::~AppPreloadService() = default;
// static
AppPreloadService* AppPreloadService::Get(Profile* profile) {
return AppPreloadServiceFactory::GetForProfile(profile);
}
// static
void AppPreloadService::RegisterProfilePrefs(
user_prefs::PrefRegistrySyncable* registry) {
registry->RegisterDictionaryPref(prefs::kApsStateManager);
}
void AppPreloadService::StartFirstLoginFlowForTesting(
PreloadStatusCallback callback) {
installation_complete_callback_ = std::move(callback);
StartFirstLoginFlow();
}
// static
base::AutoReset<bool> AppPreloadService::DisablePreloadsOnStartupForTesting() {
return base::AutoReset<bool>(&g_disable_preloads_on_startup_for_testing_,
true);
}
void AppPreloadService::GetPinApps(GetPinAppsCallback callback) {
if (data_ready_) {
std::move(callback).Run(pin_apps_, pin_order_);
} else {
get_pin_apps_callbacks_.push_back(std::move(callback));
}
}
void AppPreloadService::GetLauncherOrdering(
base::OnceCallback<void(const LauncherOrdering&)> callback) {
if (data_ready_) {
std::move(callback).Run(launcher_ordering_);
} else {
get_launcher_ordering_callbacks_.push_back(std::move(callback));
}
}
void AppPreloadService::StartFirstLoginFlow() {
auto start_time = base::TimeTicks::Now();
// Preloads currently run for new users only. The "completed" pref is only set
// when preloads finish successfully, so preloads will be retried if they have
// been "started" but never "completed".
if (user_manager::UserManager::Get()->IsCurrentUserNew()) {
ScopedDictPrefUpdate(profile_->GetPrefs(), prefs::kApsStateManager)
->Set(kFirstLoginFlowStartedKey, true);
}
bool first_run_started =
GetStateManager().FindBool(kFirstLoginFlowStartedKey).value_or(false);
bool first_run_complete =
GetStateManager().FindBool(kFirstLoginFlowCompletedKey).value_or(false);
if ((first_run_started && !first_run_complete) ||
base::FeatureList::IsEnabled(kAppPreloadServiceForceRun)) {
device_info_manager_->GetDeviceInfo(
base::BindOnce(&AppPreloadService::StartAppInstallationForFirstLogin,
weak_ptr_factory_.GetWeakPtr(), start_time));
} else {
data_ready_ = true;
}
}
void AppPreloadService::StartAppInstallationForFirstLogin(
base::TimeTicks start_time,
DeviceInfo device_info) {
scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory =
profile_->GetURLLoaderFactory();
if (!url_loader_factory.get()) {
// `url_loader_factory` should only be null if we are in a non-preload
// related test. Tests that use profile builder to create their profile
// won't have `url_loader_factory` set up by default, so we bypass preloads
// code being called for those tests.
CHECK_IS_TEST();
return;
}
app_preload_almanac_endpoint::GetAppsForFirstLogin(
device_info, *url_loader_factory,
base::BindOnce(&AppPreloadService::OnGetAppsForFirstLoginCompleted,
weak_ptr_factory_.GetWeakPtr(), start_time));
}
void AppPreloadService::OnGetAppsForFirstLoginCompleted(
base::TimeTicks start_time,
std::optional<std::vector<PreloadAppDefinition>> apps,
LauncherOrdering launcher_ordering,
ShelfPinOrdering shelf_pin_ordering) {
data_ready_ = true;
if (!apps.has_value()) {
OnFirstLoginFlowComplete(start_time, /*success=*/false);
return;
}
// TODO(crbug.com/327058999): Implement launcher ordering.
std::vector<const PreloadAppDefinition*> apps_to_install;
for (const PreloadAppDefinition& app : apps.value()) {
if (ShouldInstallApp(app)) {
apps_to_install.push_back(&app);
}
}
if (base::FeatureList::IsEnabled(apps::kAppPreloadServiceEnableShelfPin)) {
pin_apps_.clear();
pin_order_.clear();
// Collect the apps that will be pinned after install.
for (auto* const app : apps_to_install) {
if (shelf_pin_ordering.contains(*app->GetPackageId())) {
pin_apps_.push_back(*app->GetPackageId());
}
}
// Sort shelf pin ordering.
std::vector<std::pair<apps::PackageId, uint32_t>> pins(
shelf_pin_ordering.begin(), shelf_pin_ordering.end());
base::ranges::sort(pins, {}, &std::pair<apps::PackageId, uint32_t>::second);
base::ranges::transform(pins, std::back_inserter(pin_order_),
&std::pair<apps::PackageId, uint32_t>::first);
}
for (auto& callback : get_pin_apps_callbacks_) {
std::move(callback).Run(pin_apps_, pin_order_);
}
get_pin_apps_callbacks_.clear();
if (base::FeatureList::IsEnabled(
apps::kAppPreloadServiceEnableLauncherOrder)) {
launcher_ordering_ = launcher_ordering;
}
for (auto& callback : get_launcher_ordering_callbacks_) {
std::move(callback).Run(launcher_ordering_);
}
get_launcher_ordering_callbacks_.clear();
const auto install_barrier_callback = base::BarrierCallback<bool>(
apps_to_install.size(),
base::BindOnce(&AppPreloadService::OnAppInstallationsCompleted,
weak_ptr_factory_.GetWeakPtr(), start_time));
AppInstallService& install_service =
AppServiceProxyFactory::GetForProfile(profile_)->AppInstallService();
for (const PreloadAppDefinition* app : apps_to_install) {
install_service.InstallAppHeadless(
app->IsOemApp() ? AppInstallSurface::kAppPreloadServiceOem
: AppInstallSurface::kAppPreloadServiceDefault,
app->ToAppInstallData(), install_barrier_callback);
}
}
void AppPreloadService::OnAppInstallationsCompleted(
base::TimeTicks start_time,
const std::vector<bool>& results) {
OnFirstLoginFlowComplete(start_time,
base::ranges::all_of(results, std::identity{}));
}
void AppPreloadService::OnFirstLoginFlowComplete(base::TimeTicks start_time,
bool success) {
if (success) {
ScopedDictPrefUpdate(profile_->GetPrefs(), prefs::kApsStateManager)
->Set(kFirstLoginFlowCompletedKey, true);
}
base::UmaHistogramMediumTimes(success ? kFirstLoginFlowHistogramSuccessName
: kFirstLoginFlowHistogramFailureName,
base::TimeTicks::Now() - start_time);
if (installation_complete_callback_) {
std::move(installation_complete_callback_).Run(success);
}
}
bool AppPreloadService::ShouldInstallApp(const PreloadAppDefinition& app) {
// We preload android apps (when feature enabled) and web apps.
if (app.GetPlatform() == PackageType::kArc) {
if (!base::FeatureList::IsEnabled(apps::kAppPreloadServiceEnableArcApps)) {
return false;
}
} else if (app.GetPlatform() != PackageType::kWeb) {
return false;
}
// We currently install apps which were requested by the device OEM or
// installed by default (i.e. by Google). If the testing feature is enabled,
// also install test apps.
bool install_reason_allowed = app.IsOemApp() || app.IsDefaultApp() ||
(app.IsTestApp() && AreTestAppsEnabled());
if (!install_reason_allowed) {
return false;
}
// If the app is already installed with the relevant install reason, we do not
// need to reinstall it. This avoids extra work in the case where we are
// retrying the flow after an install error for a different app.
InstallReason expected_reason =
app.IsOemApp() ? InstallReason::kOem : InstallReason::kDefault;
AppServiceProxy* proxy = AppServiceProxyFactory::GetForProfile(profile_);
bool installed = false;
proxy->AppRegistryCache().ForEachApp(
[&installed, expected_reason, app](const apps::AppUpdate& update) {
// It's possible that if APS requests the same app to be installed for
// multiple reasons, this check could incorrectly return false, as App
// Service only reports the highest priority install reason. This is
// acceptable since the check is just an optimization.
if (update.InstallerPackageId() == app.GetPackageId() &&
apps_util::IsInstalled(update.Readiness()) &&
update.InstallReason() == expected_reason) {
installed = true;
}
});
return !installed;
}
const base::Value::Dict& AppPreloadService::GetStateManager() const {
const base::Value::Dict& value =
profile_->GetPrefs()->GetDict(prefs::kApsStateManager);
return value;
}
} // namespace apps