blob: 86f84a0472e6389a77d150f44cff5ca242c04740 [file] [log] [blame]
// Copyright 2018 The Chromium Authors. All rights reserved.
// 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/system_web_app_manager.h"
#include <algorithm>
#include <string>
#include <tuple>
#include <utility>
#include <vector>
#include "base/bind.h"
#include "base/command_line.h"
#include "base/metrics/histogram_functions.h"
#include "base/run_loop.h"
#include "base/stl_util.h"
#include "base/strings/string_number_conversions.h"
#include "base/version.h"
#include "chrome/browser/browser_process.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/web_applications/components/app_registrar.h"
#include "chrome/browser/web_applications/components/app_registry_controller.h"
#include "chrome/browser/web_applications/components/file_handler_manager.h"
#include "chrome/browser/web_applications/components/web_app_constants.h"
#include "chrome/browser/web_applications/components/web_app_install_utils.h"
#include "chrome/browser/web_applications/components/web_app_ui_manager.h"
#include "chrome/browser/web_applications/components/web_app_utils.h"
#include "chrome/common/chrome_features.h"
#include "chrome/common/pref_names.h"
#include "chrome/common/webui_url_constants.h"
#include "chrome/grit/generated_resources.h"
#include "components/pref_registry/pref_registry_syncable.h"
#include "components/prefs/pref_service.h"
#include "components/version_info/version_info.h"
#include "content/public/browser/navigation_handle.h"
#include "content/public/browser/url_data_source.h"
#include "content/public/common/content_switches.h"
#include "content/public/common/url_constants.h"
#include "ui/base/l10n/l10n_util.h"
#if defined(OS_CHROMEOS)
#include "ash/public/cpp/app_list/internal_app_id_constants.h"
#include "base/values.h"
#include "chrome/browser/chromeos/policy/system_features_disable_list_policy_handler.h"
#include "chrome/browser/chromeos/web_applications/default_web_app_ids.h"
#include "chrome/browser/chromeos/web_applications/terminal_source.h"
#include "chromeos/components/help_app_ui/url_constants.h"
#include "chromeos/components/media_app_ui/url_constants.h"
#include "chromeos/constants/chromeos_features.h"
#include "chromeos/strings/grit/chromeos_strings.h"
#include "components/policy/core/common/policy_pref_names.h"
#include "extensions/common/constants.h"
#endif // defined(OS_CHROMEOS)
namespace web_app {
namespace {
// Copy the origin trial name from runtime_enabled_features.json5, to avoid
// complex dependencies.
const char kFileHandlingOriginTrial[] = "FileHandling";
// Use #if defined to avoid compiler error on unused function.
#if defined(OS_CHROMEOS)
// A convenience method to create OriginTrialsMap. Note, we only support simple
// cases for chrome:// and chrome-untrusted:// URLs. We don't support complex
// cases such as about:blank (which inherits origins from the embedding frame).
url::Origin GetOrigin(const char* url) {
GURL gurl = GURL(url);
DCHECK(gurl.is_valid());
url::Origin origin = url::Origin::Create(gurl);
DCHECK(!origin.opaque());
return origin;
}
#endif // OS_CHROMEOS
base::flat_map<SystemAppType, SystemAppInfo> CreateSystemWebApps() {
base::flat_map<SystemAppType, SystemAppInfo> infos;
// TODO(calamity): Split this into per-platform functions.
#if defined(OS_CHROMEOS)
// SystemAppInfo's |name| field should be defined. These names are persisted
// to logs and should not be renamed.
// If new names are added, update tool/metrics/histograms/histograms.xml:
// "SystemWebAppName"
if (SystemWebAppManager::IsAppEnabled(SystemAppType::DISCOVER)) {
infos.emplace(
SystemAppType::DISCOVER,
SystemAppInfo("Discover", GURL(chrome::kChromeUIDiscoverURL)));
}
if (SystemWebAppManager::IsAppEnabled(SystemAppType::CAMERA)) {
infos.emplace(SystemAppType::CAMERA,
SystemAppInfo("Camera", GURL("chrome://camera/pwa.html")));
infos.at(SystemAppType::CAMERA).uninstall_and_replace = {
extension_misc::kCameraAppId};
}
infos.emplace(
SystemAppType::SETTINGS,
SystemAppInfo("OSSettings", GURL("chrome://os-settings/pwa.html")));
infos.at(SystemAppType::SETTINGS).uninstall_and_replace = {
chromeos::default_web_apps::kSettingsAppId, ash::kInternalAppIdSettings};
// Large enough to see the heading text "Settings" in the top-left.
infos.at(SystemAppType::SETTINGS).minimum_window_size = {300, 100};
if (SystemWebAppManager::IsAppEnabled(SystemAppType::TERMINAL)) {
infos.emplace(
SystemAppType::TERMINAL,
SystemAppInfo("Terminal",
GURL("chrome-untrusted://terminal/html/pwa.html")));
infos.at(SystemAppType::TERMINAL).single_window = false;
}
if (SystemWebAppManager::IsAppEnabled(SystemAppType::HELP)) {
infos.emplace(SystemAppType::HELP,
SystemAppInfo("Help", GURL("chrome://help-app/pwa.html")));
infos.at(SystemAppType::HELP).additional_search_terms = {
IDS_GENIUS_APP_NAME, IDS_HELP_APP_PERKS, IDS_HELP_APP_OFFERS};
}
if (SystemWebAppManager::IsAppEnabled(SystemAppType::MEDIA)) {
infos.emplace(SystemAppType::MEDIA,
SystemAppInfo("Media", GURL("chrome://media-app/pwa.html")));
infos.at(SystemAppType::MEDIA).include_launch_directory = true;
infos.at(SystemAppType::MEDIA).show_in_launcher = false;
infos.at(SystemAppType::MEDIA).show_in_search = false;
infos.at(SystemAppType::MEDIA).enabled_origin_trials =
OriginTrialsMap({{GetOrigin("chrome://media-app"),
{"FileHandling", "NativeFileSystem2"}}});
}
if (SystemWebAppManager::IsAppEnabled(SystemAppType::PRINT_MANAGEMENT)) {
infos.emplace(
std::piecewise_construct,
std::forward_as_tuple(SystemAppType::PRINT_MANAGEMENT),
std::forward_as_tuple("PrintManagement",
GURL("chrome://print-management/pwa.html")));
infos.at(SystemAppType::PRINT_MANAGEMENT).show_in_launcher = false;
}
#if !defined(OFFICIAL_BUILD)
infos.emplace(
SystemAppType::SAMPLE,
SystemAppInfo("Sample", GURL("chrome://sample-system-web-app/pwa.html")));
// Frobulate is the name for Sample Origin Trial API, and has no impact on the
// Web App's functionality. Here we use it to demonstrate how to enable origin
// trials for a System Web App.
infos.at(SystemAppType::SAMPLE).enabled_origin_trials = OriginTrialsMap(
{{GetOrigin("chrome://sample-system-web-app"), {"Frobulate"}},
{GetOrigin("chrome-untrusted://sample-system-web-app"), {"Frobulate"}}});
#endif // !defined(OFFICIAL_BUILD)
#endif // OS_CHROMEOS
return infos;
}
ExternalInstallOptions CreateInstallOptionsForSystemApp(
const SystemAppInfo& info,
bool force_update,
bool is_disabled) {
DCHECK(info.install_url.scheme() == content::kChromeUIScheme ||
info.install_url.scheme() == content::kChromeUIUntrustedScheme);
ExternalInstallOptions install_options(
info.install_url, DisplayMode::kStandalone,
ExternalInstallSource::kSystemInstalled);
install_options.add_to_applications_menu = info.show_in_launcher;
install_options.add_to_desktop = false;
install_options.add_to_quick_launch_bar = false;
install_options.add_to_search = info.show_in_search;
install_options.add_to_management = false;
install_options.is_disabled = is_disabled;
install_options.bypass_service_worker_check = true;
install_options.force_reinstall = force_update;
install_options.uninstall_and_replace = info.uninstall_and_replace;
const auto& search_terms = info.additional_search_terms;
std::transform(search_terms.begin(), search_terms.end(),
std::back_inserter(install_options.additional_search_terms),
[](int term) { return l10n_util::GetStringUTF8(term); });
return install_options;
}
std::set<SystemAppType> GetDisabledSystemWebApps() {
std::set<SystemAppType> disabled_system_apps;
#if defined(OS_CHROMEOS)
PrefService* const local_state = g_browser_process->local_state();
if (!local_state) // Sometimes it's not available in tests.
return disabled_system_apps;
const base::ListValue* disabled_system_features_pref =
local_state->GetList(policy::policy_prefs::kSystemFeaturesDisableList);
if (!disabled_system_features_pref)
return disabled_system_apps;
for (const auto& entry : *disabled_system_features_pref) {
switch (entry.GetInt()) {
case policy::SystemFeature::CAMERA:
disabled_system_apps.insert(SystemAppType::CAMERA);
break;
case policy::SystemFeature::OS_SETTINGS:
disabled_system_apps.insert(SystemAppType::SETTINGS);
break;
}
}
#endif // defined(OS_CHROMEOS)
return disabled_system_apps;
}
} // namespace
SystemAppInfo::SystemAppInfo(const std::string& name_for_logging,
const GURL& install_url)
: name_for_logging(name_for_logging), install_url(install_url) {}
SystemAppInfo::SystemAppInfo(const SystemAppInfo& other) = default;
SystemAppInfo::~SystemAppInfo() = default;
// static
const char SystemWebAppManager::kInstallResultHistogramName[];
const char SystemWebAppManager::kInstallDurationHistogramName[];
// static
bool SystemWebAppManager::IsAppEnabled(SystemAppType type) {
#if defined(OS_CHROMEOS)
switch (type) {
case SystemAppType::SETTINGS:
return true;
case SystemAppType::DISCOVER:
return base::FeatureList::IsEnabled(chromeos::features::kDiscoverApp);
case SystemAppType::CAMERA:
return base::FeatureList::IsEnabled(
chromeos::features::kCameraSystemWebApp);
case SystemAppType::TERMINAL:
return base::FeatureList::IsEnabled(features::kTerminalSystemApp);
case SystemAppType::MEDIA:
return base::FeatureList::IsEnabled(chromeos::features::kMediaApp);
case SystemAppType::HELP:
return base::FeatureList::IsEnabled(chromeos::features::kHelpAppV2);
case SystemAppType::PRINT_MANAGEMENT:
return base::FeatureList::IsEnabled(
chromeos::features::kPrintJobManagementApp);
#if !defined(OFFICIAL_BUILD)
case SystemAppType::SAMPLE:
NOTREACHED();
return false;
#endif // !defined(OFFICIAL_BUILD)
}
#else
return false;
#endif // OS_CHROMEOS
}
SystemWebAppManager::SystemWebAppManager(Profile* profile)
: profile_(profile),
on_apps_synchronized_(new base::OneShotEvent()),
install_result_per_profile_histogram_name_(
std::string(kInstallResultHistogramName) + ".Profiles." +
GetProfileCategoryForLogging(profile)),
pref_service_(profile_->GetPrefs()) {
if (base::CommandLine::ForCurrentProcess()->HasSwitch(
::switches::kTestType)) {
// Always update in tests, and return early to avoid populating with real
// system apps.
update_policy_ = UpdatePolicy::kAlwaysUpdate;
return;
}
#if defined(OFFICIAL_BUILD)
// Official builds should trigger updates whenever the version number changes.
update_policy_ = UpdatePolicy::kOnVersionChange;
#else
// Dev builds should update every launch.
update_policy_ = UpdatePolicy::kAlwaysUpdate;
#endif
system_app_infos_ = CreateSystemWebApps();
}
SystemWebAppManager::~SystemWebAppManager() = default;
void SystemWebAppManager::Shutdown() {
shutting_down_ = true;
}
void SystemWebAppManager::SetSubsystems(
PendingAppManager* pending_app_manager,
AppRegistrar* registrar,
AppRegistryController* registry_controller,
WebAppUiManager* ui_manager,
FileHandlerManager* file_handler_manager) {
pending_app_manager_ = pending_app_manager;
registrar_ = registrar;
registry_controller_ = registry_controller;
ui_manager_ = ui_manager;
file_handler_manager_ = file_handler_manager;
}
void SystemWebAppManager::Start() {
const base::TimeTicks install_start_time = base::TimeTicks::Now();
#if DCHECK_IS_ON()
// Check Origin Trials are defined correctly.
for (const auto& type_and_app_info : system_app_infos_) {
for (const auto& origin_to_trial_names :
type_and_app_info.second.enabled_origin_trials) {
// Only allow force enabled origin trials on chrome:// and
// chrome-untrusted:// URLs.
const auto& scheme = origin_to_trial_names.first.scheme();
DCHECK(scheme == content::kChromeUIScheme ||
scheme == content::kChromeUIUntrustedScheme);
}
}
// TODO(https://crbug.com/1043843): Find some ways to validate supplied origin
// trial names. Ideally, construct them from some static const char*.
#endif // DCHECK_IS_ON()
#if defined(OS_CHROMEOS)
if (SystemWebAppManager::IsAppEnabled(SystemAppType::TERMINAL)) {
// We need to set up the terminal data source before installing it.
content::URLDataSource::Add(profile_,
std::make_unique<TerminalSource>(profile_));
}
#endif // defined(OS_CHROMEOS)
std::vector<ExternalInstallOptions> install_options_list;
if (IsEnabled()) {
const auto disabled_system_apps = GetDisabledSystemWebApps();
// Skipping this will uninstall all System Apps currently installed.
for (const auto& app : system_app_infos_) {
install_options_list.push_back(CreateInstallOptionsForSystemApp(
app.second, NeedsUpdate(),
base::Contains(disabled_system_apps, app.first)));
}
}
pending_app_manager_->SynchronizeInstalledApps(
std::move(install_options_list), ExternalInstallSource::kSystemInstalled,
base::BindOnce(&SystemWebAppManager::OnAppsSynchronized,
weak_ptr_factory_.GetWeakPtr(), install_start_time));
#if defined(OS_CHROMEOS)
PrefService* const local_state = g_browser_process->local_state();
if (local_state) { // Sometimes it's not available in tests.
local_state_pref_change_registrar_.Init(local_state);
// Sometimes this function gets called twice in tests.
if (!local_state_pref_change_registrar_.IsObserved(
policy::policy_prefs::kSystemFeaturesDisableList)) {
local_state_pref_change_registrar_.Add(
policy::policy_prefs::kSystemFeaturesDisableList,
base::Bind(&SystemWebAppManager::OnAppsPolicyChanged,
base::Unretained(this)));
}
}
#endif // defined(OS_CHROMEOS)
}
void SystemWebAppManager::InstallSystemAppsForTesting() {
on_apps_synchronized_.reset(new base::OneShotEvent());
system_app_infos_ = CreateSystemWebApps();
Start();
// Wait for the System Web Apps to install.
base::RunLoop run_loop;
on_apps_synchronized().Post(FROM_HERE, run_loop.QuitClosure());
run_loop.Run();
}
base::Optional<AppId> SystemWebAppManager::GetAppIdForSystemApp(
SystemAppType id) const {
auto app_url_it = system_app_infos_.find(id);
if (app_url_it == system_app_infos_.end())
return base::Optional<AppId>();
return registrar_->LookupExternalAppId(app_url_it->second.install_url);
}
base::Optional<SystemAppType> SystemWebAppManager::GetSystemAppTypeForAppId(
AppId app_id) const {
auto it = app_id_to_app_type_.find(app_id);
if (it == app_id_to_app_type_.end())
return base::nullopt;
return it->second;
}
std::vector<AppId> SystemWebAppManager::GetAppIds() const {
std::vector<AppId> app_ids;
for (const auto& app_id_to_app_type : app_id_to_app_type_) {
app_ids.push_back(app_id_to_app_type.first);
}
return app_ids;
}
bool SystemWebAppManager::IsSystemWebApp(const AppId& app_id) const {
return app_id_to_app_type_.contains(app_id);
}
bool SystemWebAppManager::IsSingleWindow(SystemAppType type) const {
auto it = system_app_infos_.find(type);
if (it == system_app_infos_.end())
return false;
return it->second.single_window;
}
bool SystemWebAppManager::AppShouldReceiveLaunchDirectory(
SystemAppType type) const {
auto it = system_app_infos_.find(type);
if (it == system_app_infos_.end())
return false;
return it->second.include_launch_directory;
}
const std::vector<std::string>* SystemWebAppManager::GetEnabledOriginTrials(
const SystemAppType type,
const GURL& url) {
const auto& origin_to_origin_trials =
system_app_infos_.at(type).enabled_origin_trials;
auto iter_trials = origin_to_origin_trials.find(url::Origin::Create(url));
if (iter_trials == origin_to_origin_trials.end())
return nullptr;
return &iter_trials->second;
}
bool SystemWebAppManager::AppHasFileHandlingOriginTrial(SystemAppType type) {
const auto& info = system_app_infos_.at(type);
const std::vector<std::string>* trials =
GetEnabledOriginTrials(type, info.install_url);
return trials && base::Contains(*trials, kFileHandlingOriginTrial);
}
void SystemWebAppManager::OnReadyToCommitNavigation(
const AppId& app_id,
content::NavigationHandle* navigation_handle) {
// No need to setup origin trials for intra-document navigation.
if (navigation_handle->IsSameDocument())
return;
const base::Optional<SystemAppType> type = GetSystemAppTypeForAppId(app_id);
// This function should only be called when an navigation happens inside a
// System App. So the |app_id| should always have a valid associated System
// App type.
DCHECK(type.has_value());
const std::vector<std::string>* trials =
GetEnabledOriginTrials(type.value(), navigation_handle->GetURL());
if (trials)
navigation_handle->ForceEnableOriginTrials(*trials);
}
std::vector<std::string> SystemWebAppManager::GetAdditionalSearchTerms(
SystemAppType type) const {
auto it = system_app_infos_.find(type);
if (it == system_app_infos_.end())
return {};
const auto& search_terms = it->second.additional_search_terms;
std::vector<std::string> search_terms_strings;
std::transform(search_terms.begin(), search_terms.end(),
std::back_inserter(search_terms_strings),
[](int term) { return l10n_util::GetStringUTF8(term); });
return search_terms_strings;
}
bool SystemWebAppManager::ShouldShowInLauncher(SystemAppType type) const {
auto it = system_app_infos_.find(type);
if (it == system_app_infos_.end())
return false;
return it->second.show_in_launcher;
}
bool SystemWebAppManager::ShouldShowInSearch(SystemAppType type) const {
auto it = system_app_infos_.find(type);
if (it == system_app_infos_.end())
return false;
return it->second.show_in_search;
}
gfx::Size SystemWebAppManager::GetMinimumWindowSize(const AppId& app_id) const {
auto app_type_it = app_id_to_app_type_.find(app_id);
if (app_type_it == app_id_to_app_type_.end())
return gfx::Size();
const SystemAppType& app_type = app_type_it->second;
auto app_info_it = system_app_infos_.find(app_type);
if (app_info_it == system_app_infos_.end())
return gfx::Size();
return app_info_it->second.minimum_window_size;
}
void SystemWebAppManager::SetSystemAppsForTesting(
base::flat_map<SystemAppType, SystemAppInfo> system_apps) {
system_app_infos_ = std::move(system_apps);
}
void SystemWebAppManager::SetUpdatePolicyForTesting(UpdatePolicy policy) {
update_policy_ = policy;
}
// static
bool SystemWebAppManager::IsEnabled() {
return base::FeatureList::IsEnabled(features::kSystemWebApps);
}
// static
void SystemWebAppManager::RegisterProfilePrefs(
user_prefs::PrefRegistrySyncable* registry) {
registry->RegisterStringPref(prefs::kSystemWebAppLastUpdateVersion, "");
registry->RegisterStringPref(prefs::kSystemWebAppLastInstalledLocale, "");
}
const base::Version& SystemWebAppManager::CurrentVersion() const {
return version_info::GetVersion();
}
const std::string& SystemWebAppManager::CurrentLocale() const {
return g_browser_process->GetApplicationLocale();
}
void SystemWebAppManager::RecordSystemWebAppInstallMetrics(
const std::map<GURL, InstallResultCode>& install_results,
const base::TimeDelta& install_duration) const {
// Record the time spent to install system web apps.
if (!shutting_down_) {
base::UmaHistogramMediumTimes(kInstallDurationHistogramName,
install_duration);
}
// Record aggregate result.
for (const auto& url_and_result : install_results)
base::UmaHistogramEnumeration(
kInstallResultHistogramName,
shutting_down_
? InstallResultCode::kCancelledOnWebAppProviderShuttingDown
: url_and_result.second);
// Record per-app result.
for (const auto& type_and_app_info : system_app_infos_) {
const GURL& install_url = type_and_app_info.second.install_url;
const auto url_and_result = install_results.find(install_url);
if (url_and_result != install_results.cend()) {
const std::string app_histogram_name =
std::string(kInstallResultHistogramName) + ".Apps." +
type_and_app_info.second.name_for_logging;
base::UmaHistogramEnumeration(
app_histogram_name,
shutting_down_
? InstallResultCode::kCancelledOnWebAppProviderShuttingDown
: url_and_result->second);
}
}
// Record per-profile result.
for (const auto& url_and_result : install_results) {
base::UmaHistogramEnumeration(
install_result_per_profile_histogram_name_,
shutting_down_
? InstallResultCode::kCancelledOnWebAppProviderShuttingDown
: url_and_result.second);
}
}
void SystemWebAppManager::OnAppsSynchronized(
const base::TimeTicks& install_start_time,
std::map<GURL, InstallResultCode> install_results,
std::map<GURL, bool> uninstall_results) {
// TODO(crbug.com/1053371): Clean up File Handler install. We install SWA file
// handlers here, because the code that registers file handlers for regular
// Web Apps, does not run when for apps installed in the background.
for (const auto& it : system_app_infos_) {
const SystemAppType& type = it.first;
base::Optional<AppId> app_id = GetAppIdForSystemApp(type);
if (!app_id)
continue;
if (AppHasFileHandlingOriginTrial(type)) {
file_handler_manager_->ForceEnableFileHandlingOriginTrial(app_id.value());
} else {
file_handler_manager_->DisableForceEnabledFileHandlingOriginTrial(
app_id.value());
}
}
const base::TimeDelta install_duration =
install_start_time - base::TimeTicks::Now();
if (IsEnabled()) {
// TODO(qjw): Figure out where install_results come from, decide if
// installation failures need to be handled
pref_service_->SetString(prefs::kSystemWebAppLastUpdateVersion,
CurrentVersion().GetString());
pref_service_->SetString(prefs::kSystemWebAppLastInstalledLocale,
CurrentLocale());
}
RecordSystemWebAppInstallMetrics(install_results, install_duration);
// Build the map from installed app id to app type.
for (const auto& it : system_app_infos_) {
const SystemAppType& app_type = it.first;
base::Optional<AppId> app_id =
registrar_->LookupExternalAppId(it.second.install_url);
if (app_id.has_value())
app_id_to_app_type_[app_id.value()] = app_type;
}
// May be called more than once in tests.
if (!on_apps_synchronized_->is_signaled()) {
on_apps_synchronized_->Signal();
OnAppsPolicyChanged();
}
}
bool SystemWebAppManager::NeedsUpdate() const {
if (update_policy_ == UpdatePolicy::kAlwaysUpdate)
return true;
base::Version last_update_version(
pref_service_->GetString(prefs::kSystemWebAppLastUpdateVersion));
const std::string& last_installed_locale(
pref_service_->GetString(prefs::kSystemWebAppLastInstalledLocale));
// If Chrome version rolls back for some reason, ensure System Web Apps are
// always in sync with Chrome version.
bool versionIsDifferent =
!last_update_version.IsValid() || last_update_version != CurrentVersion();
// If system language changes, ensure System Web Apps launcher localization
// are in sync with current language.
bool localeIsDifferent = last_installed_locale != CurrentLocale();
return versionIsDifferent || localeIsDifferent;
}
void SystemWebAppManager::OnAppsPolicyChanged() {
#if defined(OS_CHROMEOS)
if (!on_apps_synchronized_->is_signaled())
return;
auto disabled_system_apps = GetDisabledSystemWebApps();
for (const auto& id_and_type : app_id_to_app_type_) {
const bool is_disabled =
base::Contains(disabled_system_apps, id_and_type.second);
registry_controller_->SetAppIsDisabled(id_and_type.first, is_disabled);
}
#endif // defined(OS_CHROMEOS)
}
} // namespace web_app