Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(596)

Side by Side Diff: chrome/browser/profiles/profile_info_cache.cc

Issue 222313005: [Profiles] Download high-res avatars using the --new-profile-management flag (Closed) Base URL: svn://svn.chromium.org/chrome/trunk/src
Patch Set: typo Created 6 years, 7 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch | Annotate | Revision Log
OLDNEW
1 // Copyright (c) 2012 The Chromium Authors. All rights reserved. 1 // Copyright (c) 2012 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/profiles/profile_info_cache.h" 5 #include "chrome/browser/profiles/profile_info_cache.h"
6 6
7 #include "base/bind.h" 7 #include "base/bind.h"
8 #include "base/file_util.h" 8 #include "base/file_util.h"
9 #include "base/i18n/case_conversion.h" 9 #include "base/i18n/case_conversion.h"
10 #include "base/logging.h" 10 #include "base/logging.h"
11 #include "base/memory/scoped_ptr.h" 11 #include "base/memory/scoped_ptr.h"
12 #include "base/path_service.h"
13 #include "base/prefs/pref_registry_simple.h" 12 #include "base/prefs/pref_registry_simple.h"
14 #include "base/prefs/pref_service.h" 13 #include "base/prefs/pref_service.h"
15 #include "base/prefs/scoped_user_pref_update.h" 14 #include "base/prefs/scoped_user_pref_update.h"
16 #include "base/rand_util.h" 15 #include "base/rand_util.h"
17 #include "base/stl_util.h" 16 #include "base/stl_util.h"
18 #include "base/strings/string_number_conversions.h" 17 #include "base/strings/string_number_conversions.h"
19 #include "base/strings/string_piece.h" 18 #include "base/strings/string_piece.h"
20 #include "base/strings/utf_string_conversions.h" 19 #include "base/strings/utf_string_conversions.h"
21 #include "base/values.h" 20 #include "base/values.h"
22 #include "chrome/browser/browser_process.h" 21 #include "chrome/browser/browser_process.h"
23 #include "chrome/browser/chrome_notification_types.h" 22 #include "chrome/browser/chrome_notification_types.h"
23 #include "chrome/browser/profiles/profile_avatar_downloader.h"
24 #include "chrome/browser/profiles/profile_avatar_icon_util.h" 24 #include "chrome/browser/profiles/profile_avatar_icon_util.h"
25 #include "chrome/common/chrome_paths.h" 25 #include "chrome/browser/profiles/profiles_state.h"
26 #include "chrome/common/pref_names.h" 26 #include "chrome/common/pref_names.h"
27 #include "components/signin/core/common/profile_management_switches.h" 27 #include "components/signin/core/common/profile_management_switches.h"
28 #include "content/public/browser/browser_thread.h" 28 #include "content/public/browser/browser_thread.h"
29 #include "content/public/browser/notification_service.h" 29 #include "content/public/browser/notification_service.h"
30 #include "grit/generated_resources.h" 30 #include "grit/generated_resources.h"
31 #include "grit/theme_resources.h" 31 #include "grit/theme_resources.h"
32 #include "third_party/skia/include/core/SkBitmap.h"
33 #include "ui/base/l10n/l10n_util.h" 32 #include "ui/base/l10n/l10n_util.h"
34 #include "ui/base/resource/resource_bundle.h" 33 #include "ui/base/resource/resource_bundle.h"
35 #include "ui/gfx/image/image.h" 34 #include "ui/gfx/image/image.h"
36 #include "ui/gfx/image/image_util.h" 35 #include "ui/gfx/image/image_util.h"
37 36
38 using content::BrowserThread; 37 using content::BrowserThread;
39 38
40 namespace { 39 namespace {
41 40
42 const char kNameKey[] = "name"; 41 const char kNameKey[] = "name";
(...skipping 143 matching lines...) Expand 10 before | Expand all | Expand 10 after
186 info->Remove(kIsManagedKey, NULL); 185 info->Remove(kIsManagedKey, NULL);
187 info->SetString(kManagedUserId, is_managed ? "DUMMY_ID" : std::string()); 186 info->SetString(kManagedUserId, is_managed ? "DUMMY_ID" : std::string());
188 } 187 }
189 info->SetBoolean(kIsUsingDefaultName, IsDefaultName(name)); 188 info->SetBoolean(kIsUsingDefaultName, IsDefaultName(name));
190 } 189 }
191 } 190 }
192 191
193 ProfileInfoCache::~ProfileInfoCache() { 192 ProfileInfoCache::~ProfileInfoCache() {
194 STLDeleteContainerPairSecondPointers( 193 STLDeleteContainerPairSecondPointers(
195 cached_avatar_images_.begin(), cached_avatar_images_.end()); 194 cached_avatar_images_.begin(), cached_avatar_images_.end());
195 STLDeleteContainerPairSecondPointers(
196 avatar_images_downloads_in_progress_.begin(),
197 avatar_images_downloads_in_progress_.end());
196 } 198 }
197 199
198 void ProfileInfoCache::AddProfileToCache(const base::FilePath& profile_path, 200 void ProfileInfoCache::AddProfileToCache(const base::FilePath& profile_path,
199 const base::string16& name, 201 const base::string16& name,
200 const base::string16& username, 202 const base::string16& username,
201 size_t icon_index, 203 size_t icon_index,
202 const std::string& managed_user_id) { 204 const std::string& managed_user_id) {
203 std::string key = CacheKeyFromProfilePath(profile_path); 205 std::string key = CacheKeyFromProfilePath(profile_path);
204 DictionaryPrefUpdate update(prefs_, prefs::kProfileInfoCache); 206 DictionaryPrefUpdate update(prefs_, prefs::kProfileInfoCache);
205 base::DictionaryValue* cache = update.Get(); 207 base::DictionaryValue* cache = update.Get();
(...skipping 119 matching lines...) Expand 10 before | Expand all | Expand 10 after
325 327
326 const gfx::Image& ProfileInfoCache::GetAvatarIconOfProfileAtIndex( 328 const gfx::Image& ProfileInfoCache::GetAvatarIconOfProfileAtIndex(
327 size_t index) const { 329 size_t index) const {
328 if (IsUsingGAIAPictureOfProfileAtIndex(index)) { 330 if (IsUsingGAIAPictureOfProfileAtIndex(index)) {
329 const gfx::Image* image = GetGAIAPictureOfProfileAtIndex(index); 331 const gfx::Image* image = GetGAIAPictureOfProfileAtIndex(index);
330 if (image) 332 if (image)
331 return *image; 333 return *image;
332 } 334 }
333 335
334 // Use the high resolution version of the avatar if it exists. 336 // Use the high resolution version of the avatar if it exists.
335 if (switches::IsNewProfileManagement()) { 337 if (switches::IsNewAvatarMenu()) {
336 const gfx::Image* image = GetHighResAvatarOfProfileAtIndex(index); 338 const gfx::Image* image = GetHighResAvatarOfProfileAtIndex(index);
337 if (image) 339 if (image)
338 return *image; 340 return *image;
339 } 341 }
340 342
341 int resource_id = profiles::GetDefaultAvatarIconResourceIDAtIndex( 343 int resource_id = profiles::GetDefaultAvatarIconResourceIDAtIndex(
342 GetAvatarIconIndexOfProfileAtIndex(index)); 344 GetAvatarIconIndexOfProfileAtIndex(index));
343 return ResourceBundle::GetSharedInstance().GetNativeImageNamed(resource_id); 345 return ResourceBundle::GetSharedInstance().GetNativeImageNamed(resource_id);
344 } 346 }
345 347
(...skipping 38 matching lines...) Expand 10 before | Expand all | Expand 10 after
384 kGAIAPictureFileNameKey, &file_name); 386 kGAIAPictureFileNameKey, &file_name);
385 387
386 // If the picture is not on disk then return NULL. 388 // If the picture is not on disk then return NULL.
387 if (file_name.empty()) 389 if (file_name.empty())
388 return NULL; 390 return NULL;
389 391
390 base::FilePath image_path = path.AppendASCII(file_name); 392 base::FilePath image_path = path.AppendASCII(file_name);
391 return LoadAvatarPictureFromPath(key, image_path); 393 return LoadAvatarPictureFromPath(key, image_path);
392 } 394 }
393 395
394 const gfx::Image* ProfileInfoCache::GetHighResAvatarOfProfileAtIndex(
395 size_t index) const {
396 int avatar_index = GetAvatarIconIndexOfProfileAtIndex(index);
397 std::string key = profiles::GetDefaultAvatarIconFileNameAtIndex(avatar_index);
398
399 if (!strcmp(key.c_str(), profiles::GetNoHighResAvatarFileName()))
400 return NULL;
401
402 base::FilePath user_data_dir;
403 PathService::Get(chrome::DIR_USER_DATA, &user_data_dir);
404 base::FilePath image_path = user_data_dir.
405 AppendASCII(profiles::kHighResAvatarFolderName).AppendASCII(key);
406 return LoadAvatarPictureFromPath(key, image_path);
407 }
408
409 const gfx::Image* ProfileInfoCache::LoadAvatarPictureFromPath(
410 const std::string& key,
411 const base::FilePath& image_path) const {
412 // If the picture is already loaded then use it.
413 if (cached_avatar_images_.count(key)) {
414 if (cached_avatar_images_[key]->IsEmpty())
415 return NULL;
416 return cached_avatar_images_[key];
417 }
418
419 // If the picture is already being loaded then don't try loading it again.
420 if (cached_avatar_images_loading_[key])
421 return NULL;
422 cached_avatar_images_loading_[key] = true;
423
424 gfx::Image** image = new gfx::Image*;
425 BrowserThread::PostTaskAndReply(BrowserThread::FILE, FROM_HERE,
426 base::Bind(&ReadBitmap, image_path, image),
427 base::Bind(&ProfileInfoCache::OnAvatarPictureLoaded,
428 const_cast<ProfileInfoCache*>(this)->AsWeakPtr(), key, image));
429 return NULL;
430 }
431
432 bool ProfileInfoCache::ProfileIsManagedAtIndex(size_t index) const { 396 bool ProfileInfoCache::ProfileIsManagedAtIndex(size_t index) const {
433 return !GetManagedUserIdOfProfileAtIndex(index).empty(); 397 return !GetManagedUserIdOfProfileAtIndex(index).empty();
434 } 398 }
435 399
436 bool ProfileInfoCache::IsOmittedProfileAtIndex(size_t index) const { 400 bool ProfileInfoCache::IsOmittedProfileAtIndex(size_t index) const {
437 bool value = false; 401 bool value = false;
438 GetInfoForProfileAtIndex(index)->GetBoolean(kIsOmittedFromProfileListKey, 402 GetInfoForProfileAtIndex(index)->GetBoolean(kIsOmittedFromProfileListKey,
439 &value); 403 &value);
440 return value; 404 return value;
441 } 405 }
(...skipping 16 matching lines...) Expand all
458 GetInfoForProfileAtIndex(index)->GetBoolean(kProfileIsEphemeral, &value); 422 GetInfoForProfileAtIndex(index)->GetBoolean(kProfileIsEphemeral, &value);
459 return value; 423 return value;
460 } 424 }
461 425
462 bool ProfileInfoCache::ProfileIsUsingDefaultNameAtIndex(size_t index) const { 426 bool ProfileInfoCache::ProfileIsUsingDefaultNameAtIndex(size_t index) const {
463 bool value = false; 427 bool value = false;
464 GetInfoForProfileAtIndex(index)->GetBoolean(kIsUsingDefaultName, &value); 428 GetInfoForProfileAtIndex(index)->GetBoolean(kIsUsingDefaultName, &value);
465 return value; 429 return value;
466 } 430 }
467 431
468 void ProfileInfoCache::OnAvatarPictureLoaded(const std::string& key,
469 gfx::Image** image) const {
470 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
471
472 cached_avatar_images_loading_[key] = false;
473
474 delete cached_avatar_images_[key];
475 if (*image) {
476 cached_avatar_images_[key] = *image;
477 } else {
478 // Place an empty image in the cache to avoid reloading it again.
479 cached_avatar_images_[key] = new gfx::Image();
480 }
481 delete image;
482
483 content::NotificationService::current()->Notify(
484 chrome::NOTIFICATION_PROFILE_CACHED_INFO_CHANGED,
485 content::NotificationService::AllSources(),
486 content::NotificationService::NoDetails());
487 }
488
489 void ProfileInfoCache::OnGAIAPictureSaved(const base::FilePath& path,
490 bool* success) const {
491 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
492
493 if (*success) {
494 content::NotificationService::current()->Notify(
495 chrome::NOTIFICATION_PROFILE_CACHE_PICTURE_SAVED,
496 content::NotificationService::AllSources(),
497 content::NotificationService::NoDetails());
498 }
499 delete success;
500 }
501
502 bool ProfileInfoCache::IsUsingGAIAPictureOfProfileAtIndex( 432 bool ProfileInfoCache::IsUsingGAIAPictureOfProfileAtIndex(
503 size_t index) const { 433 size_t index) const {
504 bool value = false; 434 bool value = false;
505 GetInfoForProfileAtIndex(index)->GetBoolean(kUseGAIAPictureKey, &value); 435 GetInfoForProfileAtIndex(index)->GetBoolean(kUseGAIAPictureKey, &value);
506 return value; 436 return value;
507 } 437 }
508 438
509 size_t ProfileInfoCache::GetAvatarIconIndexOfProfileAtIndex(size_t index) 439 size_t ProfileInfoCache::GetAvatarIconIndexOfProfileAtIndex(size_t index)
510 const { 440 const {
511 std::string icon_url; 441 std::string icon_url;
(...skipping 176 matching lines...) Expand 10 before | Expand all | Expand 10 after
688 618
689 if (!image) { 619 if (!image) {
690 // Delete the old bitmap from disk. 620 // Delete the old bitmap from disk.
691 if (!old_file_name.empty()) { 621 if (!old_file_name.empty()) {
692 base::FilePath image_path = path.AppendASCII(old_file_name); 622 base::FilePath image_path = path.AppendASCII(old_file_name);
693 BrowserThread::PostTask(BrowserThread::FILE, FROM_HERE, 623 BrowserThread::PostTask(BrowserThread::FILE, FROM_HERE,
694 base::Bind(&DeleteBitmap, image_path)); 624 base::Bind(&DeleteBitmap, image_path));
695 } 625 }
696 } else { 626 } else {
697 // Save the new bitmap to disk. 627 // Save the new bitmap to disk.
698 cached_avatar_images_[key] = new gfx::Image(*image); 628 new_file_name =
699 scoped_ptr<ImageData> data(new ImageData); 629 old_file_name.empty() ? profiles::kGAIAPictureFileName : old_file_name;
700 scoped_refptr<base::RefCountedMemory> png_data = image->As1xPNGBytes(); 630 base::FilePath image_path = path.AppendASCII(new_file_name);
701 data->assign(png_data->front(), png_data->front() + png_data->size()); 631 SaveAvatarImageAtPath(image, key, image_path);
702 if (!data->size()) {
703 LOG(ERROR) << "Failed to PNG encode the image.";
704 } else {
705 new_file_name = old_file_name.empty() ?
706 profiles::kGAIAPictureFileName : old_file_name;
707 base::FilePath image_path = path.AppendASCII(new_file_name);
708 bool* success = new bool;
709 BrowserThread::PostTaskAndReply(BrowserThread::FILE, FROM_HERE,
710 base::Bind(&SaveBitmap, data.release(), image_path, success),
711 base::Bind(&ProfileInfoCache::OnGAIAPictureSaved, AsWeakPtr(),
712 path, success));
713 }
714 } 632 }
715 633
716 scoped_ptr<base::DictionaryValue> info( 634 scoped_ptr<base::DictionaryValue> info(
717 GetInfoForProfileAtIndex(index)->DeepCopy()); 635 GetInfoForProfileAtIndex(index)->DeepCopy());
718 info->SetString(kGAIAPictureFileNameKey, new_file_name); 636 info->SetString(kGAIAPictureFileNameKey, new_file_name);
719 // This takes ownership of |info|. 637 // This takes ownership of |info|.
720 SetInfoForProfileAtIndex(index, info.release()); 638 SetInfoForProfileAtIndex(index, info.release());
721 639
722 FOR_EACH_OBSERVER(ProfileInfoCacheObserver, 640 FOR_EACH_OBSERVER(ProfileInfoCacheObserver,
723 observer_list_, 641 observer_list_,
(...skipping 200 matching lines...) Expand 10 before | Expand all | Expand 10 after
924 DCHECK(key_it != sorted_keys_.end()); 842 DCHECK(key_it != sorted_keys_.end());
925 sorted_keys_.erase(key_it); 843 sorted_keys_.erase(key_it);
926 sorted_keys_.insert(FindPositionForProfile(key, name), key); 844 sorted_keys_.insert(FindPositionForProfile(key, name), key);
927 845
928 content::NotificationService::current()->Notify( 846 content::NotificationService::current()->Notify(
929 chrome::NOTIFICATION_PROFILE_CACHED_INFO_CHANGED, 847 chrome::NOTIFICATION_PROFILE_CACHED_INFO_CHANGED,
930 content::NotificationService::AllSources(), 848 content::NotificationService::AllSources(),
931 content::NotificationService::NoDetails()); 849 content::NotificationService::NoDetails());
932 } 850 }
933 851
852 const gfx::Image* ProfileInfoCache::GetHighResAvatarOfProfileAtIndex(
853 size_t index) const {
854 int avatar_index = GetAvatarIconIndexOfProfileAtIndex(index);
855 std::string key = profiles::GetDefaultAvatarIconFileNameAtIndex(avatar_index);
msw 2014/04/28 20:46:37 This is no longer checked against GetNoHighResAvat
noms (inactive) 2014/04/29 19:12:27 Argh, no, that was a rebase disaster. Thank you fo
856 base::FilePath image_path =
857 profiles::GetPathOfHighResAvatarAtIndex(avatar_index);
858 return LoadAvatarPictureFromPath(key, image_path);
859 }
860
861 const gfx::Image* ProfileInfoCache::LoadAvatarPictureFromPath(
862 const std::string& key,
863 const base::FilePath& image_path) const {
864 // If the picture is already loaded then use it.
865 if (cached_avatar_images_.count(key)) {
866 if (cached_avatar_images_[key]->IsEmpty())
867 return NULL;
868 return cached_avatar_images_[key];
869 }
870
871 // If the picture is already being loaded then don't try loading it again.
872 if (cached_avatar_images_loading_[key])
873 return NULL;
874 cached_avatar_images_loading_[key] = true;
875
876 gfx::Image** image = new gfx::Image*;
877 BrowserThread::PostTaskAndReply(BrowserThread::FILE, FROM_HERE,
878 base::Bind(&ReadBitmap, image_path, image),
879 base::Bind(&ProfileInfoCache::OnAvatarPictureLoaded,
880 const_cast<ProfileInfoCache*>(this)->AsWeakPtr(), key, image));
881 return NULL;
882 }
883
884 void ProfileInfoCache::OnAvatarPictureLoaded(const std::string& key,
885 gfx::Image** image) const {
886 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
887
888 cached_avatar_images_loading_[key] = false;
889 delete cached_avatar_images_[key];
890
891 if (*image) {
892 cached_avatar_images_[key] = *image;
893 } else {
894 // Place an empty image in the cache to avoid reloading it again.
895 cached_avatar_images_[key] = new gfx::Image();
896 }
897 delete image;
898
899 content::NotificationService::current()->Notify(
900 chrome::NOTIFICATION_PROFILE_CACHED_INFO_CHANGED,
901 content::NotificationService::AllSources(),
902 content::NotificationService::NoDetails());
903 }
904
905 void ProfileInfoCache::OnAvatarPictureSaved(const std::string& file_name,
906 bool* success) {
907
msw 2014/04/28 20:46:37 nit: remove new blank line
noms (inactive) 2014/04/29 19:12:27 Done.
908 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
909
910 if (*success) {
911 content::NotificationService::current()->Notify(
912 chrome::NOTIFICATION_PROFILE_CACHE_PICTURE_SAVED,
msw 2014/04/28 20:46:37 nit: these should be indented two more spaces.
noms (inactive) 2014/04/29 19:12:27 Done.
913 content::NotificationService::AllSources(),
914 content::NotificationService::NoDetails());
915 }
916 delete success;
917
918 // Remove the file from the list of downloads in progress. Note that this list
919 // only contains the high resolution avatars, and not the Gaia profile images.
920 if (!avatar_images_downloads_in_progress_[file_name])
921 return;
922
923 delete avatar_images_downloads_in_progress_[file_name];
924 avatar_images_downloads_in_progress_[file_name] = NULL;
925 }
926
934 // static 927 // static
935 std::vector<base::string16> ProfileInfoCache::GetProfileNames() { 928 std::vector<base::string16> ProfileInfoCache::GetProfileNames() {
936 std::vector<base::string16> names; 929 std::vector<base::string16> names;
937 PrefService* local_state = g_browser_process->local_state(); 930 PrefService* local_state = g_browser_process->local_state();
938 const base::DictionaryValue* cache = local_state->GetDictionary( 931 const base::DictionaryValue* cache = local_state->GetDictionary(
939 prefs::kProfileInfoCache); 932 prefs::kProfileInfoCache);
940 base::string16 name; 933 base::string16 name;
941 for (base::DictionaryValue::Iterator it(*cache); !it.IsAtEnd(); 934 for (base::DictionaryValue::Iterator it(*cache); !it.IsAtEnd();
942 it.Advance()) { 935 it.Advance()) {
943 const base::DictionaryValue* info = NULL; 936 const base::DictionaryValue* info = NULL;
944 it.value().GetAsDictionary(&info); 937 it.value().GetAsDictionary(&info);
945 info->GetString(kNameKey, &name); 938 info->GetString(kNameKey, &name);
946 names.push_back(name); 939 names.push_back(name);
947 } 940 }
948 return names; 941 return names;
949 } 942 }
950 943
951 // static 944 // static
952 void ProfileInfoCache::RegisterPrefs(PrefRegistrySimple* registry) { 945 void ProfileInfoCache::RegisterPrefs(PrefRegistrySimple* registry) {
953 registry->RegisterDictionaryPref(prefs::kProfileInfoCache); 946 registry->RegisterDictionaryPref(prefs::kProfileInfoCache);
954 } 947 }
948
949 void ProfileInfoCache::DownloadHighResAvatar(
msw 2014/04/28 20:46:37 The order of all the function definitions in this
noms (inactive) 2014/04/29 19:12:27 Done.
950 ProfileAvatarDownloader* avatar_downloader,
951 size_t icon_index) {
952 std::string file_name = profiles::GetDefaultAvatarIconFileNameAtIndex(
953 icon_index);
954 // If the file is already being downloaded, don't start another download.
955 if (avatar_images_downloads_in_progress_[file_name])
956 return;
957 avatar_images_downloads_in_progress_[file_name] = avatar_downloader;
958 avatar_downloader->Start();
959 }
960
961 void ProfileInfoCache::SaveAvatarImageAtPath(
962 const gfx::Image* image,
963 const std::string& key,
964 const base::FilePath& image_path) {
965
msw 2014/04/28 20:46:37 nit: remove blank line
noms (inactive) 2014/04/29 19:12:27 Done.
966 cached_avatar_images_[key] = new gfx::Image(*image);
967
968 scoped_ptr<ImageData> data(new ImageData);
969 scoped_refptr<base::RefCountedMemory> png_data = image->As1xPNGBytes();
970 data->assign(png_data->front(), png_data->front() + png_data->size());
971
972 if (!data->size()) {
973 LOG(ERROR) << "Failed to PNG encode the image.";
974 } else {
975 bool* success = new bool;
msw 2014/04/28 20:46:37 Why is this allocated here? SaveBitmap can call On
noms (inactive) 2014/04/29 19:12:27 I've changed it to passing the callback, and only
976 BrowserThread::PostTaskAndReply(BrowserThread::FILE, FROM_HERE,
977 base::Bind(&SaveBitmap, data.release(), image_path, success),
978 base::Bind(&ProfileInfoCache::OnAvatarPictureSaved, AsWeakPtr(),
979 key, success));
980 }
981 }
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698