1 // Copyright 2015 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4
5 #include "chrome/browser/ui/webui/settings/chromeos/change_picture_handler.h"
6
7 #include <memory>
8 #include <utility>
9
10 #include "base/base64.h"
11 #include "base/bind.h"
12 #include "base/callback_helpers.h"
13 #include "base/command_line.h"
14 #include "base/metrics/histogram_macros.h"
15 #include "base/no_destructor.h"
16 #include "base/path_service.h"
17 #include "base/stl_util.h"
18 #include "base/strings/string_util.h"
19 #include "base/strings/utf_string_conversions.h"
20 #include "base/task/post_task.h"
21 #include "base/task/thread_pool.h"
22 #include "base/values.h"
23 #include "chrome/browser/chromeos/accessibility/accessibility_manager.h"
24 #include "chrome/browser/chromeos/camera_presence_notifier.h"
25 #include "chrome/browser/chromeos/login/users/avatar/user_image_manager.h"
26 #include "chrome/browser/chromeos/login/users/chrome_user_manager.h"
27 #include "chrome/browser/chromeos/login/users/default_user_image/default_user_images.h"
28 #include "chrome/browser/chromeos/profiles/profile_helper.h"
29 #include "chrome/browser/ui/browser_finder.h"
30 #include "chrome/browser/ui/browser_window.h"
31 #include "chrome/browser/ui/chrome_select_file_policy.h"
32 #include "chrome/common/chrome_paths.h"
33 #include "chrome/common/chrome_switches.h"
34 #include "chrome/common/url_constants.h"
35 #include "chrome/grit/browser_resources.h"
36 #include "chrome/grit/generated_resources.h"
37 #include "chromeos/audio/chromeos_sounds.h"
38 #include "components/user_manager/user.h"
39 #include "components/user_manager/user_image/user_image.h"
40 #include "components/user_manager/user_manager.h"
41 #include "content/public/browser/browser_thread.h"
42 #include "content/public/browser/web_ui.h"
43 #include "content/public/common/url_constants.h"
44 #include "net/base/data_url.h"
45 #include "services/audio/public/cpp/sounds/sounds_manager.h"
46 #include "ui/base/l10n/l10n_util.h"
47 #include "ui/base/resource/resource_bundle.h"
48 #include "ui/base/webui/web_ui_util.h"
49 #include "ui/views/widget/widget.h"
50 #include "url/gurl.h"
51
52 using content::BrowserThread;
53
54 namespace chromeos {
55 namespace settings {
56
57 namespace {
58
59 // Returns info about extensions for files we support as user images.
GetUserImageFileTypeInfo()60 ui::SelectFileDialog::FileTypeInfo GetUserImageFileTypeInfo() {
61 ui::SelectFileDialog::FileTypeInfo file_type_info;
62 file_type_info.extensions.resize(1);
63
64 file_type_info.extensions[0].push_back(FILE_PATH_LITERAL("bmp"));
65
66 file_type_info.extensions[0].push_back(FILE_PATH_LITERAL("jpg"));
67 file_type_info.extensions[0].push_back(FILE_PATH_LITERAL("jpeg"));
68
69 file_type_info.extensions[0].push_back(FILE_PATH_LITERAL("png"));
70
71 file_type_info.extensions[0].push_back(FILE_PATH_LITERAL("tif"));
72 file_type_info.extensions[0].push_back(FILE_PATH_LITERAL("tiff"));
73
74 file_type_info.extension_description_overrides.resize(1);
75 file_type_info.extension_description_overrides[0] =
76 l10n_util::GetStringUTF16(IDS_IMAGE_FILES);
77
78 return file_type_info;
79 }
80
81 } // namespace
82
ChangePictureHandler()83 ChangePictureHandler::ChangePictureHandler()
84 : previous_image_index_(user_manager::User::USER_IMAGE_INVALID) {
85 ui::ResourceBundle& bundle = ui::ResourceBundle::GetSharedInstance();
86 audio::SoundsManager* manager = audio::SoundsManager::Get();
87 manager->Initialize(SOUND_OBJECT_DELETE,
88 bundle.GetRawDataResource(IDR_SOUND_OBJECT_DELETE_WAV));
89 manager->Initialize(SOUND_CAMERA_SNAP,
90 bundle.GetRawDataResource(IDR_SOUND_CAMERA_SNAP_WAV));
91 }
92
~ChangePictureHandler()93 ChangePictureHandler::~ChangePictureHandler() {
94 if (select_file_dialog_.get())
95 select_file_dialog_->ListenerDestroyed();
96 }
97
RegisterMessages()98 void ChangePictureHandler::RegisterMessages() {
99 web_ui()->RegisterMessageCallback(
100 "chooseFile", base::BindRepeating(&ChangePictureHandler::HandleChooseFile,
101 base::Unretained(this)));
102 web_ui()->RegisterMessageCallback(
103 "photoTaken", base::BindRepeating(&ChangePictureHandler::HandlePhotoTaken,
104 base::Unretained(this)));
105 web_ui()->RegisterMessageCallback(
106 "discardPhoto",
107 base::BindRepeating(&ChangePictureHandler::HandleDiscardPhoto,
108 base::Unretained(this)));
109 web_ui()->RegisterMessageCallback(
110 "onChangePicturePageInitialized",
111 base::BindRepeating(&ChangePictureHandler::HandlePageInitialized,
112 base::Unretained(this)));
113 web_ui()->RegisterMessageCallback(
114 "selectImage",
115 base::BindRepeating(&ChangePictureHandler::HandleSelectImage,
116 base::Unretained(this)));
117 web_ui()->RegisterMessageCallback(
118 "requestSelectedImage",
119 base::BindRepeating(&ChangePictureHandler::HandleRequestSelectedImage,
120 base::Unretained(this)));
121 }
122
OnJavascriptAllowed()123 void ChangePictureHandler::OnJavascriptAllowed() {
124 user_manager_observer_.Add(user_manager::UserManager::Get());
125 camera_observer_.Add(CameraPresenceNotifier::GetInstance());
126 }
127
OnJavascriptDisallowed()128 void ChangePictureHandler::OnJavascriptDisallowed() {
129 user_manager_observer_.Remove(user_manager::UserManager::Get());
130 camera_observer_.Remove(CameraPresenceNotifier::GetInstance());
131 }
132
SendDefaultImages()133 void ChangePictureHandler::SendDefaultImages() {
134 base::DictionaryValue result;
135 result.SetInteger("first", default_user_image::GetFirstDefaultImage());
136 std::unique_ptr<base::ListValue> default_images =
137 default_user_image::GetAsDictionary(true /* all */);
138 result.Set("images", std::move(default_images));
139 FireWebUIListener("default-images-changed", result);
140 }
141
HandleChooseFile(const base::ListValue * args)142 void ChangePictureHandler::HandleChooseFile(const base::ListValue* args) {
143 DCHECK(args && args->empty());
144 select_file_dialog_ = ui::SelectFileDialog::Create(
145 this,
146 std::make_unique<ChromeSelectFilePolicy>(web_ui()->GetWebContents()));
147
148 base::FilePath downloads_path;
149 if (!base::PathService::Get(chrome::DIR_DEFAULT_DOWNLOADS, &downloads_path)) {
150 NOTREACHED();
151 return;
152 }
153
154 // Static so we initialize it only once.
155 static base::NoDestructor<ui::SelectFileDialog::FileTypeInfo> file_type_info(
156 GetUserImageFileTypeInfo());
157
158 select_file_dialog_->SelectFile(
159 ui::SelectFileDialog::SELECT_OPEN_FILE,
160 l10n_util::GetStringUTF16(IDS_DOWNLOAD_TITLE), downloads_path,
161 file_type_info.get(), 0, FILE_PATH_LITERAL(""), GetBrowserWindow(), NULL);
162 }
163
HandleDiscardPhoto(const base::ListValue * args)164 void ChangePictureHandler::HandleDiscardPhoto(const base::ListValue* args) {
165 DCHECK(args->empty());
166 AccessibilityManager::Get()->PlayEarcon(
167 SOUND_OBJECT_DELETE, PlaySoundOption::ONLY_IF_SPOKEN_FEEDBACK_ENABLED);
168 }
169
HandlePhotoTaken(const base::ListValue * args)170 void ChangePictureHandler::HandlePhotoTaken(const base::ListValue* args) {
171 DCHECK_CURRENTLY_ON(BrowserThread::UI);
172 AccessibilityManager::Get()->PlayEarcon(
173 SOUND_CAMERA_SNAP, PlaySoundOption::ONLY_IF_SPOKEN_FEEDBACK_ENABLED);
174
175 std::string image_url;
176 if (!args || args->GetSize() != 1 || !args->GetString(0, &image_url))
177 NOTREACHED();
178 DCHECK(!image_url.empty());
179
180 std::string raw_data;
181 base::StringPiece url(image_url);
182 const char kDataUrlPrefix[] = "data:image/png;base64,";
183 const size_t kDataUrlPrefixLength = base::size(kDataUrlPrefix) - 1;
184 if (!base::StartsWith(url, kDataUrlPrefix) ||
185 !base::Base64Decode(url.substr(kDataUrlPrefixLength), &raw_data)) {
186 LOG(WARNING) << "Invalid image URL";
187 return;
188 }
189
190 // Use |raw_data| as image but first verify that it can be decoded.
191 user_photo_ = gfx::ImageSkia();
192 std::vector<unsigned char> photo_data(raw_data.begin(), raw_data.end());
193 user_photo_data_ = base::RefCountedBytes::TakeVector(&photo_data);
194
195 ImageDecoder::Cancel(this);
196 ImageDecoder::Start(this, raw_data);
197 }
198
HandlePageInitialized(const base::ListValue * args)199 void ChangePictureHandler::HandlePageInitialized(const base::ListValue* args) {
200 DCHECK(args && args->empty());
201
202 AllowJavascript();
203
204 SendDefaultImages();
205 SendSelectedImage();
206 UpdateProfileImage();
207 }
208
SendSelectedImage()209 void ChangePictureHandler::SendSelectedImage() {
210 const user_manager::User* user = GetUser();
211 DCHECK(user->GetAccountId().is_valid());
212
213 previous_image_index_ = user->image_index();
214 switch (previous_image_index_) {
215 case user_manager::User::USER_IMAGE_EXTERNAL: {
216 // User has image from camera/file, record it and add to the image list.
217 previous_image_ = user->GetImage();
218 previous_image_format_ = user->image_format();
219 if (previous_image_format_ == user_manager::UserImage::FORMAT_PNG &&
220 user->has_image_bytes()) {
221 previous_image_bytes_ = user->image_bytes();
222 SendOldImage(webui::GetPngDataUrl(previous_image_bytes_->front(),
223 previous_image_bytes_->size()));
224 } else {
225 previous_image_bytes_ = nullptr;
226 DCHECK(previous_image_.IsThreadSafe());
227 // Post a task because GetBitmapDataUrl does PNG encoding, which is
228 // slow for large images.
229 base::ThreadPool::PostTaskAndReplyWithResult(
230 FROM_HERE, {base::TaskPriority::USER_BLOCKING},
231 base::BindOnce(&webui::GetBitmapDataUrl, *previous_image_.bitmap()),
232 base::BindOnce(&ChangePictureHandler::SendOldImage,
233 weak_ptr_factory_.GetWeakPtr()));
234 }
235 break;
236 }
237 case user_manager::User::USER_IMAGE_PROFILE: {
238 // User has their Profile image as the current image.
239 SendProfileImage(user->GetImage(), true);
240 break;
241 }
242 default: {
243 if (default_user_image::IsInCurrentImageSet(previous_image_index_)) {
244 // User has image from the current set of default images.
245 base::Value image_url(
246 default_user_image::GetDefaultImageUrl(previous_image_index_));
247 FireWebUIListener("selected-image-changed", image_url);
248 } else {
249 // User has an old default image, so present it in the same manner as a
250 // previous image from file.
251 previous_image_ = user->GetImage();
252 previous_image_bytes_ = nullptr;
253 previous_image_format_ = user_manager::UserImage::FORMAT_UNKNOWN;
254 SendOldImageWithIndex(
255 default_user_image::GetDefaultImageUrl(previous_image_index_),
256 previous_image_index_);
257 }
258 }
259 }
260 }
261
SendProfileImage(const gfx::ImageSkia & image,bool should_select)262 void ChangePictureHandler::SendProfileImage(const gfx::ImageSkia& image,
263 bool should_select) {
264 base::Value data_url(webui::GetBitmapDataUrl(*image.bitmap()));
265 base::Value select(should_select);
266 FireWebUIListener("profile-image-changed", data_url, select);
267 }
268
UpdateProfileImage()269 void ChangePictureHandler::UpdateProfileImage() {
270 UserImageManager* user_image_manager =
271 ChromeUserManager::Get()->GetUserImageManager(GetUser()->GetAccountId());
272 // If we have a downloaded profile image and haven't sent it in
273 // |SendSelectedImage|, send it now (without selecting).
274 if (previous_image_index_ != user_manager::User::USER_IMAGE_PROFILE &&
275 !user_image_manager->DownloadedProfileImage().isNull()) {
276 SendProfileImage(user_image_manager->DownloadedProfileImage(), false);
277 }
278 user_image_manager->DownloadProfileImage();
279 }
280
SendOldImage(std::string && image_url)281 void ChangePictureHandler::SendOldImage(std::string&& image_url) {
282 SendOldImageWithIndex(std::move(image_url), -1);
283 }
284
SendOldImageWithIndex(std::string && image_url,int image_index)285 void ChangePictureHandler::SendOldImageWithIndex(std::string&& image_url,
286 int image_index) {
287 base::DictionaryValue result;
288 result.SetStringPath("url", std::move(image_url));
289 result.SetIntPath("index", image_index);
290 FireWebUIListener("old-image-changed", result);
291 }
292
HandleSelectImage(const base::ListValue * args)293 void ChangePictureHandler::HandleSelectImage(const base::ListValue* args) {
294 std::string image_url;
295 std::string image_type;
296 if (!args || args->GetSize() != 2 || !args->GetString(0, &image_url) ||
297 !args->GetString(1, &image_type)) {
298 NOTREACHED();
299 return;
300 }
301 // |image_url| may be empty unless |image_type| is "default".
302 DCHECK(!image_type.empty());
303
304 UserImageManager* user_image_manager =
305 ChromeUserManager::Get()->GetUserImageManager(GetUser()->GetAccountId());
306 bool waiting_for_camera_photo = false;
307
308 if (image_type == "old") {
309 // Previous image (from camera or manually uploaded) re-selected.
310 DCHECK(!previous_image_.isNull());
311 std::unique_ptr<user_manager::UserImage> user_image;
312 if (previous_image_format_ == user_manager::UserImage::FORMAT_PNG &&
313 previous_image_bytes_) {
314 user_image = std::make_unique<user_manager::UserImage>(
315 previous_image_, previous_image_bytes_, previous_image_format_);
316 user_image->MarkAsSafe();
317 } else {
318 user_image = user_manager::UserImage::CreateAndEncode(
319 previous_image_, user_manager::UserImage::FORMAT_JPEG);
320 }
321 user_image_manager->SaveUserImage(std::move(user_image));
322
323 VLOG(1) << "Selected old user image";
324 } else if (image_type == "default") {
325 int image_index = user_manager::User::USER_IMAGE_INVALID;
326 if (default_user_image::IsDefaultImageUrl(image_url, &image_index)) {
327 // One of the default user images.
328 user_image_manager->SaveUserDefaultImageIndex(image_index);
329
330 VLOG(1) << "Selected default user image: " << image_index;
331 } else {
332 LOG(WARNING) << "Invalid image_url for default image type: " << image_url;
333 }
334 } else if (image_type == "camera") {
335 // Camera image is selected.
336 if (user_photo_.isNull()) {
337 waiting_for_camera_photo = true;
338 VLOG(1) << "Still waiting for camera image to decode";
339 } else {
340 SetImageFromCamera(user_photo_, user_photo_data_.get());
341 }
342 } else if (image_type == "profile") {
343 // Profile image selected. Could be previous (old) user image.
344 user_image_manager->SaveUserImageFromProfileImage();
345 } else {
346 NOTREACHED() << "Unexpected image type: " << image_type;
347 }
348
349 // Ignore the result of the previous decoding if it's no longer needed.
350 if (!waiting_for_camera_photo)
351 ImageDecoder::Cancel(this);
352 }
353
HandleRequestSelectedImage(const base::ListValue * args)354 void ChangePictureHandler::HandleRequestSelectedImage(
355 const base::ListValue* args) {
356 SendSelectedImage();
357 }
358
FileSelected(const base::FilePath & path,int index,void * params)359 void ChangePictureHandler::FileSelected(const base::FilePath& path,
360 int index,
361 void* params) {
362 ChromeUserManager::Get()
363 ->GetUserImageManager(GetUser()->GetAccountId())
364 ->SaveUserImageFromFile(path);
365 VLOG(1) << "Selected image from file";
366 }
367
SetImageFromCamera(const gfx::ImageSkia & photo,base::RefCountedBytes * photo_bytes)368 void ChangePictureHandler::SetImageFromCamera(
369 const gfx::ImageSkia& photo,
370 base::RefCountedBytes* photo_bytes) {
371 std::unique_ptr<user_manager::UserImage> user_image =
372 std::make_unique<user_manager::UserImage>(
373 photo, photo_bytes, user_manager::UserImage::FORMAT_PNG);
374 user_image->MarkAsSafe();
375 ChromeUserManager::Get()
376 ->GetUserImageManager(GetUser()->GetAccountId())
377 ->SaveUserImage(std::move(user_image));
378 VLOG(1) << "Selected camera photo";
379 }
380
SetCameraPresent(bool present)381 void ChangePictureHandler::SetCameraPresent(bool present) {
382 FireWebUIListener("camera-presence-changed", base::Value(present));
383 }
384
OnCameraPresenceCheckDone(bool is_camera_present)385 void ChangePictureHandler::OnCameraPresenceCheckDone(bool is_camera_present) {
386 SetCameraPresent(is_camera_present);
387 }
388
OnUserImageChanged(const user_manager::User & user)389 void ChangePictureHandler::OnUserImageChanged(const user_manager::User& user) {
390 // Not initialized yet.
391 if (previous_image_index_ == user_manager::User::USER_IMAGE_INVALID)
392 return;
393 SendSelectedImage();
394 }
395
OnUserProfileImageUpdated(const user_manager::User & user,const gfx::ImageSkia & profile_image)396 void ChangePictureHandler::OnUserProfileImageUpdated(
397 const user_manager::User& user,
398 const gfx::ImageSkia& profile_image) {
399 // User profile image has been updated.
400 SendProfileImage(profile_image, false);
401 }
402
GetBrowserWindow() const403 gfx::NativeWindow ChangePictureHandler::GetBrowserWindow() const {
404 Browser* browser =
405 chrome::FindBrowserWithWebContents(web_ui()->GetWebContents());
406 return browser->window()->GetNativeWindow();
407 }
408
OnImageDecoded(const SkBitmap & decoded_image)409 void ChangePictureHandler::OnImageDecoded(const SkBitmap& decoded_image) {
410 user_photo_ = gfx::ImageSkia::CreateFrom1xBitmap(decoded_image);
411 SetImageFromCamera(user_photo_, user_photo_data_.get());
412 }
413
OnDecodeImageFailed()414 void ChangePictureHandler::OnDecodeImageFailed() {
415 NOTREACHED() << "Failed to decode PNG image from WebUI";
416 }
417
GetUser() const418 const user_manager::User* ChangePictureHandler::GetUser() const {
419 Profile* profile = Profile::FromWebUI(web_ui());
420 const user_manager::User* user =
421 ProfileHelper::Get()->GetUserByProfile(profile);
422 if (!user)
423 return user_manager::UserManager::Get()->GetActiveUser();
424 return user;
425 }
426
427 } // namespace settings
428 } // namespace chromeos
429