// Copyright 2022 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/crosapi/wallpaper_ash.h"
#include <string>
#include <vector>
#include "base/check.h"
#include "base/functional/bind.h"
#include "base/metrics/histogram_macros.h"
#include "base/notreached.h"
#include "chrome/browser/ash/profiles/profile_helper.h"
#include "chrome/browser/profiles/profile_manager.h"
#include "chrome/browser/ui/ash/wallpaper/wallpaper_controller_client_impl.h"
#include "chromeos/ash/components/login/login_state/login_state.h"
#include "components/account_id/account_id.h"
#include "components/user_manager/user.h"
#include "content/public/browser/browser_thread.h"
#include "extensions/browser/extension_function_crash_keys.h"
#include "services/data_decoder/public/cpp/decode_image.h"
#include "ui/gfx/codec/jpeg_codec.h"
#include "ui/gfx/image/image_skia.h"
#include "ui/gfx/image/image_skia_operations.h"
using content::BrowserThread;
namespace {
ash::WallpaperLayout GetLayoutEnum(crosapi::mojom::WallpaperLayout layout) {
switch (layout) {
case crosapi::mojom::WallpaperLayout::kStretch:
return ash::WALLPAPER_LAYOUT_STRETCH;
case crosapi::mojom::WallpaperLayout::kCenter:
return ash::WALLPAPER_LAYOUT_CENTER;
case crosapi::mojom::WallpaperLayout::kCenterCropped:
return ash::WALLPAPER_LAYOUT_CENTER_CROPPED;
default:
return ash::WALLPAPER_LAYOUT_CENTER;
}
}
constexpr int kWallpaperThumbnailWidth = 108;
constexpr int kWallpaperThumbnailHeight = 68;
// Returns an image of |size| that contains as much of |image| as possible
// without distorting the |image|. Unused areas are cropped away.
gfx::ImageSkia ScaleAspectRatioAndCropCenter(const gfx::Size& size,
const gfx::ImageSkia& image) {
float scale = std::min(static_cast<float>(image.width()) / size.width(),
static_cast<float>(image.height()) / size.height());
gfx::Size scaled_size = {
std::max(1, base::ClampFloor(scale * size.width())),
std::max(1, base::ClampFloor(scale * size.height()))};
gfx::Rect bounds{{0, 0}, image.size()};
bounds.ClampToCenteredSize(scaled_size);
auto scaled_and_cropped_image = gfx::ImageSkiaOperations::CreateTiledImage(
image, bounds.x(), bounds.y(), bounds.width(), bounds.height());
return gfx::ImageSkiaOperations::CreateResizedImage(
scaled_and_cropped_image, skia::ImageOperations::RESIZE_LANCZOS3, size);
}
const int kThumbnailEncodeQuality = 90;
void RecordCustomWallpaperLayout(const ash::WallpaperLayout& layout) {
UMA_HISTOGRAM_ENUMERATION("Ash.Wallpaper.CustomLayout", layout,
ash::NUM_WALLPAPER_LAYOUT);
}
std::vector<uint8_t> GenerateThumbnail(const gfx::ImageSkia& image,
const gfx::Size& size) {
std::vector<uint8_t> data_out;
gfx::JPEGCodec::Encode(*ScaleAspectRatioAndCropCenter(size, image).bitmap(),
kThumbnailEncodeQuality, &data_out);
return data_out;
}
} // namespace
namespace crosapi {
WallpaperAsh::WallpaperAsh() = default;
WallpaperAsh::~WallpaperAsh() = default;
void WallpaperAsh::BindReceiver(
mojo::PendingReceiver<mojom::Wallpaper> pending_receiver) {
receivers_.Add(this, std::move(pending_receiver));
}
void WallpaperAsh::SetWallpaperDeprecated(
mojom::WallpaperSettingsPtr wallpaper_settings,
const std::string& extension_id,
const std::string& extension_name,
SetWallpaperDeprecatedCallback callback) {
// Delete this method once deletion is supported. https://crbug.com/1156872.
NOTIMPLEMENTED();
}
void WallpaperAsh::SetWallpaper(mojom::WallpaperSettingsPtr wallpaper_settings,
const std::string& extension_id,
const std::string& extension_name,
SetWallpaperCallback callback) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
CHECK(ash::LoginState::Get()->IsUserLoggedIn());
// Prevent any in progress decodes from changing wallpaper.
weak_ptr_factory_.InvalidateWeakPtrs();
// Notify the last pending request, if any, that it is canceled.
if (pending_callback_) {
SendErrorResult(
"Received a new SetWallpaper request that overrides this one.");
}
extension_id_ = extension_id;
extensions::extension_function_crash_keys::StartExtensionFunctionCall(
extension_id_);
pending_callback_ = std::move(callback);
const std::vector<uint8_t>& data = wallpaper_settings->data;
data_decoder::DecodeImage(
&data_decoder_, data, data_decoder::mojom::ImageCodec::kDefault,
/*shrink_to_fit=*/true, data_decoder::kDefaultMaxSizeInBytes,
/*desired_image_frame_size=*/gfx::Size(),
base::BindOnce(&WallpaperAsh::OnWallpaperDecoded,
weak_ptr_factory_.GetWeakPtr(),
std::move(wallpaper_settings)));
}
void WallpaperAsh::OnWallpaperDecoded(
mojom::WallpaperSettingsPtr wallpaper_settings,
const SkBitmap& bitmap) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
if (bitmap.isNull()) {
LOG(ERROR) << "Decoding wallpaper data failed from extension_id '"
<< extension_id_ << "'";
SendErrorResult("Decoding wallpaper data failed.");
return;
}
ash::WallpaperLayout layout = GetLayoutEnum(wallpaper_settings->layout);
RecordCustomWallpaperLayout(layout);
Profile* profile = ProfileManager::GetPrimaryUserProfile();
const user_manager::User* user =
ash::ProfileHelper::Get()->GetUserByProfile(profile);
auto account_id = user->GetAccountId();
const std::string file_name =
base::FilePath(wallpaper_settings->filename).BaseName().value();
// Make the SkBitmap immutable as we won't modify it. This is important
// because otherwise it gets duplicated during painting, wasting memory.
SkBitmap immutable_bitmap(bitmap);
immutable_bitmap.setImmutable();
gfx::ImageSkia image = gfx::ImageSkia::CreateFrom1xBitmap(immutable_bitmap);
image.MakeThreadSafe();
bool success = WallpaperControllerClientImpl::Get()->SetThirdPartyWallpaper(
account_id, file_name, layout, image);
if (!success) {
const std::string error =
"Setting the wallpaper failed due to user permissions.";
LOG(ERROR) << error;
SendErrorResult(error);
return;
}
// We need to generate thumbnail image anyway to make the current third party
// wallpaper syncable through different devices.
image.EnsureRepsForSupportedScales();
std::vector<uint8_t> thumbnail_data = GenerateThumbnail(
image, gfx::Size(kWallpaperThumbnailWidth, kWallpaperThumbnailHeight));
SendSuccessResult(thumbnail_data);
}
void WallpaperAsh::SendErrorResult(const std::string& response) {
std::move(pending_callback_)
.Run(crosapi::mojom::SetWallpaperResult::NewErrorMessage(response));
extensions::extension_function_crash_keys::EndExtensionFunctionCall(
extension_id_);
extension_id_.clear();
}
void WallpaperAsh::SendSuccessResult(
const std::vector<uint8_t>& thumbnail_data) {
std::move(pending_callback_)
.Run(
crosapi::mojom::SetWallpaperResult::NewThumbnailData(thumbnail_data));
extensions::extension_function_crash_keys::EndExtensionFunctionCall(
extension_id_);
extension_id_.clear();
}
} // namespace crosapi