// Copyright 2019 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/ash/android_sms/android_sms_app_setup_controller_impl.h"
#include <optional>
#include <string>
#include <vector>
#include "base/containers/flat_map.h"
#include "base/feature_list.h"
#include "base/functional/bind.h"
#include "base/functional/callback.h"
#include "base/metrics/histogram_macros.h"
#include "base/task/single_thread_task_runner.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/web_applications/external_install_options.h"
#include "chrome/browser/web_applications/mojom/user_display_mode.mojom.h"
#include "chrome/browser/web_applications/web_app_command_scheduler.h"
#include "chrome/browser/web_applications/web_app_helpers.h"
#include "chrome/browser/web_applications/web_app_install_finalizer.h"
#include "chrome/browser/web_applications/web_app_provider.h"
#include "chromeos/ash/components/multidevice/logging/logging.h"
#include "components/content_settings/core/browser/host_content_settings_map.h"
#include "components/webapps/browser/install_result_code.h"
#include "components/webapps/browser/installable/installable_metrics.h"
#include "components/webapps/browser/uninstall_result_code.h"
#include "content/public/browser/browser_context.h"
#include "content/public/browser/storage_partition.h"
#include "net/base/url_util.h"
#include "net/cookies/cookie_util.h"
#include "services/network/public/mojom/cookie_manager.mojom.h"
#include "third_party/blink/public/mojom/manifest/display_mode.mojom.h"
#include "url/gurl.h"
namespace ash {
namespace android_sms {
namespace {
const char kDefaultToPersistCookieName[] = "default_to_persist";
const char kMigrationCookieName[] = "cros_migrated_to";
const char kDefaultToPersistCookieValue[] = "true";
} // namespace
// static
const base::TimeDelta AndroidSmsAppSetupControllerImpl::kInstallRetryDelay =
base::Seconds(5);
const size_t AndroidSmsAppSetupControllerImpl::kMaxInstallRetryCount = 7u;
AndroidSmsAppSetupControllerImpl::PwaDelegate::PwaDelegate() = default;
AndroidSmsAppSetupControllerImpl::PwaDelegate::~PwaDelegate() = default;
std::optional<webapps::AppId>
AndroidSmsAppSetupControllerImpl::PwaDelegate::GetPwaForUrl(
const GURL& install_url,
Profile* profile) {
return web_app::FindInstalledAppWithUrlInScope(profile, install_url);
}
network::mojom::CookieManager*
AndroidSmsAppSetupControllerImpl::PwaDelegate::GetCookieManager(
Profile* profile) {
return profile->GetDefaultStoragePartition()
->GetCookieManagerForBrowserProcess();
}
void AndroidSmsAppSetupControllerImpl::PwaDelegate::RemovePwa(
const webapps::AppId& app_id,
Profile* profile,
SuccessCallback callback) {
// |provider| will be nullptr if Lacros web apps are enabled.
auto* provider = web_app::WebAppProvider::GetForWebApps(profile);
if (!provider) {
std::move(callback).Run(false);
return;
}
provider->scheduler().RemoveInstallManagementMaybeUninstall(
app_id, web_app::WebAppManagement::kDefault,
webapps::WebappUninstallSource::kInternalPreinstalled,
base::BindOnce(
[](SuccessCallback callback, webapps::UninstallResultCode code) {
std::move(callback).Run(UninstallSucceeded(code));
},
std::move(callback)));
}
AndroidSmsAppSetupControllerImpl::AndroidSmsAppSetupControllerImpl(
Profile* profile,
web_app::ExternallyManagedAppManager* externally_managed_app_manager,
HostContentSettingsMap* host_content_settings_map)
: profile_(profile),
externally_managed_app_manager_(externally_managed_app_manager),
host_content_settings_map_(host_content_settings_map),
pwa_delegate_(std::make_unique<PwaDelegate>()) {}
AndroidSmsAppSetupControllerImpl::~AndroidSmsAppSetupControllerImpl() = default;
void AndroidSmsAppSetupControllerImpl::SetUpApp(const GURL& app_url,
const GURL& install_url,
SuccessCallback callback) {
PA_LOG(VERBOSE) << "AndroidSmsAppSetupControllerImpl::SetUpApp(): Setting "
<< "DefaultToPersist cookie at " << app_url << " before PWA "
<< "installation.";
net::CookieOptions options;
options.set_same_site_cookie_context(
net::CookieOptions::SameSiteCookieContext::MakeInclusive());
net::CanonicalCookie cookie = *net::CanonicalCookie::CreateSanitizedCookie(
app_url, kDefaultToPersistCookieName, kDefaultToPersistCookieValue,
std::string() /* domain */, std::string() /* path */,
base::Time::Now() /* creation_time */, base::Time() /* expiration_time */,
base::Time::Now() /* last_access_time */,
!net::IsLocalhost(app_url) /* secure */, false /* http_only */,
net::CookieSameSite::STRICT_MODE, net::COOKIE_PRIORITY_DEFAULT,
std::nullopt /* partition_key */, /*status=*/nullptr);
// TODO(crbug.com/1069974): The cookie source url must be faked here because
// otherwise, this would fail to set a secure cookie if |app_url| is insecure.
// Consider instead to use url::Replacements to force the scheme to be https.
pwa_delegate_->GetCookieManager(profile_)->SetCanonicalCookie(
cookie, net::cookie_util::SimulatedCookieSource(cookie, "https"), options,
base::BindOnce(&AndroidSmsAppSetupControllerImpl::
OnSetRememberDeviceByDefaultCookieResult,
weak_ptr_factory_.GetWeakPtr(), app_url, install_url,
std::move(callback)));
}
std::optional<webapps::AppId> AndroidSmsAppSetupControllerImpl::GetPwa(
const GURL& install_url) {
return pwa_delegate_->GetPwaForUrl(install_url, profile_);
}
void AndroidSmsAppSetupControllerImpl::DeleteRememberDeviceByDefaultCookie(
const GURL& app_url,
SuccessCallback callback) {
PA_LOG(INFO) << "AndroidSmsAppSetupControllerImpl::"
<< "DeleteRememberDeviceByDefaultCookie(): Deleting "
<< "DefaultToPersist cookie at " << app_url << ".";
network::mojom::CookieDeletionFilterPtr filter(
network::mojom::CookieDeletionFilter::New());
filter->url = app_url;
filter->cookie_name = kDefaultToPersistCookieName;
pwa_delegate_->GetCookieManager(profile_)->DeleteCookies(
std::move(filter),
base::BindOnce(&AndroidSmsAppSetupControllerImpl::
OnDeleteRememberDeviceByDefaultCookieResult,
weak_ptr_factory_.GetWeakPtr(), app_url,
std::move(callback)));
}
void AndroidSmsAppSetupControllerImpl::RemoveApp(
const GURL& app_url,
const GURL& install_url,
const GURL& migrated_to_app_url,
SuccessCallback callback) {
std::optional<webapps::AppId> app_id =
pwa_delegate_->GetPwaForUrl(install_url, profile_);
// If there is no app installed at |url|, there is nothing more to do.
if (!app_id) {
PA_LOG(VERBOSE) << "AndroidSmsAppSetupControllerImpl::RemoveApp(): No app "
<< "is installed at " << install_url
<< "; skipping removal process.";
std::move(callback).Run(true /* success */);
return;
}
PA_LOG(INFO) << "AndroidSmsAppSetupControllerImpl::RemoveApp(): "
<< "Uninstalling app at " << install_url << ".";
pwa_delegate_->RemovePwa(
*app_id, profile_,
base::BindOnce(&AndroidSmsAppSetupControllerImpl::OnAppRemoved,
weak_ptr_factory_.GetWeakPtr(), std::move(callback),
app_url, install_url, migrated_to_app_url));
}
void AndroidSmsAppSetupControllerImpl::OnAppRemoved(
SuccessCallback callback,
const GURL& app_url,
const GURL& install_url,
const GURL& migrated_to_app_url,
bool uninstalled) {
UMA_HISTOGRAM_BOOLEAN("AndroidSms.PWAUninstallationResult", uninstalled);
if (!uninstalled) {
PA_LOG(ERROR) << "AndroidSmsAppSetupControllerImpl::RemoveApp(): "
<< "PWA for " << install_url << " failed to uninstall.";
std::move(callback).Run(false /* success */);
return;
}
SetMigrationCookie(app_url, migrated_to_app_url, std::move(callback));
}
void AndroidSmsAppSetupControllerImpl::OnSetRememberDeviceByDefaultCookieResult(
const GURL& app_url,
const GURL& install_url,
SuccessCallback callback,
net::CookieAccessResult result) {
if (!result.status.IsInclude()) {
PA_LOG(WARNING)
<< "AndroidSmsAppSetupControllerImpl::"
<< "OnSetRememberDeviceByDefaultCookieResult(): Failed to set "
<< "DefaultToPersist cookie at " << app_url << ". Proceeding "
<< "to remove migration cookie.";
}
// Delete migration cookie in case it was set by a previous RemoveApp call.
network::mojom::CookieDeletionFilterPtr filter(
network::mojom::CookieDeletionFilter::New());
filter->url = app_url;
filter->cookie_name = kMigrationCookieName;
pwa_delegate_->GetCookieManager(profile_)->DeleteCookies(
std::move(filter),
base::BindOnce(
&AndroidSmsAppSetupControllerImpl::OnDeleteMigrationCookieResult,
weak_ptr_factory_.GetWeakPtr(), app_url, install_url,
std::move(callback)));
}
void AndroidSmsAppSetupControllerImpl::OnDeleteMigrationCookieResult(
const GURL& app_url,
const GURL& install_url,
SuccessCallback callback,
uint32_t num_deleted) {
// If the app is already installed at |url|, there is nothing more to do.
if (pwa_delegate_->GetPwaForUrl(install_url, profile_)) {
PA_LOG(VERBOSE) << "AndroidSmsAppSetupControllerImpl::"
<< "OnDeleteMigrationCookieResult():"
<< "App is already installed at " << install_url
<< "; skipping setup process.";
std::move(callback).Run(true /* success */);
return;
}
TryInstallApp(install_url, app_url, 0 /* num_attempts_so_far */,
std::move(callback));
}
void AndroidSmsAppSetupControllerImpl::TryInstallApp(const GURL& install_url,
const GURL& app_url,
size_t num_attempts_so_far,
SuccessCallback callback) {
PA_LOG(VERBOSE) << "AndroidSmsAppSetupControllerImpl::TryInstallApp(): "
<< "Trying to install PWA for " << install_url
<< ". Num attempts so far # " << num_attempts_so_far;
web_app::ExternalInstallOptions options(
install_url, web_app::mojom::UserDisplayMode::kStandalone,
web_app::ExternalInstallSource::kInternalDefault);
options.override_previous_user_uninstall = true;
options.require_manifest = true;
externally_managed_app_manager_->Install(
std::move(options),
base::BindOnce(&AndroidSmsAppSetupControllerImpl::OnAppInstallResult,
weak_ptr_factory_.GetWeakPtr(), std::move(callback),
num_attempts_so_far, app_url));
}
void AndroidSmsAppSetupControllerImpl::OnAppInstallResult(
SuccessCallback callback,
size_t num_attempts_so_far,
const GURL& app_url,
const GURL& install_url,
web_app::ExternallyManagedAppManager::InstallResult result) {
UMA_HISTOGRAM_ENUMERATION("AndroidSms.PWAInstallationResult", result.code);
const bool install_succeeded = webapps::IsSuccess(result.code);
if (!install_succeeded && num_attempts_so_far < kMaxInstallRetryCount) {
base::TimeDelta retry_delay =
kInstallRetryDelay * (1 << num_attempts_so_far);
PA_LOG(VERBOSE)
<< "AndroidSmsAppSetupControllerImpl::OnAppInstallResult(): "
<< "PWA for " << install_url << " failed to install."
<< "InstallResultCode: " << static_cast<int>(result.code)
<< " Retrying again in " << retry_delay;
base::SingleThreadTaskRunner::GetCurrentDefault()->PostDelayedTask(
FROM_HERE,
base::BindOnce(&AndroidSmsAppSetupControllerImpl::TryInstallApp,
weak_ptr_factory_.GetWeakPtr(), install_url, app_url,
num_attempts_so_far + 1, std::move(callback)),
retry_delay);
return;
}
UMA_HISTOGRAM_BOOLEAN("AndroidSms.EffectivePWAInstallationSuccess",
install_succeeded);
if (!install_succeeded) {
PA_LOG(WARNING)
<< "AndroidSmsAppSetupControllerImpl::OnAppInstallResult(): "
<< "PWA for " << install_url << " failed to install. "
<< "InstallResultCode: " << static_cast<int>(result.code);
std::move(callback).Run(false /* success */);
return;
}
PA_LOG(INFO) << "AndroidSmsAppSetupControllerImpl::OnAppInstallResult(): "
<< "PWA for " << install_url << " was installed successfully.";
UMA_HISTOGRAM_EXACT_LINEAR("AndroidSms.NumAttemptsForSuccessfulInstallation",
num_attempts_so_far + 1, kMaxInstallRetryCount);
// Grant notification permission for the PWA.
host_content_settings_map_->SetContentSettingDefaultScope(
app_url, GURL() /* top_level_url */, ContentSettingsType::NOTIFICATIONS,
ContentSetting::CONTENT_SETTING_ALLOW);
std::move(callback).Run(true /* success */);
}
void AndroidSmsAppSetupControllerImpl::SetMigrationCookie(
const GURL& app_url,
const GURL& migrated_to_app_url,
SuccessCallback callback) {
// Set migration cookie on the client for which the PWA was just uninstalled.
// The client checks for this cookie to redirect users to the new domain. This
// prevents unwanted connection stealing between old and new clients should
// the user try to open old client.
net::CookieOptions options;
options.set_same_site_cookie_context(
net::CookieOptions::SameSiteCookieContext::MakeInclusive());
net::CanonicalCookie cookie = *net::CanonicalCookie::CreateSanitizedCookie(
app_url, kMigrationCookieName, migrated_to_app_url.GetContent(),
std::string() /* domain */, std::string() /* path */,
base::Time::Now() /* creation_time */, base::Time() /* expiration_time */,
base::Time::Now() /* last_access_time */,
!net::IsLocalhost(app_url) /* secure */, false /* http_only */,
net::CookieSameSite::STRICT_MODE, net::COOKIE_PRIORITY_DEFAULT,
std::nullopt /* partition_key */, /*status=*/nullptr);
// TODO(crbug.com/1069974): The cookie source url must be faked here because
// otherwise, this would fail to set a secure cookie if |app_url| is insecure.
// Consider instead to use url::Replacements to force the scheme to be https.
pwa_delegate_->GetCookieManager(profile_)->SetCanonicalCookie(
cookie, net::cookie_util::SimulatedCookieSource(cookie, "https"), options,
base::BindOnce(
&AndroidSmsAppSetupControllerImpl::OnSetMigrationCookieResult,
weak_ptr_factory_.GetWeakPtr(), app_url, std::move(callback)));
}
void AndroidSmsAppSetupControllerImpl::OnSetMigrationCookieResult(
const GURL& app_url,
SuccessCallback callback,
net::CookieAccessResult result) {
if (!result.status.IsInclude()) {
PA_LOG(ERROR)
<< "AndroidSmsAppSetupControllerImpl::OnSetMigrationCookieResult(): "
<< "Failed to set migration cookie for " << app_url << ". Proceeding "
<< "to remove DefaultToPersist cookie.";
}
DeleteRememberDeviceByDefaultCookie(app_url, std::move(callback));
}
void AndroidSmsAppSetupControllerImpl::
OnDeleteRememberDeviceByDefaultCookieResult(const GURL& app_url,
SuccessCallback callback,
uint32_t num_deleted) {
if (num_deleted != 1u) {
PA_LOG(WARNING) << "AndroidSmsAppSetupControllerImpl::"
<< "OnDeleteRememberDeviceByDefaultCookieResult(): "
<< "Tried to delete a single cookie at " << app_url
<< ", but " << num_deleted << " cookies were deleted.";
}
// Even if an unexpected number of cookies was deleted, consider this a
// success. If SetUpApp() failed to install a cookie earlier, the setup
// process is still considered a success, so failing to delete a cookie should
// also be considered a success.
std::move(callback).Run(true /* success */);
}
void AndroidSmsAppSetupControllerImpl::SetPwaDelegateForTesting(
std::unique_ptr<PwaDelegate> test_pwa_delegate) {
pwa_delegate_ = std::move(test_pwa_delegate);
}
} // namespace android_sms
} // namespace ash