blob: f2cf630a8649d7d1a8039fdfc1e64cc9cb4be0d6 [file] [log] [blame]
// Copyright 2018 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/app_icon_factory.h"
#include <utility>
#include <vector>
#include "base/bind.h"
#include "base/callback.h"
#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/task/post_task.h"
#include "base/task/task_traits.h"
#include "chrome/browser/apps/app_service/dip_px_util.h"
#include "chrome/browser/extensions/chrome_app_icon.h"
#include "chrome/browser/extensions/chrome_app_icon_loader.h"
#include "chrome/browser/extensions/extension_service.h"
#include "content/public/common/service_manager_connection.h"
#include "extensions/browser/component_extension_resource_manager.h"
#include "extensions/browser/extension_system.h"
#include "extensions/browser/extensions_browser_client.h"
#include "extensions/browser/image_loader.h"
#include "extensions/common/manifest.h"
#include "extensions/common/manifest_handlers/icons_handler.h"
#include "extensions/grit/extensions_browser_resources.h"
#include "services/data_decoder/public/cpp/decode_image.h"
#include "ui/base/resource/resource_bundle.h"
#include "ui/gfx/image/image.h"
#include "ui/gfx/image/image_skia.h"
#include "ui/gfx/image/image_skia_operations.h"
#if defined(OS_CHROMEOS)
#include "chrome/browser/ui/app_list/md_icon_normalizer.h"
#endif
namespace {
void ApplyIconEffects(apps::IconEffects icon_effects,
int size_hint_in_dip,
gfx::ImageSkia* image_skia) {
extensions::ChromeAppIcon::ResizeFunction resize_function;
#if defined(OS_CHROMEOS)
if (icon_effects & apps::IconEffects::kResizeAndPad) {
// TODO(crbug.com/826982): khmel@ notes that MD post-processing is not
// always applied: "See legacy code:
// https://cs.chromium.org/search/?q=ChromeAppIconLoader&type=cs In one
// cases MD design is used in another not."
resize_function =
base::BindRepeating(&app_list::MaybeResizeAndPadIconForMd);
}
#endif
bool apply_chrome_badge = icon_effects & apps::IconEffects::kBadge;
bool app_launchable = !(icon_effects & apps::IconEffects::kGray);
bool from_bookmark = icon_effects & apps::IconEffects::kRoundCorners;
extensions::ChromeAppIcon::ApplyEffects(size_hint_in_dip, resize_function,
apply_chrome_badge, app_launchable,
from_bookmark, image_skia);
}
std::vector<uint8_t> ReadFileAsCompressedData(const base::FilePath path) {
std::string data;
base::ReadFileToString(path, &data);
return std::vector<uint8_t>(data.begin(), data.end());
}
std::vector<uint8_t> CompressedDataFromResource(
extensions::ExtensionResource resource) {
const base::FilePath& path = resource.GetFilePath();
if (path.empty()) {
return std::vector<uint8_t>();
}
return ReadFileAsCompressedData(path);
}
// Runs |callback| passing an IconValuePtr with a compressed image: a
// std::vector<uint8_t>.
//
// It will fall back to the |default_icon_resource| if the data is empty.
void RunCallbackWithCompressedData(
int size_hint_in_dip,
int default_icon_resource,
bool is_placeholder_icon,
apps::IconEffects icon_effects,
apps::mojom::Publisher::LoadIconCallback callback,
std::vector<uint8_t> data) {
// TODO(crbug.com/826982): if icon_effects is non-zero, we should arguably
// decompress the image, apply the icon_effects, and recompress the
// post-processed image.
//
// Even if there are no icon_effects, we might also want to do this if the
// size_hint_in_dip doesn't match the compressed image's pixel size. This
// isn't trivial, though, as determining the compressed image's pixel size
// might involve a sandboxed decoder process.
if (!data.empty()) {
apps::mojom::IconValuePtr iv = apps::mojom::IconValue::New();
iv->icon_compression = apps::mojom::IconCompression::kCompressed;
iv->compressed = std::move(data);
iv->is_placeholder_icon = is_placeholder_icon;
std::move(callback).Run(std::move(iv));
return;
}
if (default_icon_resource) {
LoadIconFromResource(apps::mojom::IconCompression::kCompressed,
size_hint_in_dip, default_icon_resource,
is_placeholder_icon, icon_effects,
std::move(callback));
return;
}
std::move(callback).Run(apps::mojom::IconValue::New());
}
// Like RunCallbackWithCompressedData, but calls "fallback(callback)" if the
// data is empty.
void RunCallbackWithCompressedDataWithFallback(
int size_hint_in_dip,
bool is_placeholder_icon,
apps::IconEffects icon_effects,
apps::mojom::Publisher::LoadIconCallback callback,
base::OnceCallback<void(apps::mojom::Publisher::LoadIconCallback)> fallback,
std::vector<uint8_t> data) {
if (data.empty()) {
std::move(fallback).Run(std::move(callback));
return;
}
constexpr int default_icon_resource = 0;
RunCallbackWithCompressedData(size_hint_in_dip, default_icon_resource,
is_placeholder_icon, icon_effects,
std::move(callback), std::move(data));
}
// Runs |callback| passing an IconValuePtr with an uncompressed image: a
// SkBitmap.
void RunCallbackWithUncompressedSkBitmap(
int size_hint_in_dip,
bool is_placeholder_icon,
apps::IconEffects icon_effects,
apps::mojom::Publisher::LoadIconCallback callback,
const SkBitmap& bitmap) {
apps::mojom::IconValuePtr iv = apps::mojom::IconValue::New();
iv->icon_compression = apps::mojom::IconCompression::kUncompressed;
iv->uncompressed = gfx::ImageSkia(gfx::ImageSkiaRep(bitmap, 0.0f));
iv->is_placeholder_icon = is_placeholder_icon;
if (icon_effects && !iv->uncompressed.isNull()) {
ApplyIconEffects(icon_effects, size_hint_in_dip, &iv->uncompressed);
}
std::move(callback).Run(std::move(iv));
}
// Runs |callback| after converting (in a separate sandboxed process) from a
// std::vector<uint8_t> to a SkBitmap. It calls "fallback(callback)" if the
// data is empty.
void RunCallbackWithCompressedDataToUncompressWithFallback(
int size_hint_in_dip,
bool is_placeholder_icon,
apps::IconEffects icon_effects,
apps::mojom::Publisher::LoadIconCallback callback,
base::OnceCallback<void(apps::mojom::Publisher::LoadIconCallback)> fallback,
std::vector<uint8_t> data) {
if (data.empty()) {
std::move(fallback).Run(std::move(callback));
return;
}
data_decoder::DecodeImage(
content::ServiceManagerConnection::GetForProcess()->GetConnector(), data,
data_decoder::mojom::ImageCodec::DEFAULT, false,
data_decoder::kDefaultMaxSizeInBytes, gfx::Size(),
base::BindOnce(&RunCallbackWithUncompressedSkBitmap, size_hint_in_dip,
is_placeholder_icon, icon_effects, std::move(callback)));
}
// Runs |callback| passing an IconValuePtr with an uncompressed image: an
// ImageSkia.
//
// It will fall back to the |default_icon_resource| if the image is null.
void RunCallbackWithUncompressedImageSkia(
int size_hint_in_dip,
int default_icon_resource,
bool is_placeholder_icon,
apps::IconEffects icon_effects,
apps::mojom::Publisher::LoadIconCallback callback,
const gfx::ImageSkia image) {
if (!image.isNull()) {
apps::mojom::IconValuePtr iv = apps::mojom::IconValue::New();
iv->icon_compression = apps::mojom::IconCompression::kUncompressed;
iv->uncompressed = image;
iv->is_placeholder_icon = is_placeholder_icon;
if (icon_effects && !iv->uncompressed.isNull()) {
ApplyIconEffects(icon_effects, size_hint_in_dip, &iv->uncompressed);
}
std::move(callback).Run(std::move(iv));
return;
}
if (default_icon_resource) {
LoadIconFromResource(apps::mojom::IconCompression::kUncompressed,
size_hint_in_dip, default_icon_resource,
is_placeholder_icon, icon_effects,
std::move(callback));
return;
}
std::move(callback).Run(apps::mojom::IconValue::New());
}
// Runs |callback| passing an IconValuePtr with an uncompressed image: an
// Image.
void RunCallbackWithUncompressedImage(
int size_hint_in_dip,
int default_icon_resource,
bool is_placeholder_icon,
apps::IconEffects icon_effects,
apps::mojom::Publisher::LoadIconCallback callback,
const gfx::Image& image) {
RunCallbackWithUncompressedImageSkia(
size_hint_in_dip, default_icon_resource, is_placeholder_icon,
icon_effects, std::move(callback), image.AsImageSkia());
}
} // namespace
namespace apps {
void LoadIconFromExtension(apps::mojom::IconCompression icon_compression,
int size_hint_in_dip,
content::BrowserContext* context,
const std::string& extension_id,
IconEffects icon_effects,
apps::mojom::Publisher::LoadIconCallback callback) {
constexpr bool is_placeholder_icon = false;
int size_hint_in_px = apps_util::ConvertDipToPx(size_hint_in_dip);
// This is the default icon for AppType::kExtension. Other app types might
// use a different default icon, such as IDR_LOGO_CROSTINI_DEFAULT_192.
constexpr int default_icon_resource = IDR_APP_DEFAULT_ICON;
const extensions::Extension* extension =
extensions::ExtensionSystem::Get(context)
->extension_service()
->GetInstalledExtension(extension_id);
if (extension) {
extensions::ExtensionResource ext_resource =
extensions::IconsInfo::GetIconResource(extension, size_hint_in_px,
ExtensionIconSet::MATCH_BIGGER);
switch (icon_compression) {
case apps::mojom::IconCompression::kUnknown:
break;
case apps::mojom::IconCompression::kUncompressed: {
extensions::ImageLoader::Get(context)->LoadImageAsync(
extension, std::move(ext_resource),
gfx::Size(size_hint_in_px, size_hint_in_px),
base::BindOnce(&RunCallbackWithUncompressedImage, size_hint_in_dip,
default_icon_resource, is_placeholder_icon,
icon_effects, std::move(callback)));
return;
}
case apps::mojom::IconCompression::kCompressed: {
// Load some component extensions' icons from statically compiled
// resources (built into the Chrome binary), and other extensions'
// icons (whether component extensions or otherwise) from files on
// disk.
//
// For the kUncompressed case above, RunCallbackWithUncompressedImage
// calls extensions::ImageLoader::LoadImageAsync, which already handles
// that distinction. We can't use LoadImageAsync here, because the
// caller has asked for compressed icons (i.e. PNG-formatted data), not
// uncompressed (i.e. a gfx::ImageSkia).
if (extension->location() == extensions::Manifest::COMPONENT) {
int resource_id = 0;
const extensions::ComponentExtensionResourceManager* manager =
extensions::ExtensionsBrowserClient::Get()
->GetComponentExtensionResourceManager();
if (manager && manager->IsComponentExtensionResource(
extension->path(), ext_resource.relative_path(),
&resource_id)) {
base::StringPiece data =
ui::ResourceBundle::GetSharedInstance().GetRawDataResource(
resource_id);
RunCallbackWithCompressedData(
size_hint_in_dip, default_icon_resource, is_placeholder_icon,
icon_effects, std::move(callback),
std::vector<uint8_t>(data.begin(), data.end()));
return;
}
}
// Try and load data from the resource file.
base::PostTaskWithTraitsAndReplyWithResult(
FROM_HERE, {base::MayBlock(), base::TaskPriority::USER_VISIBLE},
base::BindOnce(&CompressedDataFromResource,
std::move(ext_resource)),
base::BindOnce(&RunCallbackWithCompressedData, size_hint_in_dip,
default_icon_resource, is_placeholder_icon,
icon_effects, std::move(callback)));
return;
}
}
}
// Fall back to the default_icon_resource.
LoadIconFromResource(icon_compression, size_hint_in_dip,
default_icon_resource, is_placeholder_icon, icon_effects,
std::move(callback));
}
void LoadIconFromFileWithFallback(
apps::mojom::IconCompression icon_compression,
int size_hint_in_dip,
const base::FilePath& path,
IconEffects icon_effects,
apps::mojom::Publisher::LoadIconCallback callback,
base::OnceCallback<void(apps::mojom::Publisher::LoadIconCallback)>
fallback) {
constexpr bool is_placeholder_icon = false;
switch (icon_compression) {
case apps::mojom::IconCompression::kUnknown:
break;
case apps::mojom::IconCompression::kUncompressed: {
base::PostTaskWithTraitsAndReplyWithResult(
FROM_HERE, {base::MayBlock(), base::TaskPriority::USER_VISIBLE},
base::BindOnce(&ReadFileAsCompressedData, path),
base::BindOnce(&RunCallbackWithCompressedDataToUncompressWithFallback,
size_hint_in_dip, is_placeholder_icon, icon_effects,
std::move(callback), std::move(fallback)));
return;
}
case apps::mojom::IconCompression::kCompressed: {
base::PostTaskWithTraitsAndReplyWithResult(
FROM_HERE, {base::MayBlock(), base::TaskPriority::USER_VISIBLE},
base::BindOnce(&ReadFileAsCompressedData, path),
base::BindOnce(&RunCallbackWithCompressedDataWithFallback,
size_hint_in_dip, is_placeholder_icon, icon_effects,
std::move(callback), std::move(fallback)));
return;
}
}
std::move(callback).Run(apps::mojom::IconValue::New());
}
void LoadIconFromResource(apps::mojom::IconCompression icon_compression,
int size_hint_in_dip,
int resource_id,
bool is_placeholder_icon,
IconEffects icon_effects,
apps::mojom::Publisher::LoadIconCallback callback) {
// This must be zero, to avoid a potential infinite loop if the
// RunCallbackWithXxx functions could otherwise call back into
// LoadIconFromResource.
constexpr int default_icon_resource = 0;
if (resource_id != 0) {
switch (icon_compression) {
case apps::mojom::IconCompression::kUnknown:
break;
case apps::mojom::IconCompression::kUncompressed: {
gfx::ImageSkia* unscaled =
ui::ResourceBundle::GetSharedInstance().GetImageSkiaNamed(
resource_id);
RunCallbackWithUncompressedImageSkia(
size_hint_in_dip, default_icon_resource, is_placeholder_icon,
icon_effects, std::move(callback),
gfx::ImageSkiaOperations::CreateResizedImage(
*unscaled, skia::ImageOperations::RESIZE_BEST,
gfx::Size(size_hint_in_dip, size_hint_in_dip)));
return;
}
case apps::mojom::IconCompression::kCompressed: {
base::StringPiece data =
ui::ResourceBundle::GetSharedInstance().GetRawDataResource(
resource_id);
RunCallbackWithCompressedData(
size_hint_in_dip, default_icon_resource, is_placeholder_icon,
icon_effects, std::move(callback),
std::vector<uint8_t>(data.begin(), data.end()));
return;
}
}
}
std::move(callback).Run(apps::mojom::IconValue::New());
}
} // namespace apps