blob: aefa699f4420025c425ea470d42af36001aaa813 [file] [log] [blame]
// Copyright 2020 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/web_app_mover.h"
#include "base/barrier_closure.h"
#include "base/callback_helpers.h"
#include "base/feature_list.h"
#include "base/metrics/field_trial_params.h"
#include "base/metrics/histogram_functions.h"
#include "base/no_destructor.h"
#include "base/strings/string_util.h"
#include "base/strings/utf_string_conversions.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/sync/sync_service_factory.h"
#include "chrome/browser/web_applications/web_app_install_finalizer.h"
#include "chrome/browser/web_applications/web_app_install_manager.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/keep_alive_registry/keep_alive_types.h"
#include "components/webapps/browser/installable/installable_metrics.h"
#include "content/public/browser/web_contents.h"
#include "third_party/re2/src/re2/re2.h"
namespace {
bool g_disabled_for_testing = false;
bool g_skip_wait_for_sync_for_testing = false;
base::OnceClosure& GetCompletedCallbackForTesting() {
static base::NoDestructor<base::OnceClosure> callback;
return *callback;
}
} // namespace
namespace web_app {
std::unique_ptr<WebAppMover> WebAppMover::CreateIfNeeded(
Profile* profile,
WebAppRegistrar* registrar,
WebAppInstallFinalizer* install_finalizer,
WebAppInstallManager* install_manager,
WebAppSyncBridge* sync_bridge) {
if (g_disabled_for_testing)
return nullptr;
if (!base::FeatureList::IsEnabled(features::kMoveWebApp))
return nullptr;
std::string uninstall_url_prefix =
features::kMoveWebAppUninstallStartUrlPrefix.Get();
std::string uninstall_pattern_str =
features::kMoveWebAppUninstallStartUrlPattern.Get();
std::string install_url_str = features::kMoveWebAppInstallStartUrl.Get();
// Only continue if exactly one of the uninstall settings is set, and the
// install url is set.
if (install_url_str.empty()) {
base::UmaHistogramEnumeration("WebApp.Mover.Result",
WebAppMoverResult::kInvalidConfiguration);
return nullptr;
}
GURL install_url = GURL(install_url_str);
// |install_url| has to be a valid URL.
if (!install_url.is_valid()) {
base::UmaHistogramEnumeration("WebApp.Mover.Result",
WebAppMoverResult::kInvalidConfiguration);
return nullptr;
}
WebAppMover::UninstallMode uninstall_mode;
std::string uninstall_prefix_or_pattern;
if (!uninstall_url_prefix.empty()) {
DCHECK(uninstall_pattern_str.empty());
// The installation URL cannot be contained in the uninstall prefix.
if (base::StartsWith(install_url.spec(), uninstall_url_prefix)) {
base::UmaHistogramEnumeration("WebApp.Mover.Result",
WebAppMoverResult::kInvalidConfiguration);
return nullptr;
}
uninstall_mode = WebAppMover::UninstallMode::kPrefix;
uninstall_prefix_or_pattern = uninstall_url_prefix;
} else if (!uninstall_pattern_str.empty()) {
re2::RE2 uninstall_pattern(uninstall_pattern_str);
// The pattern must be valid, and the install URL must not match the
// pattern.
if (uninstall_pattern.error_code() != re2::RE2::NoError ||
re2::RE2::FullMatch(install_url.spec(), uninstall_pattern)) {
base::UmaHistogramEnumeration("WebApp.Mover.Result",
WebAppMoverResult::kInvalidConfiguration);
return nullptr;
}
uninstall_mode = WebAppMover::UninstallMode::kPattern;
uninstall_prefix_or_pattern = uninstall_pattern_str;
} else {
base::UmaHistogramEnumeration("WebApp.Mover.Result",
WebAppMoverResult::kInvalidConfiguration);
return nullptr;
}
return std::make_unique<WebAppMover>(
profile, registrar, install_finalizer, install_manager, sync_bridge,
uninstall_mode, uninstall_prefix_or_pattern, install_url);
}
void WebAppMover::DisableForTesting() {
g_disabled_for_testing = true;
}
void WebAppMover::SkipWaitForSyncForTesting() {
g_skip_wait_for_sync_for_testing = true;
}
void WebAppMover::SetCompletedCallbackForTesting(base::OnceClosure callback) {
GetCompletedCallbackForTesting() = std::move(callback);
}
WebAppMover::WebAppMover(Profile* profile,
WebAppRegistrar* registrar,
WebAppInstallFinalizer* install_finalizer,
WebAppInstallManager* install_manager,
WebAppSyncBridge* sync_bridge,
UninstallMode uninstall_mode,
std::string uninstall_url_prefix_or_pattern,
const GURL& install_url)
: profile_(profile),
registrar_(registrar),
install_finalizer_(install_finalizer),
install_manager_(install_manager),
sync_bridge_(sync_bridge),
uninstall_mode_(uninstall_mode),
uninstall_url_prefix_or_pattern_(uninstall_url_prefix_or_pattern),
install_url_(install_url) {}
WebAppMover::~WebAppMover() = default;
void WebAppMover::Start() {
// We cannot grab the SyncService in the constructor without creating a
// circular KeyedService dependency.
sync_service_ = SyncServiceFactory::GetForProfile(profile_);
// This can be a nullptr if the --disable-sync switch is specified.
if (sync_service_)
sync_observer_.Observe(sync_service_);
// We must wait for sync to complete at least one cycle (if it is turned on).
// This avoids our local updates accidentally re-installing any web apps that
// were uninstalled on other devices. Installing the replacement app will send
// that record to sync servers, and if the user had uninstalled the 'source'
// app on another computer, we could miss that message and accidentally end up
// with the 'destination' app installed when it shouldn't have been installed
// in the first place (as the user uninstalled the 'source' app).
WaitForFirstSyncCycle(base::BindOnce(&WebAppMover::OnFirstSyncCycleComplete,
weak_ptr_factory_.GetWeakPtr()));
}
void WebAppMover::Shutdown() {
weak_ptr_factory_.InvalidateWeakPtrs();
sync_observer_.Reset();
migration_keep_alive_.reset();
}
void WebAppMover::OnSyncCycleCompleted(syncer::SyncService* sync_service) {
DCHECK_EQ(sync_service_, sync_service);
if (sync_ready_callback_)
std::move(sync_ready_callback_).Run();
// Only the first cycle cycle matters, as this triggers the WebAppMover logic,
// and |sync_ready_callback_| is never set again. Thus we can stop observing.
sync_observer_.Reset();
}
void WebAppMover::OnSyncShutdown(syncer::SyncService* sync_service) {
DCHECK_EQ(sync_service_, sync_service);
sync_observer_.Reset();
sync_service_ = nullptr;
}
void WebAppMover::WaitForFirstSyncCycle(base::OnceClosure callback) {
DCHECK(!sync_ready_callback_);
if (g_skip_wait_for_sync_for_testing || !sync_service_ ||
sync_service_->HasCompletedSyncCycle() ||
!sync_service_->IsSyncFeatureEnabled()) {
std::move(callback).Run();
return;
}
sync_ready_callback_ = std::move(callback);
}
void WebAppMover::OnFirstSyncCycleComplete() {
DCHECK(apps_to_uninstall_.empty());
base::ScopedClosureRunner complete_callback_runner;
if (GetCompletedCallbackForTesting()) {
complete_callback_runner.ReplaceClosure(
std::move(GetCompletedCallbackForTesting()));
}
for (const AppId& id : registrar_->GetAppIds()) {
// Stop if the destination app is already installed.
const GURL& start_url = registrar_->GetAppStartUrl(id);
if (start_url == install_url_) {
RecordResults(WebAppMoverResult::kInstallAppExists);
return;
}
// To avoid edge cases only consider installed apps to uninstall.
if (!registrar_->IsInstalled(id))
continue;
switch (uninstall_mode_) {
case UninstallMode::kPattern:
if (re2::RE2::FullMatch(start_url.spec(),
uninstall_url_prefix_or_pattern_)) {
apps_to_uninstall_.push_back(id);
new_app_open_as_window_ =
registrar_->GetAppUserDisplayMode(id) == DisplayMode::kStandalone;
}
break;
case UninstallMode::kPrefix:
if (base::StartsWith(start_url.spec(),
uninstall_url_prefix_or_pattern_)) {
apps_to_uninstall_.push_back(id);
new_app_open_as_window_ =
registrar_->GetAppUserDisplayMode(id) == DisplayMode::kStandalone;
}
}
}
if (apps_to_uninstall_.empty()) {
RecordResults(WebAppMoverResult::kNoAppsToUninstall);
return;
}
install_manager_->LoadWebAppAndCheckManifest(
install_url_, webapps::WebappInstallSource::OMNIBOX_INSTALL_ICON,
base::BindOnce(&WebAppMover::OnInstallManifestFetched,
weak_ptr_factory_.GetWeakPtr(),
std::move(complete_callback_runner)));
}
void WebAppMover::OnInstallManifestFetched(
base::ScopedClosureRunner complete_callback_runner,
std::unique_ptr<content::WebContents> web_contents,
InstallableCheckResult result,
absl::optional<AppId> app_id) {
switch (result) {
case InstallableCheckResult::kAlreadyInstalled:
LOG(WARNING) << "App already installed.";
return;
case InstallableCheckResult::kNotInstallable:
// If the app is not installable, then abort.
RecordResults(WebAppMoverResult::kNotInstallable);
return;
case InstallableCheckResult::kInstallable:
break;
}
DCHECK(!apps_to_uninstall_.empty());
migration_keep_alive_ = std::make_unique<ScopedKeepAlive>(
KeepAliveOrigin::APP_START_URL_MIGRATION,
KeepAliveRestartOption::DISABLED);
scoped_refptr<base::RefCountedData<bool>> success_accumulator =
base::MakeRefCounted<base::RefCountedData<bool>>(true);
auto barrier = base::BarrierClosure(
apps_to_uninstall_.size(),
base::BindOnce(&WebAppMover::OnAllUninstalled,
weak_ptr_factory_.GetWeakPtr(),
std::move(complete_callback_runner),
std::move(web_contents), success_accumulator));
for (const AppId& id : apps_to_uninstall_) {
install_finalizer_->UninstallWebApp(
id, webapps::WebappUninstallSource::kMigration,
base::BindOnce(
[](base::OnceClosure done,
scoped_refptr<base::RefCountedData<bool>> success_accumulator,
bool success) {
if (!success) {
LOG(WARNING)
<< "Uninstallation unsuccesful in app move operation.";
success_accumulator->data = false;
}
std::move(done).Run();
},
barrier, success_accumulator));
}
}
void WebAppMover::OnAllUninstalled(
base::ScopedClosureRunner complete_callback_runner,
std::unique_ptr<content::WebContents> web_contents_for_install,
scoped_refptr<base::RefCountedData<bool>> success_accumulator) {
if (!success_accumulator->data) {
RecordResults(WebAppMoverResult::kUninstallFailure);
return;
}
auto* web_contents = web_contents_for_install.get();
install_manager_->InstallWebAppFromManifest(
web_contents, true, webapps::WebappInstallSource::OMNIBOX_INSTALL_ICON,
base::BindOnce(
[](content::WebContents* initiator_web_contents,
std::unique_ptr<WebApplicationInfo> web_app_info,
ForInstallableSite for_installable_site,
WebAppInstallationAcceptanceCallback acceptance_callback) {
// Note: |open_as_window| is set to false here (which it should be
// by default), because if that is true the WebAppInstallTask will
// try to reparent the the web contents into an app browser. This is
// impossible, as this web contents is internal & not visible to the
// user (and we will segfault). Instead, set the user display mode
// after installation is complete.
DCHECK(web_app_info);
web_app_info->user_display_mode =
blink::mojom::DisplayMode::kBrowser;
std::move(acceptance_callback).Run(true, std::move(web_app_info));
}),
base::BindOnce(&WebAppMover::OnInstallCompleted,
weak_ptr_factory_.GetWeakPtr(),
std::move(complete_callback_runner),
std::move(web_contents_for_install)));
}
void WebAppMover::OnInstallCompleted(
base::ScopedClosureRunner complete_callback_runner,
std::unique_ptr<content::WebContents> web_contents_for_install,
const AppId& id,
InstallResultCode code) {
if (code == InstallResultCode::kSuccessNewInstall) {
if (new_app_open_as_window_)
sync_bridge_->SetAppUserDisplayMode(id, DisplayMode::kStandalone, false);
RecordResults(WebAppMoverResult::kSuccess);
} else {
LOG(WARNING) << "Installation in app move operation failed: " << code;
RecordResults(WebAppMoverResult::kInstallFailure);
}
migration_keep_alive_.reset();
}
void WebAppMover::RecordResults(WebAppMoverResult result) {
if (results_recorded_)
return;
results_recorded_ = true;
base::UmaHistogramEnumeration("WebApp.Mover.Result", result);
}
} // namespace web_app