| // Copyright 2021 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "components/webapps/browser/android/webapk/webapk_proto_builder.h" |
| |
| #include <string> |
| |
| #include "base/android/apk_info.h" |
| #include "base/feature_list.h" |
| #include "base/files/file_util.h" |
| #include "base/strings/utf_string_conversions.h" |
| #include "base/system/sys_info.h" |
| #include "base/task/thread_pool.h" |
| #include "base/threading/scoped_blocking_call.h" |
| #include "components/version_info/version_info.h" |
| #include "components/webapk/webapk.pb.h" |
| #include "components/webapps/browser/android/shortcut_info.h" |
| #include "components/webapps/browser/android/webapk/webapk_icons_hasher.h" |
| #include "components/webapps/browser/android/webapk/webapk_types.h" |
| #include "components/webapps/browser/android/webapp_icon.h" |
| #include "components/webapps/browser/features.h" |
| #include "third_party/blink/public/common/manifest/manifest_util.h" |
| #include "third_party/blink/public/mojom/manifest/manifest.mojom.h" |
| #include "ui/android/color_utils_android.h" |
| #include "ui/gfx/codec/png_codec.h" |
| |
| namespace webapps { |
| namespace { |
| |
| // Limit the icon size to 512KB. |
| constexpr size_t kMaxIconSizeInBytes = 512 * 1024; |
| |
| webapk::WebApk_UpdateReason ConvertUpdateReasonToProtoEnum( |
| WebApkUpdateReason update_reason) { |
| switch (update_reason) { |
| case WebApkUpdateReason::NONE: |
| return webapk::WebApk::NONE; |
| case WebApkUpdateReason::OLD_SHELL_APK: |
| return webapk::WebApk::OLD_SHELL_APK; |
| case WebApkUpdateReason::PRIMARY_ICON_HASH_DIFFERS: |
| return webapk::WebApk::PRIMARY_ICON_HASH_DIFFERS; |
| case WebApkUpdateReason::PRIMARY_ICON_MASKABLE_DIFFERS: |
| return webapk::WebApk::PRIMARY_ICON_MASKABLE_DIFFERS; |
| case WebApkUpdateReason::SPLASH_ICON_HASH_DIFFERS: |
| return webapk::WebApk::SPLASH_ICON_HASH_DIFFERS; |
| case WebApkUpdateReason::SCOPE_DIFFERS: |
| return webapk::WebApk::SCOPE_DIFFERS; |
| case WebApkUpdateReason::START_URL_DIFFERS: |
| return webapk::WebApk::START_URL_DIFFERS; |
| case WebApkUpdateReason::SHORT_NAME_DIFFERS: |
| return webapk::WebApk::SHORT_NAME_DIFFERS; |
| case WebApkUpdateReason::NAME_DIFFERS: |
| return webapk::WebApk::NAME_DIFFERS; |
| case WebApkUpdateReason::BACKGROUND_COLOR_DIFFERS: |
| return webapk::WebApk::BACKGROUND_COLOR_DIFFERS; |
| case WebApkUpdateReason::THEME_COLOR_DIFFERS: |
| return webapk::WebApk::THEME_COLOR_DIFFERS; |
| case WebApkUpdateReason::ORIENTATION_DIFFERS: |
| return webapk::WebApk::ORIENTATION_DIFFERS; |
| case WebApkUpdateReason::DISPLAY_MODE_DIFFERS: |
| return webapk::WebApk::DISPLAY_MODE_DIFFERS; |
| case WebApkUpdateReason::WEB_SHARE_TARGET_DIFFERS: |
| return webapk::WebApk::WEB_SHARE_TARGET_DIFFERS; |
| case WebApkUpdateReason::MANUALLY_TRIGGERED: |
| return webapk::WebApk::MANUALLY_TRIGGERED; |
| case WebApkUpdateReason::SHORTCUTS_DIFFER: |
| return webapk::WebApk::SHORTCUTS_DIFFER; |
| case WebApkUpdateReason::DARK_BACKGROUND_COLOR_DIFFERS: |
| return webapk::WebApk::DARK_BACKGROUND_COLOR_DIFFERS; |
| case WebApkUpdateReason::DARK_THEME_COLOR_DIFFERS: |
| return webapk::WebApk::DARK_THEME_COLOR_DIFFERS; |
| case WebApkUpdateReason::PRIMARY_ICON_CHANGE_BELOW_THRESHOLD: |
| return webapk::WebApk::PRIMARY_ICON_CHANGE_BELOW_THRESHOLD; |
| } |
| } |
| |
| // Get Chrome's current ABI. It depends on whether Chrome is running as a 32 bit |
| // app or 64 bit, and the device's cpu architecture as well. Note: please keep |
| // this function stay in sync with |chromium_android_linker::GetCpuAbi()|. |
| std::string getCurrentAbi() { |
| #if defined(__arm__) && defined(__ARM_ARCH_7A__) |
| return "armeabi-v7a"; |
| #elif defined(__arm__) |
| return "armeabi"; |
| #elif defined(__i386__) |
| return "x86"; |
| #elif defined(__mips__) |
| return "mips"; |
| #elif defined(__x86_64__) |
| return "x86_64"; |
| #elif defined(__aarch64__) |
| return "arm64-v8a"; |
| #elif defined(__riscv) && (__riscv_xlen == 64) |
| return "riscv64"; |
| #else |
| #error "Unsupported target abi" |
| #endif |
| } |
| |
| void AddWebApkIcon(webapk::WebAppManifest* web_app_manifest, |
| const WebappIcon* webapk_icon) { |
| CHECK(webapk_icon); |
| webapk::Image* icon_image = web_app_manifest->add_icons(); |
| if (webapk_icon->url().is_valid()) { |
| icon_image->set_src(webapk_icon->url().spec()); |
| } |
| if (!webapk_icon->hash().empty()) { |
| icon_image->set_hash(webapk_icon->hash()); |
| } |
| if (webapk_icon->has_unsafe_data()) { |
| icon_image->set_image_data(webapk_icon->unsafe_data()); |
| } |
| for (auto usage : webapk_icon->usages()) { |
| icon_image->add_usages(usage); |
| } |
| icon_image->add_purposes(webapk_icon->purpose()); |
| } |
| |
| void AddShortcutIcon(webapk::Image* icon_image, |
| const GURL& icon_url, |
| std::map<GURL, std::unique_ptr<WebappIcon>>& icons) { |
| icon_image->set_src(icon_url.spec()); |
| |
| auto it = icons.find(icon_url); |
| if (it != icons.end()) { |
| icon_image->set_hash(it->second->hash()); |
| if (it->second->unsafe_data().size() <= kMaxIconSizeInBytes) { |
| icon_image->set_image_data(it->second->ExtractData()); |
| } |
| for (auto usage : it->second->usages()) { |
| icon_image->add_usages(usage); |
| } |
| icon_image->add_purposes(it->second->purpose()); |
| } |
| } |
| |
| // Add other WebAPK images that are not used in primary, splash or shortcuts. |
| // We include only the url and hash for them. |
| // TODO(eirage): we had these icons in the request proto to compare with harpoon |
| // result, they are not needed now and should be removed. |
| void AddOtherWebApkImage(webapk::WebAppManifest* web_app_manifest, |
| const GURL& icon_url, |
| std::map<GURL, std::unique_ptr<WebappIcon>>& icons) { |
| webapk::Image* icon_image = web_app_manifest->add_icons(); |
| icon_image->set_src(icon_url.spec()); |
| auto it = icons.find(icon_url); |
| if (it != icons.end()) { |
| icon_image->set_hash(it->second->hash()); |
| } |
| } |
| |
| } // namespace |
| |
| std::unique_ptr<std::string> BuildProtoInBackground( |
| const ShortcutInfo& shortcut_info, |
| const GURL& app_key, |
| std::unique_ptr<webapps::WebappIcon> primary_icon, |
| std::unique_ptr<webapps::WebappIcon> splash_icon, |
| const std::string& package_name, |
| const std::string& version, |
| std::map<GURL, std::unique_ptr<WebappIcon>> icons, |
| bool is_manifest_stale, |
| bool is_app_identity_update_supported, |
| std::vector<WebApkUpdateReason> update_reasons) { |
| std::unique_ptr<webapk::WebApk> webapk(new webapk::WebApk); |
| webapk->set_manifest_url(shortcut_info.manifest_url.spec()); |
| webapk->set_requester_application_package( |
| base::android::apk_info::package_name()); |
| webapk->set_requester_application_version( |
| std::string(version_info::GetVersionNumber())); |
| webapk->set_android_abi(getCurrentAbi()); |
| webapk->set_package_name(package_name); |
| webapk->set_version(version); |
| webapk->set_stale_manifest(is_manifest_stale); |
| webapk->set_app_identity_update_supported(is_app_identity_update_supported); |
| webapk->set_android_version(base::SysInfo::OperatingSystemVersion()); |
| |
| for (auto update_reason : update_reasons) { |
| webapk->add_update_reasons(ConvertUpdateReasonToProtoEnum(update_reason)); |
| } |
| |
| webapk::WebAppManifest* web_app_manifest = webapk->mutable_manifest(); |
| web_app_manifest->set_has_custom_name(shortcut_info.has_custom_title); |
| if (shortcut_info.has_custom_title) { |
| web_app_manifest->set_name(base::UTF16ToUTF8(shortcut_info.user_title)); |
| web_app_manifest->set_short_name( |
| base::UTF16ToUTF8(shortcut_info.user_title)); |
| } else { |
| web_app_manifest->set_name(base::UTF16ToUTF8(shortcut_info.name)); |
| web_app_manifest->set_short_name( |
| base::UTF16ToUTF8(shortcut_info.short_name)); |
| } |
| web_app_manifest->set_start_url(shortcut_info.url.spec()); |
| web_app_manifest->set_orientation( |
| blink::WebScreenOrientationLockTypeToString(shortcut_info.orientation)); |
| web_app_manifest->set_display_mode( |
| blink::DisplayModeToString(shortcut_info.display)); |
| web_app_manifest->set_background_color( |
| ui::OptionalSkColorToString(shortcut_info.background_color)); |
| web_app_manifest->set_theme_color( |
| ui::OptionalSkColorToString(shortcut_info.theme_color)); |
| web_app_manifest->set_dark_background_color( |
| ui::OptionalSkColorToString(shortcut_info.dark_background_color)); |
| web_app_manifest->set_dark_theme_color( |
| ui::OptionalSkColorToString(shortcut_info.dark_theme_color)); |
| |
| web_app_manifest->set_id(shortcut_info.manifest_id.spec()); |
| webapk->set_app_key(app_key.spec()); |
| |
| std::string* scope = web_app_manifest->add_scopes(); |
| scope->assign(shortcut_info.scope.spec()); |
| |
| if (shortcut_info.share_target) { |
| webapk::ShareTarget* share_target = web_app_manifest->add_share_targets(); |
| share_target->set_action(shortcut_info.share_target->action.spec()); |
| if (shortcut_info.share_target->method == |
| blink::mojom::ManifestShareTarget_Method::kPost) { |
| share_target->set_method("POST"); |
| } else { |
| share_target->set_method("GET"); |
| } |
| if (shortcut_info.share_target->enctype == |
| blink::mojom::ManifestShareTarget_Enctype::kMultipartFormData) { |
| share_target->set_enctype("multipart/form-data"); |
| } else { |
| share_target->set_enctype("application/x-www-form-urlencoded"); |
| } |
| webapk::ShareTargetParams* share_target_params = |
| share_target->mutable_params(); |
| share_target_params->set_title( |
| base::UTF16ToUTF8(shortcut_info.share_target->params.title)); |
| share_target_params->set_text( |
| base::UTF16ToUTF8(shortcut_info.share_target->params.text)); |
| |
| for (const webapps::ShareTargetParamsFile& share_target_params_file : |
| shortcut_info.share_target->params.files) { |
| webapk::ShareTargetParamsFile* share_files = |
| share_target_params->add_files(); |
| share_files->set_name(base::UTF16ToUTF8(share_target_params_file.name)); |
| for (std::u16string mime_type : share_target_params_file.accept) { |
| share_files->add_accept(base::UTF16ToUTF8(mime_type)); |
| } |
| } |
| } |
| |
| if (primary_icon) { |
| AddWebApkIcon(web_app_manifest, primary_icon.get()); |
| } else if (shortcut_info.best_primary_icon_url.is_valid()) { |
| auto icon_it = icons.find(shortcut_info.best_primary_icon_url); |
| if (icon_it != icons.end()) { |
| // Pass the pointer instead of move the unique_ptr, the same icon may be |
| // used for shortcuts. |
| AddWebApkIcon(web_app_manifest, icon_it->second.get()); |
| } |
| } |
| |
| if (splash_icon) { |
| AddWebApkIcon(web_app_manifest, splash_icon.get()); |
| } else if (shortcut_info.splash_image_url.is_valid() && |
| shortcut_info.splash_image_url != |
| shortcut_info.best_primary_icon_url) { |
| auto icon_it = icons.find(shortcut_info.splash_image_url); |
| if (icon_it != icons.end()) { |
| AddWebApkIcon(web_app_manifest, icon_it->second.get()); |
| } |
| } |
| |
| for (const std::string& icon_url : shortcut_info.icon_urls) { |
| if (icon_url.empty() || |
| icon_url == shortcut_info.best_primary_icon_url.spec() || |
| icon_url == shortcut_info.splash_image_url) { |
| continue; |
| } |
| |
| AddOtherWebApkImage(web_app_manifest, GURL(icon_url), icons); |
| } |
| |
| for (const auto& manifest_shortcut_item : shortcut_info.shortcut_items) { |
| auto* shortcut_item = web_app_manifest->add_shortcuts(); |
| shortcut_item->set_name(base::UTF16ToUTF8(manifest_shortcut_item.name)); |
| shortcut_item->set_short_name(base::UTF16ToUTF8( |
| manifest_shortcut_item.short_name.value_or(std::u16string()))); |
| shortcut_item->set_url(manifest_shortcut_item.url.spec()); |
| |
| for (const auto& manifest_icon : manifest_shortcut_item.icons) { |
| AddShortcutIcon(shortcut_item->add_icons(), manifest_icon.src, icons); |
| } |
| } |
| |
| std::unique_ptr<std::string> serialized_proto = |
| std::make_unique<std::string>(); |
| webapk->SerializeToString(serialized_proto.get()); |
| return serialized_proto; |
| } |
| |
| // Returns task runner for running background tasks. |
| scoped_refptr<base::TaskRunner> GetBackgroundTaskRunner() { |
| return base::ThreadPool::CreateTaskRunner( |
| {base::MayBlock(), base::TaskPriority::BEST_EFFORT, |
| base::TaskShutdownBehavior::SKIP_ON_SHUTDOWN}); |
| } |
| |
| void BuildProto( |
| const webapps::ShortcutInfo& shortcut_info, |
| const GURL& app_key, |
| std::unique_ptr<webapps::WebappIcon> primary_icon, |
| std::unique_ptr<webapps::WebappIcon> splash_icon, |
| const std::string& package_name, |
| const std::string& version, |
| std::map<GURL, std::unique_ptr<WebappIcon>> icons, |
| bool is_manifest_stale, |
| bool is_app_identity_update_supported, |
| base::OnceCallback<void(std::unique_ptr<std::string>)> callback) { |
| GetBackgroundTaskRunner()->PostTaskAndReplyWithResult( |
| FROM_HERE, |
| base::BindOnce(&webapps::BuildProtoInBackground, shortcut_info, app_key, |
| std::move(primary_icon), std::move(splash_icon), |
| package_name, version, std::move(icons), is_manifest_stale, |
| is_app_identity_update_supported, |
| std::vector<webapps::WebApkUpdateReason>()), |
| std::move(callback)); |
| } |
| |
| // Builds the WebAPK proto for an update request and stores it to |
| // |update_request_path|. Returns whether the proto was successfully written to |
| // disk. |
| bool StoreUpdateRequestToFileInBackground( |
| const base::FilePath& update_request_path, |
| const webapps::ShortcutInfo& shortcut_info, |
| const GURL& app_key, |
| std::unique_ptr<webapps::WebappIcon> primary_icon, |
| std::unique_ptr<webapps::WebappIcon> splash_icon, |
| const std::string& package_name, |
| const std::string& version, |
| std::map<GURL, std::unique_ptr<WebappIcon>> icons, |
| bool is_manifest_stale, |
| bool is_app_identity_update_supported, |
| std::vector<WebApkUpdateReason> update_reasons) { |
| base::ScopedBlockingCall scoped_blocking_call(FROM_HERE, |
| base::BlockingType::MAY_BLOCK); |
| |
| std::unique_ptr<std::string> proto = BuildProtoInBackground( |
| shortcut_info, app_key, std::move(primary_icon), std::move(splash_icon), |
| package_name, version, std::move(icons), is_manifest_stale, |
| is_app_identity_update_supported, std::move(update_reasons)); |
| |
| // Create directory if it does not exist. |
| base::CreateDirectory(update_request_path.DirName()); |
| |
| return base::WriteFile(update_request_path, *proto); |
| } |
| |
| } // namespace webapps |