| OLD | NEW |
| 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/chromeos/extensions/file_manager/event_router.h" | 5 #include "chrome/browser/chromeos/extensions/file_manager/event_router.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/files/file_util.h" | 9 #include "base/files/file_util.h" |
| 10 #include "base/message_loop/message_loop.h" | 10 #include "base/message_loop/message_loop.h" |
| 11 #include "base/prefs/pref_change_registrar.h" | 11 #include "base/prefs/pref_change_registrar.h" |
| 12 #include "base/prefs/pref_service.h" | 12 #include "base/prefs/pref_service.h" |
| 13 #include "base/stl_util.h" | 13 #include "base/stl_util.h" |
| 14 #include "base/thread_task_runner_handle.h" | 14 #include "base/thread_task_runner_handle.h" |
| 15 #include "base/threading/sequenced_worker_pool.h" | 15 #include "base/threading/sequenced_worker_pool.h" |
| 16 #include "base/values.h" | 16 #include "base/values.h" |
| 17 #include "chrome/browser/app_mode/app_mode_utils.h" | 17 #include "chrome/browser/app_mode/app_mode_utils.h" |
| 18 #include "chrome/browser/chromeos/drive/drive_integration_service.h" | 18 #include "chrome/browser/chromeos/drive/drive_integration_service.h" |
| 19 #include "chrome/browser/chromeos/drive/file_change.h" | 19 #include "chrome/browser/chromeos/drive/file_change.h" |
| 20 #include "chrome/browser/chromeos/drive/file_system_interface.h" | 20 #include "chrome/browser/chromeos/drive/file_system_interface.h" |
| 21 #include "chrome/browser/chromeos/drive/file_system_util.h" | 21 #include "chrome/browser/chromeos/drive/file_system_util.h" |
| 22 #include "chrome/browser/chromeos/extensions/file_manager/device_event_router.h" | |
| 23 #include "chrome/browser/chromeos/extensions/file_manager/private_api_util.h" | 22 #include "chrome/browser/chromeos/extensions/file_manager/private_api_util.h" |
| 24 #include "chrome/browser/chromeos/file_manager/app_id.h" | 23 #include "chrome/browser/chromeos/file_manager/app_id.h" |
| 25 #include "chrome/browser/chromeos/file_manager/fileapi_util.h" | 24 #include "chrome/browser/chromeos/file_manager/fileapi_util.h" |
| 26 #include "chrome/browser/chromeos/file_manager/open_util.h" | 25 #include "chrome/browser/chromeos/file_manager/open_util.h" |
| 27 #include "chrome/browser/chromeos/file_manager/volume_manager.h" | 26 #include "chrome/browser/chromeos/file_manager/volume_manager.h" |
| 28 #include "chrome/browser/chromeos/login/lock/screen_locker.h" | 27 #include "chrome/browser/chromeos/login/lock/screen_locker.h" |
| 29 #include "chrome/browser/chromeos/login/ui/login_display_host_impl.h" | 28 #include "chrome/browser/chromeos/login/ui/login_display_host_impl.h" |
| 30 #include "chrome/browser/drive/drive_service_interface.h" | 29 #include "chrome/browser/drive/drive_service_interface.h" |
| 31 #include "chrome/browser/extensions/extension_service.h" | 30 #include "chrome/browser/extensions/extension_service.h" |
| 32 #include "chrome/browser/extensions/extension_util.h" | 31 #include "chrome/browser/extensions/extension_util.h" |
| (...skipping 19 matching lines...) Expand all Loading... |
| 52 using content::BrowserThread; | 51 using content::BrowserThread; |
| 53 using drive::DriveIntegrationService; | 52 using drive::DriveIntegrationService; |
| 54 using drive::DriveIntegrationServiceFactory; | 53 using drive::DriveIntegrationServiceFactory; |
| 55 using file_manager::util::EntryDefinition; | 54 using file_manager::util::EntryDefinition; |
| 56 using file_manager::util::FileDefinition; | 55 using file_manager::util::FileDefinition; |
| 57 | 56 |
| 58 namespace file_manager_private = extensions::api::file_manager_private; | 57 namespace file_manager_private = extensions::api::file_manager_private; |
| 59 | 58 |
| 60 namespace file_manager { | 59 namespace file_manager { |
| 61 namespace { | 60 namespace { |
| 62 // Constants for the "transferState" field of onFileTransferUpdated event. | |
| 63 const char kFileTransferStateAdded[] = "added"; | |
| 64 const char kFileTransferStateStarted[] = "started"; | |
| 65 const char kFileTransferStateInProgress[] = "in_progress"; | |
| 66 const char kFileTransferStateCompleted[] = "completed"; | |
| 67 const char kFileTransferStateFailed[] = "failed"; | |
| 68 | 61 |
| 69 // Frequency of sending onFileTransferUpdated. | 62 // Frequency of sending onFileTransferUpdated. |
| 70 const int64 kProgressEventFrequencyInMilliseconds = 1000; | 63 const int64 kProgressEventFrequencyInMilliseconds = 1000; |
| 71 | 64 |
| 72 // Maximim size of detailed change info on directory change event. If the size | 65 // Maximim size of detailed change info on directory change event. If the size |
| 73 // exceeds the maximum size, the detailed info is omitted and the force refresh | 66 // exceeds the maximum size, the detailed info is omitted and the force refresh |
| 74 // is kicked. | 67 // is kicked. |
| 75 const size_t kDirectoryChangeEventMaxDetailInfoSize = 1000; | 68 const size_t kDirectoryChangeEventMaxDetailInfoSize = 1000; |
| 76 | 69 |
| 77 // This time(millisecond) is used for confirm following event exists. | 70 // This time(millisecond) is used for confirm following event exists. |
| 78 const int64 kFileTransferEventDelayTimeInMilliseconds = 300; | 71 const int64 kFileTransferEventDelayTimeInMilliseconds = 300; |
| 79 | 72 |
| 80 // Utility function to check if |job_info| is a file uploading job. | |
| 81 bool IsUploadJob(drive::JobType type) { | |
| 82 return (type == drive::TYPE_UPLOAD_NEW_FILE || | |
| 83 type == drive::TYPE_UPLOAD_EXISTING_FILE); | |
| 84 } | |
| 85 | |
| 86 size_t CountActiveFileTransferJobInfo( | |
| 87 const std::vector<drive::JobInfo>& job_info_list) { | |
| 88 size_t num_active_file_transfer_job_info = 0; | |
| 89 for (size_t i = 0; i < job_info_list.size(); ++i) { | |
| 90 if (IsActiveFileTransferJobInfo(job_info_list[i])) | |
| 91 ++num_active_file_transfer_job_info; | |
| 92 } | |
| 93 return num_active_file_transfer_job_info; | |
| 94 } | |
| 95 | |
| 96 // Converts the job info to a IDL generated type. | |
| 97 void JobInfoToTransferStatus( | |
| 98 Profile* profile, | |
| 99 const std::string& extension_id, | |
| 100 const std::string& job_status, | |
| 101 const drive::JobInfo& job_info, | |
| 102 file_manager_private::FileTransferStatus* status) { | |
| 103 DCHECK(IsActiveFileTransferJobInfo(job_info)); | |
| 104 | |
| 105 scoped_ptr<base::DictionaryValue> result(new base::DictionaryValue); | |
| 106 GURL url = util::ConvertDrivePathToFileSystemUrl( | |
| 107 profile, job_info.file_path, extension_id); | |
| 108 status->file_url = url.spec(); | |
| 109 status->transfer_state = file_manager_private::ParseTransferState(job_status); | |
| 110 status->transfer_type = | |
| 111 IsUploadJob(job_info.job_type) ? | |
| 112 file_manager_private::TRANSFER_TYPE_UPLOAD : | |
| 113 file_manager_private::TRANSFER_TYPE_DOWNLOAD; | |
| 114 DriveIntegrationService* const integration_service = | |
| 115 DriveIntegrationServiceFactory::FindForProfile(profile); | |
| 116 status->num_total_jobs = CountActiveFileTransferJobInfo( | |
| 117 integration_service->job_list()->GetJobInfoList()); | |
| 118 // JavaScript does not have 64-bit integers. Instead we use double, which | |
| 119 // is in IEEE 754 formant and accurate up to 52-bits in JS, and in practice | |
| 120 // in C++. Larger values are rounded. | |
| 121 status->processed.reset( | |
| 122 new double(static_cast<double>(job_info.num_completed_bytes))); | |
| 123 status->total.reset( | |
| 124 new double(static_cast<double>(job_info.num_total_bytes))); | |
| 125 } | |
| 126 | |
| 127 // Checks if the Recovery Tool is running. This is a temporary solution. | 73 // Checks if the Recovery Tool is running. This is a temporary solution. |
| 128 // TODO(mtomasz): Replace with crbug.com/341902 solution. | 74 // TODO(mtomasz): Replace with crbug.com/341902 solution. |
| 129 bool IsRecoveryToolRunning(Profile* profile) { | 75 bool IsRecoveryToolRunning(Profile* profile) { |
| 130 extensions::ExtensionPrefs* extension_prefs = | 76 extensions::ExtensionPrefs* extension_prefs = |
| 131 extensions::ExtensionPrefs::Get(profile); | 77 extensions::ExtensionPrefs::Get(profile); |
| 132 if (!extension_prefs) | 78 if (!extension_prefs) |
| 133 return false; | 79 return false; |
| 134 | 80 |
| 135 const std::string kRecoveryToolIds[] = { | 81 const std::string kRecoveryToolIds[] = { |
| 136 "kkebgepbbgbcmghedmmdfcbdcodlkngh", // Recovery tool staging | 82 "kkebgepbbgbcmghedmmdfcbdcodlkngh", // Recovery tool staging |
| (...skipping 220 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 357 DCHECK_CURRENTLY_ON(BrowserThread::UI); | 303 DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| 358 return profile_->GetPrefs()->GetBoolean(prefs::kExternalStorageDisabled); | 304 return profile_->GetPrefs()->GetBoolean(prefs::kExternalStorageDisabled); |
| 359 } | 305 } |
| 360 | 306 |
| 361 private: | 307 private: |
| 362 Profile* const profile_; | 308 Profile* const profile_; |
| 363 | 309 |
| 364 DISALLOW_COPY_AND_ASSIGN(DeviceEventRouterImpl); | 310 DISALLOW_COPY_AND_ASSIGN(DeviceEventRouterImpl); |
| 365 }; | 311 }; |
| 366 | 312 |
| 313 class JobEventRouterImpl : public JobEventRouter { |
| 314 public: |
| 315 explicit JobEventRouterImpl(Profile* profile) |
| 316 : JobEventRouter(base::TimeDelta::FromMilliseconds( |
| 317 kFileTransferEventDelayTimeInMilliseconds)), |
| 318 profile_(profile) {} |
| 319 |
| 320 protected: |
| 321 GURL ConvertDrivePathToFileSystemUrl(const base::FilePath& path, |
| 322 const std::string& id) const override { |
| 323 return file_manager::util::ConvertDrivePathToFileSystemUrl(profile_, path, |
| 324 id); |
| 325 } |
| 326 void BroadcastEvent(const std::string& event_name, |
| 327 scoped_ptr<base::ListValue> event_args) override { |
| 328 ::file_manager::BroadcastEvent(profile_, event_name, event_args.Pass()); |
| 329 } |
| 330 |
| 331 private: |
| 332 Profile* const profile_; |
| 333 |
| 334 DISALLOW_COPY_AND_ASSIGN(JobEventRouterImpl); |
| 335 }; |
| 336 |
| 367 } // namespace | 337 } // namespace |
| 368 | 338 |
| 369 // Pass dummy value to JobInfo's constructor for make it default constructible. | |
| 370 EventRouter::DriveJobInfoWithStatus::DriveJobInfoWithStatus() | |
| 371 : job_info(drive::TYPE_DOWNLOAD_FILE) { | |
| 372 } | |
| 373 | |
| 374 EventRouter::DriveJobInfoWithStatus::DriveJobInfoWithStatus( | |
| 375 const drive::JobInfo& info, const std::string& status) | |
| 376 : job_info(info), status(status) { | |
| 377 } | |
| 378 | |
| 379 EventRouter::EventRouter(Profile* profile) | 339 EventRouter::EventRouter(Profile* profile) |
| 380 : pref_change_registrar_(new PrefChangeRegistrar), | 340 : pref_change_registrar_(new PrefChangeRegistrar), |
| 381 profile_(profile), | 341 profile_(profile), |
| 382 device_event_router_(new DeviceEventRouterImpl(profile)), | 342 device_event_router_(new DeviceEventRouterImpl(profile)), |
| 343 job_event_router_(new JobEventRouterImpl(profile)), |
| 383 dispatch_directory_change_event_impl_( | 344 dispatch_directory_change_event_impl_( |
| 384 base::Bind(&EventRouter::DispatchDirectoryChangeEventImpl, | 345 base::Bind(&EventRouter::DispatchDirectoryChangeEventImpl, |
| 385 base::Unretained(this))), | 346 base::Unretained(this))), |
| 386 weak_factory_(this) { | 347 weak_factory_(this) { |
| 387 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); | 348 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); |
| 388 ObserveEvents(); | 349 ObserveEvents(); |
| 389 } | 350 } |
| 390 | 351 |
| 391 EventRouter::~EventRouter() { | 352 EventRouter::~EventRouter() { |
| 392 } | 353 } |
| (...skipping 15 matching lines...) Expand all Loading... |
| 408 if (NetworkHandler::IsInitialized()) { | 369 if (NetworkHandler::IsInitialized()) { |
| 409 NetworkHandler::Get()->network_state_handler()->RemoveObserver(this, | 370 NetworkHandler::Get()->network_state_handler()->RemoveObserver(this, |
| 410 FROM_HERE); | 371 FROM_HERE); |
| 411 } | 372 } |
| 412 | 373 |
| 413 DriveIntegrationService* const integration_service = | 374 DriveIntegrationService* const integration_service = |
| 414 DriveIntegrationServiceFactory::FindForProfile(profile_); | 375 DriveIntegrationServiceFactory::FindForProfile(profile_); |
| 415 if (integration_service) { | 376 if (integration_service) { |
| 416 integration_service->file_system()->RemoveObserver(this); | 377 integration_service->file_system()->RemoveObserver(this); |
| 417 integration_service->drive_service()->RemoveObserver(this); | 378 integration_service->drive_service()->RemoveObserver(this); |
| 418 integration_service->job_list()->RemoveObserver(this); | 379 integration_service->job_list()->RemoveObserver(job_event_router_.get()); |
| 419 } | 380 } |
| 420 | 381 |
| 421 VolumeManager* const volume_manager = VolumeManager::Get(profile_); | 382 VolumeManager* const volume_manager = VolumeManager::Get(profile_); |
| 422 if (volume_manager) { | 383 if (volume_manager) { |
| 423 volume_manager->RemoveObserver(this); | 384 volume_manager->RemoveObserver(this); |
| 424 volume_manager->RemoveObserver(device_event_router_.get()); | 385 volume_manager->RemoveObserver(device_event_router_.get()); |
| 425 } | 386 } |
| 426 | 387 |
| 427 chromeos::PowerManagerClient* const power_manager_client = | 388 chromeos::PowerManagerClient* const power_manager_client = |
| 428 chromeos::DBusThreadManager::Get()->GetPowerManagerClient(); | 389 chromeos::DBusThreadManager::Get()->GetPowerManagerClient(); |
| (...skipping 26 matching lines...) Expand all Loading... |
| 455 | 416 |
| 456 chromeos::PowerManagerClient* const power_manager_client = | 417 chromeos::PowerManagerClient* const power_manager_client = |
| 457 chromeos::DBusThreadManager::Get()->GetPowerManagerClient(); | 418 chromeos::DBusThreadManager::Get()->GetPowerManagerClient(); |
| 458 power_manager_client->AddObserver(device_event_router_.get()); | 419 power_manager_client->AddObserver(device_event_router_.get()); |
| 459 | 420 |
| 460 DriveIntegrationService* const integration_service = | 421 DriveIntegrationService* const integration_service = |
| 461 DriveIntegrationServiceFactory::FindForProfile(profile_); | 422 DriveIntegrationServiceFactory::FindForProfile(profile_); |
| 462 if (integration_service) { | 423 if (integration_service) { |
| 463 integration_service->drive_service()->AddObserver(this); | 424 integration_service->drive_service()->AddObserver(this); |
| 464 integration_service->file_system()->AddObserver(this); | 425 integration_service->file_system()->AddObserver(this); |
| 465 integration_service->job_list()->AddObserver(this); | 426 integration_service->job_list()->AddObserver(job_event_router_.get()); |
| 466 } | 427 } |
| 467 | 428 |
| 468 if (NetworkHandler::IsInitialized()) { | 429 if (NetworkHandler::IsInitialized()) { |
| 469 NetworkHandler::Get()->network_state_handler()->AddObserver(this, | 430 NetworkHandler::Get()->network_state_handler()->AddObserver(this, |
| 470 FROM_HERE); | 431 FROM_HERE); |
| 471 } | 432 } |
| 472 | 433 |
| 473 pref_change_registrar_->Init(profile_->GetPrefs()); | 434 pref_change_registrar_->Init(profile_->GetPrefs()); |
| 474 base::Closure callback = | 435 base::Closure callback = |
| 475 base::Bind(&EventRouter::OnFileManagerPrefsChanged, | 436 base::Bind(&EventRouter::OnFileManagerPrefsChanged, |
| (...skipping 149 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 625 NOTREACHED(); | 586 NOTREACHED(); |
| 626 return; | 587 return; |
| 627 } | 588 } |
| 628 | 589 |
| 629 BroadcastEvent( | 590 BroadcastEvent( |
| 630 profile_, | 591 profile_, |
| 631 file_manager_private::OnPreferencesChanged::kEventName, | 592 file_manager_private::OnPreferencesChanged::kEventName, |
| 632 file_manager_private::OnPreferencesChanged::Create()); | 593 file_manager_private::OnPreferencesChanged::Create()); |
| 633 } | 594 } |
| 634 | 595 |
| 635 void EventRouter::OnJobAdded(const drive::JobInfo& job_info) { | |
| 636 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); | |
| 637 if (!drive::IsActiveFileTransferJobInfo(job_info)) | |
| 638 return; | |
| 639 ScheduleDriveFileTransferEvent( | |
| 640 job_info, kFileTransferStateAdded, false /* immediate */); | |
| 641 } | |
| 642 | |
| 643 void EventRouter::OnJobUpdated(const drive::JobInfo& job_info) { | |
| 644 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); | |
| 645 if (!drive::IsActiveFileTransferJobInfo(job_info)) | |
| 646 return; | |
| 647 | |
| 648 bool is_new_job = (drive_jobs_.find(job_info.job_id) == drive_jobs_.end()); | |
| 649 | |
| 650 const std::string status = | |
| 651 is_new_job ? kFileTransferStateStarted : kFileTransferStateInProgress; | |
| 652 | |
| 653 // Replace with the latest job info. | |
| 654 drive_jobs_[job_info.job_id] = DriveJobInfoWithStatus(job_info, status); | |
| 655 | |
| 656 ScheduleDriveFileTransferEvent(job_info, status, false /* immediate */); | |
| 657 } | |
| 658 | |
| 659 void EventRouter::OnJobDone(const drive::JobInfo& job_info, | |
| 660 drive::FileError error) { | |
| 661 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); | |
| 662 if (!drive::IsActiveFileTransferJobInfo(job_info)) | |
| 663 return; | |
| 664 | |
| 665 const std::string status = error == drive::FILE_ERROR_OK | |
| 666 ? kFileTransferStateCompleted | |
| 667 : kFileTransferStateFailed; | |
| 668 | |
| 669 ScheduleDriveFileTransferEvent(job_info, status, true /* immediate */); | |
| 670 | |
| 671 // Forget about the job. | |
| 672 drive_jobs_.erase(job_info.job_id); | |
| 673 } | |
| 674 | |
| 675 void EventRouter::ScheduleDriveFileTransferEvent(const drive::JobInfo& job_info, | |
| 676 const std::string& status, | |
| 677 bool immediate) { | |
| 678 const bool no_pending_task = !drive_job_info_for_scheduled_event_; | |
| 679 // Update the latest event. | |
| 680 drive_job_info_for_scheduled_event_.reset( | |
| 681 new DriveJobInfoWithStatus(job_info, status)); | |
| 682 if (immediate) { | |
| 683 SendDriveFileTransferEvent(); | |
| 684 } else if (no_pending_task) { | |
| 685 const base::TimeDelta delay = base::TimeDelta::FromMilliseconds( | |
| 686 kFileTransferEventDelayTimeInMilliseconds); | |
| 687 base::ThreadTaskRunnerHandle::Get()->PostDelayedTask( | |
| 688 FROM_HERE, | |
| 689 base::Bind(&EventRouter::SendDriveFileTransferEvent, | |
| 690 weak_factory_.GetWeakPtr()), | |
| 691 delay); | |
| 692 } | |
| 693 } | |
| 694 | |
| 695 void EventRouter::SendDriveFileTransferEvent() { | |
| 696 if (!drive_job_info_for_scheduled_event_) | |
| 697 return; | |
| 698 | |
| 699 file_manager_private::FileTransferStatus status; | |
| 700 JobInfoToTransferStatus(profile_, | |
| 701 kFileManagerAppId, | |
| 702 drive_job_info_for_scheduled_event_->status, | |
| 703 drive_job_info_for_scheduled_event_->job_info, | |
| 704 &status); | |
| 705 | |
| 706 drive_job_info_for_scheduled_event_.reset(); | |
| 707 | |
| 708 BroadcastEvent(profile_, | |
| 709 file_manager_private::OnFileTransfersUpdated::kEventName, | |
| 710 file_manager_private::OnFileTransfersUpdated::Create(status)); | |
| 711 } | |
| 712 | |
| 713 void EventRouter::OnDirectoryChanged(const base::FilePath& drive_path) { | 596 void EventRouter::OnDirectoryChanged(const base::FilePath& drive_path) { |
| 714 HandleFileWatchNotification(NULL, drive_path, false); | 597 HandleFileWatchNotification(NULL, drive_path, false); |
| 715 } | 598 } |
| 716 | 599 |
| 717 void EventRouter::OnFileChanged(const drive::FileChange& changed_files) { | 600 void EventRouter::OnFileChanged(const drive::FileChange& changed_files) { |
| 718 // In this method, we convert changed_files to a map which can be handled by | 601 // In this method, we convert changed_files to a map which can be handled by |
| 719 // HandleFileWatchNotification. | 602 // HandleFileWatchNotification. |
| 720 // | 603 // |
| 721 // e.g. | 604 // e.g. |
| 722 // /a/b DIRECTORY:DELETE | 605 // /a/b DIRECTORY:DELETE |
| (...skipping 307 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 1030 void EventRouter::SetDispatchDirectoryChangeEventImplForTesting( | 913 void EventRouter::SetDispatchDirectoryChangeEventImplForTesting( |
| 1031 const DispatchDirectoryChangeEventImplCallback& callback) { | 914 const DispatchDirectoryChangeEventImplCallback& callback) { |
| 1032 dispatch_directory_change_event_impl_ = callback; | 915 dispatch_directory_change_event_impl_ = callback; |
| 1033 } | 916 } |
| 1034 | 917 |
| 1035 base::WeakPtr<EventRouter> EventRouter::GetWeakPtr() { | 918 base::WeakPtr<EventRouter> EventRouter::GetWeakPtr() { |
| 1036 return weak_factory_.GetWeakPtr(); | 919 return weak_factory_.GetWeakPtr(); |
| 1037 } | 920 } |
| 1038 | 921 |
| 1039 } // namespace file_manager | 922 } // namespace file_manager |
| OLD | NEW |