OLD | NEW |
---|---|
1 // Copyright (c) 2013 The Chromium Authors. All rights reserved. | 1 // Copyright (c) 2013 The Chromium Authors. All rights reserved. |
2 // Use of this source code is governed by a BSD-style license that can be | 2 // Use of this source code is governed by a BSD-style license that can be |
3 // found in the LICENSE file. | 3 // found in the LICENSE file. |
4 | 4 |
5 #include "chrome/browser/chromeos/login/user_image_manager_impl.h" | 5 #include "chrome/browser/chromeos/login/user_image_manager_impl.h" |
6 | 6 |
7 #include "base/bind.h" | 7 #include "base/bind.h" |
8 #include "base/command_line.h" | 8 #include "base/command_line.h" |
9 #include "base/debug/trace_event.h" | 9 #include "base/debug/trace_event.h" |
10 #include "base/file_util.h" | 10 #include "base/file_util.h" |
11 #include "base/files/file_path.h" | 11 #include "base/files/file_path.h" |
12 #include "base/logging.h" | 12 #include "base/logging.h" |
13 #include "base/metrics/histogram.h" | 13 #include "base/metrics/histogram.h" |
14 #include "base/path_service.h" | 14 #include "base/path_service.h" |
15 #include "base/prefs/pref_registry_simple.h" | 15 #include "base/prefs/pref_registry_simple.h" |
16 #include "base/prefs/pref_service.h" | 16 #include "base/prefs/pref_service.h" |
17 #include "base/prefs/scoped_user_pref_update.h" | 17 #include "base/prefs/scoped_user_pref_update.h" |
18 #include "base/rand_util.h" | 18 #include "base/rand_util.h" |
19 #include "base/sequenced_task_runner.h" | 19 #include "base/sequenced_task_runner.h" |
20 #include "base/task_runner_util.h" | |
20 #include "base/threading/sequenced_worker_pool.h" | 21 #include "base/threading/sequenced_worker_pool.h" |
21 #include "base/threading/worker_pool.h" | |
22 #include "base/time/time.h" | 22 #include "base/time/time.h" |
23 #include "base/values.h" | |
24 #include "chrome/browser/browser_process.h" | 23 #include "chrome/browser/browser_process.h" |
25 #include "chrome/browser/chrome_notification_types.h" | 24 #include "chrome/browser/chrome_notification_types.h" |
26 #include "chrome/browser/chromeos/login/default_user_images.h" | 25 #include "chrome/browser/chromeos/login/default_user_images.h" |
27 #include "chrome/browser/chromeos/login/helper.h" | 26 #include "chrome/browser/chromeos/login/helper.h" |
28 #include "chrome/browser/chromeos/login/user_image.h" | 27 #include "chrome/browser/chromeos/login/user_image.h" |
29 #include "chrome/browser/chromeos/login/user_image_sync_observer.h" | 28 #include "chrome/browser/chromeos/login/user_image_sync_observer.h" |
30 #include "chrome/browser/chromeos/login/user_manager.h" | 29 #include "chrome/browser/chromeos/login/user_manager.h" |
31 #include "chrome/browser/profiles/profile_downloader.h" | 30 #include "chrome/browser/profiles/profile_downloader.h" |
32 #include "chrome/browser/profiles/profile_manager.h" | 31 #include "chrome/browser/profiles/profile_manager.h" |
33 #include "chrome/common/chrome_paths.h" | 32 #include "chrome/common/chrome_paths.h" |
34 #include "chrome/common/chrome_switches.h" | 33 #include "chrome/common/chrome_switches.h" |
35 #include "chromeos/chromeos_switches.h" | 34 #include "chromeos/chromeos_switches.h" |
36 #include "content/public/browser/browser_thread.h" | 35 #include "content/public/browser/browser_thread.h" |
37 #include "content/public/browser/notification_service.h" | 36 #include "content/public/browser/notification_service.h" |
38 #include "content/public/common/url_constants.h" | |
39 #include "ui/base/webui/web_ui_util.h" | 37 #include "ui/base/webui/web_ui_util.h" |
40 #include "ui/gfx/image/image_skia.h" | 38 #include "ui/gfx/image/image_skia.h" |
41 | 39 |
42 using content::BrowserThread; | |
43 | |
44 namespace chromeos { | 40 namespace chromeos { |
45 | 41 |
46 namespace { | 42 namespace { |
47 | 43 |
48 // A dictionary that maps usernames to old user image data with images stored in | 44 // A dictionary that maps user_ids to old user image data with images stored in |
49 // PNG format. Deprecated. | 45 // PNG format. Deprecated. |
50 // TODO(ivankr): remove this const char after migration is gone. | 46 // TODO(ivankr): remove this const char after migration is gone. |
51 const char kUserImages[] = "UserImages"; | 47 const char kUserImages[] = "UserImages"; |
52 | 48 |
53 // A dictionary that maps usernames to user image data with images stored in | 49 // A dictionary that maps user_ids to user image data with images stored in |
54 // JPEG format. | 50 // JPEG format. |
55 const char kUserImageProperties[] = "user_image_info"; | 51 const char kUserImageProperties[] = "user_image_info"; |
56 | 52 |
57 // Names of user image properties. | 53 // Names of user image properties. |
58 const char kImagePathNodeName[] = "path"; | 54 const char kImagePathNodeName[] = "path"; |
59 const char kImageIndexNodeName[] = "index"; | 55 const char kImageIndexNodeName[] = "index"; |
60 const char kImageURLNodeName[] = "url"; | 56 const char kImageURLNodeName[] = "url"; |
61 | 57 |
62 // Delay betweeen user login and user image migration. | |
63 const int kUserImageMigrationDelaySec = 50; | |
bartfab (slow)
2013/11/13 23:12:09
I removed this because it was not working correctl
| |
64 | |
65 // Delay betweeen user login and attempt to update user's profile data. | 58 // Delay betweeen user login and attempt to update user's profile data. |
66 const int kProfileDataDownloadDelaySec = 10; | 59 const int kProfileDataDownloadDelaySec = 10; |
67 | 60 |
68 // Interval betweeen retries to update user's profile data. | 61 // Interval betweeen retries to update user's profile data. |
69 const int kProfileDataDownloadRetryIntervalSec = 300; | 62 const int kProfileDataDownloadRetryIntervalSec = 300; |
70 | 63 |
71 // Delay betweeen subsequent profile refresh attempts (24 hrs). | 64 // Delay betweeen subsequent profile refresh attempts (24 hrs). |
72 const int kProfileRefreshIntervalSec = 24 * 3600; | 65 const int kProfileRefreshIntervalSec = 24 * 3600; |
73 | 66 |
74 const char kSafeImagePathExtension[] = ".jpg"; | 67 const char kSafeImagePathExtension[] = ".jpg"; |
(...skipping 22 matching lines...) Expand all Loading... | |
97 // Time histogram prefix for a successful profile image download. | 90 // Time histogram prefix for a successful profile image download. |
98 const char kProfileDownloadSuccessTime[] = | 91 const char kProfileDownloadSuccessTime[] = |
99 "UserImage.ProfileDownloadTime.Success"; | 92 "UserImage.ProfileDownloadTime.Success"; |
100 // Time histogram suffix for a profile image download after login. | 93 // Time histogram suffix for a profile image download after login. |
101 const char kProfileDownloadReasonLoggedIn[] = "LoggedIn"; | 94 const char kProfileDownloadReasonLoggedIn[] = "LoggedIn"; |
102 // Time histogram suffix for a scheduled profile image download. | 95 // Time histogram suffix for a scheduled profile image download. |
103 const char kProfileDownloadReasonScheduled[] = "Scheduled"; | 96 const char kProfileDownloadReasonScheduled[] = "Scheduled"; |
104 // Time histogram suffix for a profile image download retry. | 97 // Time histogram suffix for a profile image download retry. |
105 const char kProfileDownloadReasonRetry[] = "Retry"; | 98 const char kProfileDownloadReasonRetry[] = "Retry"; |
106 | 99 |
107 // Add a histogram showing the time it takes to download a profile image. | 100 // Add a histogram showing the time it takes to download profile image. |
108 // Separate histograms are reported for each download |reason| and |result|. | 101 // Separate histograms are reported for each download |reason| and |result|. |
109 void AddProfileImageTimeHistogram(ProfileDownloadResult result, | 102 void AddProfileImageTimeHistogram(ProfileDownloadResult result, |
110 const std::string& download_reason, | 103 const std::string& download_reason, |
111 const base::TimeDelta& time_delta) { | 104 const base::TimeDelta& time_delta) { |
112 std::string histogram_name; | 105 std::string histogram_name; |
113 switch (result) { | 106 switch (result) { |
114 case kDownloadFailure: | 107 case kDownloadFailure: |
115 histogram_name = kProfileDownloadFailureTime; | 108 histogram_name = kProfileDownloadFailureTime; |
116 break; | 109 break; |
117 case kDownloadDefault: | 110 case kDownloadDefault: |
(...skipping 18 matching lines...) Expand all Loading... | |
136 const size_t bucket_count(50); | 129 const size_t bucket_count(50); |
137 | 130 |
138 base::HistogramBase* counter = base::Histogram::FactoryTimeGet( | 131 base::HistogramBase* counter = base::Histogram::FactoryTimeGet( |
139 histogram_name, min_time, max_time, bucket_count, | 132 histogram_name, min_time, max_time, bucket_count, |
140 base::HistogramBase::kUmaTargetedHistogramFlag); | 133 base::HistogramBase::kUmaTargetedHistogramFlag); |
141 counter->AddTime(time_delta); | 134 counter->AddTime(time_delta); |
142 | 135 |
143 DVLOG(1) << "Profile image download time: " << time_delta.InSecondsF(); | 136 DVLOG(1) << "Profile image download time: " << time_delta.InSecondsF(); |
144 } | 137 } |
145 | 138 |
146 // Deletes image file. | |
147 void DeleteImageFile(const std::string& image_path) { | |
148 if (image_path.empty()) | |
149 return; | |
150 base::FilePath fp(image_path); | |
151 BrowserThread::PostTask( | |
152 BrowserThread::FILE, | |
153 FROM_HERE, | |
154 base::Bind(base::IgnoreResult(&base::DeleteFile), | |
155 fp, /* recursive= */ false)); | |
156 } | |
157 | |
158 // Converts |image_index| to UMA histogram value. | 139 // Converts |image_index| to UMA histogram value. |
159 int ImageIndexToHistogramIndex(int image_index) { | 140 int ImageIndexToHistogramIndex(int image_index) { |
160 switch (image_index) { | 141 switch (image_index) { |
161 case User::kExternalImageIndex: | 142 case User::kExternalImageIndex: |
162 // TODO(ivankr): Distinguish this from selected from file. | 143 // TODO(ivankr): Distinguish this from selected from file. |
163 return kHistogramImageFromCamera; | 144 return kHistogramImageFromCamera; |
164 case User::kProfileImageIndex: | 145 case User::kProfileImageIndex: |
165 return kHistogramImageFromProfile; | 146 return kHistogramImageFromProfile; |
166 default: | 147 default: |
167 return image_index; | 148 return image_index; |
168 } | 149 } |
169 } | 150 } |
170 | 151 |
152 bool SaveImage(const UserImage& user_image, const base::FilePath& image_path) { | |
153 UserImage safe_image; | |
154 const UserImage::RawImage* encoded_image = NULL; | |
155 if (!user_image.is_safe_format()) { | |
156 safe_image = UserImage::CreateAndEncode(user_image.image()); | |
157 encoded_image = &safe_image.raw_image(); | |
158 UMA_HISTOGRAM_MEMORY_KB("UserImage.RecodedJpegSize", encoded_image->size()); | |
159 } else if (user_image.has_raw_image()) { | |
160 encoded_image = &user_image.raw_image(); | |
161 } else { | |
162 NOTREACHED() << "Raw image missing."; | |
163 return false; | |
164 } | |
165 | |
166 if (file_util::WriteFile(image_path, | |
167 reinterpret_cast<const char*>(&(*encoded_image)[0]), | |
168 encoded_image->size()) == -1) { | |
169 LOG(ERROR) << "Failed to save image to file."; | |
170 return false; | |
171 } | |
172 | |
173 return true; | |
174 } | |
175 | |
171 } // namespace | 176 } // namespace |
172 | 177 |
173 // static | 178 // static |
174 int UserImageManagerImpl::user_image_migration_delay_sec = | |
175 kUserImageMigrationDelaySec; | |
176 | |
177 // static | |
178 void UserImageManager::RegisterPrefs(PrefRegistrySimple* registry) { | 179 void UserImageManager::RegisterPrefs(PrefRegistrySimple* registry) { |
179 registry->RegisterDictionaryPref(kUserImages); | 180 registry->RegisterDictionaryPref(kUserImages); |
180 registry->RegisterDictionaryPref(kUserImageProperties); | 181 registry->RegisterDictionaryPref(kUserImageProperties); |
181 } | 182 } |
182 | 183 |
184 // Every image load or update is encapsulated by a Job. The Job is allowed to | |
185 // perform tasks on background threads or in helper processes but: | |
186 // * Changes to User objects and local state as well as any calls to the | |
187 // |parent_| must be performed on the thread that the Job is created on only. | |
188 // * File writes and deletions must be performed via the |parent_|'s | |
189 // |background_task_runner_| only. | |
190 // | |
191 // Only one of the Load*() and Set*() methods may be called per Job. | |
192 class UserImageManagerImpl::Job { | |
193 public: | |
194 // The |Job| will update the |user| object for |user_id|. | |
195 Job(UserImageManagerImpl* parent, const std::string& user_id); | |
196 ~Job(); | |
197 | |
198 // Loads the image at |image_path| or one of the default images, depending on | |
199 // |image_index|, and updates the |user| object for |user_id_| with the new | |
200 // image. | |
201 void LoadImage(base::FilePath image_path, | |
202 const int image_index, | |
203 const GURL& image_url); | |
204 | |
205 // Sets the user image for |user_id_| in local state to the default image | |
206 // indicated by |default_image_index|. Also updates the |user| object for | |
207 // |user_id_| with the new image. | |
208 void SetToDefaultImage(int default_image_index); | |
209 | |
210 // Saves the |user_image| to disk and sets the user image for |user_id_| in | |
211 // local state to that image. Also updates the |user| object for |user_id_| | |
212 // with the new image. | |
213 void SetToImage(int image_index, | |
214 const UserImage& user_image); | |
215 | |
216 // Loads the the image at |path|, transcodes it to JPEG format, saves the | |
217 // image to disk and sets the user image for |user_id_| in local state to that | |
218 // image. If |resize| is true, the image is cropped and resized before | |
219 // transcoding. Also updates the |user| object for |user_id_| with the new | |
220 // image. | |
221 void SetToPath(const base::FilePath& path, | |
222 int image_index, | |
223 const GURL& image_url, | |
224 bool resize); | |
225 | |
226 private: | |
227 // Called back after an image has been loaded from disk. | |
228 void OnLoadImageDone(bool save, const UserImage& user_image); | |
229 | |
230 // Updates the |user| object for |user_id_| with |user_image_|. | |
231 void UpdateUser(); | |
232 | |
233 // Saves |user_image_| to disk in JPEG format. Local state will be updated | |
234 // when a callback indicates that the save has been successful. | |
235 void SaveImageAndUpdateLocalState(); | |
236 | |
237 // Called back after the |user_image_| has been saved to disk. If |success| is | |
238 // true sets the user image for |user_id_| in local state to that image. | |
239 void OnSaveImageDone(bool success); | |
240 | |
241 // Updates the user image for |user_id_| in local state, setting it to | |
242 // one of the default images or the saved |user_image_|, depending on | |
243 // |image_index_|. | |
244 void UpdateLocalState(); | |
245 | |
246 // Notifies the |parent_| that the Job is done. | |
247 void NotifyJobDone(); | |
248 | |
249 UserImageManagerImpl* parent_; | |
250 const std::string user_id_; | |
251 | |
252 // Whether one of the Load*() or Set*() methods has been run already. | |
253 bool run_; | |
254 | |
255 int image_index_; | |
256 GURL image_url_; | |
257 base::FilePath image_path_; | |
258 | |
259 UserImage user_image_; | |
260 | |
261 base::WeakPtrFactory<Job> weak_factory_; | |
262 | |
263 DISALLOW_COPY_AND_ASSIGN(Job); | |
264 }; | |
265 | |
266 UserImageManagerImpl::Job::Job(UserImageManagerImpl* parent, | |
267 const std::string& user_id) | |
268 : parent_(parent), | |
269 user_id_(user_id), | |
270 run_(false), | |
271 weak_factory_(this) { | |
272 } | |
273 | |
274 UserImageManagerImpl::Job::~Job() { | |
275 } | |
276 | |
277 void UserImageManagerImpl::Job::LoadImage(base::FilePath image_path, | |
278 const int image_index, | |
279 const GURL& image_url) { | |
280 DCHECK(!run_); | |
281 run_ = true; | |
282 | |
283 image_index_ = image_index; | |
284 image_url_ = image_url; | |
285 image_path_ = image_path; | |
286 | |
287 if (image_index_ >= 0 && image_index_ < kDefaultImagesCount) { | |
288 // Load one of the default images. This happens synchronously. | |
289 user_image_ = UserImage(GetDefaultImage(image_index_)); | |
290 UpdateUser(); | |
291 NotifyJobDone(); | |
292 } else if (image_index_ == User::kExternalImageIndex || | |
293 image_index_ == User::kProfileImageIndex) { | |
294 // Load the user image from a file referenced by |image_path|. This happens | |
295 // asynchronously. The JPEG image loader can be used here because | |
296 // LoadImage() is called only for users whose user image has previously | |
297 // been set by one of the Set*() methods, which transcode to JPEG format. | |
298 parent_->image_loader_->Start(image_path_.value(), | |
Nikita (slow)
2013/11/14 15:17:13
DCHECK that image_path is not empty.
bartfab (slow)
2013/11/14 15:55:10
There actually is a DCHECK for this in the code th
| |
299 0, | |
300 base::Bind(&Job::OnLoadImageDone, | |
301 weak_factory_.GetWeakPtr(), | |
302 false)); | |
303 } else { | |
304 NOTREACHED(); | |
305 NotifyJobDone(); | |
306 } | |
307 } | |
308 | |
309 void UserImageManagerImpl::Job::SetToDefaultImage(int default_image_index) { | |
310 DCHECK(!run_); | |
311 run_ = true; | |
312 | |
313 DCHECK_LE(0, default_image_index); | |
314 DCHECK_GT(kDefaultImagesCount, default_image_index); | |
315 | |
316 image_index_ = default_image_index; | |
317 user_image_ = UserImage(GetDefaultImage(image_index_)); | |
318 | |
319 UpdateUser(); | |
320 UpdateLocalState(); | |
321 NotifyJobDone(); | |
322 } | |
323 | |
324 void UserImageManagerImpl::Job::SetToImage(int image_index, | |
325 const UserImage& user_image) { | |
326 DCHECK(!run_); | |
327 run_ = true; | |
328 | |
329 DCHECK(image_index == User::kExternalImageIndex || | |
330 image_index == User::kProfileImageIndex); | |
331 | |
332 image_index_ = image_index; | |
333 user_image_ = user_image; | |
334 | |
335 UpdateUser(); | |
336 SaveImageAndUpdateLocalState(); | |
337 } | |
338 | |
339 void UserImageManagerImpl::Job::SetToPath(const base::FilePath& path, | |
340 int image_index, | |
341 const GURL& image_url, | |
342 bool resize) { | |
343 DCHECK(!run_); | |
Nikita (slow)
2013/11/14 15:17:13
DCHECK path.
bartfab (slow)
2013/11/14 15:55:10
Done.
| |
344 run_ = true; | |
345 | |
346 image_index_ = image_index; | |
347 image_url_ = image_url; | |
348 | |
349 parent_->unsafe_image_loader_->Start(path.value(), | |
350 resize ? login::kMaxUserImageSize : 0, | |
351 base::Bind(&Job::OnLoadImageDone, | |
352 weak_factory_.GetWeakPtr(), | |
353 true)); | |
354 } | |
355 | |
356 void UserImageManagerImpl::Job::OnLoadImageDone(bool save, | |
357 const UserImage& user_image) { | |
358 user_image_ = user_image; | |
359 UpdateUser(); | |
360 if (save) | |
361 SaveImageAndUpdateLocalState(); | |
362 else | |
363 NotifyJobDone(); | |
364 } | |
365 | |
366 void UserImageManagerImpl::Job::UpdateUser() { | |
367 User* user = const_cast<User*>(UserManager::Get()->FindUser(user_id_)); | |
Nikita (slow)
2013/11/14 15:17:13
How can const_cast be avoided?
Declare this class
bartfab (slow)
2013/11/14 15:55:10
I actually inherited this case from the original i
| |
368 if (!user) | |
369 return; | |
370 | |
371 if (!user_image_.image().isNull()) | |
372 user->SetImage(user_image_, image_index_); | |
373 else | |
374 user->SetStubImage(image_index_, false); | |
375 user->SetImageURL(image_url_); | |
376 | |
377 parent_->OnJobChangedUserImage(user); | |
378 } | |
379 | |
380 void UserImageManagerImpl::Job::SaveImageAndUpdateLocalState() { | |
381 base::FilePath user_data_dir; | |
382 PathService::Get(chrome::DIR_USER_DATA, &user_data_dir); | |
383 image_path_ = user_data_dir.Append(user_id_ + kSafeImagePathExtension); | |
384 | |
385 base::PostTaskAndReplyWithResult( | |
386 parent_->background_task_runner_, | |
387 FROM_HERE, | |
388 base::Bind(&SaveImage, user_image_, image_path_), | |
389 base::Bind(&Job::OnSaveImageDone, weak_factory_.GetWeakPtr())); | |
390 } | |
391 | |
392 void UserImageManagerImpl::Job::OnSaveImageDone(bool success) { | |
393 if (success) | |
394 UpdateLocalState(); | |
395 NotifyJobDone(); | |
396 } | |
397 | |
398 void UserImageManagerImpl::Job::UpdateLocalState() { | |
399 // Ignore if data stored or cached outside the user's cryptohome is to be | |
400 // treated as ephemeral. | |
401 if (UserManager::Get()->IsUserNonCryptohomeDataEphemeral(user_id_)) | |
402 return; | |
403 | |
404 scoped_ptr<base::DictionaryValue> entry(new base::DictionaryValue); | |
405 entry->Set(kImagePathNodeName, new base::StringValue(image_path_.value())); | |
406 entry->Set(kImageIndexNodeName, new base::FundamentalValue(image_index_)); | |
407 if (!image_url_.is_empty()) | |
408 entry->Set(kImageURLNodeName, new StringValue(image_url_.spec())); | |
409 DictionaryPrefUpdate update(g_browser_process->local_state(), | |
410 kUserImageProperties); | |
411 update->SetWithoutPathExpansion(user_id_, entry.release()); | |
412 | |
413 UserManager::Get()->NotifyLocalStateChanged(); | |
414 } | |
415 | |
416 void UserImageManagerImpl::Job::NotifyJobDone() { | |
417 parent_->OnJobDone(user_id_); | |
418 // |parent_| drops ownership of |this| in OnJobDone(), requiring the Job to | |
419 // destroy itself. | |
420 delete this; | |
Nikita (slow)
2013/11/14 15:17:13
Any reasons why me might want to use DeleteSoon()
bartfab (slow)
2013/11/14 15:55:10
I was sure I had a good reason not to use DeleteSo
| |
421 } | |
422 | |
183 UserImageManagerImpl::UserImageManagerImpl() | 423 UserImageManagerImpl::UserImageManagerImpl() |
184 : last_image_set_async_(false), | 424 : downloading_profile_image_(false), |
185 downloaded_profile_image_data_url_(content::kAboutBlankURL), | 425 profile_image_requested_(false), |
186 downloading_profile_image_(false), | 426 weak_factory_(this) { |
187 migrate_current_user_on_load_(false) { | 427 base::SequencedWorkerPool* blocking_pool = |
188 base::SequencedWorkerPool* blocking_pool = BrowserThread::GetBlockingPool(); | 428 content::BrowserThread::GetBlockingPool(); |
189 // Background task runner on which file I/O, image decoding and resizing are | 429 background_task_runner_ = |
190 // done. | |
191 scoped_refptr<base::SequencedTaskRunner> task_runner = | |
192 blocking_pool->GetSequencedTaskRunnerWithShutdownBehavior( | 430 blocking_pool->GetSequencedTaskRunnerWithShutdownBehavior( |
193 blocking_pool->GetSequenceToken(), | 431 blocking_pool->GetSequenceToken(), |
194 base::SequencedWorkerPool::CONTINUE_ON_SHUTDOWN); | 432 base::SequencedWorkerPool::CONTINUE_ON_SHUTDOWN); |
195 image_loader_ = new UserImageLoader(ImageDecoder::ROBUST_JPEG_CODEC, | 433 image_loader_ = new UserImageLoader(ImageDecoder::ROBUST_JPEG_CODEC, |
196 task_runner); | 434 background_task_runner_); |
197 unsafe_image_loader_ = new UserImageLoader(ImageDecoder::DEFAULT_CODEC, | 435 unsafe_image_loader_ = new UserImageLoader(ImageDecoder::DEFAULT_CODEC, |
198 task_runner); | 436 background_task_runner_); |
199 } | 437 } |
200 | 438 |
201 UserImageManagerImpl::~UserImageManagerImpl() { | 439 UserImageManagerImpl::~UserImageManagerImpl() { |
202 } | 440 } |
203 | 441 |
204 void UserImageManagerImpl::LoadUserImages(const UserList& users) { | 442 void UserImageManagerImpl::LoadUserImages(const UserList& users) { |
205 PrefService* local_state = g_browser_process->local_state(); | 443 PrefService* local_state = g_browser_process->local_state(); |
206 const DictionaryValue* prefs_images_unsafe = | 444 const DictionaryValue* prefs_images_unsafe = |
207 local_state->GetDictionary(kUserImages); | 445 local_state->GetDictionary(kUserImages); |
208 const DictionaryValue* prefs_images = | 446 const DictionaryValue* prefs_images = |
209 local_state->GetDictionary(kUserImageProperties); | 447 local_state->GetDictionary(kUserImageProperties); |
210 if (!prefs_images && !prefs_images_unsafe) | 448 if (!prefs_images && !prefs_images_unsafe) |
211 return; | 449 return; |
212 | 450 |
213 for (UserList::const_iterator it = users.begin(); it != users.end(); ++it) { | 451 for (UserList::const_iterator it = users.begin(); it != users.end(); ++it) { |
214 User* user = *it; | 452 User* user = *it; |
453 const std::string& user_id = user->email(); | |
454 bool needs_migration = false; | |
455 | |
456 // If entries are found in both |prefs_images_unsafe| and |prefs_images|, | |
457 // |prefs_images| is honored for now but |prefs_images_unsafe| will be | |
458 // migrated, overwriting the |prefs_images| entry, when the user logs in. | |
215 const base::DictionaryValue* image_properties = NULL; | 459 const base::DictionaryValue* image_properties = NULL; |
216 bool needs_migration = false; // |true| if user has image in old format. | |
217 bool safe_source = false; // |true| if image loaded from safe source. | |
218 | |
219 if (prefs_images_unsafe) { | 460 if (prefs_images_unsafe) { |
220 needs_migration = prefs_images_unsafe->GetDictionaryWithoutPathExpansion( | 461 needs_migration = prefs_images_unsafe->GetDictionaryWithoutPathExpansion( |
221 user->email(), &image_properties); | 462 user_id, &image_properties); |
463 if (needs_migration) | |
464 users_to_migrate_.insert(user_id); | |
222 } | 465 } |
223 if (prefs_images) { | 466 if (prefs_images) { |
224 safe_source = prefs_images->GetDictionaryWithoutPathExpansion( | 467 prefs_images->GetDictionaryWithoutPathExpansion(user_id, |
Nikita (slow)
2013/11/14 15:17:13
Can you please describe how migration implementati
bartfab (slow)
2013/11/14 15:55:10
Added to CL description.
| |
225 user->email(), &image_properties); | 468 &image_properties); |
226 } | 469 } |
227 | 470 |
228 if (needs_migration) | 471 if (!image_properties) { |
229 users_to_migrate_.insert(user->email()); | 472 SetInitialUserImage(user_id); |
473 continue; | |
474 } | |
230 | 475 |
231 if (!image_properties) { | 476 int image_index = User::kInvalidImageIndex; |
232 SetInitialUserImage(user->email()); | 477 image_properties->GetInteger(kImageIndexNodeName, &image_index); |
233 } else { | 478 if (image_index >= 0 && image_index < kDefaultImagesCount) { |
234 int image_index = User::kInvalidImageIndex; | 479 user->SetImage(UserImage(GetDefaultImage(image_index)), |
235 image_properties->GetInteger(kImageIndexNodeName, &image_index); | 480 image_index); |
481 continue; | |
482 } | |
236 | 483 |
237 if (image_index >= 0 && image_index < kDefaultImagesCount) { | 484 if (image_index != User::kExternalImageIndex && |
238 user->SetImage(UserImage(GetDefaultImage(image_index)), | 485 image_index != User::kProfileImageIndex) { |
239 image_index); | 486 NOTREACHED(); |
240 } else if (image_index == User::kExternalImageIndex || | 487 continue; |
241 image_index == User::kProfileImageIndex) { | 488 } |
242 std::string image_path; | |
243 image_properties->GetString(kImagePathNodeName, &image_path); | |
244 // Path may be empty for profile images (meaning that the image | |
245 // hasn't been downloaded for the first time yet, in which case a | |
246 // download will be scheduled for |kProfileDataDownloadDelayMs| | |
247 // after user logs in). | |
248 DCHECK(!image_path.empty() || image_index == User::kProfileImageIndex); | |
249 std::string image_url; | |
250 image_properties->GetString(kImageURLNodeName, &image_url); | |
251 GURL image_gurl(image_url); | |
252 // Until image has been loaded, use the stub image (gray avatar). | |
253 user->SetStubImage(image_index, true); | |
254 user->SetImageURL(image_gurl); | |
255 if (!image_path.empty()) { | |
256 if (needs_migration) { | |
257 // Non-JPG image will be migrated once user logs in. | |
258 // Stub image will be used for now. Continue with other users. | |
259 continue; | |
260 } | |
261 DCHECK(safe_source); | |
262 if (!safe_source) | |
263 continue; | |
264 | 489 |
265 // Load user image asynchronously - at this point we are able to use | 490 std::string image_url_string; |
266 // JPEG image loaded since image comes from safe pref source | 491 image_properties->GetString(kImageURLNodeName, &image_url_string); |
267 // i.e. converted to JPEG. | 492 GURL image_url(image_url_string); |
268 image_loader_->Start( | 493 std::string image_path; |
269 image_path, 0 /* no resize */, | 494 image_properties->GetString(kImagePathNodeName, &image_path); |
270 base::Bind(&UserImageManagerImpl::SetUserImage, | 495 |
271 base::Unretained(this), | 496 user->SetImageURL(image_url); |
272 user->email(), image_index, image_gurl)); | 497 DCHECK(!image_path.empty() || image_index == User::kProfileImageIndex); |
273 } | 498 if (image_path.empty() || needs_migration) { |
274 } else { | 499 // Use a stub image (gray avatar) if either of the following is true: |
275 NOTREACHED(); | 500 // * The profile image is to be used but has not been downloaded yet. The |
276 } | 501 // profile image will be downloaded after login. |
502 // * The image needs migration. Migration will be performed after login. | |
503 user->SetStubImage(image_index, true); | |
504 continue; | |
277 } | 505 } |
506 | |
507 linked_ptr<Job>& job = jobs_[user_id]; | |
508 job.reset(new Job(this, user_id)); | |
509 job->LoadImage(base::FilePath(image_path), image_index, image_url); | |
278 } | 510 } |
279 } | 511 } |
280 | 512 |
281 void UserImageManagerImpl::UserLoggedIn(const std::string& email, | 513 void UserImageManagerImpl::UserLoggedIn(const std::string& user_id, |
282 bool user_is_new, | 514 bool user_is_new, |
283 bool user_is_local) { | 515 bool user_is_local) { |
284 User* user = UserManager::Get()->GetLoggedInUser(); | 516 User* user = UserManager::Get()->GetLoggedInUser(); |
285 if (user_is_new) { | 517 if (user_is_new) { |
286 if (!user_is_local) | 518 if (!user_is_local) |
287 SetInitialUserImage(email); | 519 SetInitialUserImage(user_id); |
288 } else { | 520 } else { |
289 if (!user_is_local) { | |
290 // If current user image is profile image, it needs to be refreshed. | |
291 bool download_profile_image = | |
292 user->image_index() == User::kProfileImageIndex; | |
293 if (download_profile_image) | |
294 InitDownloadedProfileImage(); | |
295 | |
296 // Download user's profile data (full name and optionally image) to see if | |
297 // it has changed. | |
298 BrowserThread::PostDelayedTask( | |
299 BrowserThread::UI, | |
300 FROM_HERE, | |
301 base::Bind(&UserImageManagerImpl::DownloadProfileData, | |
302 base::Unretained(this), | |
303 kProfileDownloadReasonLoggedIn, | |
304 download_profile_image), | |
305 base::TimeDelta::FromSeconds(kProfileDataDownloadDelaySec)); | |
306 } | |
307 | |
308 UMA_HISTOGRAM_ENUMERATION("UserImage.LoggedIn", | 521 UMA_HISTOGRAM_ENUMERATION("UserImage.LoggedIn", |
309 ImageIndexToHistogramIndex(user->image_index()), | 522 ImageIndexToHistogramIndex(user->image_index()), |
310 kHistogramImagesCount); | 523 kHistogramImagesCount); |
311 | 524 |
312 if (users_to_migrate_.count(email)) { | 525 if (users_to_migrate_.find(user_id) != users_to_migrate_.end()) { |
313 const DictionaryValue* prefs_images_unsafe = | 526 const DictionaryValue* prefs_images_unsafe = |
314 g_browser_process->local_state()->GetDictionary(kUserImages); | 527 g_browser_process->local_state()->GetDictionary(kUserImages); |
315 const base::DictionaryValue* image_properties = NULL; | 528 const base::DictionaryValue* image_properties = NULL; |
316 if (prefs_images_unsafe->GetDictionaryWithoutPathExpansion( | 529 if (prefs_images_unsafe->GetDictionaryWithoutPathExpansion( |
317 user->email(), &image_properties)) { | 530 user_id, &image_properties)) { |
318 std::string image_path; | 531 std::string image_path; |
319 image_properties->GetString(kImagePathNodeName, &image_path); | 532 image_properties->GetString(kImagePathNodeName, &image_path); |
533 linked_ptr<Job>& job = jobs_[user_id]; | |
534 job.reset(new Job(this, user_id)); | |
320 if (!image_path.empty()) { | 535 if (!image_path.empty()) { |
321 // User needs image format migration but | 536 LOG(INFO) << "Loading old user image, then migrating it."; |
322 // first we need to load and decode that image. | 537 job->SetToPath(base::FilePath(image_path), |
323 LOG(INFO) << "Waiting for user image to load before migration"; | 538 user->image_index(), |
324 migrate_current_user_on_load_ = true; | 539 user->image_url(), |
325 unsafe_image_loader_->Start( | 540 false); |
326 image_path, 0 /* no resize */, | |
327 base::Bind(&UserImageManagerImpl::SetUserImage, | |
328 base::Unretained(this), | |
329 user->email(), | |
330 user->image_index(), | |
331 user->image_url())); | |
332 } else { | 541 } else { |
333 // Otherwise migrate user image properties right away. | 542 job->SetToDefaultImage(user->image_index()); |
334 BrowserThread::PostDelayedTask( | |
335 BrowserThread::UI, | |
336 FROM_HERE, | |
337 base::Bind(&UserImageManagerImpl::MigrateUserImage, | |
338 base::Unretained(this)), | |
339 base::TimeDelta::FromSeconds(user_image_migration_delay_sec)); | |
340 } | 543 } |
341 } | 544 } |
342 } | 545 } |
343 } | 546 } |
344 | 547 |
345 if (!user_is_local) { | 548 // Reset the downloaded profile image as a new user logged in. |
346 // Set up a repeating timer for refreshing the profile data. | 549 downloaded_profile_image_ = gfx::ImageSkia(); |
347 profile_download_timer_.Start( | 550 downloaded_profile_image_data_url_.clear(); |
348 FROM_HERE, base::TimeDelta::FromSeconds(kProfileRefreshIntervalSec), | 551 profile_image_url_ = GURL(); |
349 this, &UserImageManagerImpl::DownloadProfileDataScheduled); | 552 profile_image_requested_ = false; |
553 | |
554 if (UserManager::Get()->IsLoggedInAsRegularUser()) { | |
555 TryToInitDownloadedProfileImage(); | |
556 | |
557 // Schedule an initial download of the profile data (full name and | |
558 // optionally image). | |
559 profile_download_one_shot_timer_.Start( | |
560 FROM_HERE, | |
561 base::TimeDelta::FromSeconds(kProfileDataDownloadDelaySec), | |
562 base::Bind(&UserImageManagerImpl::DownloadProfileData, | |
563 base::Unretained(this), | |
564 kProfileDownloadReasonLoggedIn)); | |
565 // Schedule periodic refreshes of the profile data. | |
566 profile_download_periodic_timer_.Start( | |
567 FROM_HERE, | |
568 base::TimeDelta::FromSeconds(kProfileRefreshIntervalSec), | |
569 base::Bind(&UserImageManagerImpl::DownloadProfileData, | |
570 base::Unretained(this), | |
571 kProfileDownloadReasonScheduled)); | |
572 } else { | |
573 profile_download_one_shot_timer_.Stop(); | |
574 profile_download_periodic_timer_.Stop(); | |
350 } | 575 } |
351 CommandLine* command_line = CommandLine::ForCurrentProcess(); | 576 |
577 const CommandLine* command_line = CommandLine::ForCurrentProcess(); | |
578 if (user_image_sync_observer_.get() && | |
579 !command_line->HasSwitch(::switches::kMultiProfiles)) { | |
580 NOTREACHED() << "User logged in more than once."; | |
581 } | |
582 | |
352 if (user->CanSyncImage() && | 583 if (user->CanSyncImage() && |
353 !command_line->HasSwitch(chromeos::switches::kDisableUserImageSync)) { | 584 !command_line->HasSwitch(chromeos::switches::kDisableUserImageSync)) { |
354 if (user_image_sync_observer_.get() && | |
355 !command_line->HasSwitch(::switches::kMultiProfiles)) | |
356 NOTREACHED() << "User logged in second time."; | |
357 user_image_sync_observer_.reset(new UserImageSyncObserver(user)); | 585 user_image_sync_observer_.reset(new UserImageSyncObserver(user)); |
586 } else { | |
587 user_image_sync_observer_.reset(); | |
358 } | 588 } |
359 } | 589 } |
360 | 590 |
361 void UserImageManagerImpl::SaveUserDefaultImageIndex( | 591 void UserImageManagerImpl::SaveUserDefaultImageIndex(const std::string& user_id, |
362 const std::string& username, | 592 int default_image_index) { |
363 int image_index) { | 593 linked_ptr<Job>& job = jobs_[user_id]; |
364 DCHECK(image_index >= 0 && image_index < kDefaultImagesCount); | 594 job.reset(new Job(this, user_id)); |
365 SetUserImage(username, image_index, GURL(), | 595 job->SetToDefaultImage(default_image_index); |
366 UserImage(GetDefaultImage(image_index))); | |
367 SaveImageToLocalState(username, "", image_index, GURL(), false); | |
368 } | 596 } |
369 | 597 |
370 void UserImageManagerImpl::SaveUserImage(const std::string& username, | 598 void UserImageManagerImpl::SaveUserImage(const std::string& user_id, |
371 const UserImage& user_image) { | 599 const UserImage& user_image) { |
372 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); | 600 linked_ptr<Job>& job = jobs_[user_id]; |
373 SaveUserImageInternal(username, User::kExternalImageIndex, | 601 job.reset(new Job(this, user_id)); |
374 GURL(), user_image); | 602 job->SetToImage(User::kExternalImageIndex, user_image); |
375 } | 603 } |
376 | 604 |
377 void UserImageManagerImpl::SaveUserImageFromFile(const std::string& username, | 605 void UserImageManagerImpl::SaveUserImageFromFile(const std::string& user_id, |
378 const base::FilePath& path) { | 606 const base::FilePath& path) { |
379 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); | 607 linked_ptr<Job>& job = jobs_[user_id]; |
380 // Always use unsafe image loader because we resize the image when saving | 608 job.reset(new Job(this, user_id)); |
381 // anyway. | 609 job->SetToPath(path, User::kExternalImageIndex, GURL(), true); |
382 unsafe_image_loader_->Start( | |
383 path.value(), login::kMaxUserImageSize, | |
384 base::Bind(&UserImageManagerImpl::SaveUserImage, | |
385 base::Unretained(this), username)); | |
386 } | 610 } |
387 | 611 |
388 void UserImageManagerImpl::SaveUserImageFromProfileImage( | 612 void UserImageManagerImpl::SaveUserImageFromProfileImage( |
389 const std::string& username) { | 613 const std::string& user_id) { |
390 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); | 614 // Use the profile image if it has been downloaded already. Otherwise, use a |
391 if (!downloaded_profile_image_.isNull()) { | 615 // stub image (gray avatar). |
392 // Profile image has already been downloaded, so save it to file right now. | 616 linked_ptr<Job>& job = jobs_[user_id]; |
393 DCHECK(profile_image_url_.is_valid()); | 617 job.reset(new Job(this, user_id)); |
394 SaveUserImageInternal( | 618 job->SetToImage(User::kProfileImageIndex, |
395 username, | 619 downloaded_profile_image_.isNull() ? |
396 User::kProfileImageIndex, profile_image_url_, | 620 UserImage() : |
397 UserImage::CreateAndEncode(downloaded_profile_image_)); | 621 UserImage::CreateAndEncode(downloaded_profile_image_)); |
398 } else { | |
399 // No profile image - use the stub image (gray avatar). | |
400 SetUserImage(username, User::kProfileImageIndex, GURL(), UserImage()); | |
401 SaveImageToLocalState(username, "", User::kProfileImageIndex, | |
402 GURL(), false); | |
403 } | |
404 } | 622 } |
405 | 623 |
406 void UserImageManagerImpl::DeleteUserImage(const std::string& username) { | 624 void UserImageManagerImpl::DeleteUserImage(const std::string& user_id) { |
407 // Delete from the old dictionary, if present. | 625 jobs_.erase(user_id); |
408 DeleteOldUserImage(username); | 626 DeleteUserImageAndLocalStateEntry(user_id, kUserImages); |
409 | 627 DeleteUserImageAndLocalStateEntry(user_id, kUserImageProperties); |
410 PrefService* prefs = g_browser_process->local_state(); | |
411 DictionaryPrefUpdate prefs_images_update(prefs, kUserImageProperties); | |
412 const base::DictionaryValue* image_properties; | |
413 if (prefs_images_update->GetDictionaryWithoutPathExpansion( | |
414 username, &image_properties)) { | |
415 std::string image_path; | |
416 image_properties->GetString(kImageURLNodeName, &image_path); | |
417 prefs_images_update->RemoveWithoutPathExpansion(username, NULL); | |
418 DeleteImageFile(image_path); | |
419 } | |
420 } | 628 } |
421 | 629 |
422 void UserImageManagerImpl::DownloadProfileImage(const std::string& reason) { | 630 void UserImageManagerImpl::DownloadProfileImage(const std::string& reason) { |
423 DownloadProfileData(reason, true); | 631 profile_image_requested_ = true; |
632 DownloadProfileData(reason); | |
424 } | 633 } |
425 | 634 |
426 UserImageSyncObserver* UserImageManagerImpl::GetSyncObserver() const { | 635 UserImageSyncObserver* UserImageManagerImpl::GetSyncObserver() const { |
427 return user_image_sync_observer_.get(); | 636 return user_image_sync_observer_.get(); |
428 } | 637 } |
429 | 638 |
430 void UserImageManagerImpl::Shutdown() { | 639 void UserImageManagerImpl::Shutdown() { |
431 profile_image_downloader_.reset(); | 640 profile_downloader_.reset(); |
432 user_image_sync_observer_.reset(); | 641 user_image_sync_observer_.reset(); |
433 } | 642 } |
434 | 643 |
435 const gfx::ImageSkia& UserImageManagerImpl::DownloadedProfileImage() const { | 644 const gfx::ImageSkia& UserImageManagerImpl::DownloadedProfileImage() const { |
436 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); | |
437 return downloaded_profile_image_; | 645 return downloaded_profile_image_; |
438 } | 646 } |
439 | 647 |
440 base::FilePath UserImageManagerImpl::GetImagePathForUser( | 648 void UserImageManagerImpl::SetInitialUserImage(const std::string& user_id) { |
441 const std::string& username) { | 649 // Choose a random default image. |
442 std::string filename = username + kSafeImagePathExtension; | 650 SaveUserDefaultImageIndex(user_id, |
443 base::FilePath user_data_dir; | 651 base::RandInt(kFirstDefaultImageIndex, |
444 PathService::Get(chrome::DIR_USER_DATA, &user_data_dir); | 652 kDefaultImagesCount - 1)); |
445 return user_data_dir.AppendASCII(filename); | |
446 } | 653 } |
447 | 654 |
448 void UserImageManagerImpl::SetInitialUserImage(const std::string& username) { | 655 void UserImageManagerImpl::TryToInitDownloadedProfileImage() { |
449 // Choose a random default image. | 656 const User* user = UserManager::Get()->GetLoggedInUser(); |
450 int image_id = | 657 if (user->image_index() == User::kProfileImageIndex && |
451 base::RandInt(kFirstDefaultImageIndex, kDefaultImagesCount - 1); | 658 downloaded_profile_image_.isNull() && |
452 SaveUserDefaultImageIndex(username, image_id); | 659 !user->image_is_stub()) { |
453 } | 660 // Initialize the |downloaded_profile_image_| for the currently logged-in |
454 | 661 // user if it has not been initialized already, the user image is the |
455 void UserImageManagerImpl::SetUserImage(const std::string& username, | 662 // profile image and the user image has been loaded successfully. |
456 int image_index, | 663 VLOG(1) << "Profile image initialized from disk."; |
457 const GURL& image_url, | 664 downloaded_profile_image_ = user->image(); |
458 const UserImage& user_image) { | 665 downloaded_profile_image_data_url_ = |
459 User* user = const_cast<User*>(UserManager::Get()->FindUser(username)); | 666 webui::GetBitmapDataUrl(*downloaded_profile_image_.bitmap()); |
460 // User may have been removed by now. | 667 profile_image_url_ = user->image_url(); |
461 if (user) { | |
462 bool image_changed = user->image_index() != User::kInvalidImageIndex; | |
463 bool is_current_user = user == UserManager::Get()->GetLoggedInUser(); | |
464 if (!user_image.image().isNull()) | |
465 user->SetImage(user_image, image_index); | |
466 else | |
467 user->SetStubImage(image_index, false); | |
468 user->SetImageURL(image_url); | |
469 // For the logged-in user with a profile picture, initialize | |
470 // |downloaded_profile_picture_|. | |
471 if (is_current_user && image_index == User::kProfileImageIndex) { | |
472 InitDownloadedProfileImage(); | |
473 } | |
474 if (image_changed) { | |
475 // Unless this is first-time setting with |SetInitialUserImage|, | |
476 // send a notification about image change. | |
477 content::NotificationService::current()->Notify( | |
478 chrome::NOTIFICATION_LOGIN_USER_IMAGE_CHANGED, | |
479 content::Source<UserImageManager>(this), | |
480 content::Details<const User>(user)); | |
481 } | |
482 if (is_current_user && migrate_current_user_on_load_) | |
483 MigrateUserImage(); | |
484 } | 668 } |
485 } | 669 } |
486 | 670 |
487 void UserImageManagerImpl::SaveUserImageInternal(const std::string& username, | 671 bool UserImageManagerImpl::NeedProfileImage() const { |
488 int image_index, | 672 return UserManager::Get()->IsLoggedInAsRegularUser() && |
489 const GURL& image_url, | 673 (UserManager::Get()->GetLoggedInUser()->image_index() == |
490 const UserImage& user_image) { | 674 User::kProfileImageIndex || |
491 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); | 675 profile_image_requested_); |
492 | |
493 SetUserImage(username, image_index, image_url, user_image); | |
494 | |
495 // Ignore if data stored or cached outside the user's cryptohome is to be | |
496 // treated as ephemeral. | |
497 if (UserManager::Get()->IsUserNonCryptohomeDataEphemeral(username)) | |
498 return; | |
499 | |
500 base::FilePath image_path = GetImagePathForUser(username); | |
501 DVLOG(1) << "Saving user image to " << image_path.value(); | |
502 | |
503 last_image_set_async_ = true; | |
504 | |
505 base::WorkerPool::PostTask( | |
506 FROM_HERE, | |
507 base::Bind(&UserImageManagerImpl::SaveImageToFile, | |
508 base::Unretained(this), | |
509 username, user_image, image_path, image_index, image_url), | |
510 /* is_slow= */ false); | |
511 } | 676 } |
512 | 677 |
513 void UserImageManagerImpl::SaveImageToFile(const std::string& username, | 678 void UserImageManagerImpl::DownloadProfileData(const std::string& reason) { |
514 const UserImage& user_image, | |
515 const base::FilePath& image_path, | |
516 int image_index, | |
517 const GURL& image_url) { | |
518 if (!SaveBitmapToFile(user_image, image_path)) | |
519 return; | |
520 | |
521 BrowserThread::PostTask( | |
522 BrowserThread::UI, | |
523 FROM_HERE, | |
524 base::Bind(&UserImageManagerImpl::SaveImageToLocalState, | |
525 base::Unretained(this), | |
526 username, image_path.value(), image_index, image_url, true)); | |
527 } | |
528 | |
529 void UserImageManagerImpl::SaveImageToLocalState(const std::string& username, | |
530 const std::string& image_path, | |
531 int image_index, | |
532 const GURL& image_url, | |
533 bool is_async) { | |
534 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); | |
535 | |
536 // Ignore if data stored or cached outside the user's cryptohome is to be | |
537 // treated as ephemeral. | |
538 if (UserManager::Get()->IsUserNonCryptohomeDataEphemeral(username)) | |
539 return; | |
540 | |
541 // TODO(ivankr): use unique filenames for user images each time | |
542 // a new image is set so that only the last image update is saved | |
543 // to Local State and notified. | |
544 if (is_async && !last_image_set_async_) { | |
545 DVLOG(1) << "Ignoring saved image because it has changed"; | |
546 return; | |
547 } else if (!is_async) { | |
548 // Reset the async image save flag if called directly from the UI thread. | |
549 last_image_set_async_ = false; | |
550 } | |
551 | |
552 PrefService* local_state = g_browser_process->local_state(); | |
553 DictionaryPrefUpdate images_update(local_state, kUserImageProperties); | |
554 base::DictionaryValue* image_properties = new base::DictionaryValue(); | |
555 image_properties->Set(kImagePathNodeName, new StringValue(image_path)); | |
556 image_properties->Set(kImageIndexNodeName, | |
557 new base::FundamentalValue(image_index)); | |
558 if (!image_url.is_empty()) { | |
559 image_properties->Set(kImageURLNodeName, | |
560 new StringValue(image_url.spec())); | |
561 } else { | |
562 image_properties->Remove(kImageURLNodeName, NULL); | |
563 } | |
564 images_update->SetWithoutPathExpansion(username, image_properties); | |
565 DVLOG(1) << "Saving path to user image in Local State."; | |
566 | |
567 if (users_to_migrate_.count(username)) { | |
568 DeleteOldUserImage(username); | |
569 users_to_migrate_.erase(username); | |
570 } | |
571 | |
572 UserManager::Get()->NotifyLocalStateChanged(); | |
573 } | |
574 | |
575 bool UserImageManagerImpl::SaveBitmapToFile(const UserImage& user_image, | |
576 const base::FilePath& image_path) { | |
577 UserImage safe_image; | |
578 const UserImage::RawImage* encoded_image = NULL; | |
579 if (!user_image.is_safe_format()) { | |
580 safe_image = UserImage::CreateAndEncode(user_image.image()); | |
581 encoded_image = &safe_image.raw_image(); | |
582 UMA_HISTOGRAM_MEMORY_KB("UserImage.RecodedJpegSize", encoded_image->size()); | |
583 } else if (user_image.has_raw_image()) { | |
584 encoded_image = &user_image.raw_image(); | |
585 } else { | |
586 NOTREACHED() << "Raw image missing."; | |
587 return false; | |
588 } | |
589 | |
590 if (file_util::WriteFile(image_path, | |
591 reinterpret_cast<const char*>(&(*encoded_image)[0]), | |
592 encoded_image->size()) == -1) { | |
593 LOG(ERROR) << "Failed to save image to file."; | |
594 return false; | |
595 } | |
596 return true; | |
597 } | |
598 | |
599 void UserImageManagerImpl::InitDownloadedProfileImage() { | |
600 const User* logged_in_user = UserManager::Get()->GetLoggedInUser(); | |
601 DCHECK_EQ(logged_in_user->image_index(), User::kProfileImageIndex); | |
602 if (downloaded_profile_image_.isNull() && !logged_in_user->image_is_stub()) { | |
603 VLOG(1) << "Profile image initialized"; | |
604 downloaded_profile_image_ = logged_in_user->image(); | |
605 downloaded_profile_image_data_url_ = | |
606 webui::GetBitmapDataUrl(*downloaded_profile_image_.bitmap()); | |
607 profile_image_url_ = logged_in_user->image_url(); | |
608 } | |
609 } | |
610 | |
611 void UserImageManagerImpl::DownloadProfileData(const std::string& reason, | |
612 bool download_image) { | |
613 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); | |
614 | |
615 // GAIA profiles exist for regular users only. | 679 // GAIA profiles exist for regular users only. |
616 if (!UserManager::Get()->IsLoggedInAsRegularUser()) | 680 if (!UserManager::Get()->IsLoggedInAsRegularUser()) |
617 return; | 681 return; |
618 | 682 |
619 // Mark profile picture as needed. | 683 // If a download is already in progress, allow it to continue, with one |
620 downloading_profile_image_ |= download_image; | 684 // exception: If the current download does not include the profile image but |
685 // the image has since become necessary, start a new download that includes | |
686 // the profile image. | |
687 if (profile_downloader_ && | |
688 (downloading_profile_image_ || !NeedProfileImage())) { | |
689 return; | |
690 } | |
621 | 691 |
622 // Another download is already in progress | 692 downloading_profile_image_ = NeedProfileImage(); |
623 if (profile_image_downloader_.get()) | |
624 return; | |
625 | |
626 profile_image_download_reason_ = reason; | 693 profile_image_download_reason_ = reason; |
627 profile_image_load_start_time_ = base::Time::Now(); | 694 profile_image_load_start_time_ = base::TimeTicks::Now(); |
628 profile_image_downloader_.reset(new ProfileDownloader(this)); | 695 profile_downloader_.reset(new ProfileDownloader(this)); |
629 profile_image_downloader_->Start(); | 696 profile_downloader_->Start(); |
630 } | 697 } |
631 | 698 |
632 void UserImageManagerImpl::DownloadProfileDataScheduled() { | |
633 const User* logged_in_user = UserManager::Get()->GetLoggedInUser(); | |
634 // If current user image is profile image, it needs to be refreshed. | |
635 bool download_profile_image = | |
636 logged_in_user->image_index() == User::kProfileImageIndex; | |
637 DownloadProfileData(kProfileDownloadReasonScheduled, download_profile_image); | |
638 } | |
639 | |
640 void UserImageManagerImpl::DownloadProfileDataRetry(bool download_image) { | |
641 DownloadProfileData(kProfileDownloadReasonRetry, download_image); | |
642 } | |
643 | |
644 // ProfileDownloaderDelegate override. | |
645 bool UserImageManagerImpl::NeedsProfilePicture() const { | 699 bool UserImageManagerImpl::NeedsProfilePicture() const { |
646 return downloading_profile_image_; | 700 return downloading_profile_image_; |
647 } | 701 } |
648 | 702 |
649 // ProfileDownloaderDelegate override. | |
650 int UserImageManagerImpl::GetDesiredImageSideLength() const { | 703 int UserImageManagerImpl::GetDesiredImageSideLength() const { |
651 return GetCurrentUserImageSize(); | 704 return GetCurrentUserImageSize(); |
652 } | 705 } |
653 | 706 |
654 // ProfileDownloaderDelegate override. | |
655 std::string UserImageManagerImpl::GetCachedPictureURL() const { | 707 std::string UserImageManagerImpl::GetCachedPictureURL() const { |
656 return profile_image_url_.spec(); | 708 return profile_image_url_.spec(); |
657 } | 709 } |
658 | 710 |
659 Profile* UserImageManagerImpl::GetBrowserProfile() { | 711 Profile* UserImageManagerImpl::GetBrowserProfile() { |
660 return ProfileManager::GetDefaultProfile(); | 712 return ProfileManager::GetDefaultProfile(); |
661 } | 713 } |
662 | 714 |
663 void UserImageManagerImpl::OnProfileDownloadSuccess( | 715 void UserImageManagerImpl::OnProfileDownloadSuccess( |
664 ProfileDownloader* downloader) { | 716 ProfileDownloader* downloader) { |
665 // Make sure that |ProfileDownloader| gets deleted after return. | 717 // Ensure that the |profile_downloader_| is deleted when this method returns. |
666 scoped_ptr<ProfileDownloader> profile_image_downloader( | 718 scoped_ptr<ProfileDownloader> profile_downloader( |
667 profile_image_downloader_.release()); | 719 profile_downloader_.release()); |
668 DCHECK_EQ(downloader, profile_image_downloader.get()); | 720 DCHECK_EQ(downloader, profile_downloader.get()); |
669 | 721 |
670 UserManager* user_manager = UserManager::Get(); | 722 const User* user = UserManager::Get()->GetLoggedInUser(); |
671 const User* user = user_manager->GetLoggedInUser(); | 723 const std::string& user_id = user->email(); |
672 | 724 |
673 user_manager->UpdateUserAccountData(user->email(), | 725 UserManager::Get()->UpdateUserAccountData(user_id, |
674 downloader->GetProfileFullName(), | 726 downloader->GetProfileFullName(), |
675 downloader->GetProfileLocale()); | 727 downloader->GetProfileLocale()); |
676 | 728 |
677 bool requested_image = downloading_profile_image_; | 729 if (!downloading_profile_image_) |
678 downloading_profile_image_ = false; | |
679 if (!requested_image) | |
680 return; | 730 return; |
681 | 731 |
682 ProfileDownloadResult result = kDownloadFailure; | 732 ProfileDownloadResult result = kDownloadFailure; |
683 switch (downloader->GetProfilePictureStatus()) { | 733 switch (downloader->GetProfilePictureStatus()) { |
684 case ProfileDownloader::PICTURE_SUCCESS: | 734 case ProfileDownloader::PICTURE_SUCCESS: |
685 result = kDownloadSuccess; | 735 result = kDownloadSuccess; |
686 break; | 736 break; |
687 case ProfileDownloader::PICTURE_CACHED: | 737 case ProfileDownloader::PICTURE_CACHED: |
688 result = kDownloadCached; | 738 result = kDownloadCached; |
689 break; | 739 break; |
690 case ProfileDownloader::PICTURE_DEFAULT: | 740 case ProfileDownloader::PICTURE_DEFAULT: |
691 result = kDownloadDefault; | 741 result = kDownloadDefault; |
692 break; | 742 break; |
693 default: | 743 default: |
694 NOTREACHED(); | 744 NOTREACHED(); |
695 } | 745 } |
696 | 746 |
697 UMA_HISTOGRAM_ENUMERATION("UserImage.ProfileDownloadResult", | 747 UMA_HISTOGRAM_ENUMERATION("UserImage.ProfileDownloadResult", |
698 result, kDownloadResultsCount); | 748 result, |
749 kDownloadResultsCount); | |
750 DCHECK(!profile_image_load_start_time_.is_null()); | |
751 AddProfileImageTimeHistogram( | |
752 result, | |
753 profile_image_download_reason_, | |
754 base::TimeTicks::Now() - profile_image_load_start_time_); | |
699 | 755 |
700 DCHECK(!profile_image_load_start_time_.is_null()); | 756 // Ignore the image if it is no longer needed. |
701 base::TimeDelta delta = base::Time::Now() - profile_image_load_start_time_; | 757 if (!NeedProfileImage()) |
702 AddProfileImageTimeHistogram(result, profile_image_download_reason_, delta); | 758 return; |
703 | 759 |
704 if (result == kDownloadDefault) { | 760 if (result == kDownloadDefault) { |
705 content::NotificationService::current()->Notify( | 761 content::NotificationService::current()->Notify( |
706 chrome::NOTIFICATION_PROFILE_IMAGE_UPDATE_FAILED, | 762 chrome::NOTIFICATION_PROFILE_IMAGE_UPDATE_FAILED, |
707 content::Source<UserImageManager>(this), | 763 content::Source<UserImageManager>(this), |
708 content::NotificationService::NoDetails()); | 764 content::NotificationService::NoDetails()); |
709 } | 765 } |
710 | 766 |
711 // Nothing to do if picture is cached or the default avatar. | 767 // Nothing to do if the picture is cached or is the default avatar. |
712 if (result != kDownloadSuccess) | 768 if (result != kDownloadSuccess) |
713 return; | 769 return; |
714 | 770 |
771 profile_image_requested_ = false; | |
772 | |
715 // Check if this image is not the same as already downloaded. | 773 // Check if this image is not the same as already downloaded. |
716 SkBitmap new_bitmap(downloader->GetProfilePicture()); | 774 const std::string new_image_data_url = |
717 std::string new_image_data_url = webui::GetBitmapDataUrl(new_bitmap); | 775 webui::GetBitmapDataUrl(SkBitmap(downloader->GetProfilePicture())); |
718 if (!downloaded_profile_image_data_url_.empty() && | 776 if (!downloaded_profile_image_data_url_.empty() && |
719 new_image_data_url == downloaded_profile_image_data_url_) | 777 new_image_data_url == downloaded_profile_image_data_url_) { |
720 return; | 778 return; |
779 } | |
721 | 780 |
722 downloaded_profile_image_data_url_ = new_image_data_url; | 781 downloaded_profile_image_data_url_ = new_image_data_url; |
723 downloaded_profile_image_ = gfx::ImageSkia::CreateFrom1xBitmap( | 782 downloaded_profile_image_ = gfx::ImageSkia::CreateFrom1xBitmap( |
724 downloader->GetProfilePicture()); | 783 downloader->GetProfilePicture()); |
725 profile_image_url_ = GURL(downloader->GetProfilePictureURL()); | 784 profile_image_url_ = GURL(downloader->GetProfilePictureURL()); |
726 | 785 |
727 if (user->image_index() == User::kProfileImageIndex) { | 786 if (user->image_index() == User::kProfileImageIndex) { |
728 VLOG(1) << "Updating profile image for logged-in user"; | 787 VLOG(1) << "Updating profile image for logged-in user."; |
729 UMA_HISTOGRAM_ENUMERATION("UserImage.ProfileDownloadResult", | 788 UMA_HISTOGRAM_ENUMERATION("UserImage.ProfileDownloadResult", |
730 kDownloadSuccessChanged, | 789 kDownloadSuccessChanged, |
731 kDownloadResultsCount); | 790 kDownloadResultsCount); |
732 // This will persist |downloaded_profile_image_| to file. | 791 // This will persist |downloaded_profile_image_| to disk. |
733 SaveUserImageFromProfileImage(user->email()); | 792 SaveUserImageFromProfileImage(user_id); |
734 } | 793 } |
735 | 794 |
736 content::NotificationService::current()->Notify( | 795 content::NotificationService::current()->Notify( |
737 chrome::NOTIFICATION_PROFILE_IMAGE_UPDATED, | 796 chrome::NOTIFICATION_PROFILE_IMAGE_UPDATED, |
738 content::Source<UserImageManager>(this), | 797 content::Source<UserImageManager>(this), |
739 content::Details<const gfx::ImageSkia>(&downloaded_profile_image_)); | 798 content::Details<const gfx::ImageSkia>(&downloaded_profile_image_)); |
740 } | 799 } |
741 | 800 |
742 void UserImageManagerImpl::OnProfileDownloadFailure( | 801 void UserImageManagerImpl::OnProfileDownloadFailure( |
743 ProfileDownloader* downloader, | 802 ProfileDownloader* downloader, |
744 ProfileDownloaderDelegate::FailureReason reason) { | 803 ProfileDownloaderDelegate::FailureReason reason) { |
745 DCHECK_EQ(downloader, profile_image_downloader_.get()); | 804 DCHECK_EQ(downloader, profile_downloader_.get()); |
746 profile_image_downloader_.reset(); | 805 profile_downloader_.reset(); |
747 | 806 |
748 UMA_HISTOGRAM_ENUMERATION("UserImage.ProfileDownloadResult", | 807 if (downloading_profile_image_) { |
749 kDownloadFailure, kDownloadResultsCount); | 808 UMA_HISTOGRAM_ENUMERATION("UserImage.ProfileDownloadResult", |
750 | 809 kDownloadFailure, |
751 DCHECK(!profile_image_load_start_time_.is_null()); | 810 kDownloadResultsCount); |
752 base::TimeDelta delta = base::Time::Now() - profile_image_load_start_time_; | 811 DCHECK(!profile_image_load_start_time_.is_null()); |
753 AddProfileImageTimeHistogram(kDownloadFailure, profile_image_download_reason_, | 812 AddProfileImageTimeHistogram( |
754 delta); | 813 kDownloadFailure, |
814 profile_image_download_reason_, | |
815 base::TimeTicks::Now() - profile_image_load_start_time_); | |
816 } | |
755 | 817 |
756 UserManager* user_manager = UserManager::Get(); | 818 UserManager* user_manager = UserManager::Get(); |
757 const User* user = user_manager->GetLoggedInUser(); | 819 const User* user = user_manager->GetLoggedInUser(); |
758 | 820 |
759 // Need note that at least one attempt finished. | |
760 user_manager->UpdateUserAccountData(user->email(), string16(), ""); | 821 user_manager->UpdateUserAccountData(user->email(), string16(), ""); |
761 | 822 |
762 // Retry download after some time if a network error has occured. | |
763 if (reason == ProfileDownloaderDelegate::NETWORK_ERROR) { | 823 if (reason == ProfileDownloaderDelegate::NETWORK_ERROR) { |
764 BrowserThread::PostDelayedTask( | 824 // Retry download after a delay if a network error occurred. |
765 BrowserThread::UI, | 825 profile_download_one_shot_timer_.Start( |
766 FROM_HERE, | 826 FROM_HERE, |
767 base::Bind(&UserImageManagerImpl::DownloadProfileDataRetry, | 827 base::TimeDelta::FromSeconds(kProfileDataDownloadRetryIntervalSec), |
828 base::Bind(&UserImageManagerImpl::DownloadProfileData, | |
768 base::Unretained(this), | 829 base::Unretained(this), |
769 downloading_profile_image_), | 830 kProfileDownloadReasonRetry)); |
770 base::TimeDelta::FromSeconds(kProfileDataDownloadRetryIntervalSec)); | |
771 } | 831 } |
772 | 832 |
773 downloading_profile_image_ = false; | |
774 | |
775 content::NotificationService::current()->Notify( | 833 content::NotificationService::current()->Notify( |
776 chrome::NOTIFICATION_PROFILE_IMAGE_UPDATE_FAILED, | 834 chrome::NOTIFICATION_PROFILE_IMAGE_UPDATE_FAILED, |
777 content::Source<UserImageManager>(this), | 835 content::Source<UserImageManager>(this), |
778 content::NotificationService::NoDetails()); | 836 content::NotificationService::NoDetails()); |
779 } | 837 } |
780 | 838 |
781 void UserImageManagerImpl::MigrateUserImage() { | 839 void UserImageManagerImpl::DeleteUserImageAndLocalStateEntry( |
782 User* user = UserManager::Get()->GetLoggedInUser(); | 840 const std::string& user_id, |
783 if (user->image_is_loading()) { | 841 const char* prefs_dict_root) { |
784 LOG(INFO) << "Waiting for user image to load before migration"; | 842 DictionaryPrefUpdate update(g_browser_process->local_state(), |
785 migrate_current_user_on_load_ = true; | 843 prefs_dict_root); |
844 const base::DictionaryValue* image_properties; | |
845 if (!update->GetDictionaryWithoutPathExpansion(user_id, &image_properties)) | |
846 return; | |
847 | |
848 std::string image_path; | |
849 image_properties->GetString(kImagePathNodeName, &image_path); | |
850 if (!image_path.empty()) { | |
851 background_task_runner_->PostTask( | |
852 FROM_HERE, | |
853 base::Bind(base::IgnoreResult(&base::DeleteFile), | |
854 base::FilePath(image_path), | |
855 false)); | |
856 } | |
857 update->RemoveWithoutPathExpansion(user_id, NULL); | |
858 } | |
859 | |
860 void UserImageManagerImpl::OnJobChangedUserImage(const User* user) { | |
861 if (user == UserManager::Get()->GetLoggedInUser()) | |
862 TryToInitDownloadedProfileImage(); | |
863 | |
864 content::NotificationService::current()->Notify( | |
865 chrome::NOTIFICATION_LOGIN_USER_IMAGE_CHANGED, | |
866 content::Source<UserImageManagerImpl>(this), | |
867 content::Details<const User>(user)); | |
868 } | |
869 | |
870 void UserImageManagerImpl::OnJobDone(const std::string& user_id) { | |
871 std::map<std::string, linked_ptr<Job> >::iterator it = | |
872 jobs_.find(user_id); | |
873 if (it != jobs_.end()) { | |
874 // The Job for |user_id| is done and must be destroyed. Since control will | |
875 // return to the Job when this method finishes, it cannot be destroyed from | |
876 // here. Instead, ownership is dropped and the Job destroys itself. | |
877 it->second.release(); | |
878 jobs_.erase(it); | |
879 } else { | |
880 NOTREACHED(); | |
881 } | |
882 | |
883 if (users_to_migrate_.find(user_id) == users_to_migrate_.end()) | |
884 return; | |
885 // Migration completed for |user_id|. | |
886 users_to_migrate_.erase(user_id); | |
887 | |
888 const DictionaryValue* prefs_images_unsafe = | |
889 g_browser_process->local_state()->GetDictionary(kUserImages); | |
890 const base::DictionaryValue* image_properties = NULL; | |
891 if (!prefs_images_unsafe->GetDictionaryWithoutPathExpansion( | |
892 user_id, &image_properties)) { | |
893 NOTREACHED(); | |
786 return; | 894 return; |
787 } | 895 } |
788 migrate_current_user_on_load_ = false; | 896 |
789 if (user->has_raw_image() && user->image_is_safe_format()) { | 897 int image_index = User::kInvalidImageIndex; |
790 // Nothing to migrate already, make sure we delete old image. | 898 image_properties->GetInteger(kImageIndexNodeName, &image_index); |
791 DeleteOldUserImage(user->email()); | 899 UMA_HISTOGRAM_ENUMERATION("UserImage.Migration", |
792 users_to_migrate_.erase(user->email()); | 900 ImageIndexToHistogramIndex(image_index), |
793 return; | 901 kHistogramImagesCount); |
794 } | 902 |
795 if (user->HasDefaultImage()) { | 903 std::string image_path; |
796 SaveUserDefaultImageIndex(user->email(), user->image_index()); | 904 image_properties->GetString(kImagePathNodeName, &image_path); |
905 if (!image_path.empty()) { | |
906 // If an old image exists, delete it and remove |user_id| from the old prefs | |
907 // dictionary only after the deletion has completed. This ensures that no | |
908 // orphaned image is left behind if the browser crashes before the deletion | |
909 // has been performed: In that case, local state will be unchanged and the | |
910 // migration will be run again on the user's next login. | |
911 background_task_runner_->PostTaskAndReply( | |
912 FROM_HERE, | |
913 base::Bind(base::IgnoreResult(&base::DeleteFile), | |
914 base::FilePath(image_path), | |
915 false), | |
916 base::Bind(&UserImageManagerImpl::UpdateLocalStateAfterMigration, | |
917 weak_factory_.GetWeakPtr(), | |
918 user_id)); | |
797 } else { | 919 } else { |
798 SaveUserImageInternal(user->email(), user->image_index(), | 920 // If no old image exists, remove |user_id| from the old prefs dictionary. |
799 user->image_url(), user->user_image()); | 921 UpdateLocalStateAfterMigration(user_id); |
800 } | |
801 UMA_HISTOGRAM_ENUMERATION("UserImage.Migration", | |
802 ImageIndexToHistogramIndex(user->image_index()), | |
803 kHistogramImagesCount); | |
804 } | |
805 | |
806 void UserImageManagerImpl::DeleteOldUserImage(const std::string& username) { | |
807 PrefService* prefs = g_browser_process->local_state(); | |
808 DictionaryPrefUpdate prefs_images_update(prefs, kUserImages); | |
809 const base::DictionaryValue* image_properties; | |
810 if (prefs_images_update->GetDictionaryWithoutPathExpansion( | |
811 username, &image_properties)) { | |
812 std::string image_path; | |
813 image_properties->GetString(kImagePathNodeName, &image_path); | |
814 prefs_images_update->RemoveWithoutPathExpansion(username, NULL); | |
815 DeleteImageFile(image_path); | |
816 } | 922 } |
817 } | 923 } |
818 | 924 |
925 void UserImageManagerImpl::UpdateLocalStateAfterMigration( | |
926 const std::string& user_id) { | |
927 DictionaryPrefUpdate update(g_browser_process->local_state(), | |
928 kUserImages); | |
929 update->RemoveWithoutPathExpansion(user_id, NULL); | |
930 } | |
931 | |
819 } // namespace chromeos | 932 } // namespace chromeos |
OLD | NEW |