blob: c1c991561f4a755d8619f199456eb5a349a136ae [file] [log] [blame]
// Copyright 2015 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/ui/webui/settings/chromeos/change_picture_handler.h"
#include <memory>
#include <utility>
#include "ash/components/audio/sounds.h"
#include "base/base64.h"
#include "base/bind.h"
#include "base/callback_helpers.h"
#include "base/command_line.h"
#include "base/cxx17_backports.h"
#include "base/metrics/histogram_functions.h"
#include "base/metrics/histogram_macros.h"
#include "base/no_destructor.h"
#include "base/path_service.h"
#include "base/strings/string_util.h"
#include "base/strings/utf_string_conversions.h"
#include "base/task/post_task.h"
#include "base/task/thread_pool.h"
#include "base/values.h"
#include "chrome/browser/ash/accessibility/accessibility_manager.h"
#include "chrome/browser/ash/camera_presence_notifier.h"
#include "chrome/browser/ash/login/users/avatar/user_image_manager.h"
#include "chrome/browser/ash/login/users/chrome_user_manager.h"
#include "chrome/browser/ash/login/users/default_user_image/default_user_images.h"
#include "chrome/browser/ash/profiles/profile_helper.h"
#include "chrome/browser/ui/browser_finder.h"
#include "chrome/browser/ui/browser_window.h"
#include "chrome/browser/ui/chrome_select_file_policy.h"
#include "chrome/common/chrome_paths.h"
#include "chrome/common/chrome_switches.h"
#include "chrome/common/url_constants.h"
#include "chrome/grit/browser_resources.h"
#include "chrome/grit/generated_resources.h"
#include "components/user_manager/user.h"
#include "components/user_manager/user_image/user_image.h"
#include "components/user_manager/user_manager.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/web_ui.h"
#include "content/public/common/url_constants.h"
#include "net/base/data_url.h"
#include "services/audio/public/cpp/sounds/sounds_manager.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/resource/resource_bundle.h"
#include "ui/base/webui/web_ui_util.h"
#include "ui/views/widget/widget.h"
#include "url/gurl.h"
namespace chromeos {
namespace settings {
namespace {
using ::ash::AccessibilityManager;
using ::ash::PlaySoundOption;
using ::content::BrowserThread;
void RecordUserImageChanged(int sample) {
// Although |ChangePictureHandler::kUserImageChangedHistogramName| is an
// enumerated histogram, we intentionally use UmaHistogramExactLinear() to
// emit the metric rather than UmaHistogramEnumeration(). This is because the
// enums.xml values correspond to (a) special constants and (b) indexes of an
// array containing resource IDs.
base::UmaHistogramExactLinear(
ChangePictureHandler::kUserImageChangedHistogramName, sample,
default_user_image::kHistogramImagesCount + 1);
}
} // namespace
const char ChangePictureHandler::kUserImageChangedHistogramName[] =
"UserImage.Changed2";
ChangePictureHandler::ChangePictureHandler()
: previous_image_index_(user_manager::User::USER_IMAGE_INVALID) {
ui::ResourceBundle& bundle = ui::ResourceBundle::GetSharedInstance();
audio::SoundsManager* manager = audio::SoundsManager::Get();
manager->Initialize(static_cast<int>(Sound::kObjectDelete),
bundle.GetRawDataResource(IDR_SOUND_OBJECT_DELETE_WAV));
manager->Initialize(static_cast<int>(Sound::kCameraSnap),
bundle.GetRawDataResource(IDR_SOUND_CAMERA_SNAP_WAV));
}
ChangePictureHandler::~ChangePictureHandler() = default;
void ChangePictureHandler::RegisterMessages() {
web_ui()->RegisterMessageCallback(
"chooseFile", base::BindRepeating(&ChangePictureHandler::HandleChooseFile,
base::Unretained(this)));
web_ui()->RegisterMessageCallback(
"photoTaken", base::BindRepeating(&ChangePictureHandler::HandlePhotoTaken,
base::Unretained(this)));
web_ui()->RegisterMessageCallback(
"discardPhoto",
base::BindRepeating(&ChangePictureHandler::HandleDiscardPhoto,
base::Unretained(this)));
web_ui()->RegisterMessageCallback(
"onChangePicturePageInitialized",
base::BindRepeating(&ChangePictureHandler::HandlePageInitialized,
base::Unretained(this)));
web_ui()->RegisterMessageCallback(
"selectImage",
base::BindRepeating(&ChangePictureHandler::HandleSelectImage,
base::Unretained(this)));
web_ui()->RegisterMessageCallback(
"requestSelectedImage",
base::BindRepeating(&ChangePictureHandler::HandleRequestSelectedImage,
base::Unretained(this)));
}
void ChangePictureHandler::OnJavascriptAllowed() {
user_manager_observation_.Observe(user_manager::UserManager::Get());
camera_observation_.Observe(CameraPresenceNotifier::GetInstance());
}
void ChangePictureHandler::OnJavascriptDisallowed() {
DCHECK(user_manager_observation_.IsObservingSource(
user_manager::UserManager::Get()));
user_manager_observation_.Reset();
DCHECK(camera_observation_.IsObservingSource(
CameraPresenceNotifier::GetInstance()));
camera_observation_.Reset();
user_image_file_selector_.reset();
}
void ChangePictureHandler::SendDefaultImages() {
base::DictionaryValue result;
std::unique_ptr<base::ListValue> current_default_images =
default_user_image::GetCurrentImageSetAsListValue();
result.SetKey(
"current_default_images",
base::Value::FromUniquePtrValue(std::move(current_default_images)));
FireWebUIListener("default-images-changed", result);
}
void ChangePictureHandler::HandleChooseFile(base::Value::ConstListView args) {
DCHECK(args.empty());
user_image_file_selector_ =
std::make_unique<ash::UserImageFileSelector>(web_ui());
user_image_file_selector_->SelectFile(
base::BindOnce(&ChangePictureHandler::FileSelected,
weak_ptr_factory_.GetWeakPtr()),
base::BindOnce(&ChangePictureHandler::FileSelectionCanceled,
weak_ptr_factory_.GetWeakPtr()));
}
void ChangePictureHandler::HandleDiscardPhoto(base::Value::ConstListView args) {
DCHECK(args.empty());
AccessibilityManager::Get()->PlayEarcon(
Sound::kObjectDelete, PlaySoundOption::kOnlyIfSpokenFeedbackEnabled);
}
void ChangePictureHandler::HandlePhotoTaken(base::Value::ConstListView args) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
AccessibilityManager::Get()->PlayEarcon(
Sound::kCameraSnap, PlaySoundOption::kOnlyIfSpokenFeedbackEnabled);
if (args.size() != 1 || !args[0].is_string())
NOTREACHED();
const std::string& image_url = args[0].GetString();
DCHECK(!image_url.empty());
std::string raw_data;
base::StringPiece url(image_url);
const char kDataUrlPrefix[] = "data:image/png;base64,";
const size_t kDataUrlPrefixLength = base::size(kDataUrlPrefix) - 1;
if (!base::StartsWith(url, kDataUrlPrefix) ||
!base::Base64Decode(url.substr(kDataUrlPrefixLength), &raw_data)) {
LOG(WARNING) << "Invalid image URL";
return;
}
// Use |raw_data| as image but first verify that it can be decoded.
user_photo_ = gfx::ImageSkia();
std::vector<unsigned char> photo_data(raw_data.begin(), raw_data.end());
user_photo_data_ = base::RefCountedBytes::TakeVector(&photo_data);
ImageDecoder::Cancel(this);
ImageDecoder::Start(this, std::move(raw_data));
}
void ChangePictureHandler::HandlePageInitialized(
base::Value::ConstListView args) {
DCHECK(args.empty());
AllowJavascript();
SendDefaultImages();
SendSelectedImage();
UpdateProfileImage();
}
void ChangePictureHandler::SendSelectedImage() {
const user_manager::User* user = GetUser();
DCHECK(user->GetAccountId().is_valid());
previous_image_index_ = user->image_index();
switch (previous_image_index_) {
case user_manager::User::USER_IMAGE_EXTERNAL: {
// User has image from camera/file, record it and add to the image list.
previous_image_ = user->GetImage();
previous_image_format_ = user->image_format();
if (previous_image_format_ == user_manager::UserImage::FORMAT_PNG &&
user->has_image_bytes()) {
previous_image_bytes_ = user->image_bytes();
SendOldImage(webui::GetPngDataUrl(previous_image_bytes_->front(),
previous_image_bytes_->size()));
} else {
previous_image_bytes_ = nullptr;
DCHECK(previous_image_.IsThreadSafe());
// Post a task because GetBitmapDataUrl does PNG encoding, which is
// slow for large images.
base::ThreadPool::PostTaskAndReplyWithResult(
FROM_HERE, {base::TaskPriority::USER_BLOCKING},
base::BindOnce(&webui::GetBitmapDataUrl, *previous_image_.bitmap()),
base::BindOnce(&ChangePictureHandler::SendOldImage,
weak_ptr_factory_.GetWeakPtr()));
}
break;
}
case user_manager::User::USER_IMAGE_PROFILE: {
// User has their Profile image as the current image.
SendProfileImage(user->GetImage(), true);
break;
}
default: {
if (default_user_image::IsInCurrentImageSet(previous_image_index_)) {
// User has image from the current set of default images.
base::Value image_url(
default_user_image::GetDefaultImageUrl(previous_image_index_)
.spec());
FireWebUIListener("selected-image-changed", image_url);
} else {
// User has a deprecated default image, send it for preview.
previous_image_ = user->GetImage();
previous_image_bytes_ = nullptr;
previous_image_format_ = user_manager::UserImage::FORMAT_UNKNOWN;
base::DictionaryValue result;
result.SetStringPath(
"url", default_user_image::GetDefaultImageUrl(previous_image_index_)
.spec());
auto source_info = default_user_image::GetDefaultImageSourceInfo(
previous_image_index_);
if (source_info.has_value()) {
result.SetStringPath("author", l10n_util::GetStringUTF16(std::move(
source_info.value().author_id)));
result.SetStringPath("website", l10n_util::GetStringUTF16(std::move(
source_info.value().website_id)));
}
FireWebUIListener("preview-deprecated-image", result);
}
}
}
}
void ChangePictureHandler::SendProfileImage(const gfx::ImageSkia& image,
bool should_select) {
base::Value data_url(webui::GetBitmapDataUrl(*image.bitmap()));
base::Value select(should_select);
FireWebUIListener("profile-image-changed", data_url, select);
}
void ChangePictureHandler::UpdateProfileImage() {
auto* user_image_manager =
ChromeUserManager::Get()->GetUserImageManager(GetUser()->GetAccountId());
// If we have a downloaded profile image and haven't sent it in
// |SendSelectedImage|, send it now (without selecting).
if (previous_image_index_ != user_manager::User::USER_IMAGE_PROFILE &&
!user_image_manager->DownloadedProfileImage().isNull()) {
SendProfileImage(user_image_manager->DownloadedProfileImage(), false);
}
user_image_manager->DownloadProfileImage();
}
void ChangePictureHandler::SendOldImage(std::string&& image_url) {
FireWebUIListener("old-image-changed", base::Value(image_url));
}
void ChangePictureHandler::HandleSelectImage(base::Value::ConstListView args) {
if (args.size() != 2 || !args[0].is_string() || !args[1].is_string()) {
NOTREACHED();
return;
}
const std::string& image_url = args[0].GetString();
const std::string& image_type = args[1].GetString();
// |image_url| may be empty unless |image_type| is "default".
DCHECK(!image_type.empty());
auto* user_image_manager =
ChromeUserManager::Get()->GetUserImageManager(GetUser()->GetAccountId());
bool waiting_for_camera_photo = false;
// Track the index of previous selected message to be compared with the index
// of the new image.
int previous_image_index = GetUser()->image_index();
if (image_type == "old") {
// Previous image (from camera or manually uploaded) re-selected.
DCHECK(!previous_image_.isNull());
std::unique_ptr<user_manager::UserImage> user_image;
if (previous_image_format_ == user_manager::UserImage::FORMAT_PNG &&
previous_image_bytes_) {
user_image = std::make_unique<user_manager::UserImage>(
previous_image_, previous_image_bytes_, previous_image_format_);
user_image->MarkAsSafe();
} else {
user_image = user_manager::UserImage::CreateAndEncode(
previous_image_, user_manager::UserImage::FORMAT_JPEG);
}
user_image_manager->SaveUserImage(std::move(user_image));
VLOG(1) << "Selected old user image";
} else if (image_type == "default") {
int image_index = user_manager::User::USER_IMAGE_INVALID;
if (default_user_image::IsDefaultImageUrl(image_url, &image_index)) {
// One of the default user images.
user_image_manager->SaveUserDefaultImageIndex(image_index);
VLOG(1) << "Selected default user image: " << image_index;
} else {
LOG(WARNING) << "Invalid image_url for default image type: " << image_url;
}
} else if (image_type == "profile") {
// Profile image selected. Could be previous (old) user image.
user_image_manager->SaveUserImageFromProfileImage();
} else {
NOTREACHED() << "Unexpected image type: " << image_type;
}
int image_index = GetUser()->image_index();
// `previous_image_index` is used instead of `previous_image_index_` as the
// latter has the same value of `image_index` after new image is selected.
if (previous_image_index != image_index) {
RecordUserImageChanged(
user_image_manager->ImageIndexToHistogramIndex(image_index));
}
// Ignore the result of the previous decoding if it's no longer needed.
if (!waiting_for_camera_photo)
ImageDecoder::Cancel(this);
}
void ChangePictureHandler::HandleRequestSelectedImage(
base::Value::ConstListView args) {
SendSelectedImage();
}
void ChangePictureHandler::FileSelected(const base::FilePath& path) {
auto* user_image_manager =
ChromeUserManager::Get()->GetUserImageManager(GetUser()->GetAccountId());
// Log an impression if image is selected from a file.
RecordUserImageChanged(user_image_manager->ImageIndexToHistogramIndex(
user_manager::User::USER_IMAGE_EXTERNAL));
user_image_manager->SaveUserImageFromFile(path);
VLOG(1) << "Selected image from file";
}
void ChangePictureHandler::FileSelectionCanceled() {
SendSelectedImage();
}
void ChangePictureHandler::SetImageFromCamera(
const gfx::ImageSkia& photo,
base::RefCountedBytes* photo_bytes) {
std::unique_ptr<user_manager::UserImage> user_image =
std::make_unique<user_manager::UserImage>(
photo, photo_bytes, user_manager::UserImage::FORMAT_PNG);
user_image->MarkAsSafe();
ChromeUserManager::Get()
->GetUserImageManager(GetUser()->GetAccountId())
->SaveUserImage(std::move(user_image));
// Log an impression if image is taken from photo.
RecordUserImageChanged(default_user_image::kHistogramImageFromCamera);
VLOG(1) << "Selected camera photo";
}
void ChangePictureHandler::SetCameraPresent(bool present) {
FireWebUIListener("camera-presence-changed", base::Value(present));
}
void ChangePictureHandler::OnCameraPresenceCheckDone(bool is_camera_present) {
SetCameraPresent(is_camera_present);
}
void ChangePictureHandler::OnUserImageChanged(const user_manager::User& user) {
// Not initialized yet.
if (previous_image_index_ == user_manager::User::USER_IMAGE_INVALID)
return;
SendSelectedImage();
}
void ChangePictureHandler::OnUserProfileImageUpdated(
const user_manager::User& user,
const gfx::ImageSkia& profile_image) {
// User profile image has been updated.
SendProfileImage(profile_image, false);
}
void ChangePictureHandler::OnImageDecoded(const SkBitmap& decoded_image) {
user_photo_ = gfx::ImageSkia::CreateFrom1xBitmap(decoded_image);
SetImageFromCamera(user_photo_, user_photo_data_.get());
}
void ChangePictureHandler::OnDecodeImageFailed() {
NOTREACHED() << "Failed to decode PNG image from WebUI";
}
const user_manager::User* ChangePictureHandler::GetUser() {
Profile* profile = Profile::FromWebUI(web_ui());
const user_manager::User* user =
ProfileHelper::Get()->GetUserByProfile(profile);
if (!user)
return user_manager::UserManager::Get()->GetActiveUser();
return user;
}
} // namespace settings
} // namespace chromeos