| // Copyright 2014 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "chrome/browser/media_galleries/gallery_watch_manager.h" |
| |
| #include <stddef.h> |
| |
| #include <tuple> |
| |
| #include "base/bind.h" |
| #include "base/logging.h" |
| #include "base/macros.h" |
| #include "base/sequence_checker.h" |
| #include "base/stl_util.h" |
| #include "base/task/post_task.h" |
| #include "base/task/task_traits.h" |
| #include "base/time/time.h" |
| #include "chrome/browser/browser_process.h" |
| #include "chrome/browser/media_galleries/gallery_watch_manager_observer.h" |
| #include "chrome/browser/media_galleries/media_file_system_registry.h" |
| #include "chrome/browser/media_galleries/media_galleries_preferences_factory.h" |
| #include "chrome/browser/profiles/profile.h" |
| #include "components/keyed_service/content/browser_context_keyed_service_shutdown_notifier_factory.h" |
| #include "components/storage_monitor/storage_monitor.h" |
| #include "content/public/browser/browser_context.h" |
| #include "content/public/browser/browser_task_traits.h" |
| #include "content/public/browser/browser_thread.h" |
| #include "extensions/common/extension.h" |
| |
| using content::BrowserContext; |
| using content::BrowserThread; |
| |
| namespace { |
| |
| // Don't send a notification more than once per 3 seconds (chosen arbitrarily). |
| const int kMinNotificationDelayInSeconds = 3; |
| |
| class GalleryWatchManagerShutdownNotifierFactory |
| : public BrowserContextKeyedServiceShutdownNotifierFactory { |
| public: |
| static GalleryWatchManagerShutdownNotifierFactory* GetInstance() { |
| return base::Singleton<GalleryWatchManagerShutdownNotifierFactory>::get(); |
| } |
| |
| private: |
| friend struct base::DefaultSingletonTraits< |
| GalleryWatchManagerShutdownNotifierFactory>; |
| |
| GalleryWatchManagerShutdownNotifierFactory() |
| : BrowserContextKeyedServiceShutdownNotifierFactory( |
| "GalleryWatchManager") { |
| DependsOn(MediaGalleriesPreferencesFactory::GetInstance()); |
| } |
| ~GalleryWatchManagerShutdownNotifierFactory() override {} |
| |
| DISALLOW_COPY_AND_ASSIGN(GalleryWatchManagerShutdownNotifierFactory); |
| }; |
| |
| } // namespace. |
| |
| const char GalleryWatchManager::kInvalidGalleryIDError[] = "Invalid gallery ID"; |
| const char GalleryWatchManager::kNoPermissionError[] = |
| "No permission for gallery ID."; |
| const char GalleryWatchManager::kCouldNotWatchGalleryError[] = |
| "Could not watch gallery path."; |
| |
| // Manages a collection of file path watchers on a sequenced task runner and |
| // relays the change events to |callback| on the UI thread. This file is |
| // constructed on the UI thread, but operates and is destroyed on a sequenced |
| // task runner. If |callback| is called with an error, all watches on that path |
| // have been dropped. |
| class GalleryWatchManager::FileWatchManager { |
| public: |
| explicit FileWatchManager(const base::FilePathWatcher::Callback& callback); |
| ~FileWatchManager(); |
| |
| // Posts success or failure via |callback| to the UI thread. |
| void AddFileWatch(const base::FilePath& path, |
| const base::Callback<void(bool)>& callback); |
| |
| void RemoveFileWatch(const base::FilePath& path); |
| |
| base::WeakPtr<FileWatchManager> GetWeakPtr(); |
| |
| private: |
| using WatcherMap = |
| std::map<base::FilePath, std::unique_ptr<base::FilePathWatcher>>; |
| |
| void OnFilePathChanged(const base::FilePath& path, bool error); |
| |
| WatcherMap watchers_; |
| |
| base::FilePathWatcher::Callback callback_; |
| |
| SEQUENCE_CHECKER(sequence_checker_); |
| |
| base::WeakPtrFactory<FileWatchManager> weak_factory_; |
| |
| DISALLOW_COPY_AND_ASSIGN(FileWatchManager); |
| }; |
| |
| GalleryWatchManager::FileWatchManager::FileWatchManager( |
| const base::FilePathWatcher::Callback& callback) |
| : callback_(callback), weak_factory_(this) { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| |
| // Bind to the sequenced task runner, not the UI thread. |
| DETACH_FROM_SEQUENCE(sequence_checker_); |
| } |
| |
| GalleryWatchManager::FileWatchManager::~FileWatchManager() { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| } |
| |
| void GalleryWatchManager::FileWatchManager::AddFileWatch( |
| const base::FilePath& path, |
| const base::Callback<void(bool)>& callback) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| |
| // This can occur if the GalleryWatchManager attempts to watch the same path |
| // again before recieving the callback. It's benign. |
| if (base::ContainsKey(watchers_, path)) { |
| base::PostTaskWithTraits(FROM_HERE, {BrowserThread::UI}, |
| base::BindOnce(callback, false)); |
| return; |
| } |
| |
| auto watcher = std::make_unique<base::FilePathWatcher>(); |
| bool success = watcher->Watch(path, |
| true /*recursive*/, |
| base::Bind(&FileWatchManager::OnFilePathChanged, |
| weak_factory_.GetWeakPtr())); |
| |
| if (success) |
| watchers_[path] = std::move(watcher); |
| |
| base::PostTaskWithTraits(FROM_HERE, {BrowserThread::UI}, |
| base::BindOnce(callback, success)); |
| } |
| |
| void GalleryWatchManager::FileWatchManager::RemoveFileWatch( |
| const base::FilePath& path) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| |
| size_t erased = watchers_.erase(path); |
| DCHECK_EQ(erased, 1u); |
| } |
| |
| base::WeakPtr<GalleryWatchManager::FileWatchManager> |
| GalleryWatchManager::FileWatchManager::GetWeakPtr() { |
| return weak_factory_.GetWeakPtr(); |
| } |
| |
| void GalleryWatchManager::FileWatchManager::OnFilePathChanged( |
| const base::FilePath& path, |
| bool error) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| |
| if (error) |
| RemoveFileWatch(path); |
| base::PostTaskWithTraits(FROM_HERE, {BrowserThread::UI}, |
| base::BindOnce(callback_, path, error)); |
| } |
| |
| GalleryWatchManager::WatchOwner::WatchOwner(BrowserContext* browser_context, |
| const std::string& extension_id, |
| MediaGalleryPrefId gallery_id) |
| : browser_context(browser_context), |
| extension_id(extension_id), |
| gallery_id(gallery_id) { |
| } |
| |
| bool GalleryWatchManager::WatchOwner::operator<(const WatchOwner& other) const { |
| return std::tie(browser_context, extension_id, gallery_id) < |
| std::tie(other.browser_context, other.extension_id, other.gallery_id); |
| } |
| |
| GalleryWatchManager::NotificationInfo::NotificationInfo() |
| : delayed_notification_pending(false) { |
| } |
| |
| GalleryWatchManager::NotificationInfo::NotificationInfo( |
| const NotificationInfo& other) = default; |
| |
| GalleryWatchManager::NotificationInfo::~NotificationInfo() { |
| } |
| |
| GalleryWatchManager::GalleryWatchManager() |
| : storage_monitor_observed_(false), |
| watch_manager_task_runner_(base::CreateSequencedTaskRunnerWithTraits( |
| {base::MayBlock(), base::TaskPriority::BEST_EFFORT})), |
| weak_factory_(this) { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| watch_manager_.reset(new FileWatchManager(base::Bind( |
| &GalleryWatchManager::OnFilePathChanged, weak_factory_.GetWeakPtr()))); |
| } |
| |
| GalleryWatchManager::~GalleryWatchManager() { |
| weak_factory_.InvalidateWeakPtrs(); |
| |
| if (storage_monitor_observed_) { |
| DCHECK(storage_monitor::StorageMonitor::GetInstance()); |
| storage_monitor::StorageMonitor::GetInstance()->RemoveObserver(this); |
| } |
| |
| watch_manager_task_runner_->DeleteSoon(FROM_HERE, watch_manager_.release()); |
| } |
| |
| void GalleryWatchManager::AddObserver(BrowserContext* browser_context, |
| GalleryWatchManagerObserver* observer) { |
| DCHECK(browser_context); |
| DCHECK(observer); |
| DCHECK(!base::ContainsKey(observers_, browser_context)); |
| observers_[browser_context] = observer; |
| } |
| |
| void GalleryWatchManager::RemoveObserver(BrowserContext* browser_context) { |
| DCHECK(browser_context); |
| size_t erased = observers_.erase(browser_context); |
| DCHECK_EQ(erased, 1u); |
| } |
| |
| void GalleryWatchManager::ShutdownBrowserContext( |
| BrowserContext* browser_context) { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| DCHECK(browser_context); |
| |
| MediaGalleriesPreferences* preferences = |
| g_browser_process->media_file_system_registry()->GetPreferences( |
| Profile::FromBrowserContext(browser_context)); |
| size_t observed = observed_preferences_.erase(preferences); |
| if (observed > 0) |
| preferences->RemoveGalleryChangeObserver(this); |
| |
| auto it = watches_.begin(); |
| while (it != watches_.end()) { |
| if (it->first.browser_context == browser_context) { |
| DeactivateFileWatch(it->first, it->second); |
| // Post increment moves iterator to next element while deleting current. |
| watches_.erase(it++); |
| } else { |
| ++it; |
| } |
| } |
| |
| browser_context_subscription_map_.erase(browser_context); |
| } |
| |
| void GalleryWatchManager::AddWatch(BrowserContext* browser_context, |
| const extensions::Extension* extension, |
| MediaGalleryPrefId gallery_id, |
| const ResultCallback& callback) { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| DCHECK(browser_context); |
| DCHECK(extension); |
| |
| WatchOwner owner(browser_context, extension->id(), gallery_id); |
| if (base::ContainsKey(watches_, owner)) { |
| callback.Run(std::string()); |
| return; |
| } |
| |
| MediaGalleriesPreferences* preferences = |
| g_browser_process->media_file_system_registry()->GetPreferences( |
| Profile::FromBrowserContext(browser_context)); |
| |
| if (!base::ContainsKey(preferences->known_galleries(), gallery_id)) { |
| callback.Run(kInvalidGalleryIDError); |
| return; |
| } |
| |
| MediaGalleryPrefIdSet permitted = |
| preferences->GalleriesForExtension(*extension); |
| if (!base::ContainsKey(permitted, gallery_id)) { |
| callback.Run(kNoPermissionError); |
| return; |
| } |
| |
| base::FilePath path = |
| preferences->known_galleries().find(gallery_id)->second.AbsolutePath(); |
| |
| if (!storage_monitor_observed_) { |
| storage_monitor_observed_ = true; |
| storage_monitor::StorageMonitor::GetInstance()->AddObserver(this); |
| } |
| |
| // Observe the preferences if we haven't already. |
| if (!base::ContainsKey(observed_preferences_, preferences)) { |
| observed_preferences_.insert(preferences); |
| preferences->AddGalleryChangeObserver(this); |
| } |
| |
| watches_[owner] = path; |
| EnsureBrowserContextSubscription(owner.browser_context); |
| |
| // Start the FilePathWatcher on |gallery_path| if necessary. |
| if (base::ContainsKey(watched_paths_, path)) { |
| OnFileWatchActivated(owner, path, callback, true); |
| } else { |
| base::Callback<void(bool)> on_watch_added = |
| base::Bind(&GalleryWatchManager::OnFileWatchActivated, |
| weak_factory_.GetWeakPtr(), |
| owner, |
| path, |
| callback); |
| watch_manager_task_runner_->PostTask( |
| FROM_HERE, |
| base::BindOnce(&FileWatchManager::AddFileWatch, |
| watch_manager_->GetWeakPtr(), path, on_watch_added)); |
| } |
| } |
| |
| void GalleryWatchManager::RemoveWatch(BrowserContext* browser_context, |
| const std::string& extension_id, |
| MediaGalleryPrefId gallery_id) { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| DCHECK(browser_context); |
| |
| WatchOwner owner(browser_context, extension_id, gallery_id); |
| auto it = watches_.find(owner); |
| if (it != watches_.end()) { |
| DeactivateFileWatch(owner, it->second); |
| watches_.erase(it); |
| } |
| } |
| |
| void GalleryWatchManager::RemoveAllWatches(BrowserContext* browser_context, |
| const std::string& extension_id) { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| DCHECK(browser_context); |
| |
| auto it = watches_.begin(); |
| while (it != watches_.end()) { |
| if (it->first.extension_id == extension_id) { |
| DeactivateFileWatch(it->first, it->second); |
| // Post increment moves iterator to next element while deleting current. |
| watches_.erase(it++); |
| } else { |
| ++it; |
| } |
| } |
| } |
| |
| MediaGalleryPrefIdSet GalleryWatchManager::GetWatchSet( |
| BrowserContext* browser_context, |
| const std::string& extension_id) { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| DCHECK(browser_context); |
| |
| MediaGalleryPrefIdSet result; |
| for (WatchesMap::const_iterator it = watches_.begin(); it != watches_.end(); |
| ++it) { |
| if (it->first.browser_context == browser_context && |
| it->first.extension_id == extension_id) { |
| result.insert(it->first.gallery_id); |
| } |
| } |
| return result; |
| } |
| |
| void GalleryWatchManager::EnsureBrowserContextSubscription( |
| BrowserContext* browser_context) { |
| auto it = browser_context_subscription_map_.find(browser_context); |
| if (it == browser_context_subscription_map_.end()) { |
| browser_context_subscription_map_[browser_context] = |
| GalleryWatchManagerShutdownNotifierFactory::GetInstance() |
| ->Get(browser_context) |
| ->Subscribe(base::Bind(&GalleryWatchManager::ShutdownBrowserContext, |
| base::Unretained(this), browser_context)); |
| } |
| } |
| |
| void GalleryWatchManager::DeactivateFileWatch(const WatchOwner& owner, |
| const base::FilePath& path) { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| auto it = watched_paths_.find(path); |
| if (it == watched_paths_.end()) |
| return; |
| |
| it->second.owners.erase(owner); |
| if (it->second.owners.empty()) { |
| watched_paths_.erase(it); |
| watch_manager_task_runner_->PostTask( |
| FROM_HERE, base::BindOnce(&FileWatchManager::RemoveFileWatch, |
| watch_manager_->GetWeakPtr(), path)); |
| } |
| } |
| |
| void GalleryWatchManager::OnFileWatchActivated(const WatchOwner& owner, |
| const base::FilePath& path, |
| const ResultCallback& callback, |
| bool success) { |
| if (success) { |
| // |watched_paths_| doesn't necessarily to contain |path| yet. |
| // In that case, it calls the default constructor for NotificationInfo. |
| watched_paths_[path].owners.insert(owner); |
| callback.Run(std::string()); |
| } else { |
| callback.Run(kCouldNotWatchGalleryError); |
| } |
| } |
| |
| void GalleryWatchManager::OnFilePathChanged(const base::FilePath& path, |
| bool error) { |
| auto notification_info = watched_paths_.find(path); |
| if (notification_info == watched_paths_.end()) |
| return; |
| |
| // On error, all watches on that path are dropped, so update records and |
| // notify observers. |
| if (error) { |
| // Make a copy, as |watched_paths_| is modified as we erase watches. |
| std::set<WatchOwner> owners = notification_info->second.owners; |
| for (auto it = owners.begin(); it != owners.end(); ++it) { |
| Profile* profile = Profile::FromBrowserContext(it->browser_context); |
| RemoveWatch(it->browser_context, it->extension_id, it->gallery_id); |
| if (base::ContainsKey(observers_, profile)) |
| observers_[profile]->OnGalleryWatchDropped(it->extension_id, |
| it->gallery_id); |
| } |
| |
| return; |
| } |
| |
| base::TimeDelta time_since_last_notify = |
| base::Time::Now() - notification_info->second.last_notify_time; |
| if (time_since_last_notify < |
| base::TimeDelta::FromSeconds(kMinNotificationDelayInSeconds)) { |
| if (!notification_info->second.delayed_notification_pending) { |
| notification_info->second.delayed_notification_pending = true; |
| base::TimeDelta delay_to_next_valid_time = |
| notification_info->second.last_notify_time + |
| base::TimeDelta::FromSeconds(kMinNotificationDelayInSeconds) - |
| base::Time::Now(); |
| base::PostDelayedTaskWithTraits( |
| FROM_HERE, {BrowserThread::UI}, |
| base::BindOnce(&GalleryWatchManager::OnFilePathChanged, |
| weak_factory_.GetWeakPtr(), path, error), |
| delay_to_next_valid_time); |
| } |
| return; |
| } |
| notification_info->second.delayed_notification_pending = false; |
| notification_info->second.last_notify_time = base::Time::Now(); |
| |
| std::set<WatchOwner>::const_iterator it; |
| for (it = notification_info->second.owners.begin(); |
| it != notification_info->second.owners.end(); |
| ++it) { |
| DCHECK(base::ContainsKey(watches_, *it)); |
| if (base::ContainsKey(observers_, it->browser_context)) { |
| observers_[it->browser_context]->OnGalleryChanged(it->extension_id, |
| it->gallery_id); |
| } |
| } |
| } |
| |
| void GalleryWatchManager::OnPermissionRemoved(MediaGalleriesPreferences* pref, |
| const std::string& extension_id, |
| MediaGalleryPrefId pref_id) { |
| RemoveWatch(pref->profile(), extension_id, pref_id); |
| if (base::ContainsKey(observers_, pref->profile())) |
| observers_[pref->profile()]->OnGalleryWatchDropped(extension_id, pref_id); |
| } |
| |
| void GalleryWatchManager::OnGalleryRemoved(MediaGalleriesPreferences* pref, |
| MediaGalleryPrefId pref_id) { |
| // Removing a watch may update |watches_|, so extract the extension ids first. |
| std::set<std::string> extension_ids; |
| for (WatchesMap::const_iterator it = watches_.begin(); it != watches_.end(); |
| ++it) { |
| if (it->first.browser_context == pref->profile() && |
| it->first.gallery_id == pref_id) { |
| extension_ids.insert(it->first.extension_id); |
| } |
| } |
| |
| for (auto it = extension_ids.begin(); it != extension_ids.end(); ++it) { |
| RemoveWatch(pref->profile(), *it, pref_id); |
| if (base::ContainsKey(observers_, pref->profile())) |
| observers_[pref->profile()]->OnGalleryWatchDropped(*it, pref_id); |
| } |
| } |
| |
| void GalleryWatchManager::OnRemovableStorageDetached( |
| const storage_monitor::StorageInfo& info) { |
| auto it = watches_.begin(); |
| while (it != watches_.end()) { |
| MediaGalleriesPreferences* preferences = |
| g_browser_process->media_file_system_registry()->GetPreferences( |
| Profile::FromBrowserContext(it->first.browser_context)); |
| MediaGalleryPrefIdSet detached_ids = |
| preferences->LookUpGalleriesByDeviceId(info.device_id()); |
| |
| if (base::ContainsKey(detached_ids, it->first.gallery_id)) { |
| WatchOwner owner = it->first; |
| DeactivateFileWatch(owner, it->second); |
| // Post increment moves iterator to next element while deleting current. |
| watches_.erase(it++); |
| observers_[preferences->profile()]->OnGalleryWatchDropped( |
| owner.extension_id, owner.gallery_id); |
| } else { |
| ++it; |
| } |
| } |
| } |