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

Side by Side Diff: chrome/browser/chromeos/extensions/file_browser_event_router.cc

Issue 7471024: Formatting feature initial commit for ChromeOS Tree (Closed) Base URL: http://git.chromium.org/git/chromium.git@trunk
Patch Set: newest (not working?) Created 9 years, 4 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) 2011 The Chromium Authors. All rights reserved. 1 // Copyright (c) 2011 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/extensions/file_browser_event_router.h" 5 #include "chrome/browser/chromeos/extensions/file_browser_event_router.h"
6 6
7 #include "base/bind.h"
7 #include "base/json/json_writer.h" 8 #include "base/json/json_writer.h"
9 #include "base/memory/singleton.h"
10 #include "base/message_loop.h"
8 #include "base/stl_util.h" 11 #include "base/stl_util.h"
9 #include "base/values.h" 12 #include "base/values.h"
10 #include "chrome/browser/chromeos/cros/cros_library.h" 13 #include "chrome/browser/chromeos/cros/cros_library.h"
11 #include "chrome/browser/chromeos/login/user_manager.h" 14 #include "chrome/browser/chromeos/login/user_manager.h"
12 #include "chrome/browser/chromeos/notifications/system_notification.h" 15 #include "chrome/browser/chromeos/notifications/system_notification.h"
13 #include "chrome/browser/extensions/extension_event_names.h" 16 #include "chrome/browser/extensions/extension_event_names.h"
14 #include "chrome/browser/extensions/extension_event_router.h" 17 #include "chrome/browser/extensions/extension_event_router.h"
15 #include "chrome/browser/extensions/extension_service.h" 18 #include "chrome/browser/extensions/extension_service.h"
16 #include "chrome/browser/extensions/file_manager_util.h" 19 #include "chrome/browser/extensions/file_manager_util.h"
17 #include "chrome/browser/profiles/profile.h" 20 #include "chrome/browser/profiles/profile.h"
18 #include "content/browser/browser_thread.h" 21 #include "content/browser/browser_thread.h"
19 #include "grit/generated_resources.h" 22 #include "grit/generated_resources.h"
20 #include "grit/theme_resources.h" 23 #include "grit/theme_resources.h"
21 #include "ui/base/l10n/l10n_util.h" 24 #include "ui/base/l10n/l10n_util.h"
22 #include "webkit/fileapi/file_system_types.h" 25 #include "webkit/fileapi/file_system_types.h"
23 #include "webkit/fileapi/file_system_util.h" 26 #include "webkit/fileapi/file_system_util.h"
24 27
25 const char kDiskAddedEventType[] = "added"; 28 namespace {
26 const char kDiskRemovedEventType[] = "removed"; 29 const char kDiskAddedEventType[] = "added";
30 const char kDiskRemovedEventType[] = "removed";
27 31
28 const char kPathChanged[] = "changed"; 32 const char kPathChanged[] = "changed";
29 const char kPathWatchError[] = "error"; 33 const char kPathWatchError[] = "error";
30 34
31 const char* DeviceTypeToString(chromeos::DeviceType type) { 35 const char* DeviceTypeToString(chromeos::DeviceType type) {
32 switch (type) { 36 switch (type) {
33 case chromeos::FLASH: 37 case chromeos::FLASH:
34 return "flash"; 38 return "flash";
35 case chromeos::HDD: 39 case chromeos::HDD:
36 return "hdd"; 40 return "hdd";
37 case chromeos::OPTICAL: 41 case chromeos::OPTICAL:
38 return "optical"; 42 return "optical";
39 default: 43 default:
40 break; 44 break;
45 }
46 return "undefined";
41 } 47 }
42 return "undefined"; 48
49 DictionaryValue* DiskToDictionaryValue(
50 const chromeos::MountLibrary::Disk* disk) {
51 DictionaryValue* result = new DictionaryValue();
52 result->SetString("mountPath", disk->mount_path());
53 result->SetString("devicePath", disk->device_path());
54 result->SetString("label", disk->device_label());
55 result->SetString("deviceType", DeviceTypeToString(disk->device_type()));
56 result->SetInteger("totalSizeKB", disk->total_size() / 1024);
57 result->SetBoolean("readOnly", disk->is_read_only());
58 return result;
59 }
43 } 60 }
44 61
45 const char* MountErrorToString(chromeos::MountError error) { 62 const char* MountErrorToString(chromeos::MountError error) {
46 switch (error) { 63 switch (error) {
47 case chromeos::MOUNT_ERROR_NONE: 64 case chromeos::MOUNT_ERROR_NONE:
48 return "success"; 65 return "success";
49 case chromeos::MOUNT_ERROR_UNKNOWN: 66 case chromeos::MOUNT_ERROR_UNKNOWN:
50 return "error_unknown"; 67 return "error_unknown";
51 case chromeos::MOUNT_ERROR_INTERNAL: 68 case chromeos::MOUNT_ERROR_INTERNAL:
52 return "error_internal"; 69 return "error_internal";
53 case chromeos::MOUNT_ERROR_UNKNOWN_FILESYSTEM: 70 case chromeos::MOUNT_ERROR_UNKNOWN_FILESYSTEM:
54 return "error_unknown_filesystem"; 71 return "error_unknown_filesystem";
55 case chromeos::MOUNT_ERROR_UNSUPORTED_FILESYSTEM: 72 case chromeos::MOUNT_ERROR_UNSUPORTED_FILESYSTEM:
56 return "error_unsuported_filesystem"; 73 return "error_unsuported_filesystem";
57 case chromeos::MOUNT_ERROR_INVALID_ARCHIVE: 74 case chromeos::MOUNT_ERROR_INVALID_ARCHIVE:
58 return "error_invalid_archive"; 75 return "error_invalid_archive";
59 case chromeos::MOUNT_ERROR_LIBRARY_NOT_LOADED: 76 case chromeos::MOUNT_ERROR_LIBRARY_NOT_LOADED:
60 return "error_libcros_missing"; 77 return "error_libcros_missing";
61 default: 78 default:
62 NOTREACHED(); 79 NOTREACHED();
63 } 80 }
64 return ""; 81 return "";
65 } 82 }
66 83
67 DictionaryValue* DiskToDictionaryValue( 84 void HideFileBrowserNotificationExternally(const std::string& category,
68 const chromeos::MountLibrary::Disk* disk) { 85 const std::string& system_path, ExtensionFileBrowserEventRouter* that) {
69 DictionaryValue* result = new DictionaryValue(); 86 that->HideFileBrowserNotification(category, system_path);
70 result->SetString("mountPath", disk->mount_path());
71 result->SetString("devicePath", disk->device_path());
72 result->SetString("label", disk->device_label());
73 result->SetString("deviceType", DeviceTypeToString(disk->device_type()));
74 result->SetInteger("totalSizeKB", disk->total_size() / 1024);
75 result->SetBoolean("readOnly", disk->is_read_only());
76 return result;
77 } 87 }
78 88
79 ExtensionFileBrowserEventRouter::ExtensionFileBrowserEventRouter( 89 ExtensionFileBrowserEventRouter::ExtensionFileBrowserEventRouter(
80 Profile* profile) 90 Profile* profile)
81 : delegate_(new ExtensionFileBrowserEventRouter::FileWatcherDelegate(this)), 91 : delegate_(new ExtensionFileBrowserEventRouter::FileWatcherDelegate(this)),
82 profile_(profile) { 92 profile_(profile) {
83 } 93 }
84 94
85 ExtensionFileBrowserEventRouter::~ExtensionFileBrowserEventRouter() { 95 ExtensionFileBrowserEventRouter::~ExtensionFileBrowserEventRouter() {
86 DCHECK(file_watchers_.empty()); 96 DCHECK(file_watchers_.empty());
(...skipping 78 matching lines...) Expand 10 before | Expand all | Expand 10 after
165 175
166 void ExtensionFileBrowserEventRouter::DeviceChanged( 176 void ExtensionFileBrowserEventRouter::DeviceChanged(
167 chromeos::MountLibraryEventType event, 177 chromeos::MountLibraryEventType event,
168 const std::string& device_path) { 178 const std::string& device_path) {
169 if (event == chromeos::MOUNT_DEVICE_ADDED) { 179 if (event == chromeos::MOUNT_DEVICE_ADDED) {
170 OnDeviceAdded(device_path); 180 OnDeviceAdded(device_path);
171 } else if (event == chromeos::MOUNT_DEVICE_REMOVED) { 181 } else if (event == chromeos::MOUNT_DEVICE_REMOVED) {
172 OnDeviceRemoved(device_path); 182 OnDeviceRemoved(device_path);
173 } else if (event == chromeos::MOUNT_DEVICE_SCANNED) { 183 } else if (event == chromeos::MOUNT_DEVICE_SCANNED) {
174 OnDeviceScanned(device_path); 184 OnDeviceScanned(device_path);
185 } else if (event == chromeos::MOUNT_FORMATTING_STARTED) {
186 OnFormattingStarted(device_path);
187 } else if (event == chromeos::MOUNT_FORMATTING_FINISHED) {
188 OnFormattingFinished(device_path);
175 } 189 }
176 } 190 }
177 void ExtensionFileBrowserEventRouter::MountCompleted( 191 void ExtensionFileBrowserEventRouter::MountCompleted(
178 chromeos::MountLibrary::MountEvent event_type, 192 chromeos::MountLibrary::MountEvent event_type,
179 chromeos::MountError error_code, 193 chromeos::MountError error_code,
180 const chromeos::MountLibrary::MountPointInfo& mount_info) { 194 const chromeos::MountLibrary::MountPointInfo& mount_info) {
181 DispatchMountCompletedEvent(event_type, error_code, mount_info); 195 DispatchMountCompletedEvent(event_type, error_code, mount_info);
182 } 196 }
183 197
184 void ExtensionFileBrowserEventRouter::HandleFileWatchNotification( 198 void ExtensionFileBrowserEventRouter::HandleFileWatchNotification(
(...skipping 115 matching lines...) Expand 10 before | Expand all | Expand 10 after
300 314
301 void ExtensionFileBrowserEventRouter::OnDiskAdded( 315 void ExtensionFileBrowserEventRouter::OnDiskAdded(
302 const chromeos::MountLibrary::Disk* disk) { 316 const chromeos::MountLibrary::Disk* disk) {
303 VLOG(1) << "Disk added: " << disk->device_path(); 317 VLOG(1) << "Disk added: " << disk->device_path();
304 if (disk->device_path().empty()) { 318 if (disk->device_path().empty()) {
305 VLOG(1) << "Empty system path for " << disk->device_path(); 319 VLOG(1) << "Empty system path for " << disk->device_path();
306 return; 320 return;
307 } 321 }
308 if (disk->is_parent()) { 322 if (disk->is_parent()) {
309 if (!disk->has_media()) { 323 if (!disk->has_media()) {
310 HideDeviceNotification(disk->system_path()); 324 HideFileBrowserNotification("MOUNT", disk->system_path());
311 return; 325 return;
312 } 326 }
313 } 327 }
314 328
315 // If disk is not mounted yet, give it a try. 329 // If disk is not mounted yet, give it a try.
316 if (disk->mount_path().empty()) { 330 if (disk->mount_path().empty()) {
317 // Initiate disk mount operation. 331 // Initiate disk mount operation.
318 chromeos::MountLibrary* lib = 332 chromeos::MountLibrary* lib =
319 chromeos::CrosLibrary::Get()->GetMountLibrary(); 333 chromeos::CrosLibrary::Get()->GetMountLibrary();
320 lib->MountPath(disk->device_path().c_str(), 334 lib->MountPath(disk->device_path().c_str(),
321 chromeos::MOUNT_TYPE_DEVICE, 335 chromeos::MOUNT_TYPE_DEVICE,
322 chromeos::MountPathOptions()); // Unused. 336 chromeos::MountPathOptions()); // Unused.
323 } 337 }
338 DispatchMountEvent(disk, true);
324 } 339 }
325 340
326 void ExtensionFileBrowserEventRouter::OnDiskRemoved( 341 void ExtensionFileBrowserEventRouter::OnDiskRemoved(
327 const chromeos::MountLibrary::Disk* disk) { 342 const chromeos::MountLibrary::Disk* disk) {
328 VLOG(1) << "Disk removed: " << disk->device_path(); 343 VLOG(1) << "Disk removed: " << disk->device_path();
329 HideDeviceNotification(disk->system_path()); 344 HideFileBrowserNotification("MOUNT", disk->system_path());
330 MountPointMap::iterator iter = mounted_devices_.find(disk->device_path()); 345 MountPointMap::iterator iter = mounted_devices_.find(disk->device_path());
331 if (iter == mounted_devices_.end()) 346 if (iter == mounted_devices_.end())
332 return; 347 return;
333 348
334 chromeos::MountLibrary* lib = 349 chromeos::MountLibrary* lib =
335 chromeos::CrosLibrary::Get()->GetMountLibrary(); 350 chromeos::CrosLibrary::Get()->GetMountLibrary();
336 // TODO(zelidrag): This for some reason does not work as advertized. 351 // TODO(zelidrag): This for some reason does not work as advertized.
337 // we might need to clean up mount directory on FILE thread here as well. 352 // we might need to clean up mount directory on FILE thread here as well.
338 lib->UnmountPath(disk->device_path().c_str()); 353 lib->UnmountPath(disk->device_path().c_str());
339
340 DispatchMountEvent(disk, false); 354 DispatchMountEvent(disk, false);
341 mounted_devices_.erase(iter); 355 mounted_devices_.erase(iter);
342 } 356 }
343 357
344 void ExtensionFileBrowserEventRouter::OnDiskChanged( 358 void ExtensionFileBrowserEventRouter::OnDiskChanged(
345 const chromeos::MountLibrary::Disk* disk) { 359 const chromeos::MountLibrary::Disk* disk) {
346 VLOG(1) << "Disk changed : " << disk->device_path(); 360 VLOG(1) << "Disk changed : " << disk->device_path();
347 if (!disk->mount_path().empty()) { 361 if (!disk->mount_path().empty()) {
348 HideDeviceNotification(disk->system_path()); 362 HideFileBrowserNotification("MOUNT", disk->system_path());
349 // Remember this mount point. 363 // Remember this mount point.
350 if (mounted_devices_.find(disk->device_path()) == mounted_devices_.end()) { 364 if (mounted_devices_.find(disk->device_path()) == mounted_devices_.end()) {
351 mounted_devices_.insert( 365 mounted_devices_.insert(
352 std::pair<std::string, std::string>(disk->device_path(), 366 std::pair<std::string, std::string>(disk->device_path(),
353 disk->mount_path())); 367 disk->mount_path()));
354 DispatchMountEvent(disk, true); 368 DispatchMountEvent(disk, true);
355 HideDeviceNotification(disk->system_path()); 369 HideFileBrowserNotification("MOUNT", disk->system_path());
356 FileManagerUtil::ShowFullTabUrl(profile_, FilePath(disk->mount_path())); 370 FileManagerUtil::ShowFullTabUrl(profile_, FilePath(disk->mount_path()));
357 } 371 }
358 } 372 }
359 } 373 }
360 374
361 void ExtensionFileBrowserEventRouter::OnDeviceAdded( 375 void ExtensionFileBrowserEventRouter::OnDeviceAdded(
362 const std::string& device_path) { 376 const std::string& device_path) {
363 VLOG(1) << "Device added : " << device_path; 377 VLOG(1) << "Device added : " << device_path;
364 // TODO(zelidrag): Find better icon here. 378 // TODO(zelidrag): Find better icon here.
365 ShowDeviceNotification(device_path, IDR_PAGEINFO_INFO, 379 ShowFileBrowserNotification("MOUNT", device_path, IDR_PAGEINFO_INFO,
380 l10n_util::GetStringUTF16(IDS_REMOVABLE_DEVICE_DETECTION_TITLE),
366 l10n_util::GetStringUTF16(IDS_REMOVABLE_DEVICE_SCANNING_MESSAGE)); 381 l10n_util::GetStringUTF16(IDS_REMOVABLE_DEVICE_SCANNING_MESSAGE));
367
368 } 382 }
369 383
370 void ExtensionFileBrowserEventRouter::OnDeviceRemoved( 384 void ExtensionFileBrowserEventRouter::OnDeviceRemoved(
371 const std::string& system_path) { 385 const std::string& system_path) {
372 HideDeviceNotification(system_path); 386 HideFileBrowserNotification("MOUNT", system_path);
373 } 387 }
374 388
375 void ExtensionFileBrowserEventRouter::OnDeviceScanned( 389 void ExtensionFileBrowserEventRouter::OnDeviceScanned(
376 const std::string& device_path) { 390 const std::string& device_path) {
377 VLOG(1) << "Device scanned : " << device_path; 391 VLOG(1) << "Device scanned : " << device_path;
378 } 392 }
379 393
380 void ExtensionFileBrowserEventRouter::ShowDeviceNotification( 394 void ExtensionFileBrowserEventRouter::OnFormattingStarted(
381 const std::string& system_path, int icon_resource_id, 395 const std::string& device_path) {
382 const string16& message) { 396 if (device_path[0] == '!') {
383 NotificationMap::iterator iter = FindNotificationForPath(system_path); 397 ShowFileBrowserNotification("FORMAT_FINISHED", device_path.substr(1),
384 std::string mount_path; 398 IDR_PAGEINFO_WARNING_MAJOR,
385 if (iter != notifications_.end()) { 399 l10n_util::GetStringUTF16(IDS_FORMATTING_OF_DEVICE_FINISHED_TITLE),
386 iter->second->Show(message, false, false); 400 l10n_util::GetStringUTF16(IDS_FORMATTING_STARTED_FAILURE_MESSAGE));
387 } else { 401 } else {
388 if (!profile_) { 402 ShowFileBrowserNotification("FORMAT", device_path, IDR_PAGEINFO_INFO,
389 NOTREACHED(); 403 l10n_util::GetStringUTF16(IDS_FORMATTING_OF_DEVICE_PENDING_TITLE),
390 return; 404 l10n_util::GetStringUTF16(IDS_FORMATTING_OF_DEVICE_PENDING_MESSAGE));
391 }
392 chromeos::SystemNotification* notification =
393 new chromeos::SystemNotification(
394 profile_,
395 system_path,
396 icon_resource_id,
397 l10n_util::GetStringUTF16(IDS_REMOVABLE_DEVICE_DETECTION_TITLE));
398 notifications_.insert(NotificationMap::value_type(system_path,
399 linked_ptr<chromeos::SystemNotification>(notification)));
400 notification->Show(message, false, false);
401 } 405 }
402 } 406 }
403 407
404 void ExtensionFileBrowserEventRouter::HideDeviceNotification( 408 void ExtensionFileBrowserEventRouter::OnFormattingFinished(
405 const std::string& system_path) { 409 const std::string& device_path) {
406 NotificationMap::iterator iter = FindNotificationForPath(system_path); 410 if (device_path[0] == '!') {
411 HideFileBrowserNotification("FORMAT", device_path.substr(1));
412 ShowFileBrowserNotification("FORMAT_FINISHED", device_path.substr(1),
413 IDR_PAGEINFO_WARNING_MAJOR,
414 l10n_util::GetStringUTF16(IDS_FORMATTING_OF_DEVICE_FINISHED_TITLE),
415 l10n_util::GetStringUTF16(IDS_FORMATTING_FINISHED_FAILURE_MESSAGE));
416 } else {
417 HideFileBrowserNotification("FORMAT", device_path);
418 ShowFileBrowserNotification("FORMAT_FINISHED", device_path,
419 IDR_PAGEINFO_INFO,
420 l10n_util::GetStringUTF16(IDS_FORMATTING_OF_DEVICE_FINISHED_TITLE),
421 l10n_util::GetStringUTF16(IDS_FORMATTING_FINISHED_SUCCESS_MESSAGE));
422 // Hide it after a couple of seconds
423 MessageLoop::current()->PostDelayedTask(FROM_HERE,
424 base::Bind(&HideFileBrowserNotificationExternally, "FORMAT_FINISHED",
425 device_path, this),
426 4000);
427 chromeos::MountLibrary* lib =
428 chromeos::CrosLibrary::Get()->GetMountLibrary();
429 lib->MountPath(device_path.c_str(),
430 chromeos::MOUNT_TYPE_DEVICE,
431 chromeos::MountPathOptions());
432 }
433 }
434
435 void ExtensionFileBrowserEventRouter::ShowFileBrowserNotification(
436 const std::string& category, const std::string& system_path,
437 int icon_resource_id, const string16& title, const string16& message) {
438 std::string notification_id = category + system_path;
439 // New notification always created because, it might have been closed by now.
440 NotificationMap::iterator iter = FindNotificationForId(notification_id);
441 if (iter != notifications_.end())
442 notifications_.erase(iter);
443 if (!profile_) {
444 NOTREACHED();
445 return;
446 }
447 chromeos::SystemNotification* notification =
448 new chromeos::SystemNotification(
449 profile_,
450 notification_id,
451 icon_resource_id,
452 title);
453 notifications_.insert(NotificationMap::value_type(notification_id,
454 linked_ptr<chromeos::SystemNotification>(notification)));
455 notification->Show(message, false, false);
456 }
457
458 void ExtensionFileBrowserEventRouter::HideFileBrowserNotification(
459 const std::string& category, const std::string& system_path) {
460 NotificationMap::iterator iter = FindNotificationForId(
461 category + system_path);
tbarzic 2011/07/29 17:43:18 Doesn't this fit in one line
sidor 2011/07/29 18:36:53 Nope. One character :) On 2011/07/29 17:43:18, ton
407 if (iter != notifications_.end()) { 462 if (iter != notifications_.end()) {
408 iter->second->Hide(); 463 iter->second->Hide();
409 notifications_.erase(iter); 464 notifications_.erase(iter);
410 } 465 }
411 } 466 }
412 467
413 ExtensionFileBrowserEventRouter::NotificationMap::iterator 468 ExtensionFileBrowserEventRouter::NotificationMap::iterator
414 ExtensionFileBrowserEventRouter::FindNotificationForPath( 469 ExtensionFileBrowserEventRouter::FindNotificationForId(
415 const std::string& system_path) { 470 const std::string& notification_id) {
416 for (NotificationMap::iterator iter = notifications_.begin(); 471 for (NotificationMap::iterator iter = notifications_.begin();
417 iter != notifications_.end(); 472 iter != notifications_.end();
418 ++iter) { 473 ++iter) {
419 const std::string& notification_device_path = iter->first; 474 const std::string& notification_device_path = iter->first;
420 // Doing a sub string match so that we find if this new one is a subdevice 475 // Doing a sub string match so that we find if this new one is a subdevice
421 // of another already inserted device. 476 // of another already inserted device.
422 if (StartsWithASCII(system_path, notification_device_path, true)) { 477 if (StartsWithASCII(notification_id, notification_device_path, true)) {
423 return iter; 478 return iter;
424 } 479 }
425 } 480 }
426 return notifications_.end(); 481 return notifications_.end();
427 } 482 }
428 483
429 484
430 // ExtensionFileBrowserEventRouter::WatcherDelegate methods. 485 // ExtensionFileBrowserEventRouter::WatcherDelegate methods.
431 ExtensionFileBrowserEventRouter::FileWatcherDelegate::FileWatcherDelegate( 486 ExtensionFileBrowserEventRouter::FileWatcherDelegate::FileWatcherDelegate(
432 ExtensionFileBrowserEventRouter* router) : router_(router) { 487 ExtensionFileBrowserEventRouter* router) : router_(router) {
(...skipping 17 matching lines...) Expand all
450 &FileWatcherDelegate::HandleFileWatchOnUIThread, 505 &FileWatcherDelegate::HandleFileWatchOnUIThread,
451 local_path, 506 local_path,
452 true)); // got_error 507 true)); // got_error
453 } 508 }
454 509
455 void 510 void
456 ExtensionFileBrowserEventRouter::FileWatcherDelegate::HandleFileWatchOnUIThread( 511 ExtensionFileBrowserEventRouter::FileWatcherDelegate::HandleFileWatchOnUIThread(
457 const FilePath& local_path, bool got_error) { 512 const FilePath& local_path, bool got_error) {
458 router_->HandleFileWatchNotification(local_path, got_error); 513 router_->HandleFileWatchNotification(local_path, got_error);
459 } 514 }
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698