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

Side by Side Diff: chrome/browser/extensions/browser_event_router.cc

Issue 12375006: Added UMA stats for extensions events on addListener. (Closed) Base URL: https://chromium.googlesource.com/chromium/src.git@master
Patch Set: Created 7 years, 9 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 (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/extensions/browser_event_router.h" 5 #include "chrome/browser/extensions/browser_event_router.h"
6 6
7 #include "base/json/json_writer.h" 7 #include "base/json/json_writer.h"
8 #include "base/values.h" 8 #include "base/values.h"
9 #include "chrome/browser/extensions/api/extension_action/extension_page_actions_ api_constants.h" 9 #include "chrome/browser/extensions/api/extension_action/extension_page_actions_ api_constants.h"
10 #include "chrome/browser/extensions/api/tabs/tabs_constants.h" 10 #include "chrome/browser/extensions/api/tabs/tabs_constants.h"
(...skipping 10 matching lines...) Expand all
21 #include "chrome/browser/ui/browser_iterator.h" 21 #include "chrome/browser/ui/browser_iterator.h"
22 #include "chrome/browser/ui/browser_list.h" 22 #include "chrome/browser/ui/browser_list.h"
23 #include "chrome/browser/ui/tabs/tab_strip_model.h" 23 #include "chrome/browser/ui/tabs/tab_strip_model.h"
24 #include "chrome/common/extensions/api/extension_action/action_info.h" 24 #include "chrome/common/extensions/api/extension_action/action_info.h"
25 #include "chrome/common/extensions/extension_constants.h" 25 #include "chrome/common/extensions/extension_constants.h"
26 #include "content/public/browser/navigation_controller.h" 26 #include "content/public/browser/navigation_controller.h"
27 #include "content/public/browser/notification_service.h" 27 #include "content/public/browser/notification_service.h"
28 #include "content/public/browser/notification_types.h" 28 #include "content/public/browser/notification_types.h"
29 #include "content/public/browser/web_contents.h" 29 #include "content/public/browser/web_contents.h"
30 30
31 namespace events = extensions::event_names;
32 namespace tab_keys = extensions::tabs_constants; 31 namespace tab_keys = extensions::tabs_constants;
33 namespace page_actions_keys = extension_page_actions_api_constants; 32 namespace page_actions_keys = extension_page_actions_api_constants;
34 33
35 using content::NavigationController; 34 using content::NavigationController;
36 using content::WebContents; 35 using content::WebContents;
37 36
38 namespace extensions { 37 namespace extensions {
39 38
40 BrowserEventRouter::TabEntry::TabEntry() 39 BrowserEventRouter::TabEntry::TabEntry()
41 : complete_waiting_on_load_(false), 40 : complete_waiting_on_load_(false),
(...skipping 121 matching lines...) Expand 10 before | Expand all | Expand 10 after
163 event_args->Clear(); 162 event_args->Clear();
164 event_args->Append(tab_value); 163 event_args->Append(tab_value);
165 tab_value->SetBoolean(tab_keys::kSelectedKey, active); 164 tab_value->SetBoolean(tab_keys::kSelectedKey, active);
166 } 165 }
167 166
168 void BrowserEventRouter::TabCreatedAt(WebContents* contents, 167 void BrowserEventRouter::TabCreatedAt(WebContents* contents,
169 int index, 168 int index,
170 bool active) { 169 bool active) {
171 Profile* profile = Profile::FromBrowserContext(contents->GetBrowserContext()); 170 Profile* profile = Profile::FromBrowserContext(contents->GetBrowserContext());
172 scoped_ptr<ListValue> args(new ListValue()); 171 scoped_ptr<ListValue> args(new ListValue());
173 scoped_ptr<Event> event(new Event(events::kOnTabCreated, args.Pass())); 172 scoped_ptr<Event> event(new Event(event_names::kOnTabCreated, args.Pass()));
174 event->restrict_to_profile = profile; 173 event->restrict_to_profile = profile;
175 event->user_gesture = EventRouter::USER_GESTURE_NOT_ENABLED; 174 event->user_gesture = EventRouter::USER_GESTURE_NOT_ENABLED;
176 event->will_dispatch_callback = 175 event->will_dispatch_callback =
177 base::Bind(&WillDispatchTabCreatedEvent, contents, active); 176 base::Bind(&WillDispatchTabCreatedEvent, contents, active);
178 ExtensionSystem::Get(profile)->event_router()->BroadcastEvent(event.Pass()); 177 ExtensionSystem::Get(profile)->event_router()->BroadcastEvent(event.Pass());
179 178
180 RegisterForTabNotifications(contents); 179 RegisterForTabNotifications(contents);
181 } 180 }
182 181
183 void BrowserEventRouter::TabInsertedAt(WebContents* contents, 182 void BrowserEventRouter::TabInsertedAt(WebContents* contents,
(...skipping 12 matching lines...) Expand all
196 args->Append(Value::CreateIntegerValue(tab_id)); 195 args->Append(Value::CreateIntegerValue(tab_id));
197 196
198 DictionaryValue* object_args = new DictionaryValue(); 197 DictionaryValue* object_args = new DictionaryValue();
199 object_args->Set(tab_keys::kNewWindowIdKey, Value::CreateIntegerValue( 198 object_args->Set(tab_keys::kNewWindowIdKey, Value::CreateIntegerValue(
200 ExtensionTabUtil::GetWindowIdOfTab(contents))); 199 ExtensionTabUtil::GetWindowIdOfTab(contents)));
201 object_args->Set(tab_keys::kNewPositionKey, Value::CreateIntegerValue( 200 object_args->Set(tab_keys::kNewPositionKey, Value::CreateIntegerValue(
202 index)); 201 index));
203 args->Append(object_args); 202 args->Append(object_args);
204 203
205 Profile* profile = Profile::FromBrowserContext(contents->GetBrowserContext()); 204 Profile* profile = Profile::FromBrowserContext(contents->GetBrowserContext());
206 DispatchEvent(profile, events::kOnTabAttached, args.Pass(), 205 DispatchEvent(profile, event_names::kOnTabAttached, args.Pass(),
207 EventRouter::USER_GESTURE_UNKNOWN); 206 EventRouter::USER_GESTURE_UNKNOWN);
208 } 207 }
209 208
210 void BrowserEventRouter::TabDetachedAt(WebContents* contents, int index) { 209 void BrowserEventRouter::TabDetachedAt(WebContents* contents, int index) {
211 if (!GetTabEntry(contents)) { 210 if (!GetTabEntry(contents)) {
212 // The tab was removed. Don't send detach event. 211 // The tab was removed. Don't send detach event.
213 return; 212 return;
214 } 213 }
215 214
216 scoped_ptr<ListValue> args(new ListValue()); 215 scoped_ptr<ListValue> args(new ListValue());
217 args->Append(Value::CreateIntegerValue(ExtensionTabUtil::GetTabId(contents))); 216 args->Append(Value::CreateIntegerValue(ExtensionTabUtil::GetTabId(contents)));
218 217
219 DictionaryValue* object_args = new DictionaryValue(); 218 DictionaryValue* object_args = new DictionaryValue();
220 object_args->Set(tab_keys::kOldWindowIdKey, Value::CreateIntegerValue( 219 object_args->Set(tab_keys::kOldWindowIdKey, Value::CreateIntegerValue(
221 ExtensionTabUtil::GetWindowIdOfTab(contents))); 220 ExtensionTabUtil::GetWindowIdOfTab(contents)));
222 object_args->Set(tab_keys::kOldPositionKey, Value::CreateIntegerValue( 221 object_args->Set(tab_keys::kOldPositionKey, Value::CreateIntegerValue(
223 index)); 222 index));
224 args->Append(object_args); 223 args->Append(object_args);
225 224
226 Profile* profile = Profile::FromBrowserContext(contents->GetBrowserContext()); 225 Profile* profile = Profile::FromBrowserContext(contents->GetBrowserContext());
227 DispatchEvent(profile, events::kOnTabDetached, args.Pass(), 226 DispatchEvent(profile, event_names::kOnTabDetached, args.Pass(),
228 EventRouter::USER_GESTURE_UNKNOWN); 227 EventRouter::USER_GESTURE_UNKNOWN);
229 } 228 }
230 229
231 void BrowserEventRouter::TabClosingAt(TabStripModel* tab_strip_model, 230 void BrowserEventRouter::TabClosingAt(TabStripModel* tab_strip_model,
232 WebContents* contents, 231 WebContents* contents,
233 int index) { 232 int index) {
234 int tab_id = ExtensionTabUtil::GetTabId(contents); 233 int tab_id = ExtensionTabUtil::GetTabId(contents);
235 234
236 scoped_ptr<ListValue> args(new ListValue()); 235 scoped_ptr<ListValue> args(new ListValue());
237 args->Append(Value::CreateIntegerValue(tab_id)); 236 args->Append(Value::CreateIntegerValue(tab_id));
238 237
239 DictionaryValue* object_args = new DictionaryValue(); 238 DictionaryValue* object_args = new DictionaryValue();
240 object_args->SetInteger(tab_keys::kWindowIdKey, 239 object_args->SetInteger(tab_keys::kWindowIdKey,
241 ExtensionTabUtil::GetWindowIdOfTab(contents)); 240 ExtensionTabUtil::GetWindowIdOfTab(contents));
242 object_args->SetBoolean(tab_keys::kWindowClosing, 241 object_args->SetBoolean(tab_keys::kWindowClosing,
243 tab_strip_model->closing_all()); 242 tab_strip_model->closing_all());
244 args->Append(object_args); 243 args->Append(object_args);
245 244
246 Profile* profile = Profile::FromBrowserContext(contents->GetBrowserContext()); 245 Profile* profile = Profile::FromBrowserContext(contents->GetBrowserContext());
247 DispatchEvent(profile, events::kOnTabRemoved, args.Pass(), 246 DispatchEvent(profile, event_names::kOnTabRemoved, args.Pass(),
248 EventRouter::USER_GESTURE_UNKNOWN); 247 EventRouter::USER_GESTURE_UNKNOWN);
249 248
250 int removed_count = tab_entries_.erase(tab_id); 249 int removed_count = tab_entries_.erase(tab_id);
251 DCHECK_GT(removed_count, 0); 250 DCHECK_GT(removed_count, 0);
252 251
253 UnregisterForTabNotifications(contents); 252 UnregisterForTabNotifications(contents);
254 } 253 }
255 254
256 void BrowserEventRouter::ActiveTabChanged(WebContents* old_contents, 255 void BrowserEventRouter::ActiveTabChanged(WebContents* old_contents,
257 WebContents* new_contents, 256 WebContents* new_contents,
258 int index, 257 int index,
259 bool user_gesture) { 258 bool user_gesture) {
260 scoped_ptr<ListValue> args(new ListValue()); 259 scoped_ptr<ListValue> args(new ListValue());
261 int tab_id = ExtensionTabUtil::GetTabId(new_contents); 260 int tab_id = ExtensionTabUtil::GetTabId(new_contents);
262 args->Append(Value::CreateIntegerValue(tab_id)); 261 args->Append(Value::CreateIntegerValue(tab_id));
263 262
264 DictionaryValue* object_args = new DictionaryValue(); 263 DictionaryValue* object_args = new DictionaryValue();
265 object_args->Set(tab_keys::kWindowIdKey, Value::CreateIntegerValue( 264 object_args->Set(tab_keys::kWindowIdKey, Value::CreateIntegerValue(
266 ExtensionTabUtil::GetWindowIdOfTab(new_contents))); 265 ExtensionTabUtil::GetWindowIdOfTab(new_contents)));
267 args->Append(object_args); 266 args->Append(object_args);
268 267
269 // The onActivated event replaced onActiveChanged and onSelectionChanged. The 268 // The onActivated event replaced onActiveChanged and onSelectionChanged. The
270 // deprecated events take two arguments: tabId, {windowId}. 269 // deprecated events take two arguments: tabId, {windowId}.
271 Profile* profile = 270 Profile* profile =
272 Profile::FromBrowserContext(new_contents->GetBrowserContext()); 271 Profile::FromBrowserContext(new_contents->GetBrowserContext());
273 EventRouter::UserGestureState gesture = user_gesture ? 272 EventRouter::UserGestureState gesture = user_gesture ?
274 EventRouter::USER_GESTURE_ENABLED : EventRouter::USER_GESTURE_NOT_ENABLED; 273 EventRouter::USER_GESTURE_ENABLED : EventRouter::USER_GESTURE_NOT_ENABLED;
275 DispatchEvent(profile, events::kOnTabSelectionChanged, 274 DispatchEvent(profile, event_names::kOnTabSelectionChanged,
276 scoped_ptr<ListValue>(args->DeepCopy()), gesture); 275 scoped_ptr<ListValue>(args->DeepCopy()), gesture);
277 DispatchEvent(profile, events::kOnTabActiveChanged, 276 DispatchEvent(profile, event_names::kOnTabActiveChanged,
278 scoped_ptr<ListValue>(args->DeepCopy()), gesture); 277 scoped_ptr<ListValue>(args->DeepCopy()), gesture);
279 278
280 // The onActivated event takes one argument: {windowId, tabId}. 279 // The onActivated event takes one argument: {windowId, tabId}.
281 args->Remove(0, NULL); 280 args->Remove(0, NULL);
282 object_args->Set(tab_keys::kTabIdKey, Value::CreateIntegerValue(tab_id)); 281 object_args->Set(tab_keys::kTabIdKey, Value::CreateIntegerValue(tab_id));
283 DispatchEvent(profile, events::kOnTabActivated, args.Pass(), gesture); 282 DispatchEvent(profile, event_names::kOnTabActivated, args.Pass(), gesture);
284 } 283 }
285 284
286 void BrowserEventRouter::TabSelectionChanged( 285 void BrowserEventRouter::TabSelectionChanged(
287 TabStripModel* tab_strip_model, 286 TabStripModel* tab_strip_model,
288 const ui::ListSelectionModel& old_model) { 287 const ui::ListSelectionModel& old_model) {
289 ui::ListSelectionModel::SelectedIndices new_selection = 288 ui::ListSelectionModel::SelectedIndices new_selection =
290 tab_strip_model->selection_model().selected_indices(); 289 tab_strip_model->selection_model().selected_indices();
291 ListValue* all = new ListValue(); 290 ListValue* all = new ListValue();
292 291
293 for (size_t i = 0; i < new_selection.size(); ++i) { 292 for (size_t i = 0; i < new_selection.size(); ++i) {
294 int index = new_selection[i]; 293 int index = new_selection[i];
295 WebContents* contents = tab_strip_model->GetWebContentsAt(index); 294 WebContents* contents = tab_strip_model->GetWebContentsAt(index);
296 if (!contents) 295 if (!contents)
297 break; 296 break;
298 int tab_id = ExtensionTabUtil::GetTabId(contents); 297 int tab_id = ExtensionTabUtil::GetTabId(contents);
299 all->Append(Value::CreateIntegerValue(tab_id)); 298 all->Append(Value::CreateIntegerValue(tab_id));
300 } 299 }
301 300
302 scoped_ptr<ListValue> args(new ListValue()); 301 scoped_ptr<ListValue> args(new ListValue());
303 DictionaryValue* select_info = new DictionaryValue(); 302 DictionaryValue* select_info = new DictionaryValue();
304 303
305 select_info->Set(tab_keys::kWindowIdKey, Value::CreateIntegerValue( 304 select_info->Set(tab_keys::kWindowIdKey, Value::CreateIntegerValue(
306 ExtensionTabUtil::GetWindowIdOfTabStripModel(tab_strip_model))); 305 ExtensionTabUtil::GetWindowIdOfTabStripModel(tab_strip_model)));
307 306
308 select_info->Set(tab_keys::kTabIdsKey, all); 307 select_info->Set(tab_keys::kTabIdsKey, all);
309 args->Append(select_info); 308 args->Append(select_info);
310 309
311 // The onHighlighted event replaced onHighlightChanged. 310 // The onHighlighted event replaced onHighlightChanged.
312 Profile* profile = tab_strip_model->profile(); 311 Profile* profile = tab_strip_model->profile();
313 DispatchEvent(profile, events::kOnTabHighlightChanged, 312 DispatchEvent(profile, event_names::kOnTabHighlightChanged,
314 scoped_ptr<ListValue>(args->DeepCopy()), 313 scoped_ptr<ListValue>(args->DeepCopy()),
315 EventRouter::USER_GESTURE_UNKNOWN); 314 EventRouter::USER_GESTURE_UNKNOWN);
316 DispatchEvent(profile, events::kOnTabHighlighted, args.Pass(), 315 DispatchEvent(profile, event_names::kOnTabHighlighted, args.Pass(),
317 EventRouter::USER_GESTURE_UNKNOWN); 316 EventRouter::USER_GESTURE_UNKNOWN);
318 } 317 }
319 318
320 void BrowserEventRouter::TabMoved(WebContents* contents, 319 void BrowserEventRouter::TabMoved(WebContents* contents,
321 int from_index, 320 int from_index,
322 int to_index) { 321 int to_index) {
323 scoped_ptr<ListValue> args(new ListValue()); 322 scoped_ptr<ListValue> args(new ListValue());
324 args->Append(Value::CreateIntegerValue(ExtensionTabUtil::GetTabId(contents))); 323 args->Append(Value::CreateIntegerValue(ExtensionTabUtil::GetTabId(contents)));
325 324
326 DictionaryValue* object_args = new DictionaryValue(); 325 DictionaryValue* object_args = new DictionaryValue();
327 object_args->Set(tab_keys::kWindowIdKey, Value::CreateIntegerValue( 326 object_args->Set(tab_keys::kWindowIdKey, Value::CreateIntegerValue(
328 ExtensionTabUtil::GetWindowIdOfTab(contents))); 327 ExtensionTabUtil::GetWindowIdOfTab(contents)));
329 object_args->Set(tab_keys::kFromIndexKey, Value::CreateIntegerValue( 328 object_args->Set(tab_keys::kFromIndexKey, Value::CreateIntegerValue(
330 from_index)); 329 from_index));
331 object_args->Set(tab_keys::kToIndexKey, Value::CreateIntegerValue( 330 object_args->Set(tab_keys::kToIndexKey, Value::CreateIntegerValue(
332 to_index)); 331 to_index));
333 args->Append(object_args); 332 args->Append(object_args);
334 333
335 Profile* profile = Profile::FromBrowserContext(contents->GetBrowserContext()); 334 Profile* profile = Profile::FromBrowserContext(contents->GetBrowserContext());
336 DispatchEvent(profile, events::kOnTabMoved, args.Pass(), 335 DispatchEvent(profile, event_names::kOnTabMoved, args.Pass(),
337 EventRouter::USER_GESTURE_UNKNOWN); 336 EventRouter::USER_GESTURE_UNKNOWN);
338 } 337 }
339 338
340 void BrowserEventRouter::TabUpdated(WebContents* contents, bool did_navigate) { 339 void BrowserEventRouter::TabUpdated(WebContents* contents, bool did_navigate) {
341 TabEntry* entry = GetTabEntry(contents); 340 TabEntry* entry = GetTabEntry(contents);
342 scoped_ptr<DictionaryValue> changed_properties; 341 scoped_ptr<DictionaryValue> changed_properties;
343 342
344 DCHECK(entry); 343 DCHECK(entry);
345 344
346 if (did_navigate) 345 if (did_navigate)
(...skipping 81 matching lines...) Expand 10 before | Expand all | Expand 10 after
428 args_base->AppendInteger(ExtensionTabUtil::GetTabId(contents)); 427 args_base->AppendInteger(ExtensionTabUtil::GetTabId(contents));
429 428
430 // Second arg: An object containing the changes to the tab state. Filled in 429 // Second arg: An object containing the changes to the tab state. Filled in
431 // by WillDispatchTabUpdatedEvent as a copy of changed_properties, if the 430 // by WillDispatchTabUpdatedEvent as a copy of changed_properties, if the
432 // extension has the tabs permission. 431 // extension has the tabs permission.
433 432
434 // Third arg: An object containing the state of the tab. Filled in by 433 // Third arg: An object containing the state of the tab. Filled in by
435 // WillDispatchTabUpdatedEvent. 434 // WillDispatchTabUpdatedEvent.
436 Profile* profile = Profile::FromBrowserContext(contents->GetBrowserContext()); 435 Profile* profile = Profile::FromBrowserContext(contents->GetBrowserContext());
437 436
438 scoped_ptr<Event> event(new Event(events::kOnTabUpdated, args_base.Pass())); 437 scoped_ptr<Event> event(
438 new Event(event_names::kOnTabUpdated, args_base.Pass()));
439 event->restrict_to_profile = profile; 439 event->restrict_to_profile = profile;
440 event->user_gesture = EventRouter::USER_GESTURE_NOT_ENABLED; 440 event->user_gesture = EventRouter::USER_GESTURE_NOT_ENABLED;
441 event->will_dispatch_callback = 441 event->will_dispatch_callback =
442 base::Bind(&WillDispatchTabUpdatedEvent, 442 base::Bind(&WillDispatchTabUpdatedEvent,
443 contents, changed_properties.get()); 443 contents, changed_properties.get());
444 ExtensionSystem::Get(profile)->event_router()->BroadcastEvent(event.Pass()); 444 ExtensionSystem::Get(profile)->event_router()->BroadcastEvent(event.Pass());
445 } 445 }
446 446
447 BrowserEventRouter::TabEntry* BrowserEventRouter::GetTabEntry( 447 BrowserEventRouter::TabEntry* BrowserEventRouter::GetTabEntry(
448 const WebContents* contents) { 448 const WebContents* contents) {
(...skipping 35 matching lines...) Expand 10 before | Expand all | Expand 10 after
484 int index) { 484 int index) {
485 // Notify listeners that the next tabs closing or being added are due to 485 // Notify listeners that the next tabs closing or being added are due to
486 // WebContents being swapped. 486 // WebContents being swapped.
487 const int new_tab_id = ExtensionTabUtil::GetTabId(new_contents); 487 const int new_tab_id = ExtensionTabUtil::GetTabId(new_contents);
488 const int old_tab_id = ExtensionTabUtil::GetTabId(old_contents); 488 const int old_tab_id = ExtensionTabUtil::GetTabId(old_contents);
489 scoped_ptr<ListValue> args(new ListValue()); 489 scoped_ptr<ListValue> args(new ListValue());
490 args->Append(Value::CreateIntegerValue(new_tab_id)); 490 args->Append(Value::CreateIntegerValue(new_tab_id));
491 args->Append(Value::CreateIntegerValue(old_tab_id)); 491 args->Append(Value::CreateIntegerValue(old_tab_id));
492 492
493 DispatchEvent(Profile::FromBrowserContext(new_contents->GetBrowserContext()), 493 DispatchEvent(Profile::FromBrowserContext(new_contents->GetBrowserContext()),
494 events::kOnTabReplaced, 494 event_names::kOnTabReplaced,
495 args.Pass(), 495 args.Pass(),
496 EventRouter::USER_GESTURE_UNKNOWN); 496 EventRouter::USER_GESTURE_UNKNOWN);
497 497
498 // Update tab_entries_. 498 // Update tab_entries_.
499 const int removed_count = tab_entries_.erase(old_tab_id); 499 const int removed_count = tab_entries_.erase(old_tab_id);
500 DCHECK_GT(removed_count, 0); 500 DCHECK_GT(removed_count, 0);
501 UnregisterForTabNotifications(old_contents); 501 UnregisterForTabNotifications(old_contents);
502 502
503 if (!GetTabEntry(new_contents)) { 503 if (!GetTabEntry(new_contents)) {
504 tab_entries_[new_tab_id] = TabEntry(); 504 tab_entries_[new_tab_id] = TabEntry();
(...skipping 103 matching lines...) Expand 10 before | Expand all | Expand 10 after
608 608
609 DispatchEventToExtension(profile, 609 DispatchEventToExtension(profile,
610 extension_action.extension_id(), 610 extension_action.extension_id(),
611 event_name, 611 event_name,
612 args.Pass(), 612 args.Pass(),
613 EventRouter::USER_GESTURE_ENABLED); 613 EventRouter::USER_GESTURE_ENABLED);
614 } 614 }
615 } 615 }
616 616
617 } // namespace extensions 617 } // namespace extensions
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698