| // Copyright 2018 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/web_applications/web_app_icon_manager.h" |
| |
| #include <string> |
| #include <utility> |
| |
| #include "base/bind.h" |
| #include "base/callback.h" |
| #include "base/containers/adapters.h" |
| #include "base/feature_list.h" |
| #include "base/files/scoped_temp_dir.h" |
| #include "base/logging.h" |
| #include "base/memory/ref_counted_memory.h" |
| #include "base/memory/scoped_refptr.h" |
| #include "base/ranges/algorithm.h" |
| #include "base/strings/strcat.h" |
| #include "base/strings/string_number_conversions.h" |
| #include "base/strings/stringprintf.h" |
| #include "base/task/task_traits.h" |
| #include "base/task/thread_pool.h" |
| #include "chrome/browser/web_applications/file_utils_wrapper.h" |
| #include "chrome/browser/web_applications/web_app.h" |
| #include "chrome/browser/web_applications/web_app_icon_generator.h" |
| #include "chrome/browser/web_applications/web_app_install_info.h" |
| #include "chrome/browser/web_applications/web_app_utils.h" |
| #include "chrome/common/chrome_features.h" |
| #include "content/public/browser/browser_thread.h" |
| #include "skia/ext/image_operations.h" |
| #include "third_party/abseil-cpp/absl/types/variant.h" |
| #include "ui/base/layout.h" |
| #include "ui/gfx/codec/png_codec.h" |
| #include "ui/gfx/favicon_size.h" |
| #include "ui/gfx/image/image_skia_rep.h" |
| |
| namespace web_app { |
| |
| namespace { |
| |
| // This utility struct is to carry error logs between threads via return values. |
| // If we weren't generating multithreaded errors we would just append the errors |
| // to WebAppIconManager::error_log() directly. |
| template <typename T> |
| struct TypedResult { |
| T value = T(); |
| std::vector<std::string> error_log; |
| |
| bool HasErrors() const { return !error_log.empty(); } |
| void DepositErrorLog(std::vector<std::string>& other_error_log) { |
| for (std::string& error : error_log) |
| other_error_log.push_back(std::move(error)); |
| error_log.clear(); |
| } |
| }; |
| |
| std::string CreateError(std::initializer_list<base::StringPiece> parts) { |
| std::string error = base::StrCat(parts); |
| LOG(ERROR) << error; |
| return error; |
| } |
| |
| // This is not a method on WebAppIconManager to avoid having to expose |
| // TypedResult<T> beyond this cc file. |
| template <typename T> |
| void LogErrorsCallCallback(base::WeakPtr<WebAppIconManager> manager, |
| base::OnceCallback<void(T)> callback, |
| TypedResult<T> result) { |
| if (!manager) |
| return; |
| std::vector<std::string>* error_log = manager->error_log(); |
| if (error_log) |
| result.DepositErrorLog(*error_log); |
| |
| std::move(callback).Run(std::move(result.value)); |
| } |
| |
| struct IconId { |
| IconId(AppId app_id, IconPurpose purpose, SquareSizePx size) |
| : app_id(std::move(app_id)), purpose(purpose), size(size) {} |
| ~IconId() = default; |
| |
| AppId app_id; |
| IconPurpose purpose; |
| SquareSizePx size; |
| }; |
| |
| // This is a private implementation detail of WebAppIconManager, where and how |
| // to store icon files. |
| // `app_manifest_resources_directory` is the path to the app-specific |
| // subdirectory of the profile's manifest resources directory. |
| base::FilePath GetProductIconsDirectory( |
| const base::FilePath& app_manifest_resources_directory, |
| IconPurpose purpose) { |
| constexpr base::FilePath::CharType kIconsAnyDirectoryName[] = |
| FILE_PATH_LITERAL("Icons"); |
| constexpr base::FilePath::CharType kIconsMonochromeDirectoryName[] = |
| FILE_PATH_LITERAL("Icons Monochrome"); |
| constexpr base::FilePath::CharType kIconsMaskableDirectoryName[] = |
| FILE_PATH_LITERAL("Icons Maskable"); |
| switch (purpose) { |
| case IconPurpose::ANY: |
| return app_manifest_resources_directory.Append(kIconsAnyDirectoryName); |
| case IconPurpose::MONOCHROME: |
| return app_manifest_resources_directory.Append( |
| kIconsMonochromeDirectoryName); |
| case IconPurpose::MASKABLE: |
| return app_manifest_resources_directory.Append( |
| kIconsMaskableDirectoryName); |
| } |
| } |
| |
| // This is a private implementation detail of WebAppIconManager, where and how |
| // to store shortcuts menu icons files. |
| // All of the other shortcut icon directories appear under the directory for |
| // |ANY|. |
| base::FilePath GetAppShortcutsMenuIconsRelativeDirectory(IconPurpose purpose) { |
| constexpr base::FilePath::CharType kShortcutsMenuIconsDirectoryName[] = |
| FILE_PATH_LITERAL("Shortcuts Menu Icons"); |
| |
| constexpr base::FilePath::CharType |
| kShortcutsMenuIconsMonochromeDirectoryName[] = |
| FILE_PATH_LITERAL("Monochrome"); |
| constexpr base::FilePath::CharType |
| kShortcutsMenuIconsMaskableDirectoryName[] = |
| FILE_PATH_LITERAL("Maskable"); |
| |
| base::FilePath shortcuts_icons_directory(kShortcutsMenuIconsDirectoryName); |
| |
| switch (purpose) { |
| case IconPurpose::ANY: |
| return shortcuts_icons_directory; |
| case IconPurpose::MONOCHROME: |
| return shortcuts_icons_directory.Append( |
| kShortcutsMenuIconsMonochromeDirectoryName); |
| case IconPurpose::MASKABLE: |
| return shortcuts_icons_directory.Append( |
| kShortcutsMenuIconsMaskableDirectoryName); |
| } |
| } |
| |
| base::FilePath GetOtherIconsRelativeDirectory() { |
| return base::FilePath(FILE_PATH_LITERAL("Image Cache")); |
| } |
| |
| // Returns a string suitable for use as a directory for the given URL. This name |
| // is a hash of the URL. |
| std::string GetDirectoryNameForUrl(const GURL& url) { |
| return base::NumberToString(base::PersistentHash(url.spec())); |
| } |
| |
| // Performs blocking I/O. May be called on another thread. |
| // Returns true if no errors occurred. |
| bool DeleteDataBlocking(scoped_refptr<FileUtilsWrapper> utils, |
| const base::FilePath& web_apps_directory, |
| const AppId& app_id) { |
| base::FilePath app_dir = |
| GetManifestResourcesDirectoryForApp(web_apps_directory, app_id); |
| |
| return utils->DeleteFileRecursively(app_dir); |
| } |
| |
| // `web_apps_directory` is the path to the directory where all web app data is |
| // stored for the relevant profile. |
| base::FilePath GetIconFileName(const base::FilePath& web_apps_directory, |
| const IconId& icon_id) { |
| base::FilePath app_dir = |
| GetManifestResourcesDirectoryForApp(web_apps_directory, icon_id.app_id); |
| base::FilePath icons_dir = GetProductIconsDirectory(app_dir, icon_id.purpose); |
| |
| return icons_dir.AppendASCII(base::StringPrintf("%i.png", icon_id.size)); |
| } |
| |
| // `web_apps_directory` is the path to the directory where all web app data is |
| // stored for the relevant profile. |
| base::FilePath GetManifestResourcesShortcutsMenuIconFileName( |
| const base::FilePath& web_apps_directory, |
| const AppId& app_id, |
| IconPurpose purpose, |
| int index, |
| int icon_size_px) { |
| const base::FilePath manifest_shortcuts_menu_icons_dir = |
| GetManifestResourcesDirectoryForApp(web_apps_directory, app_id) |
| .Append(GetAppShortcutsMenuIconsRelativeDirectory(purpose)); |
| const base::FilePath manifest_shortcuts_menu_icon_dir = |
| manifest_shortcuts_menu_icons_dir.AppendASCII( |
| base::NumberToString(index)); |
| |
| return manifest_shortcuts_menu_icon_dir.AppendASCII( |
| base::NumberToString(icon_size_px) + ".png"); |
| } |
| |
| // Performs blocking I/O. May be called on another thread. |
| // Returns empty SkBitmap if any errors occurred. |
| TypedResult<SkBitmap> ReadIconBlocking(scoped_refptr<FileUtilsWrapper> utils, |
| const base::FilePath& web_apps_directory, |
| const IconId& icon_id) { |
| base::FilePath icon_file = GetIconFileName(web_apps_directory, icon_id); |
| |
| auto icon_data = base::MakeRefCounted<base::RefCountedString>(); |
| |
| if (!utils->ReadFileToString(icon_file, &icon_data->data())) { |
| return {.error_log = {CreateError( |
| {"Could not read icon file: ", icon_file.AsUTF8Unsafe()})}}; |
| } |
| |
| TypedResult<SkBitmap> result; |
| |
| if (!gfx::PNGCodec::Decode(icon_data->front(), icon_data->size(), |
| &result.value)) { |
| return {.error_log = {CreateError({"Could not decode icon data for file: ", |
| icon_file.AsUTF8Unsafe()})}}; |
| } |
| |
| return result; |
| } |
| |
| // Performs blocking I/O. May be called on another thread. |
| // Returns empty SkBitmap if any errors occurred. |
| TypedResult<SkBitmap> ReadShortcutsMenuIconBlocking( |
| scoped_refptr<FileUtilsWrapper> utils, |
| const base::FilePath& web_apps_directory, |
| const AppId& app_id, |
| IconPurpose purpose, |
| int index, |
| int icon_size_px) { |
| base::FilePath icon_file = GetManifestResourcesShortcutsMenuIconFileName( |
| web_apps_directory, app_id, purpose, index, icon_size_px); |
| |
| std::string icon_data; |
| |
| if (!utils->ReadFileToString(icon_file, &icon_data)) { |
| return {.error_log = {CreateError( |
| {"Could not read icon file: ", icon_file.AsUTF8Unsafe()})}}; |
| } |
| |
| TypedResult<SkBitmap> result; |
| |
| if (!gfx::PNGCodec::Decode( |
| reinterpret_cast<const unsigned char*>(icon_data.c_str()), |
| icon_data.size(), &result.value)) { |
| return {.error_log = {CreateError({"Could not decode icon data for file: ", |
| icon_file.AsUTF8Unsafe()})}}; |
| } |
| |
| return result; |
| } |
| |
| // Performs blocking I/O. May be called on another thread. |
| // Returns empty map if any errors occurred. |
| TypedResult<std::map<SquareSizePx, SkBitmap>> ReadIconAndResizeBlocking( |
| scoped_refptr<FileUtilsWrapper> utils, |
| const base::FilePath& web_apps_directory, |
| const IconId& icon_id, |
| SquareSizePx target_icon_size_px) { |
| TypedResult<std::map<SquareSizePx, SkBitmap>> result; |
| |
| TypedResult<SkBitmap> read_result = |
| ReadIconBlocking(std::move(utils), web_apps_directory, icon_id); |
| if (read_result.HasErrors()) |
| return {.error_log = std::move(read_result.error_log)}; |
| |
| SkBitmap source = std::move(read_result.value); |
| SkBitmap target; |
| |
| if (icon_id.size != target_icon_size_px) { |
| target = skia::ImageOperations::Resize( |
| source, skia::ImageOperations::RESIZE_BEST, target_icon_size_px, |
| target_icon_size_px); |
| } else { |
| target = std::move(source); |
| } |
| |
| result.value[target_icon_size_px] = std::move(target); |
| return result; |
| } |
| |
| // Performs blocking I/O. May be called on another thread. |
| TypedResult<std::map<SquareSizePx, SkBitmap>> ReadIconsBlocking( |
| scoped_refptr<FileUtilsWrapper> utils, |
| const base::FilePath& web_apps_directory, |
| const AppId& app_id, |
| IconPurpose purpose, |
| const std::vector<SquareSizePx>& icon_sizes) { |
| TypedResult<std::map<SquareSizePx, SkBitmap>> result; |
| |
| for (SquareSizePx icon_size_px : icon_sizes) { |
| IconId icon_id(app_id, purpose, icon_size_px); |
| TypedResult<SkBitmap> read_result = |
| ReadIconBlocking(utils, web_apps_directory, icon_id); |
| read_result.DepositErrorLog(result.error_log); |
| if (!read_result.value.empty()) |
| result.value[icon_size_px] = std::move(read_result.value); |
| } |
| |
| return result; |
| } |
| |
| TypedResult<IconBitmaps> ReadAllIconsBlocking( |
| scoped_refptr<FileUtilsWrapper> utils, |
| const base::FilePath& web_apps_directory, |
| const AppId& app_id, |
| const std::map<IconPurpose, std::vector<SquareSizePx>>& |
| icon_purposes_to_sizes) { |
| TypedResult<IconBitmaps> result; |
| |
| for (const auto& purpose_sizes : icon_purposes_to_sizes) { |
| TypedResult<std::map<SquareSizePx, SkBitmap>> read_result = |
| ReadIconsBlocking(utils, web_apps_directory, app_id, |
| purpose_sizes.first, purpose_sizes.second); |
| read_result.DepositErrorLog(result.error_log); |
| result.value.SetBitmapsForPurpose(purpose_sizes.first, |
| std::move(read_result.value)); |
| } |
| |
| return result; |
| } |
| |
| // Performs blocking I/O. May be called on another thread. |
| TypedResult<ShortcutsMenuIconBitmaps> ReadShortcutsMenuIconsBlocking( |
| scoped_refptr<FileUtilsWrapper> utils, |
| const base::FilePath& web_apps_directory, |
| const AppId& app_id, |
| const std::vector<IconSizes>& shortcuts_menu_icons_sizes) { |
| TypedResult<ShortcutsMenuIconBitmaps> results; |
| int curr_index = 0; |
| for (const auto& icon_sizes : shortcuts_menu_icons_sizes) { |
| IconBitmaps result; |
| |
| for (IconPurpose purpose : kIconPurposes) { |
| std::map<SquareSizePx, SkBitmap> bitmaps; |
| |
| for (SquareSizePx icon_size_px : icon_sizes.GetSizesForPurpose(purpose)) { |
| TypedResult<SkBitmap> read_result = |
| ReadShortcutsMenuIconBlocking(utils, web_apps_directory, app_id, |
| purpose, curr_index, icon_size_px); |
| read_result.DepositErrorLog(results.error_log); |
| if (!read_result.value.empty()) |
| bitmaps[icon_size_px] = std::move(read_result.value); |
| } |
| |
| result.SetBitmapsForPurpose(purpose, std::move(bitmaps)); |
| } |
| |
| ++curr_index; |
| // We always push_back (even when result is empty) to keep a given |
| // std::map's index in sync with that of its corresponding shortcuts menu |
| // item. |
| results.value.push_back(std::move(result)); |
| } |
| return results; |
| } |
| |
| // Performs blocking I/O. May be called on another thread. |
| // Returns empty vector if any errors occurred. |
| TypedResult<std::vector<uint8_t>> ReadCompressedIconBlocking( |
| scoped_refptr<FileUtilsWrapper> utils, |
| const base::FilePath& web_apps_directory, |
| const IconId& icon_id) { |
| base::FilePath icon_file = GetIconFileName(web_apps_directory, icon_id); |
| |
| std::string icon_data; |
| |
| if (!utils->ReadFileToString(icon_file, &icon_data)) { |
| return {.error_log = {CreateError( |
| {"Could not read icon file: ", icon_file.AsUTF8Unsafe()})}}; |
| } |
| |
| // Copy data: we can't std::move std::string into std::vector. |
| return {.value = {icon_data.begin(), icon_data.end()}}; |
| } |
| |
| void WrapReadCompressedIconWithPurposeCallback( |
| WebAppIconManager::ReadCompressedIconWithPurposeCallback callback, |
| IconPurpose purpose, |
| std::vector<uint8_t> data) { |
| std::move(callback).Run(purpose, std::move(data)); |
| } |
| |
| gfx::ImageSkia ConvertUiScaleFactorsBitmapsToImageSkia( |
| const std::map<SquareSizePx, SkBitmap>& icon_bitmaps, |
| SquareSizeDip size_in_dip) { |
| gfx::ImageSkia image_skia; |
| auto it = icon_bitmaps.begin(); |
| for (ui::ResourceScaleFactor scale_factor : |
| ui::GetSupportedResourceScaleFactors()) { |
| float icon_scale = ui::GetScaleForResourceScaleFactor(scale_factor); |
| SquareSizePx icon_size_in_px = |
| gfx::ScaleToFlooredSize(gfx::Size(size_in_dip, size_in_dip), icon_scale) |
| .width(); |
| |
| while (it != icon_bitmaps.end() && it->first < icon_size_in_px) |
| ++it; |
| |
| if (it == icon_bitmaps.end() || it->second.empty()) |
| break; |
| |
| SkBitmap bitmap = it->second; |
| |
| // Resize |bitmap| to match |icon_scale|. |
| if (bitmap.width() != icon_size_in_px) { |
| bitmap = skia::ImageOperations::Resize(bitmap, |
| skia::ImageOperations::RESIZE_BEST, |
| icon_size_in_px, icon_size_in_px); |
| } |
| |
| image_skia.AddRepresentation(gfx::ImageSkiaRep(bitmap, icon_scale)); |
| } |
| |
| return image_skia; |
| } |
| |
| void WrapReadIconCallback(WebAppIconManager::ReadIconCallback callback, |
| IconPurpose ignored, |
| SkBitmap bitmap) { |
| std::move(callback).Run(std::move(bitmap)); |
| } |
| |
| void WrapReadCompressedIconCallback( |
| WebAppIconManager::ReadCompressedIconCallback callback, |
| IconPurpose ignored, |
| std::vector<uint8_t> data) { |
| std::move(callback).Run(std::move(data)); |
| } |
| |
| // A utility that manages writing icons to disk for a single app. Should only be |
| // used on an I/O thread. |
| class WriteIconsJob { |
| public: |
| static TypedResult<bool> WriteIconsBlocking( |
| scoped_refptr<FileUtilsWrapper> utils, |
| base::FilePath&& web_apps_directory, |
| AppId&& app_id, |
| IconBitmaps&& icon_bitmaps, |
| ShortcutsMenuIconBitmaps&& shortcuts_menu_icon_bitmaps, |
| IconsMap&& other_icons) { |
| WriteIconsJob job(std::move(utils), std::move(web_apps_directory), |
| std::move(app_id), std::move(icon_bitmaps), |
| std::move(shortcuts_menu_icon_bitmaps), |
| std::move(other_icons)); |
| return job.Execute(); |
| } |
| |
| WriteIconsJob(const WriteIconsJob& other) = delete; |
| WriteIconsJob& operator=(const WriteIconsJob& other) = delete; |
| |
| private: |
| WriteIconsJob(scoped_refptr<FileUtilsWrapper> utils, |
| base::FilePath&& web_apps_directory, |
| AppId&& app_id, |
| IconBitmaps&& icon_bitmaps, |
| ShortcutsMenuIconBitmaps&& shortcuts_menu_icon_bitmaps, |
| IconsMap&& other_icons) |
| : utils_(std::move(utils)), |
| web_apps_directory_(std::move(web_apps_directory)), |
| app_id_(std::move(app_id)), |
| icon_bitmaps_(std::move(icon_bitmaps)), |
| shortcuts_menu_icon_bitmaps_(std::move(shortcuts_menu_icon_bitmaps)), |
| other_icons_(std::move(other_icons)) {} |
| ~WriteIconsJob() = default; |
| |
| TypedResult<bool> Execute() { |
| // Write product icons directly in the app's directory. |
| auto result = AtomicallyWriteIcons( |
| base::BindRepeating(&WriteIconsJob::WriteProductIcons, |
| base::Unretained(this)), |
| /*subdir_for_icons=*/{}); |
| if (result.HasErrors()) |
| return result; |
| |
| if (!shortcuts_menu_icon_bitmaps_.empty()) { |
| result = AtomicallyWriteIcons( |
| base::BindRepeating(&WriteIconsJob::WriteShortcutsMenuIcons, |
| base::Unretained(this)), |
| /*subdir_for_icons=*/GetAppShortcutsMenuIconsRelativeDirectory( |
| IconPurpose::ANY)); |
| |
| if (result.HasErrors()) |
| return result; |
| } |
| |
| if (!other_icons_.empty()) { |
| result = AtomicallyWriteIcons( |
| base::BindRepeating(&WriteIconsJob::WriteOtherIcons, |
| base::Unretained(this)), |
| /*subdir_for_icons=*/ |
| GetOtherIconsRelativeDirectory()); |
| } |
| |
| return result; |
| } |
| |
| // Manages writing a set of icons to a particular location on disk, making a |
| // best-effort to make it all-or-nothing. Returns true if no errors occurred. |
| // This is used for several kinds of icon data. The passed callbacks allow for |
| // varying the implementation based on data type. `write_icons_callback` is |
| // expected to write the icons data under the passed base directory. |
| // `subdir_for_icons` is a relative FilePath representing a directory which |
| // holds all the data written by `write_icons_callback`. The path is relative |
| // to the app's manifest resources directory. |
| TypedResult<bool> AtomicallyWriteIcons( |
| const base::RepeatingCallback< |
| TypedResult<bool>(const base::FilePath& path)>& write_icons_callback, |
| const base::FilePath& subdir_for_icons) { |
| DCHECK(!subdir_for_icons.IsAbsolute()); |
| // Create the temp directory under the web apps root. |
| // This guarantees it is on the same file system as the WebApp's eventual |
| // install target. |
| base::FilePath temp_dir = GetWebAppsTempDirectory(web_apps_directory_); |
| TypedResult<bool> create_result = CreateDirectory(temp_dir); |
| if (create_result.HasErrors()) |
| return create_result; |
| |
| base::ScopedTempDir app_temp_dir; |
| if (!app_temp_dir.CreateUniqueTempDirUnderPath(temp_dir)) { |
| return { |
| .error_log = {CreateError({"Could not create temp directory under: ", |
| temp_dir.AsUTF8Unsafe()})}}; |
| } |
| |
| base::FilePath manifest_resources_directory = |
| GetManifestResourcesDirectory(web_apps_directory_); |
| create_result = CreateDirectory(manifest_resources_directory); |
| if (create_result.HasErrors()) |
| return create_result; |
| |
| TypedResult<bool> write_result = |
| write_icons_callback.Run(app_temp_dir.GetPath()); |
| if (write_result.HasErrors()) |
| return write_result; |
| |
| base::FilePath app_dir = |
| GetManifestResourcesDirectoryForApp(web_apps_directory_, app_id_); |
| base::FilePath final_icons_dir = app_dir.Append(subdir_for_icons); |
| // Create app_dir if it doesn't already exist. We'll need this for |
| // WriteShortcutsMenuIconsData unittests. |
| if (final_icons_dir != app_dir) { |
| create_result = CreateDirectory(app_dir); |
| if (create_result.HasErrors()) |
| return create_result; |
| } |
| |
| // Delete the destination. Needed for update. Ignore the result. |
| utils_->DeleteFileRecursively(final_icons_dir); |
| |
| base::FilePath temp_icons_dir = |
| app_temp_dir.GetPath().Append(subdir_for_icons); |
| // Commit: move whole icons data dir to final destination in one mv |
| // operation. |
| if (!utils_->Move(temp_icons_dir, final_icons_dir)) { |
| return {.error_log = {CreateError( |
| {"Could not move: ", temp_icons_dir.AsUTF8Unsafe(), |
| " to: ", final_icons_dir.AsUTF8Unsafe()})}}; |
| } |
| |
| return {.value = true}; |
| } |
| |
| TypedResult<bool> WriteProductIcons(const base::FilePath& base_dir) { |
| for (IconPurpose purpose : kIconPurposes) { |
| base::FilePath icons_dir = GetProductIconsDirectory(base_dir, purpose); |
| |
| auto create_result = CreateDirectory(icons_dir); |
| if (create_result.HasErrors()) |
| return create_result; |
| |
| for (const std::pair<const SquareSizePx, SkBitmap>& icon_bitmap : |
| icon_bitmaps_.GetBitmapsForPurpose(purpose)) { |
| TypedResult<bool> write_result = |
| EncodeAndWriteIcon(icons_dir, icon_bitmap.second); |
| if (write_result.HasErrors()) |
| return write_result; |
| } |
| } |
| |
| return {.value = true}; |
| } |
| |
| // Writes shortcuts menu icons files to the Shortcut Icons directory. Creates |
| // a new directory per shortcut item using its index in the vector. |
| TypedResult<bool> WriteShortcutsMenuIcons( |
| const base::FilePath& app_manifest_resources_directory) { |
| for (IconPurpose purpose : kIconPurposes) { |
| const base::FilePath shortcuts_menu_icons_dir = |
| app_manifest_resources_directory.Append( |
| GetAppShortcutsMenuIconsRelativeDirectory(purpose)); |
| auto create_result = CreateDirectory(shortcuts_menu_icons_dir); |
| if (create_result.HasErrors()) |
| return create_result; |
| |
| int shortcut_index = -1; |
| for (const IconBitmaps& icon_bitmaps : shortcuts_menu_icon_bitmaps_) { |
| ++shortcut_index; |
| const std::map<SquareSizePx, SkBitmap>& bitmaps = |
| icon_bitmaps.GetBitmapsForPurpose(purpose); |
| if (bitmaps.empty()) |
| continue; |
| |
| const base::FilePath shortcuts_menu_icon_dir = |
| shortcuts_menu_icons_dir.AppendASCII( |
| base::NumberToString(shortcut_index)); |
| create_result = CreateDirectory(shortcuts_menu_icon_dir); |
| if (create_result.HasErrors()) |
| return create_result; |
| |
| for (const std::pair<const SquareSizePx, SkBitmap>& icon_bitmap : |
| bitmaps) { |
| TypedResult<bool> write_result = |
| EncodeAndWriteIcon(shortcuts_menu_icon_dir, icon_bitmap.second); |
| if (write_result.HasErrors()) |
| return write_result; |
| } |
| } |
| } |
| return {.value = true}; |
| } |
| |
| TypedResult<bool> WriteOtherIcons( |
| const base::FilePath& app_manifest_resources_directory) { |
| const base::FilePath general_icons_dir = |
| app_manifest_resources_directory.Append( |
| GetOtherIconsRelativeDirectory()); |
| auto create_result = CreateDirectory(general_icons_dir); |
| if (create_result.HasErrors()) |
| return create_result; |
| |
| for (const std::pair<const GURL, std::vector<SkBitmap>>& entry : |
| other_icons_) { |
| const base::FilePath subdir = |
| general_icons_dir.AppendASCII(GetDirectoryNameForUrl(entry.first)); |
| create_result = CreateDirectory(subdir); |
| if (create_result.HasErrors()) |
| return create_result; |
| |
| const std::vector<SkBitmap>& icon_bitmaps = entry.second; |
| for (const SkBitmap& icon_bitmap : icon_bitmaps) { |
| TypedResult<bool> write_result = |
| EncodeAndWriteIcon(subdir, icon_bitmap); |
| if (write_result.HasErrors()) |
| return write_result; |
| } |
| } |
| return {.value = true}; |
| } |
| |
| TypedResult<bool> CreateDirectory(const base::FilePath& path) { |
| if (!utils_->CreateDirectory(path)) { |
| return {.error_log = {CreateError( |
| {"Could not create directory: ", path.AsUTF8Unsafe()})}}; |
| } |
| |
| return {.value = true}; |
| } |
| |
| // Encodes `bitmap` as a PNG and writes to the given directory. |
| TypedResult<bool> EncodeAndWriteIcon(const base::FilePath& icons_dir, |
| const SkBitmap& bitmap) { |
| DCHECK_NE(bitmap.colorType(), kUnknown_SkColorType); |
| DCHECK_EQ(bitmap.width(), bitmap.height()); |
| base::FilePath icon_file = |
| icons_dir.AppendASCII(base::StringPrintf("%i.png", bitmap.width())); |
| |
| std::vector<unsigned char> image_data; |
| const bool discard_transparency = false; |
| if (!gfx::PNGCodec::EncodeBGRASkBitmap(bitmap, discard_transparency, |
| &image_data)) { |
| return {.error_log = {CreateError({"Could not encode icon data for file ", |
| icon_file.AsUTF8Unsafe()})}}; |
| } |
| |
| const char* image_data_ptr = reinterpret_cast<const char*>(&image_data[0]); |
| int size = base::checked_cast<int>(image_data.size()); |
| if (utils_->WriteFile(icon_file, image_data_ptr, size) != size) { |
| return {.error_log = {CreateError( |
| {"Could not write icon file: ", icon_file.AsUTF8Unsafe()})}}; |
| } |
| |
| return {.value = true}; |
| } |
| |
| scoped_refptr<FileUtilsWrapper> utils_; |
| base::FilePath web_apps_directory_; |
| AppId app_id_; |
| IconBitmaps icon_bitmaps_; |
| ShortcutsMenuIconBitmaps shortcuts_menu_icon_bitmaps_; |
| IconsMap other_icons_; |
| }; |
| |
| } // namespace |
| |
| WebAppIconManager::WebAppIconManager(Profile* profile, |
| scoped_refptr<FileUtilsWrapper> utils) |
| : utils_(std::move(utils)), |
| icon_task_runner_(base::ThreadPool::CreateSequencedTaskRunner( |
| {base::MayBlock(), base::TaskPriority::USER_VISIBLE, |
| base::TaskShutdownBehavior::BLOCK_SHUTDOWN})) { |
| web_apps_directory_ = GetWebAppsRootDirectory(profile); |
| if (base::FeatureList::IsEnabled(features::kRecordWebAppDebugInfo)) |
| error_log_ = std::make_unique<std::vector<std::string>>(); |
| } |
| |
| WebAppIconManager::~WebAppIconManager() = default; |
| |
| void WebAppIconManager::SetSubsystems(WebAppRegistrar* registrar, |
| WebAppInstallManager* install_manager) { |
| registrar_ = registrar; |
| install_manager_ = install_manager; |
| } |
| |
| void WebAppIconManager::WriteData( |
| AppId app_id, |
| IconBitmaps icon_bitmaps, |
| ShortcutsMenuIconBitmaps shortcuts_menu_icon_bitmaps, |
| IconsMap other_icons_map, |
| WriteDataCallback callback) { |
| DCHECK_CURRENTLY_ON(content::BrowserThread::UI); |
| |
| icon_task_runner_->PostTaskAndReplyWithResult( |
| FROM_HERE, |
| base::BindOnce( |
| &WriteIconsJob::WriteIconsBlocking, utils_, web_apps_directory_, |
| std::move(app_id), std::move(icon_bitmaps), |
| std::move(shortcuts_menu_icon_bitmaps), std::move(other_icons_map)), |
| base::BindOnce(&LogErrorsCallCallback<bool>, GetWeakPtr(), |
| std::move(callback))); |
| } |
| |
| void WebAppIconManager::DeleteData(AppId app_id, WriteDataCallback callback) { |
| DCHECK_CURRENTLY_ON(content::BrowserThread::UI); |
| |
| icon_task_runner_->PostTaskAndReplyWithResult( |
| FROM_HERE, |
| base::BindOnce(DeleteDataBlocking, utils_, web_apps_directory_, |
| std::move(app_id)), |
| std::move(callback)); |
| } |
| |
| void WebAppIconManager::Start() { |
| for (const AppId& app_id : registrar_->GetAppIds()) { |
| ReadFavicon(app_id); |
| |
| #if BUILDFLAG(IS_CHROMEOS) |
| // Notifications use a monochrome icon. |
| ReadMonochromeFavicon(app_id); |
| #endif // BUILDFLAG(IS_CHROMEOS) |
| } |
| install_manager_observation_.Observe(install_manager_); |
| } |
| |
| void WebAppIconManager::Shutdown() {} |
| |
| bool WebAppIconManager::HasIcons(const AppId& app_id, |
| IconPurpose purpose, |
| const SortedSizesPx& icon_sizes) const { |
| DCHECK_CURRENTLY_ON(content::BrowserThread::UI); |
| const WebApp* web_app = registrar_->GetAppById(app_id); |
| if (!web_app) |
| return false; |
| |
| return base::ranges::includes(web_app->downloaded_icon_sizes(purpose), |
| icon_sizes); |
| } |
| |
| absl::optional<WebAppIconManager::IconSizeAndPurpose> |
| WebAppIconManager::FindIconMatchBigger(const AppId& app_id, |
| const std::vector<IconPurpose>& purposes, |
| SquareSizePx min_size) const { |
| DCHECK_CURRENTLY_ON(content::BrowserThread::UI); |
| const WebApp* web_app = registrar_->GetAppById(app_id); |
| if (!web_app) |
| return absl::nullopt; |
| |
| // Must iterate through purposes in order given. |
| for (IconPurpose purpose : purposes) { |
| // Must iterate sizes from smallest to largest. |
| const SortedSizesPx& sizes = web_app->downloaded_icon_sizes(purpose); |
| for (SquareSizePx size : sizes) { |
| if (size >= min_size) |
| return IconSizeAndPurpose{size, purpose}; |
| } |
| } |
| |
| return absl::nullopt; |
| } |
| |
| bool WebAppIconManager::HasSmallestIcon( |
| const AppId& app_id, |
| const std::vector<IconPurpose>& purposes, |
| SquareSizePx min_size) const { |
| return FindIconMatchBigger(app_id, purposes, min_size).has_value(); |
| } |
| |
| void WebAppIconManager::ReadIcons(const AppId& app_id, |
| IconPurpose purpose, |
| const SortedSizesPx& icon_sizes, |
| ReadIconsCallback callback) { |
| DCHECK_CURRENTLY_ON(content::BrowserThread::UI); |
| |
| if (!registrar_->GetAppById(app_id)) { |
| std::move(callback).Run(std::map<SquareSizePx, SkBitmap>()); |
| return; |
| } |
| DCHECK(HasIcons(app_id, purpose, icon_sizes)); |
| |
| icon_task_runner_->PostTaskAndReplyWithResult( |
| FROM_HERE, |
| base::BindOnce( |
| ReadIconsBlocking, utils_, web_apps_directory_, app_id, purpose, |
| std::vector<SquareSizePx>(icon_sizes.begin(), icon_sizes.end())), |
| base::BindOnce(&LogErrorsCallCallback<std::map<SquareSizePx, SkBitmap>>, |
| GetWeakPtr(), std::move(callback))); |
| } |
| |
| void WebAppIconManager::ReadAllIcons(const AppId& app_id, |
| ReadIconBitmapsCallback callback) { |
| DCHECK_CURRENTLY_ON(content::BrowserThread::UI); |
| const WebApp* web_app = registrar_->GetAppById(app_id); |
| if (!web_app) { |
| std::move(callback).Run(IconBitmaps()); |
| return; |
| } |
| |
| std::map<IconPurpose, std::vector<SquareSizePx>> icon_purposes_to_sizes; |
| |
| for (IconPurpose purpose : kIconPurposes) { |
| const SortedSizesPx& sizes_px = web_app->downloaded_icon_sizes(purpose); |
| icon_purposes_to_sizes[purpose] = |
| std::vector<SquareSizePx>(sizes_px.begin(), sizes_px.end()); |
| } |
| |
| icon_task_runner_->PostTaskAndReplyWithResult( |
| FROM_HERE, |
| base::BindOnce(ReadAllIconsBlocking, utils_, web_apps_directory_, app_id, |
| std::move(icon_purposes_to_sizes)), |
| base::BindOnce(&LogErrorsCallCallback<IconBitmaps>, GetWeakPtr(), |
| std::move(callback))); |
| } |
| |
| void WebAppIconManager::ReadAllShortcutsMenuIcons( |
| const AppId& app_id, |
| ReadShortcutsMenuIconsCallback callback) { |
| DCHECK_CURRENTLY_ON(content::BrowserThread::UI); |
| const WebApp* web_app = registrar_->GetAppById(app_id); |
| if (!web_app) { |
| std::move(callback).Run(ShortcutsMenuIconBitmaps{}); |
| return; |
| } |
| |
| icon_task_runner_->PostTaskAndReplyWithResult( |
| FROM_HERE, |
| base::BindOnce(ReadShortcutsMenuIconsBlocking, utils_, |
| web_apps_directory_, app_id, |
| web_app->downloaded_shortcuts_menu_icons_sizes()), |
| base::BindOnce(&LogErrorsCallCallback<ShortcutsMenuIconBitmaps>, |
| GetWeakPtr(), std::move(callback))); |
| } |
| |
| void WebAppIconManager::ReadSmallestIcon( |
| const AppId& app_id, |
| const std::vector<IconPurpose>& purposes, |
| SquareSizePx min_size_in_px, |
| ReadIconWithPurposeCallback callback) { |
| DCHECK_CURRENTLY_ON(content::BrowserThread::UI); |
| |
| absl::optional<IconSizeAndPurpose> best_icon = |
| FindIconMatchBigger(app_id, purposes, min_size_in_px); |
| DCHECK(best_icon.has_value()); |
| IconId icon_id(app_id, best_icon->purpose, best_icon->size_px); |
| ReadIconCallback wrapped = base::BindOnce( |
| WrapReadIconWithPurposeCallback, std::move(callback), best_icon->purpose); |
| |
| icon_task_runner_->PostTaskAndReplyWithResult( |
| FROM_HERE, |
| base::BindOnce(ReadIconBlocking, utils_, web_apps_directory_, |
| std::move(icon_id)), |
| base::BindOnce(&LogErrorsCallCallback<SkBitmap>, GetWeakPtr(), |
| std::move(wrapped))); |
| } |
| |
| void WebAppIconManager::ReadSmallestCompressedIcon( |
| const AppId& app_id, |
| const std::vector<IconPurpose>& purposes, |
| SquareSizePx min_size_in_px, |
| ReadCompressedIconWithPurposeCallback callback) { |
| DCHECK_CURRENTLY_ON(content::BrowserThread::UI); |
| |
| absl::optional<IconSizeAndPurpose> best_icon = |
| FindIconMatchBigger(app_id, purposes, min_size_in_px); |
| DCHECK(best_icon.has_value()); |
| IconId icon_id(app_id, best_icon->purpose, best_icon->size_px); |
| ReadCompressedIconCallback wrapped = |
| base::BindOnce(WrapReadCompressedIconWithPurposeCallback, |
| std::move(callback), best_icon->purpose); |
| |
| icon_task_runner_->PostTaskAndReplyWithResult( |
| FROM_HERE, |
| base::BindOnce(ReadCompressedIconBlocking, utils_, web_apps_directory_, |
| std::move(icon_id)), |
| base::BindOnce(&LogErrorsCallCallback<std::vector<uint8_t>>, GetWeakPtr(), |
| std::move(wrapped))); |
| } |
| |
| void WebAppIconManager::ReadSmallestIconAny(const AppId& app_id, |
| SquareSizePx min_icon_size, |
| ReadIconCallback callback) { |
| ReadIconWithPurposeCallback wrapped = |
| base::BindOnce(WrapReadIconCallback, std::move(callback)); |
| ReadSmallestIcon(app_id, {IconPurpose::ANY}, min_icon_size, |
| std::move(wrapped)); |
| } |
| |
| void WebAppIconManager::ReadSmallestCompressedIconAny( |
| const AppId& app_id, |
| SquareSizePx min_icon_size, |
| ReadCompressedIconCallback callback) { |
| ReadCompressedIconWithPurposeCallback wrapped = |
| base::BindOnce(WrapReadCompressedIconCallback, std::move(callback)); |
| ReadSmallestCompressedIcon(app_id, {IconPurpose::ANY}, min_icon_size, |
| std::move(wrapped)); |
| } |
| |
| SkBitmap WebAppIconManager::GetFavicon(const AppId& app_id) const { |
| auto iter = favicon_cache_.find(app_id); |
| if (iter == favicon_cache_.end()) |
| return SkBitmap(); |
| |
| const gfx::ImageSkia& image_skia = iter->second; |
| |
| // A representation for 1.0 UI scale factor is mandatory. GetRepresentation() |
| // should create one. |
| return image_skia.GetRepresentation(1.0f).GetBitmap(); |
| } |
| |
| gfx::ImageSkia WebAppIconManager::GetFaviconImageSkia( |
| const AppId& app_id) const { |
| auto iter = favicon_cache_.find(app_id); |
| return iter != favicon_cache_.end() ? iter->second : gfx::ImageSkia(); |
| } |
| |
| gfx::ImageSkia WebAppIconManager::GetMonochromeFavicon( |
| const AppId& app_id) const { |
| auto iter = favicon_monochrome_cache_.find(app_id); |
| return iter != favicon_monochrome_cache_.end() ? iter->second |
| : gfx::ImageSkia(); |
| } |
| |
| void WebAppIconManager::OnWebAppInstalled(const AppId& app_id) { |
| ReadFavicon(app_id); |
| #if BUILDFLAG(IS_CHROMEOS) |
| // Notifications use a monochrome icon. |
| ReadMonochromeFavicon(app_id); |
| #endif |
| } |
| |
| void WebAppIconManager::OnWebAppInstallManagerDestroyed() { |
| install_manager_observation_.Reset(); |
| } |
| |
| void WebAppIconManager::ReadIconAndResize(const AppId& app_id, |
| IconPurpose purpose, |
| SquareSizePx desired_icon_size, |
| ReadIconsCallback callback) { |
| absl::optional<IconSizeAndPurpose> best_icon = |
| FindIconMatchBigger(app_id, {purpose}, desired_icon_size); |
| if (!best_icon) { |
| best_icon = FindIconMatchSmaller(app_id, {purpose}, desired_icon_size); |
| } |
| |
| if (!best_icon) { |
| std::move(callback).Run(std::map<SquareSizePx, SkBitmap>()); |
| return; |
| } |
| |
| IconId icon_id(app_id, best_icon->purpose, best_icon->size_px); |
| icon_task_runner_->PostTaskAndReplyWithResult( |
| FROM_HERE, |
| base::BindOnce(ReadIconAndResizeBlocking, utils_, web_apps_directory_, |
| std::move(icon_id), desired_icon_size), |
| base::BindOnce(&LogErrorsCallCallback<std::map<SquareSizePx, SkBitmap>>, |
| GetWeakPtr(), std::move(callback))); |
| } |
| |
| void WebAppIconManager::ReadUiScaleFactorsIcons( |
| const AppId& app_id, |
| IconPurpose purpose, |
| SquareSizeDip size_in_dip, |
| ReadImageSkiaCallback callback) { |
| SortedSizesPx ui_scale_factors_px_sizes; |
| for (ui::ResourceScaleFactor scale_factor : |
| ui::GetSupportedResourceScaleFactors()) { |
| auto size_and_purpose = FindIconMatchBigger( |
| app_id, {purpose}, |
| gfx::ScaleToFlooredSize( |
| gfx::Size(size_in_dip, size_in_dip), |
| ui::GetScaleForResourceScaleFactor(scale_factor)) |
| .width()); |
| if (size_and_purpose.has_value()) |
| ui_scale_factors_px_sizes.insert(size_and_purpose->size_px); |
| } |
| |
| if (ui_scale_factors_px_sizes.empty()) { |
| std::move(callback).Run(gfx::ImageSkia()); |
| return; |
| } |
| |
| ReadIcons(app_id, purpose, ui_scale_factors_px_sizes, |
| base::BindOnce(&WebAppIconManager::OnReadUiScaleFactorsIcons, |
| GetWeakPtr(), size_in_dip, std::move(callback))); |
| } |
| |
| void WebAppIconManager::OnReadUiScaleFactorsIcons( |
| SquareSizeDip size_in_dip, |
| ReadImageSkiaCallback callback, |
| std::map<SquareSizePx, SkBitmap> icon_bitmaps) { |
| std::move(callback).Run( |
| ConvertUiScaleFactorsBitmapsToImageSkia(icon_bitmaps, size_in_dip)); |
| } |
| |
| void WebAppIconManager::SetFaviconReadCallbackForTesting( |
| FaviconReadCallback callback) { |
| favicon_read_callback_ = std::move(callback); |
| } |
| |
| void WebAppIconManager::SetFaviconMonochromeReadCallbackForTesting( |
| FaviconReadCallback callback) { |
| favicon_monochrome_read_callback_ = std::move(callback); |
| } |
| |
| base::WeakPtr<const WebAppIconManager> WebAppIconManager::GetWeakPtr() const { |
| return weak_ptr_factory_.GetWeakPtr(); |
| } |
| |
| base::WeakPtr<WebAppIconManager> WebAppIconManager::GetWeakPtr() { |
| return weak_ptr_factory_.GetWeakPtr(); |
| } |
| |
| // static |
| void WebAppIconManager::WrapReadIconWithPurposeCallback( |
| ReadIconWithPurposeCallback callback, |
| IconPurpose purpose, |
| SkBitmap bitmap) { |
| std::move(callback).Run(purpose, std::move(bitmap)); |
| } |
| |
| absl::optional<WebAppIconManager::IconSizeAndPurpose> |
| WebAppIconManager::FindIconMatchSmaller( |
| const AppId& app_id, |
| const std::vector<IconPurpose>& purposes, |
| SquareSizePx max_size) const { |
| DCHECK_CURRENTLY_ON(content::BrowserThread::UI); |
| const WebApp* web_app = registrar_->GetAppById(app_id); |
| if (!web_app) |
| return absl::nullopt; |
| |
| // Must check purposes in the order given. |
| for (IconPurpose purpose : purposes) { |
| // Must iterate sizes from largest to smallest. |
| const SortedSizesPx& sizes = web_app->downloaded_icon_sizes(purpose); |
| for (SquareSizePx size : base::Reversed(sizes)) { |
| if (size <= max_size) |
| return IconSizeAndPurpose{size, purpose}; |
| } |
| } |
| |
| return absl::nullopt; |
| } |
| |
| void WebAppIconManager::ReadFavicon(const AppId& app_id) { |
| ReadUiScaleFactorsIcons( |
| app_id, IconPurpose::ANY, gfx::kFaviconSize, |
| base::BindOnce(&WebAppIconManager::OnReadFavicon, GetWeakPtr(), app_id)); |
| } |
| |
| void WebAppIconManager::OnReadFavicon(const AppId& app_id, |
| gfx::ImageSkia image_skia) { |
| if (!image_skia.isNull()) |
| favicon_cache_[app_id] = image_skia; |
| |
| if (favicon_read_callback_) |
| favicon_read_callback_.Run(app_id); |
| } |
| |
| void WebAppIconManager::ReadMonochromeFavicon(const AppId& app_id) { |
| ReadUiScaleFactorsIcons( |
| app_id, IconPurpose::MONOCHROME, gfx::kFaviconSize, |
| base::BindOnce(&WebAppIconManager::OnReadMonochromeFavicon, GetWeakPtr(), |
| app_id)); |
| } |
| |
| void WebAppIconManager::OnReadMonochromeFavicon( |
| const AppId& app_id, |
| gfx::ImageSkia manifest_monochrome_image) { |
| const WebApp* web_app = registrar_->GetAppById(app_id); |
| if (!web_app) |
| return; |
| |
| if (manifest_monochrome_image.isNull()) { |
| OnMonochromeIconConverted(app_id, manifest_monochrome_image); |
| return; |
| } |
| |
| const SkColor solid_color = |
| web_app->theme_color() ? *web_app->theme_color() : SK_ColorDKGRAY; |
| |
| manifest_monochrome_image.MakeThreadSafe(); |
| |
| icon_task_runner_->PostTaskAndReplyWithResult( |
| FROM_HERE, |
| base::BindOnce(ConvertImageToSolidFillMonochrome, solid_color, |
| std::move(manifest_monochrome_image)), |
| base::BindOnce(&WebAppIconManager::OnMonochromeIconConverted, |
| GetWeakPtr(), app_id)); |
| } |
| |
| void WebAppIconManager::OnMonochromeIconConverted( |
| const AppId& app_id, |
| gfx::ImageSkia converted_image) { |
| if (!converted_image.isNull()) |
| favicon_monochrome_cache_[app_id] = converted_image; |
| |
| if (favicon_monochrome_read_callback_) |
| favicon_monochrome_read_callback_.Run(app_id); |
| } |
| |
| } // namespace web_app |