blob: 6fd7dac24a1040bd8e31fccf97d9a3c5cf0f3c76 [file] [log] [blame]
// Copyright 2021 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/apps/app_service/webapk/webapk_install_task.h"
#include <algorithm>
#include <utility>
#include "base/bind.h"
#include "base/command_line.h"
#include "base/location.h"
#include "base/logging.h"
#include "base/metrics/histogram_functions.h"
#include "base/strings/strcat.h"
#include "base/strings/string_number_conversions.h"
#include "base/task/task_traits.h"
#include "base/task/thread_pool.h"
#include "base/threading/thread_restrictions.h"
#include "chrome/browser/apps/app_service/webapk/webapk_prefs.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/web_applications/web_app_icon_manager.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_application_info.h"
#include "chrome/common/chrome_switches.h"
#include "components/arc/arc_service_manager.h"
#include "components/arc/mojom/webapk.mojom.h"
#include "components/arc/session/arc_bridge_service.h"
#include "components/services/app_service/public/cpp/share_target.h"
#include "components/version_info/version_info.h"
#include "components/webapk/webapk.pb.h"
#include "content/public/browser/browser_context.h"
#include "content/public/browser/storage_partition.h"
#include "google_apis/google_api_keys.h"
#include "net/base/load_flags.h"
#include "net/http/http_status_code.h"
#include "net/traffic_annotation/network_traffic_annotation.h"
#include "services/network/public/cpp/resource_request.h"
#include "services/network/public/cpp/simple_url_loader.h"
#include "services/network/public/mojom/fetch_api.mojom-shared.h"
#include "services/network/public/mojom/url_response_head.mojom.h"
#include "third_party/smhasher/src/MurmurHash2.h"
#include "url/gurl.h"
namespace {
// The MIME type of the POST data sent to the server.
constexpr char kProtoMimeType[] = "application/x-protobuf";
constexpr char kRequesterPackageName[] = "org.chromium.arc.webapk";
// Android property containing the list of supported ABIs.
constexpr char kAbiListPropertyName[] = "ro.product.cpu.abilist";
const char kMinimumIconSize = 64;
// The seed to use when taking the murmur2 hash of the icon.
const uint64_t kMurmur2HashSeed = 0;
// Time to wait for a response from the Web APK minter.
constexpr base::TimeDelta kMinterResponseTimeout = base::Seconds(60);
constexpr char kWebApkServerUrl[] =
"https://webapk.googleapis.com/v1/webApks?key=";
constexpr net::NetworkTrafficAnnotationTag kWebApksTrafficAnnotation =
net::DefineNetworkTrafficAnnotation("webapk_minter_install_request",
R"(
semantics {
sender: "WebAPKs"
description:
"Chrome OS generates small Android apps called 'WebAPKs' which "
"represent the Progressive Web Apps installed in Chrome OS. These "
"apps are installed in the ARC Android environment and used to "
"improve integration between ARC and Chrome OS. This network "
"request sends the details for a single web app to the WebAPK "
"service, which returns details about the WebAPK to install."
trigger: "Installing or updating a progressive web app."
data:
"The contents of the web app manifest for the web app, plus system "
"information needed to generate the app."
destination: GOOGLE_OWNED_SERVICE
}
policy {
cookies_allowed: NO
cookies_store: "N/A"
setting: "No setting apart from disabling ARC"
chrome_policy: {
ArcAppToWebAppSharingEnabled: {
ArcAppToWebAppSharingEnabled: true
}
}
}
)");
GURL GetServerUrl() {
std::string server_url =
base::CommandLine::ForCurrentProcess()->GetSwitchValueASCII(
switches::kWebApkServerUrl);
if (server_url.empty()) {
server_url = base::StrCat({kWebApkServerUrl, google_apis::GetAPIKey()});
}
return GURL(server_url);
}
bool DoesShareTargetDiffer(webapk::WebAppManifest manifest,
arc::mojom::WebShareTargetInfoPtr share_info) {
if (!share_info) {
// There's no |share_info| in the current WebAPK, which means that a share
// target was added.
return true;
}
// There is only one share target added.
auto share_target = manifest.share_targets(0);
DCHECK_EQ(manifest.share_targets_size(), 1);
if (share_target.action() != share_info->action.value_or("") ||
share_target.method() != share_info->method.value_or("") ||
share_target.enctype() != share_info->enctype.value_or("")) {
return true;
}
auto share_param = share_target.params();
if (share_param.title() != share_info->param_title.value_or("") ||
share_param.text() != share_info->param_text.value_or("") ||
share_param.url() != share_info->param_url.value_or("")) {
return true;
}
// Compare share files.
if (share_param.files_size() != share_info->file_names.size()) {
return true;
}
for (int i = 0; i < share_param.files_size(); i++) {
if (share_param.files(i).name() != share_info->file_names[i]) {
return true;
}
if (share_param.files(i).accept_size() !=
share_info->file_accepts[i].size()) {
return true;
}
for (int j = 0; j < share_param.files(i).accept_size(); j++) {
if (share_param.files(i).accept(j) != share_info->file_accepts[i][j]) {
return true;
}
}
}
return false;
}
void AddUpdateParams(webapk::WebApk* webapk,
arc::mojom::WebApkInfoPtr web_apk_info) {
webapk->set_version(web_apk_info->apk_version);
// The |manifest_url| is used as a key on Android, so the |manifest_url| sent
// to the server to query a particular app should always be the same.
webapk->set_manifest_url(web_apk_info->manifest_url);
auto manifest = webapk->manifest();
if (manifest.short_name() != web_apk_info->name) {
webapk->add_update_reasons(webapk::WebApk::SHORT_NAME_DIFFERS);
}
if (manifest.start_url() != web_apk_info->start_url) {
webapk->add_update_reasons(webapk::WebApk::START_URL_DIFFERS);
}
// There is only one scope added to the scopes list.
DCHECK_EQ(manifest.scopes_size(), 1);
if (manifest.scopes(0) != web_apk_info->scope) {
webapk->add_update_reasons(webapk::WebApk::SCOPE_DIFFERS);
}
// There is only one icon added to the icon list.
DCHECK_EQ(manifest.icons_size(), 1);
if (manifest.icons(0).hash() != web_apk_info->icon_hash) {
webapk->add_update_reasons(webapk::WebApk::PRIMARY_ICON_HASH_DIFFERS);
}
// Check differences in share target
if (DoesShareTargetDiffer(manifest, std::move(web_apk_info->share_info))) {
webapk->add_update_reasons(webapk::WebApk::WEB_SHARE_TARGET_DIFFERS);
}
}
// Attaches icon PNG data and hash to an existing icon entry, and then
// serializes and returns the entire proto. Should be called on a worker thread.
absl::optional<std::string> AddIconDataAndSerializeProto(
std::unique_ptr<webapk::WebApk> webapk,
std::vector<uint8_t> icon_data,
arc::mojom::WebApkInfoPtr web_apk_info) {
base::AssertLongCPUWorkAllowed();
DCHECK_EQ(webapk->manifest().icons_size(), 1);
webapk::Image* icon = webapk->mutable_manifest()->mutable_icons(0);
icon->set_image_data(icon_data.data(), icon_data.size());
uint64_t icon_hash =
MurmurHash64A(icon_data.data(), icon_data.size(), kMurmur2HashSeed);
icon->set_hash(base::NumberToString(icon_hash));
if (web_apk_info) {
AddUpdateParams(webapk.get(), std::move(web_apk_info));
// If we don't have an update reason here, return before we query the server
// as there is no reason to update.
if (webapk->update_reasons_size() == 0) {
return absl::nullopt;
}
}
std::string serialized_proto;
webapk->SerializeToString(&serialized_proto);
return serialized_proto;
}
std::string GetArcAbi(const arc::ArcFeatures& arc_features) {
const std::string& property =
arc_features.build_props.at(kAbiListPropertyName);
size_t separator_pos = property.find(',');
if (separator_pos != std::string::npos) {
return property.substr(0, separator_pos);
}
return property;
}
} // namespace
namespace apps {
// Installs or updates a WebAPK.
WebApkInstallTask::WebApkInstallTask(Profile* profile,
const std::string& app_id)
: profile_(profile),
web_app_provider_(web_app::WebAppProvider::GetDeprecated(profile_)),
app_id_(app_id),
package_name_to_update_(
webapk_prefs::GetWebApkPackageName(profile_, app_id_)),
minter_timeout_(kMinterResponseTimeout) {
DCHECK(web_app_provider_);
}
WebApkInstallTask::~WebApkInstallTask() = default;
void WebApkInstallTask::Start(ResultCallback callback) {
VLOG(1) << "Generating WebAPK for app: " << app_id_;
result_callback_ = std::move(callback);
auto& registrar = web_app_provider_->registrar();
// This is already checked in WebApkManager, check again in case anything
// changed while the install request was queued.
if (!registrar.IsInstalled(app_id_) ||
!registrar.GetAppShareTarget(app_id_)) {
DeliverResult(WebApkInstallStatus::kAppInvalid);
return;
}
std::unique_ptr<webapk::WebApk> webapk = std::make_unique<webapk::WebApk>();
webapk->set_manifest_url(registrar.GetAppManifestUrl(app_id_).spec());
webapk->set_requester_application_package(kRequesterPackageName);
webapk->set_requester_application_version(version_info::GetVersionNumber());
LoadWebApkInfo(std::move(webapk));
}
void WebApkInstallTask::LoadWebApkInfo(std::unique_ptr<webapk::WebApk> webapk) {
if (!package_name_to_update_.has_value()) {
// This is a new install, continue with the installation process.
webapk->add_update_reasons(webapk::WebApk::NONE);
arc::ArcFeaturesParser::GetArcFeatures(
base::BindOnce(&WebApkInstallTask::OnArcFeaturesLoaded,
weak_ptr_factory_.GetWeakPtr(), std::move(webapk)));
return;
}
// If a package_name exists in webapk_prefs, this WebAPK is already installed,
// so we need to perform an update.
webapk->set_package_name(package_name_to_update_.value());
// Fetch details of the existing WebAPK from ARC++.
auto* arc_service_manager = arc::ArcServiceManager::Get();
DCHECK(arc_service_manager);
auto* instance = ARC_GET_INSTANCE_FOR_METHOD(
arc_service_manager->arc_bridge_service()->webapk(), GetWebApkInfo);
if (!instance) {
LOG(ERROR) << "WebApkInstance is not ready";
DeliverResult(WebApkInstallStatus::kArcUnavailable);
return;
}
instance->GetWebApkInfo(
package_name_to_update_.value(),
base::BindOnce(&WebApkInstallTask::OnWebApkInfoLoaded,
weak_ptr_factory_.GetWeakPtr(), std::move(webapk)));
}
void WebApkInstallTask::OnWebApkInfoLoaded(
std::unique_ptr<webapk::WebApk> webapk,
arc::mojom::WebApkInfoPtr result) {
if (!result) {
LOG(ERROR) << "Could not load WebApkInfo";
DeliverResult(WebApkInstallStatus::kUpdateGetWebApkInfoError);
return;
}
web_apk_info_ = std::move(result);
arc::ArcFeaturesParser::GetArcFeatures(
base::BindOnce(&WebApkInstallTask::OnArcFeaturesLoaded,
weak_ptr_factory_.GetWeakPtr(), std::move(webapk)));
}
void WebApkInstallTask::OnArcFeaturesLoaded(
std::unique_ptr<webapk::WebApk> webapk,
absl::optional<arc::ArcFeatures> arc_features) {
if (!arc_features) {
LOG(ERROR) << "Could not load ArcFeatures";
DeliverResult(WebApkInstallStatus::kArcUnavailable);
return;
}
webapk->set_android_abi(GetArcAbi(arc_features.value()));
auto& icon_manager = web_app_provider_->icon_manager();
absl::optional<web_app::WebAppIconManager::IconSizeAndPurpose>
icon_size_and_purpose = icon_manager.FindIconMatchBigger(
app_id_, {IconPurpose::MASKABLE, IconPurpose::ANY}, kMinimumIconSize);
if (!icon_size_and_purpose) {
LOG(ERROR) << "Could not find suitable icon";
DeliverResult(WebApkInstallStatus::kAppInvalid);
return;
}
// We need to send a URL for the icon, but it's possible the local image we're
// sending has been resized and so doesn't exactly match any of the images in
// the manifest. Since we can't be perfect, it's okay to be roughly correct
// and just send any URL of the correct purpose.
auto& registrar = web_app_provider_->registrar();
const auto& manifest_icons = registrar.GetAppIconInfos(app_id_);
auto it = std::find_if(
manifest_icons.begin(), manifest_icons.end(),
[&icon_size_and_purpose](const apps::IconInfo& info) {
return info.purpose ==
ManifestPurposeToIconInfoPurpose(icon_size_and_purpose->purpose);
});
if (it == manifest_icons.end()) {
LOG(ERROR) << "Could not find URL for icon";
DeliverResult(WebApkInstallStatus::kAppInvalid);
return;
}
std::string icon_url = it->url.spec();
webapk::WebAppManifest* web_app_manifest = webapk->mutable_manifest();
web_app_manifest->set_short_name(registrar.GetAppShortName(app_id_));
web_app_manifest->set_start_url(registrar.GetAppStartUrl(app_id_).spec());
web_app_manifest->add_scopes(registrar.GetAppScope(app_id_).spec());
auto* share_target = registrar.GetAppShareTarget(app_id_);
webapk::ShareTarget* proto_share_target =
web_app_manifest->add_share_targets();
proto_share_target->set_action(share_target->action.spec());
proto_share_target->set_method(
apps::ShareTarget::MethodToString(share_target->method));
proto_share_target->set_enctype(
apps::ShareTarget::EnctypeToString(share_target->enctype));
webapk::ShareTargetParams* proto_params =
proto_share_target->mutable_params();
if (!share_target->params.title.empty()) {
proto_params->set_title(share_target->params.title);
}
if (!share_target->params.text.empty()) {
proto_params->set_text(share_target->params.text);
}
if (!share_target->params.url.empty()) {
proto_params->set_url(share_target->params.url);
}
for (const auto& file : share_target->params.files) {
webapk::ShareTargetParamsFile* proto_file = proto_params->add_files();
proto_file->set_name(file.name);
for (const auto& accept_type : file.accept) {
proto_file->add_accept(accept_type);
}
}
webapk::Image* image = web_app_manifest->add_icons();
image->set_src(std::move(icon_url));
image->add_purposes(icon_size_and_purpose->purpose == IconPurpose::MASKABLE
? webapk::Image::MASKABLE
: webapk::Image::ANY);
image->add_usages(webapk::Image::PRIMARY_ICON);
icon_manager.ReadSmallestCompressedIcon(
app_id_, {icon_size_and_purpose->purpose}, icon_size_and_purpose->size_px,
base::BindOnce(&WebApkInstallTask::OnLoadedIcon,
weak_ptr_factory_.GetWeakPtr(), std::move(webapk)));
}
void WebApkInstallTask::OnLoadedIcon(std::unique_ptr<webapk::WebApk> webapk,
IconPurpose purpose,
std::vector<uint8_t> data) {
base::ThreadPool::PostTaskAndReplyWithResult(
FROM_HERE, {base::TaskPriority::BEST_EFFORT},
base::BindOnce(AddIconDataAndSerializeProto, std::move(webapk),
std::move(data), std::move(web_apk_info_)),
base::BindOnce(&WebApkInstallTask::OnProtoSerialized,
weak_ptr_factory_.GetWeakPtr()));
}
void WebApkInstallTask::OnProtoSerialized(
absl::optional<std::string> serialized_proto) {
if (!serialized_proto && !serialized_proto.has_value()) {
// We don't need to continue the update, because the existing WebAPK is up
// to date.
webapk_prefs::SetUpdateNeededForApp(profile_, app_id_,
/* update_needed= */ false);
DeliverResult(WebApkInstallStatus::kUpdateCancelledWebApkUpToDate);
return;
}
GURL server_url = GetServerUrl();
timer_.Start(FROM_HERE, minter_timeout_,
base::BindOnce(&WebApkInstallTask::DeliverResult,
weak_ptr_factory_.GetWeakPtr(),
WebApkInstallStatus::kNetworkTimeout));
auto request = std::make_unique<network::ResourceRequest>();
request->url = server_url;
request->method = "POST";
request->load_flags = net::LOAD_DISABLE_CACHE;
request->credentials_mode = network::mojom::CredentialsMode::kOmit;
auto* url_loader_factory = profile_->GetDefaultStoragePartition()
->GetURLLoaderFactoryForBrowserProcess()
.get();
url_loader_ = network::SimpleURLLoader::Create(std::move(request),
kWebApksTrafficAnnotation);
url_loader_->AttachStringForUpload(std::move(serialized_proto.value()),
kProtoMimeType);
url_loader_->DownloadToStringOfUnboundedSizeUntilCrashAndDie(
url_loader_factory,
base::BindOnce(&WebApkInstallTask::OnUrlLoaderComplete,
weak_ptr_factory_.GetWeakPtr()));
}
void WebApkInstallTask::OnUrlLoaderComplete(
std::unique_ptr<std::string> response_body) {
timer_.Stop();
int response_or_error_code = -1;
if (url_loader_->ResponseInfo() && url_loader_->ResponseInfo()->headers) {
response_or_error_code =
url_loader_->ResponseInfo()->headers->response_code();
} else {
response_or_error_code = url_loader_->NetError();
}
base::UmaHistogramSparse(kWebApkMinterErrorCodeHistogram,
response_or_error_code);
if (!response_body || response_or_error_code != net::HTTP_OK) {
LOG(WARNING) << "WebAPK server request returned error "
<< response_or_error_code;
DeliverResult(WebApkInstallStatus::kNetworkError);
return;
}
auto response = std::make_unique<webapk::WebApkResponse>();
if (!response->ParseFromString(*response_body)) {
LOG(WARNING) << "Failed to parse WebApkResponse proto";
DeliverResult(WebApkInstallStatus::kNetworkError);
return;
}
VLOG(1) << "Installing WebAPK: " << response->package_name();
auto* arc_service_manager = arc::ArcServiceManager::Get();
DCHECK(arc_service_manager);
auto* instance = ARC_GET_INSTANCE_FOR_METHOD(
arc_service_manager->arc_bridge_service()->webapk(), InstallWebApk);
if (!instance) {
LOG(ERROR) << "WebApkInstance is not ready";
DeliverResult(WebApkInstallStatus::kArcUnavailable);
return;
}
auto& registrar = web_app_provider_->registrar();
int webapk_version;
base::StringToInt(response->version(), &webapk_version);
instance->InstallWebApk(
response->package_name(), webapk_version,
registrar.GetAppShortName(app_id_), response->token(),
base::BindOnce(&WebApkInstallTask::OnInstallComplete,
weak_ptr_factory_.GetWeakPtr(), response->package_name()));
}
void WebApkInstallTask::OnInstallComplete(
const std::string& package_name,
arc::mojom::WebApkInstallResult result) {
VLOG(1) << "WebAPK installation finished with result " << result;
const bool success = result == arc::mojom::WebApkInstallResult::kSuccess;
const bool is_update = package_name_to_update_.has_value();
RecordWebApkArcResult(is_update, result);
if (success) {
if (is_update) {
webapk_prefs::SetUpdateNeededForApp(profile_, app_id_,
/* update_needed= */ false);
} else {
webapk_prefs::AddWebApk(profile_, app_id_, package_name);
}
}
DeliverResult(success ? WebApkInstallStatus::kSuccess
: WebApkInstallStatus::kGooglePlayError);
}
void WebApkInstallTask::DeliverResult(WebApkInstallStatus result) {
// Invalidate weak pointers so that in-flight tasks cannot attempt to deliver
// a second result.
weak_ptr_factory_.InvalidateWeakPtrs();
RecordWebApkInstallResult(package_name_to_update_.has_value(), result);
DCHECK(result_callback_);
std::move(result_callback_).Run(result == WebApkInstallStatus::kSuccess);
}
} // namespace apps