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

Side by Side Diff: chrome/browser/signin/easy_unlock_service.cc

Issue 577683002: Introduce EasyUnlockService class for signin profile (Closed) Base URL: https://chromium.googlesource.com/chromium/src.git@master
Patch Set: . Created 6 years, 3 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
OLDNEW
1 // Copyright 2014 The Chromium Authors. All rights reserved. 1 // Copyright 2014 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/signin/easy_unlock_service.h" 5 #include "chrome/browser/signin/easy_unlock_service.h"
6 6
7 #include <string>
8
9 #include "base/bind.h" 7 #include "base/bind.h"
10 #include "base/command_line.h" 8 #include "base/command_line.h"
11 #include "base/logging.h" 9 #include "base/logging.h"
12 #include "base/memory/ref_counted.h"
13 #include "base/metrics/field_trial.h" 10 #include "base/metrics/field_trial.h"
14 #include "base/prefs/pref_service.h" 11 #include "base/prefs/pref_service.h"
15 #include "base/prefs/scoped_user_pref_update.h"
16 #include "base/values.h" 12 #include "base/values.h"
17 #include "chrome/browser/extensions/component_loader.h" 13 #include "chrome/browser/extensions/component_loader.h"
18 #include "chrome/browser/extensions/extension_service.h" 14 #include "chrome/browser/extensions/extension_service.h"
19 #include "chrome/browser/profiles/profile.h" 15 #include "chrome/browser/profiles/profile.h"
20 #include "chrome/browser/signin/easy_unlock_screenlock_state_handler.h" 16 #include "chrome/browser/signin/easy_unlock_screenlock_state_handler.h"
21 #include "chrome/browser/signin/easy_unlock_service_factory.h" 17 #include "chrome/browser/signin/easy_unlock_service_factory.h"
22 #include "chrome/browser/signin/easy_unlock_service_observer.h" 18 #include "chrome/browser/signin/easy_unlock_service_observer.h"
23 #include "chrome/browser/signin/easy_unlock_toggle_flow.h"
24 #include "chrome/browser/signin/screenlock_bridge.h" 19 #include "chrome/browser/signin/screenlock_bridge.h"
25 #include "chrome/browser/ui/extensions/application_launch.h"
26 #include "chrome/common/chrome_switches.h" 20 #include "chrome/common/chrome_switches.h"
21 #include "chrome/common/extensions/extension_constants.h"
27 #include "chrome/common/pref_names.h" 22 #include "chrome/common/pref_names.h"
28 #include "components/pref_registry/pref_registry_syncable.h" 23 #include "components/pref_registry/pref_registry_syncable.h"
29 #include "device/bluetooth/bluetooth_adapter.h" 24 #include "device/bluetooth/bluetooth_adapter.h"
30 #include "device/bluetooth/bluetooth_adapter_factory.h" 25 #include "device/bluetooth/bluetooth_adapter_factory.h"
31 #include "extensions/browser/extension_registry.h" 26 #include "extensions/browser/extension_registry.h"
32 #include "extensions/browser/extension_system.h" 27 #include "extensions/browser/extension_system.h"
33 #include "extensions/common/one_shot_event.h" 28 #include "extensions/common/one_shot_event.h"
34 #include "grit/browser_resources.h" 29 #include "grit/browser_resources.h"
35 30
36 #if defined(OS_CHROMEOS) 31 #if defined(OS_CHROMEOS)
37 #include "chrome/browser/chromeos/profiles/profile_helper.h"
38 #include "chromeos/dbus/dbus_thread_manager.h" 32 #include "chromeos/dbus/dbus_thread_manager.h"
39 #include "chromeos/dbus/power_manager_client.h" 33 #include "chromeos/dbus/power_manager_client.h"
40 #include "components/user_manager/user_manager.h"
41 #endif 34 #endif
42 35
43 namespace { 36 namespace {
44 37
45 // Key name of the local device permit record dictonary in kEasyUnlockPairing.
46 const char kKeyPermitAccess[] = "permitAccess";
47
48 // Key name of the remote device list in kEasyUnlockPairing.
49 const char kKeyDevices[] = "devices";
50
51 // Key name of the phone public key in a device dictionary.
52 const char kKeyPhoneId[] = "permitRecord.id";
53
54 extensions::ComponentLoader* GetComponentLoader( 38 extensions::ComponentLoader* GetComponentLoader(
55 content::BrowserContext* context) { 39 content::BrowserContext* context) {
56 extensions::ExtensionSystem* extension_system = 40 extensions::ExtensionSystem* extension_system =
57 extensions::ExtensionSystem::Get(context); 41 extensions::ExtensionSystem::Get(context);
58 ExtensionService* extension_service = extension_system->extension_service(); 42 ExtensionService* extension_service = extension_system->extension_service();
59 return extension_service->component_loader(); 43 return extension_service->component_loader();
60 } 44 }
61 45
62 } // namespace 46 } // namespace
63 47
(...skipping 58 matching lines...) Expand 10 before | Expand all | Expand 10 after
122 106
123 virtual ~PowerMonitor() { 107 virtual ~PowerMonitor() {
124 chromeos::DBusThreadManager::Get()->GetPowerManagerClient()-> 108 chromeos::DBusThreadManager::Get()->GetPowerManagerClient()->
125 RemoveObserver(this); 109 RemoveObserver(this);
126 } 110 }
127 111
128 private: 112 private:
129 // chromeos::PowerManagerClient::Observer: 113 // chromeos::PowerManagerClient::Observer:
130 virtual void SuspendImminent() OVERRIDE { 114 virtual void SuspendImminent() OVERRIDE {
131 service_->DisableAppIfLoaded(); 115 service_->DisableAppIfLoaded();
132 service_->screenlock_state_handler_.reset();
133 } 116 }
134 117
135 virtual void SuspendDone(const base::TimeDelta& sleep_duration) OVERRIDE { 118 virtual void SuspendDone(const base::TimeDelta& sleep_duration) OVERRIDE {
136 service_->LoadApp(); 119 service_->LoadApp();
137 } 120 }
138 121
139 EasyUnlockService* service_; 122 EasyUnlockService* service_;
140 123
141 DISALLOW_COPY_AND_ASSIGN(PowerMonitor); 124 DISALLOW_COPY_AND_ASSIGN(PowerMonitor);
142 }; 125 };
143 #endif 126 #endif
144 127
145 EasyUnlockService::EasyUnlockService(Profile* profile) 128 EasyUnlockService::EasyUnlockService(Profile* profile)
146 : profile_(profile), 129 : profile_(profile),
147 bluetooth_detector_(new BluetoothDetector(this)), 130 bluetooth_detector_(new BluetoothDetector(this)),
148 turn_off_flow_status_(IDLE),
149 weak_ptr_factory_(this) { 131 weak_ptr_factory_(this) {
150 extensions::ExtensionSystem::Get(profile_)->ready().Post( 132 extensions::ExtensionSystem::Get(profile_)->ready().Post(
151 FROM_HERE, 133 FROM_HERE,
152 base::Bind(&EasyUnlockService::Initialize, 134 base::Bind(&EasyUnlockService::Initialize,
153 weak_ptr_factory_.GetWeakPtr())); 135 weak_ptr_factory_.GetWeakPtr()));
154 } 136 }
155 137
156 EasyUnlockService::~EasyUnlockService() { 138 EasyUnlockService::~EasyUnlockService() {
157 } 139 }
158 140
(...skipping 11 matching lines...) Expand all
170 registry->RegisterDictionaryPref( 152 registry->RegisterDictionaryPref(
171 prefs::kEasyUnlockPairing, 153 prefs::kEasyUnlockPairing,
172 new base::DictionaryValue(), 154 new base::DictionaryValue(),
173 user_prefs::PrefRegistrySyncable::UNSYNCABLE_PREF); 155 user_prefs::PrefRegistrySyncable::UNSYNCABLE_PREF);
174 registry->RegisterBooleanPref( 156 registry->RegisterBooleanPref(
175 prefs::kEasyUnlockAllowed, 157 prefs::kEasyUnlockAllowed,
176 true, 158 true,
177 user_prefs::PrefRegistrySyncable::UNSYNCABLE_PREF); 159 user_prefs::PrefRegistrySyncable::UNSYNCABLE_PREF);
178 } 160 }
179 161
180 void EasyUnlockService::LaunchSetup() {
181 ExtensionService* service =
182 extensions::ExtensionSystem::Get(profile_)->extension_service();
183 const extensions::Extension* extension =
184 service->GetExtensionById(extension_misc::kEasyUnlockAppId, false);
185
186 OpenApplication(AppLaunchParams(
187 profile_, extension, extensions::LAUNCH_CONTAINER_WINDOW, NEW_WINDOW));
188 }
189
190 bool EasyUnlockService::IsAllowed() { 162 bool EasyUnlockService::IsAllowed() {
191 #if defined(OS_CHROMEOS) 163 if (!IsAllowedInternal())
192 if (!user_manager::UserManager::Get()->IsLoggedInAsRegularUser())
193 return false; 164 return false;
194 165
195 if (!chromeos::ProfileHelper::IsPrimaryProfile(profile_)) 166 #if defined(OS_CHROMEOS)
196 return false;
197
198 if (!profile_->GetPrefs()->GetBoolean(prefs::kEasyUnlockAllowed)) 167 if (!profile_->GetPrefs()->GetBoolean(prefs::kEasyUnlockAllowed))
199 return false; 168 return false;
200 169
201 // Respect existing policy and skip finch test. 170 // Respect existing policy and skip finch test.
202 if (!profile_->GetPrefs()->IsManagedPreference(prefs::kEasyUnlockAllowed)) { 171 if (!profile_->GetPrefs()->IsManagedPreference(prefs::kEasyUnlockAllowed)) {
203 // It is disabled when the trial exists and is in "Disable" group. 172 // It is disabled when the trial exists and is in "Disable" group.
204 if (base::FieldTrialList::FindFullName("EasyUnlock") == "Disable") 173 if (base::FieldTrialList::FindFullName("EasyUnlock") == "Disable")
205 return false; 174 return false;
206 } 175 }
207 176
208 if (!bluetooth_detector_->IsPresent()) 177 if (!bluetooth_detector_->IsPresent())
209 return false; 178 return false;
210 179
211 return true; 180 return true;
212 #else 181 #else
213 // TODO(xiyuan): Revisit when non-chromeos platforms are supported. 182 // TODO(xiyuan): Revisit when non-chromeos platforms are supported.
214 return false; 183 return false;
215 #endif 184 #endif
216 } 185 }
217 186
218 EasyUnlockScreenlockStateHandler* 187 EasyUnlockScreenlockStateHandler*
219 EasyUnlockService::GetScreenlockStateHandler() { 188 EasyUnlockService::GetScreenlockStateHandler() {
220 if (!IsAllowed()) 189 if (!IsAllowed())
221 return NULL; 190 return NULL;
222 if (!screenlock_state_handler_) { 191 if (!screenlock_state_handler_) {
223 screenlock_state_handler_.reset(new EasyUnlockScreenlockStateHandler( 192 screenlock_state_handler_.reset(new EasyUnlockScreenlockStateHandler(
224 ScreenlockBridge::GetAuthenticatedUserEmail(profile_), 193 GetUserEmail(),
225 profile_->GetPrefs(), 194 GetType() == TYPE_REGULAR ? profile_->GetPrefs() : NULL,
226 ScreenlockBridge::Get())); 195 ScreenlockBridge::Get()));
227 } 196 }
228 return screenlock_state_handler_.get(); 197 return screenlock_state_handler_.get();
229 } 198 }
230 199
231 const base::DictionaryValue* EasyUnlockService::GetPermitAccess() const {
232 const base::DictionaryValue* pairing_dict =
233 profile_->GetPrefs()->GetDictionary(prefs::kEasyUnlockPairing);
234 const base::DictionaryValue* permit_dict = NULL;
235 if (pairing_dict &&
236 pairing_dict->GetDictionary(kKeyPermitAccess, &permit_dict)) {
237 return permit_dict;
238 }
239
240 return NULL;
241 }
242
243 void EasyUnlockService::SetPermitAccess(const base::DictionaryValue& permit) {
244 DictionaryPrefUpdate pairing_update(profile_->GetPrefs(),
245 prefs::kEasyUnlockPairing);
246 pairing_update->SetWithoutPathExpansion(kKeyPermitAccess, permit.DeepCopy());
247 }
248
249 void EasyUnlockService::ClearPermitAccess() {
250 DictionaryPrefUpdate pairing_update(profile_->GetPrefs(),
251 prefs::kEasyUnlockPairing);
252 pairing_update->RemoveWithoutPathExpansion(kKeyPermitAccess, NULL);
253 }
254
255 const base::ListValue* EasyUnlockService::GetRemoteDevices() const {
256 const base::DictionaryValue* pairing_dict =
257 profile_->GetPrefs()->GetDictionary(prefs::kEasyUnlockPairing);
258 const base::ListValue* devices = NULL;
259 if (pairing_dict && pairing_dict->GetList(kKeyDevices, &devices)) {
260 return devices;
261 }
262
263 return NULL;
264 }
265
266 void EasyUnlockService::SetRemoteDevices(const base::ListValue& devices) {
267 DictionaryPrefUpdate pairing_update(profile_->GetPrefs(),
268 prefs::kEasyUnlockPairing);
269 pairing_update->SetWithoutPathExpansion(kKeyDevices, devices.DeepCopy());
270 }
271
272 void EasyUnlockService::ClearRemoteDevices() {
273 DictionaryPrefUpdate pairing_update(profile_->GetPrefs(),
274 prefs::kEasyUnlockPairing);
275 pairing_update->RemoveWithoutPathExpansion(kKeyDevices, NULL);
276 }
277
278 void EasyUnlockService::AddObserver(EasyUnlockServiceObserver* observer) { 200 void EasyUnlockService::AddObserver(EasyUnlockServiceObserver* observer) {
279 observers_.AddObserver(observer); 201 observers_.AddObserver(observer);
280 } 202 }
281 203
282 void EasyUnlockService::RemoveObserver(EasyUnlockServiceObserver* observer) { 204 void EasyUnlockService::RemoveObserver(EasyUnlockServiceObserver* observer) {
283 observers_.RemoveObserver(observer); 205 observers_.RemoveObserver(observer);
284 } 206 }
285 207
286 void EasyUnlockService::RunTurnOffFlow() {
287 if (turn_off_flow_status_ == PENDING)
288 return;
289
290 SetTurnOffFlowStatus(PENDING);
291
292 // Currently there should only be one registered phone.
293 // TODO(xiyuan): Revisit this when server supports toggle for all or
294 // there are multiple phones.
295 const base::DictionaryValue* pairing_dict =
296 profile_->GetPrefs()->GetDictionary(prefs::kEasyUnlockPairing);
297 const base::ListValue* devices_list = NULL;
298 const base::DictionaryValue* first_device = NULL;
299 std::string phone_public_key;
300 if (!pairing_dict || !pairing_dict->GetList(kKeyDevices, &devices_list) ||
301 !devices_list || !devices_list->GetDictionary(0, &first_device) ||
302 !first_device ||
303 !first_device->GetString(kKeyPhoneId, &phone_public_key)) {
304 LOG(WARNING) << "Bad easy unlock pairing data, wiping out local data";
305 OnTurnOffFlowFinished(true);
306 return;
307 }
308
309 turn_off_flow_.reset(new EasyUnlockToggleFlow(
310 profile_,
311 phone_public_key,
312 false,
313 base::Bind(&EasyUnlockService::OnTurnOffFlowFinished,
314 base::Unretained(this))));
315 turn_off_flow_->Start();
316 }
317
318 void EasyUnlockService::ResetTurnOffFlow() {
319 turn_off_flow_.reset();
320 SetTurnOffFlowStatus(IDLE);
321 }
322
323 void EasyUnlockService::Initialize() {
324 registrar_.Init(profile_->GetPrefs());
325 registrar_.Add(
326 prefs::kEasyUnlockAllowed,
327 base::Bind(&EasyUnlockService::OnPrefsChanged, base::Unretained(this)));
328 OnPrefsChanged();
329
330 #if defined(OS_CHROMEOS)
331 // Only start Bluetooth detection for ChromeOS since the feature is
332 // only offered on ChromeOS. Enabling this on non-ChromeOS platforms
333 // previously introduced a performance regression: http://crbug.com/404482
334 // Make sure not to reintroduce a performance regression if re-enabling on
335 // additional platforms.
336 // TODO(xiyuan): Revisit when non-chromeos platforms are supported.
337 bluetooth_detector_->Initialize();
338 #endif // defined(OS_CHROMEOS)
339 }
340
341 void EasyUnlockService::LoadApp() { 208 void EasyUnlockService::LoadApp() {
342 DCHECK(IsAllowed()); 209 DCHECK(IsAllowed());
343 210
344 #if defined(GOOGLE_CHROME_BUILD) 211 #if defined(GOOGLE_CHROME_BUILD)
345 base::FilePath easy_unlock_path; 212 base::FilePath easy_unlock_path;
346 #if defined(OS_CHROMEOS) 213 #if defined(OS_CHROMEOS)
347 easy_unlock_path = base::FilePath("/usr/share/chromeos-assets/easy_unlock"); 214 easy_unlock_path = base::FilePath("/usr/share/chromeos-assets/easy_unlock");
348 #endif // defined(OS_CHROMEOS) 215 #endif // defined(OS_CHROMEOS)
349 216
350 #ifndef NDEBUG 217 #ifndef NDEBUG
351 // Only allow app path override switch for debug build. 218 // Only allow app path override switch for debug build.
352 const CommandLine* command_line = CommandLine::ForCurrentProcess(); 219 const CommandLine* command_line = CommandLine::ForCurrentProcess();
353 if (command_line->HasSwitch(switches::kEasyUnlockAppPath)) { 220 if (command_line->HasSwitch(switches::kEasyUnlockAppPath)) {
354 easy_unlock_path = 221 easy_unlock_path =
355 command_line->GetSwitchValuePath(switches::kEasyUnlockAppPath); 222 command_line->GetSwitchValuePath(switches::kEasyUnlockAppPath);
356 } 223 }
357 #endif // !defined(NDEBUG) 224 #endif // !defined(NDEBUG)
358 225
359 if (!easy_unlock_path.empty()) { 226 if (!easy_unlock_path.empty()) {
360 extensions::ComponentLoader* loader = GetComponentLoader(profile_); 227 extensions::ComponentLoader* loader = GetComponentLoader(profile_);
361 if (!loader->Exists(extension_misc::kEasyUnlockAppId)) { 228 if (!loader->Exists(extension_misc::kEasyUnlockAppId))
362 loader->Add(IDR_EASY_UNLOCK_MANIFEST, easy_unlock_path); 229 loader->Add(IDR_EASY_UNLOCK_MANIFEST, easy_unlock_path);
363 } 230
364 ExtensionService* extension_service = 231 ExtensionService* extension_service =
365 extensions::ExtensionSystem::Get(profile_)->extension_service(); 232 extensions::ExtensionSystem::Get(profile_)->extension_service();
366 extension_service->EnableExtension(extension_misc::kEasyUnlockAppId); 233 extension_service->EnableExtension(extension_misc::kEasyUnlockAppId);
367 } 234 }
368 #endif // defined(GOOGLE_CHROME_BUILD) 235 #endif // defined(GOOGLE_CHROME_BUILD)
369 } 236 }
370 237
371 void EasyUnlockService::DisableAppIfLoaded() { 238 void EasyUnlockService::DisableAppIfLoaded() {
239 // Make sure lock screen state set by the extension gets reset.
240 screenlock_state_handler_.reset();
241
372 extensions::ComponentLoader* loader = GetComponentLoader(profile_); 242 extensions::ComponentLoader* loader = GetComponentLoader(profile_);
373 if (!loader->Exists(extension_misc::kEasyUnlockAppId)) 243 if (!loader->Exists(extension_misc::kEasyUnlockAppId))
374 return; 244 return;
375 245
376 ExtensionService* extension_service = 246 ExtensionService* extension_service =
377 extensions::ExtensionSystem::Get(profile_)->extension_service(); 247 extensions::ExtensionSystem::Get(profile_)->extension_service();
378 extension_service->DisableExtension(extension_misc::kEasyUnlockAppId, 248 extension_service->DisableExtension(extension_misc::kEasyUnlockAppId,
379 extensions::Extension::DISABLE_RELOAD); 249 extensions::Extension::DISABLE_RELOAD);
380 } 250 }
381 251
252 void EasyUnlockService::ReloadApp() {
253 // Make sure lock screen state set by the extension gets reset.
254 screenlock_state_handler_.reset();
255
256 if (GetComponentLoader(profile_)->Exists(extension_misc::kEasyUnlockAppId)) {
257 extensions::ExtensionSystem* extension_system =
258 extensions::ExtensionSystem::Get(profile_);
259 extension_system->extension_service()->ReloadExtension(
260 extension_misc::kEasyUnlockAppId);
261 }
262 }
263
382 void EasyUnlockService::UpdateAppState() { 264 void EasyUnlockService::UpdateAppState() {
383 if (IsAllowed()) { 265 if (IsAllowed()) {
384 LoadApp(); 266 LoadApp();
385 267
386 #if defined(OS_CHROMEOS) 268 #if defined(OS_CHROMEOS)
387 if (!power_monitor_) 269 if (!power_monitor_)
388 power_monitor_.reset(new PowerMonitor(this)); 270 power_monitor_.reset(new PowerMonitor(this));
389 #endif 271 #endif
390 } else { 272 } else {
391 DisableAppIfLoaded(); 273 DisableAppIfLoaded();
392 // Reset the screenlock state handler to make sure Screenlock state set
393 // by Easy Unlock app is reset.
394 screenlock_state_handler_.reset();
395
396 #if defined(OS_CHROMEOS) 274 #if defined(OS_CHROMEOS)
397 power_monitor_.reset(); 275 power_monitor_.reset();
398 #endif 276 #endif
399 } 277 }
400 } 278 }
401 279
402 void EasyUnlockService::OnPrefsChanged() { 280 void EasyUnlockService::NotifyTurnOffOperationStatusChanged() {
403 UpdateAppState(); 281 FOR_EACH_OBSERVER(
282 EasyUnlockServiceObserver, observers_, OnTurnOffOperationStatusChanged());
283 }
284
285 void EasyUnlockService::Initialize() {
286 InitializeInternal();
287
288 #if defined(OS_CHROMEOS)
289 // Only start Bluetooth detection for ChromeOS since the feature is
290 // only offered on ChromeOS. Enabling this on non-ChromeOS platforms
291 // previously introduced a performance regression: http://crbug.com/404482
292 // Make sure not to reintroduce a performance regression if re-enabling on
293 // additional platforms.
294 // TODO(xiyuan): Revisit when non-chromeos platforms are supported.
295 bluetooth_detector_->Initialize();
296 #endif // defined(OS_CHROMEOS)
404 } 297 }
405 298
406 void EasyUnlockService::OnBluetoothAdapterPresentChanged() { 299 void EasyUnlockService::OnBluetoothAdapterPresentChanged() {
407 UpdateAppState(); 300 UpdateAppState();
408 } 301 }
409 302
410 void EasyUnlockService::SetTurnOffFlowStatus(TurnOffFlowStatus status) {
411 turn_off_flow_status_ = status;
412 FOR_EACH_OBSERVER(
413 EasyUnlockServiceObserver, observers_, OnTurnOffOperationStatusChanged());
414 }
415
416 void EasyUnlockService::OnTurnOffFlowFinished(bool success) {
417 turn_off_flow_.reset();
418
419 if (!success) {
420 SetTurnOffFlowStatus(FAIL);
421 return;
422 }
423
424 ClearRemoteDevices();
425 SetTurnOffFlowStatus(IDLE);
426
427 // Make sure lock screen state set by the extension gets reset.
428 screenlock_state_handler_.reset();
429
430 if (GetComponentLoader(profile_)->Exists(extension_misc::kEasyUnlockAppId)) {
431 extensions::ExtensionSystem* extension_system =
432 extensions::ExtensionSystem::Get(profile_);
433 extension_system->extension_service()->ReloadExtension(
434 extension_misc::kEasyUnlockAppId);
435 }
436 }
OLDNEW
« no previous file with comments | « chrome/browser/signin/easy_unlock_service.h ('k') | chrome/browser/signin/easy_unlock_service_factory.cc » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698