From 3b00c2d3dc17dfe7927b4884d029a3aa80afb6d3 Mon Sep 17 00:00:00 2001 From: "tommycli@chromium.org" Date: Tue, 1 Jul 2014 22:58:11 +0000 Subject: [PATCH] Media Galleries API: Reimplement gallery watch manager. Continuation of https://codereview.chromium.org/338623005/. Patchset 1 of this CL is Patchset 3 of https://codereview.chromium.org/338623005/. BUG=144491 Review URL: https://codereview.chromium.org/353543002 git-svn-id: svn://svn.chromium.org/chrome/trunk/src@280905 0039d316-1c4b-4281-b951-d872f2087c98 --- .../media_galleries_private_api.cc | 4 + .../media_galleries/gallery_watch_manager.cc | 443 +++++++++++++++++++++ .../media_galleries/gallery_watch_manager.h | 155 +++++++ .../gallery_watch_manager_observer.h | 25 ++ .../gallery_watch_manager_unittest.cc | 282 +++++++++++++ .../media_galleries/media_galleries_preferences.h | 4 + chrome/chrome_browser.gypi | 3 + chrome/chrome_tests_unit.gypi | 3 +- 8 files changed, 918 insertions(+), 1 deletion(-) create mode 100644 chrome/browser/media_galleries/gallery_watch_manager.cc create mode 100644 chrome/browser/media_galleries/gallery_watch_manager.h create mode 100644 chrome/browser/media_galleries/gallery_watch_manager_observer.h create mode 100644 chrome/browser/media_galleries/gallery_watch_manager_unittest.cc diff --git a/chrome/browser/extensions/api/media_galleries_private/media_galleries_private_api.cc b/chrome/browser/extensions/api/media_galleries_private/media_galleries_private_api.cc index 459cfda94624..7db144dc6d03 100644 --- a/chrome/browser/extensions/api/media_galleries_private/media_galleries_private_api.cc +++ b/chrome/browser/extensions/api/media_galleries_private/media_galleries_private_api.cc @@ -194,6 +194,10 @@ void MediaGalleriesPrivateAddGalleryWatchFunction::OnPreferencesInit( MediaGalleriesPrivateEventRouter* router = MediaGalleriesPrivateAPI::Get(GetProfile())->GetEventRouter(); + + // TODO(tommycli): The new GalleryWatchManager no longer checks that there is + // an event listener attached. There should be a check for that here. + DCHECK(router); content::BrowserThread::PostTaskAndReplyWithResult( content::BrowserThread::FILE, diff --git a/chrome/browser/media_galleries/gallery_watch_manager.cc b/chrome/browser/media_galleries/gallery_watch_manager.cc new file mode 100644 index 000000000000..cf3901b282e6 --- /dev/null +++ b/chrome/browser/media_galleries/gallery_watch_manager.cc @@ -0,0 +1,443 @@ +// 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 "base/bind.h" +#include "base/stl_util.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/profiles/profile.h" +#include "components/storage_monitor/storage_monitor.h" +#include "content/public/browser/browser_context.h" +#include "content/public/browser/browser_thread.h" +#include "extensions/common/extension.h" + +using content::BrowserContext; +using content::BrowserThread; + +namespace { + +const char kInvalidGalleryIDError[] = "Invalid gallery ID"; +const char kNoPermissionError[] = "No permission for gallery ID."; +const char kCouldNotWatchGalleryError[] = "Could not watch gallery path."; + +// Don't send a notification more than once per 3 seconds (chosen arbitrarily). +const int kMinNotificiationDelayInSeconds = 3; + +} // namespace. + +// Manages a collection of file path watchers on the FILE thread 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 the FILE thread. +// 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& callback); + + void RemoveFileWatch(const base::FilePath& path); + + base::WeakPtr GetWeakPtr(); + + private: + typedef std::map > + WatcherMap; + + void OnFilePathChanged(const base::FilePath& path, bool error); + + WatcherMap watchers_; + + base::FilePathWatcher::Callback callback_; + + base::WeakPtrFactory 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); +} + +GalleryWatchManager::FileWatchManager::~FileWatchManager() { + DCHECK_CURRENTLY_ON(BrowserThread::FILE); +} + +void GalleryWatchManager::FileWatchManager::AddFileWatch( + const base::FilePath& path, + const base::Callback& callback) { + DCHECK_CURRENTLY_ON(BrowserThread::FILE); + + // This can occur if the GalleryWatchManager attempts to watch the same path + // again before recieving the callback. It's benign. + if (ContainsKey(watchers_, path)) { + BrowserThread::PostTask( + BrowserThread::UI, FROM_HERE, base::Bind(callback, false)); + return; + } + + linked_ptr watcher(new base::FilePathWatcher); + bool success = watcher->Watch(path, + true /*recursive*/, + base::Bind(&FileWatchManager::OnFilePathChanged, + weak_factory_.GetWeakPtr())); + + if (success) + watchers_[path] = watcher; + + BrowserThread::PostTask( + BrowserThread::UI, FROM_HERE, base::Bind(callback, success)); +} + +void GalleryWatchManager::FileWatchManager::RemoveFileWatch( + const base::FilePath& path) { + DCHECK_CURRENTLY_ON(BrowserThread::FILE); + size_t erased = watchers_.erase(path); + DCHECK_EQ(erased, 1u); +} + +base::WeakPtr +GalleryWatchManager::FileWatchManager::GetWeakPtr() { + return weak_factory_.GetWeakPtr(); +} + +void GalleryWatchManager::FileWatchManager::OnFilePathChanged( + const base::FilePath& path, + bool error) { + DCHECK_CURRENTLY_ON(BrowserThread::FILE); + if (error) + RemoveFileWatch(path); + BrowserThread::PostTask( + BrowserThread::UI, FROM_HERE, base::Bind(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 browser_context < other.browser_context || + extension_id < other.extension_id || gallery_id < other.gallery_id; +} + +GalleryWatchManager::NotificationInfo::NotificationInfo() + : delayed_notification_pending(false) { +} + +GalleryWatchManager::NotificationInfo::~NotificationInfo() { +} + +GalleryWatchManager::GalleryWatchManager() + : storage_monitor_observed_(false), 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_ && + storage_monitor::StorageMonitor::GetInstance()) { + storage_monitor::StorageMonitor::GetInstance()->RemoveObserver(this); + } + + BrowserThread::DeleteSoon( + BrowserThread::FILE, FROM_HERE, watch_manager_.release()); +} + +void GalleryWatchManager::AddObserver(BrowserContext* browser_context, + GalleryWatchManagerObserver* observer) { + DCHECK(browser_context); + DCHECK(observer); + DCHECK(!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); +} + +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 (ContainsKey(watches_, owner)) { + callback.Run(std::string()); + return; + } + + MediaGalleriesPreferences* preferences = + g_browser_process->media_file_system_registry()->GetPreferences( + Profile::FromBrowserContext(browser_context)); + + if (!ContainsKey(preferences->known_galleries(), gallery_id)) { + callback.Run(kInvalidGalleryIDError); + return; + } + + MediaGalleryPrefIdSet permitted = + preferences->GalleriesForExtension(*extension); + if (!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 (!ContainsKey(observed_preferences_, preferences)) { + observed_preferences_.insert(preferences); + preferences->AddGalleryChangeObserver(this); + } + + watches_[owner] = path; + + // Start the FilePathWatcher on |gallery_path| if necessary. + if (ContainsKey(watched_paths_, path)) { + OnFileWatchActivated(owner, path, callback, true); + } else { + base::Callback on_watch_added = + base::Bind(&GalleryWatchManager::OnFileWatchActivated, + weak_factory_.GetWeakPtr(), + owner, + path, + callback); + BrowserThread::PostTask(BrowserThread::FILE, + FROM_HERE, + base::Bind(&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); + WatchesMap::iterator 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); + + WatchesMap::iterator 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::DeactivateFileWatch(const WatchOwner& owner, + const base::FilePath& path) { + DCHECK_CURRENTLY_ON(BrowserThread::UI); + WatchedPaths::iterator 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); + BrowserThread::PostTask(BrowserThread::FILE, + FROM_HERE, + base::Bind(&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) { + WatchedPaths::iterator 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 owners = notification_info->second.owners; + for (std::set::iterator 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 (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(kMinNotificiationDelayInSeconds)) { + 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(kMinNotificiationDelayInSeconds) - + base::Time::Now(); + BrowserThread::PostDelayedTask( + BrowserThread::UI, + FROM_HERE, + base::Bind(&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::const_iterator it; + for (it = notification_info->second.owners.begin(); + it != notification_info->second.owners.end(); + ++it) { + DCHECK(ContainsKey(watches_, *it)); + if (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 (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 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 (std::set::const_iterator it = extension_ids.begin(); + it != extension_ids.end(); + ++it) { + RemoveWatch(pref->profile(), *it, pref_id); + if (ContainsKey(observers_, pref->profile())) + observers_[pref->profile()]->OnGalleryWatchDropped(*it, pref_id); + } +} + +void GalleryWatchManager::OnRemovableStorageDetached( + const storage_monitor::StorageInfo& info) { + WatchesMap::iterator 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 (ContainsKey(detached_ids, it->first.gallery_id)) { + DeactivateFileWatch(it->first, it->second); + // Post increment moves iterator to next element while deleting current. + watches_.erase(it++); + observers_[preferences->profile()]->OnGalleryWatchDropped( + it->first.extension_id, it->first.gallery_id); + } else { + ++it; + } + } +} diff --git a/chrome/browser/media_galleries/gallery_watch_manager.h b/chrome/browser/media_galleries/gallery_watch_manager.h new file mode 100644 index 000000000000..6c28cd7bc72a --- /dev/null +++ b/chrome/browser/media_galleries/gallery_watch_manager.h @@ -0,0 +1,155 @@ +// 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. + +#ifndef CHROME_BROWSER_MEDIA_GALLERIES_GALLERY_WATCH_MANAGER_H_ +#define CHROME_BROWSER_MEDIA_GALLERIES_GALLERY_WATCH_MANAGER_H_ + +#include +#include + +#include "base/basictypes.h" +#include "base/callback_forward.h" +#include "base/files/file_path.h" +#include "base/files/file_path_watcher.h" +#include "base/memory/linked_ptr.h" +#include "base/memory/scoped_ptr.h" +#include "base/memory/weak_ptr.h" +#include "base/time/time.h" +#include "chrome/browser/media_galleries/media_galleries_preferences.h" +#include "components/storage_monitor/removable_storage_observer.h" + +class GalleryWatchManagerObserver; + +namespace content { +class BrowserContext; +} + +namespace extensions { +class Extension; +} + +// The GalleryWatchManager is owned by MediaFileSystemRegistry, which is global. +// This class manages all watches on media galleries, regardless of profile. +// It tracks outstanding watch requests and creates one FilePathWatcher per +// watched directory. This class lives and is called on the UI thread. +class GalleryWatchManager + : public MediaGalleriesPreferences::GalleryChangeObserver, + public storage_monitor::RemovableStorageObserver { + public: + // On success, |error| is empty. + typedef base::Callback ResultCallback; + + GalleryWatchManager(); + virtual ~GalleryWatchManager(); + + // Add or remove observer of change events - this is the only way to + // get the result of the file watches. There can only be one observer per + // browser context. + void AddObserver(content::BrowserContext* browser_context, + GalleryWatchManagerObserver* observer); + void RemoveObserver(content::BrowserContext* browser_context); + + // Must be called when |browser_context| is shut down. + void ShutdownBrowserContext(content::BrowserContext* browser_context); + + // Add a watch for |gallery_id|. + void AddWatch(content::BrowserContext* browser_context, + const extensions::Extension* extension, + MediaGalleryPrefId gallery_id, + const ResultCallback& callback); + + // Remove the watch for |gallery_id|. It is valid to call this method on + // non-existent watches. + void RemoveWatch(content::BrowserContext* browser_context, + const std::string& extension_id, + MediaGalleryPrefId gallery_id); + + // Remove all the watches for |extension_id|. + void RemoveAllWatches(content::BrowserContext* browser_context, + const std::string& extension_id); + + // Return the set of galleries being watched for |extension_id|. + MediaGalleryPrefIdSet GetWatchSet(content::BrowserContext* browser_context, + const std::string& extension_id); + + private: + class FileWatchManager; + + // Used to track the gallery watches connected to a specific path. + struct WatchOwner { + WatchOwner(content::BrowserContext* browser_context, + const std::string& extension_id, + MediaGalleryPrefId gallery_id); + + content::BrowserContext* browser_context; + const std::string extension_id; + MediaGalleryPrefId gallery_id; + + // Needed to support storage in STL set, as well as usage as map key. + bool operator<(const WatchOwner& other) const; + }; + + struct NotificationInfo { + NotificationInfo(); + ~NotificationInfo(); + + std::set owners; + base::Time last_notify_time; + bool delayed_notification_pending; + }; + + typedef std::map WatchesMap; + typedef std::map WatchedPaths; + typedef std::map + ObserverMap; + + // Stop the FilePathWatcher for |path|. Updates |watched_paths_| but not + // |registered_watches_|. + void DeactivateFileWatch(const WatchOwner& owner, const base::FilePath& path); + + // Called by FilePathWatcher on the UI thread to respond to a request to + // watch the path. + void OnFileWatchActivated(const WatchOwner& owner, + const base::FilePath& path, + const ResultCallback& callback, + bool success); + + // Called by FilePathWatcher on the UI thread on a change event for |path|. + void OnFilePathChanged(const base::FilePath& path, bool error); + + // MediaGalleriesPreferences::GalleryChangeObserver implementation. + virtual void OnPermissionRemoved(MediaGalleriesPreferences* pref, + const std::string& extension_id, + MediaGalleryPrefId pref_id) OVERRIDE; + virtual void OnGalleryRemoved(MediaGalleriesPreferences* pref, + MediaGalleryPrefId pref_id) OVERRIDE; + + // storage_monitor::RemovableStorageObserver implementation. + virtual void OnRemovableStorageDetached( + const storage_monitor::StorageInfo& info) OVERRIDE; + + // True if the we are already observing the storage monitor. + bool storage_monitor_observed_; + + // MediaGalleriesPreferences we are currently observing. + std::set observed_preferences_; + + // All registered watches, keyed by WatchOwner. + WatchesMap watches_; + + // Reverse mapping of watched paths to the set of owning WatchOwners. + WatchedPaths watched_paths_; + + // Things that want to hear about gallery changes. + ObserverMap observers_; + + // Helper that does the watches on the FILE thread. + scoped_ptr watch_manager_; + + base::WeakPtrFactory weak_factory_; + + DISALLOW_COPY_AND_ASSIGN(GalleryWatchManager); +}; + +#endif // CHROME_BROWSER_MEDIA_GALLERIES_GALLERY_WATCH_MANAGER_H_ diff --git a/chrome/browser/media_galleries/gallery_watch_manager_observer.h b/chrome/browser/media_galleries/gallery_watch_manager_observer.h new file mode 100644 index 000000000000..a4671e5e35ad --- /dev/null +++ b/chrome/browser/media_galleries/gallery_watch_manager_observer.h @@ -0,0 +1,25 @@ +// 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. + +#ifndef CHROME_BROWSER_MEDIA_GALLERIES_GALLERY_WATCH_MANAGER_OBSERVER_H_ +#define CHROME_BROWSER_MEDIA_GALLERIES_GALLERY_WATCH_MANAGER_OBSERVER_H_ + +#include "base/basictypes.h" +#include "chrome/browser/media_galleries/media_galleries_preferences.h" + +class GalleryWatchManagerObserver { + public: + virtual ~GalleryWatchManagerObserver() {} + + // Called when the gallery contents change. + virtual void OnGalleryChanged(const std::string& extension_id, + MediaGalleryPrefId gallery_id) = 0; + + // Called when the gallery watch is dropped without the caller requesting it, + // because the permission was revoked, device was detached, etc. + virtual void OnGalleryWatchDropped(const std::string& extension_id, + MediaGalleryPrefId gallery_id) = 0; +}; + +#endif // CHROME_BROWSER_MEDIA_GALLERIES_GALLERY_WATCH_MANAGER_OBSERVER_H_ diff --git a/chrome/browser/media_galleries/gallery_watch_manager_unittest.cc b/chrome/browser/media_galleries/gallery_watch_manager_unittest.cc new file mode 100644 index 000000000000..51eb6c0b6efc --- /dev/null +++ b/chrome/browser/media_galleries/gallery_watch_manager_unittest.cc @@ -0,0 +1,282 @@ +// 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 "base/bind.h" +#include "base/command_line.h" +#include "base/file_util.h" +#include "base/files/file_path.h" +#include "base/files/scoped_temp_dir.h" +#include "base/memory/scoped_ptr.h" +#include "base/run_loop.h" +#include "base/test/scoped_path_override.h" +#include "chrome/browser/extensions/test_extension_system.h" +#include "chrome/browser/media_galleries/gallery_watch_manager.h" +#include "chrome/browser/media_galleries/gallery_watch_manager_observer.h" +#include "chrome/browser/media_galleries/media_galleries_preferences.h" +#include "chrome/browser/media_galleries/media_galleries_preferences_factory.h" +#include "chrome/browser/media_galleries/media_galleries_test_util.h" +#include "chrome/test/base/testing_profile.h" +#include "components/storage_monitor/test_storage_monitor.h" +#include "content/public/test/test_browser_thread_bundle.h" +#include "extensions/browser/extension_system.h" +#include "extensions/common/extension.h" +#include "extensions/common/permissions/media_galleries_permission.h" +#include "testing/gtest/include/gtest/gtest.h" + +#if defined(OS_CHROMEOS) +#include "chrome/browser/chromeos/login/users/user_manager.h" +#include "chrome/browser/chromeos/settings/cros_settings.h" +#include "chrome/browser/chromeos/settings/device_settings_service.h" +#endif + +namespace component_updater { + +namespace { + +void ConfirmWatch(base::RunLoop* loop, const std::string& error) { + EXPECT_TRUE(error.empty()); + loop->Quit(); +} + +} // namespace + +class GalleryWatchManagerTest : public GalleryWatchManagerObserver, + public testing::Test { + public: + GalleryWatchManagerTest() + : thread_bundle_(content::TestBrowserThreadBundle::IO_MAINLOOP), + profile_(new TestingProfile()), + gallery_prefs_(NULL), + expect_gallery_changed_(false), + expect_gallery_watch_dropped_(false), + pending_loop_(NULL) {} + + virtual ~GalleryWatchManagerTest() {} + + virtual void SetUp() OVERRIDE { + ASSERT_TRUE(storage_monitor::TestStorageMonitor::CreateAndInstall()); + + extensions::TestExtensionSystem* extension_system( + static_cast( + extensions::ExtensionSystem::Get(profile_.get()))); + extension_system->CreateExtensionService( + base::CommandLine::ForCurrentProcess(), base::FilePath(), false); + + gallery_prefs_ = + MediaGalleriesPreferencesFactory::GetForProfile(profile_.get()); + base::RunLoop loop; + gallery_prefs_->EnsureInitialized(loop.QuitClosure()); + loop.Run(); + + std::vector read_permissions; + read_permissions.push_back( + extensions::MediaGalleriesPermission::kReadPermission); + extension_ = AddMediaGalleriesApp("read", read_permissions, profile_.get()); + + manager_.reset(new GalleryWatchManager); + manager_->AddObserver(profile_.get(), this); + } + + virtual void TearDown() OVERRIDE { + manager_->RemoveObserver(profile_.get()); + manager_.reset(); + storage_monitor::TestStorageMonitor::Destroy(); + } + + protected: + // Create the specified path, and add it to preferences as a gallery. + MediaGalleryPrefId AddGallery(const base::FilePath& path) { + MediaGalleryPrefInfo gallery_info; + EXPECT_FALSE(gallery_prefs_->LookUpGalleryByPath(path, &gallery_info)); + MediaGalleryPrefId id = + gallery_prefs_->AddGallery(gallery_info.device_id, + gallery_info.path, + MediaGalleryPrefInfo::kUserAdded, + gallery_info.volume_label, + gallery_info.vendor_name, + gallery_info.model_name, + gallery_info.total_size_in_bytes, + gallery_info.last_attach_time, + 0, + 0, + 0); + EXPECT_NE(kInvalidMediaGalleryPrefId, id); + + EXPECT_TRUE(gallery_prefs_->SetGalleryPermissionForExtension( + *extension_, id, true)); + return id; + } + + TestingProfile* profile() { return profile_.get(); } + + GalleryWatchManager* manager() { return manager_.get(); } + + extensions::Extension* extension() { return extension_.get(); } + + MediaGalleriesPreferences* gallery_prefs() { return gallery_prefs_; } + + void AddAndConfirmWatch(MediaGalleryPrefId gallery_id) { + base::RunLoop loop; + manager()->AddWatch(profile(), + extension(), + gallery_id, + base::Bind(&ConfirmWatch, base::Unretained(&loop))); + loop.Run(); + } + + void ExpectGalleryChanged(base::RunLoop* loop) { + expect_gallery_changed_ = true; + pending_loop_ = loop; + } + + void ExpectGalleryWatchDropped(base::RunLoop* loop) { + expect_gallery_watch_dropped_ = true; + pending_loop_ = loop; + } + + private: + // GalleryWatchManagerObserver implementation. + virtual void OnGalleryChanged(const std::string& extension_id, + MediaGalleryPrefId gallery_id) OVERRIDE { + EXPECT_TRUE(expect_gallery_changed_); + pending_loop_->Quit(); + } + + virtual void OnGalleryWatchDropped(const std::string& extension_id, + MediaGalleryPrefId gallery_id) OVERRIDE { + EXPECT_TRUE(expect_gallery_watch_dropped_); + pending_loop_->Quit(); + } + + scoped_ptr manager_; + + // Needed for extension service & friends to work. + content::TestBrowserThreadBundle thread_bundle_; + + scoped_refptr extension_; + + EnsureMediaDirectoriesExists mock_gallery_locations_; + +#if defined(OS_CHROMEOS) + chromeos::ScopedTestDeviceSettingsService test_device_settings_service_; + chromeos::ScopedTestCrosSettings test_cros_settings_; + chromeos::ScopedTestUserManager test_user_manager_; +#endif + + storage_monitor::TestStorageMonitor monitor_; + scoped_ptr profile_; + MediaGalleriesPreferences* gallery_prefs_; + + bool expect_gallery_changed_; + bool expect_gallery_watch_dropped_; + base::RunLoop* pending_loop_; + + DISALLOW_COPY_AND_ASSIGN(GalleryWatchManagerTest); +}; + +TEST_F(GalleryWatchManagerTest, AddAndRemoveTwoWatches) { + EXPECT_TRUE(manager()->GetWatchSet(profile(), extension()->id()).empty()); + + base::ScopedTempDir temp1; + ASSERT_TRUE(temp1.CreateUniqueTempDir()); + MediaGalleryPrefId id1 = AddGallery(temp1.path()); + + base::ScopedTempDir temp2; + ASSERT_TRUE(temp2.CreateUniqueTempDir()); + MediaGalleryPrefId id2 = AddGallery(temp2.path()); + + // Add first watch and test it was added correctly. + AddAndConfirmWatch(id1); + MediaGalleryPrefIdSet set1 = + manager()->GetWatchSet(profile(), extension()->id()); + EXPECT_EQ(1u, set1.size()); + EXPECT_TRUE(ContainsKey(set1, id1)); + + // Test that the second watch was added correctly too. + AddAndConfirmWatch(id2); + MediaGalleryPrefIdSet set2 = + manager()->GetWatchSet(profile(), extension()->id()); + EXPECT_EQ(2u, set2.size()); + EXPECT_TRUE(ContainsKey(set2, id1)); + EXPECT_TRUE(ContainsKey(set2, id2)); + + // Remove first watch and test that the second is still in there. + manager()->RemoveWatch(profile(), extension()->id(), id1); + MediaGalleryPrefIdSet set3 = + manager()->GetWatchSet(profile(), extension()->id()); + EXPECT_EQ(1u, set3.size()); + EXPECT_TRUE(ContainsKey(set3, id2)); + + // Try removing the first watch again and test that it has no effect. + manager()->RemoveWatch(profile(), extension()->id(), id1); + EXPECT_EQ(1u, manager()->GetWatchSet(profile(), extension()->id()).size()); + + // Remove the second watch and test that the new watch set is empty. + manager()->RemoveWatch(profile(), extension()->id(), id2); + EXPECT_TRUE(manager()->GetWatchSet(profile(), extension()->id()).empty()); +} + +TEST_F(GalleryWatchManagerTest, RemoveAllWatches) { + base::ScopedTempDir temp1; + ASSERT_TRUE(temp1.CreateUniqueTempDir()); + MediaGalleryPrefId id1 = AddGallery(temp1.path()); + + base::ScopedTempDir temp2; + ASSERT_TRUE(temp2.CreateUniqueTempDir()); + MediaGalleryPrefId id2 = AddGallery(temp2.path()); + + // Add watches. + AddAndConfirmWatch(id1); + AddAndConfirmWatch(id2); + + EXPECT_EQ(2u, manager()->GetWatchSet(profile(), extension()->id()).size()); + + // RemoveAllWatches using a different extension ID and verify watches remain. + manager()->RemoveAllWatches(profile(), "OtherExtensionId"); + EXPECT_EQ(2u, manager()->GetWatchSet(profile(), extension()->id()).size()); + + // RemoveAllWatches using the correct extension ID and verify watches gone. + + manager()->RemoveAllWatches(profile(), extension()->id()); + EXPECT_TRUE(manager()->GetWatchSet(profile(), extension()->id()).empty()); +} + +TEST_F(GalleryWatchManagerTest, DropWatchOnGalleryRemoved) { + base::ScopedTempDir temp_dir; + ASSERT_TRUE(temp_dir.CreateUniqueTempDir()); + MediaGalleryPrefId id = AddGallery(temp_dir.path()); + AddAndConfirmWatch(id); + + base::RunLoop success_loop; + ExpectGalleryWatchDropped(&success_loop); + gallery_prefs()->EraseGalleryById(id); + success_loop.Run(); +} + +TEST_F(GalleryWatchManagerTest, DropWatchOnGalleryPermissionRevoked) { + base::ScopedTempDir temp_dir; + ASSERT_TRUE(temp_dir.CreateUniqueTempDir()); + MediaGalleryPrefId id = AddGallery(temp_dir.path()); + AddAndConfirmWatch(id); + + base::RunLoop success_loop; + ExpectGalleryWatchDropped(&success_loop); + gallery_prefs()->SetGalleryPermissionForExtension(*extension(), id, false); + success_loop.Run(); +} + +TEST_F(GalleryWatchManagerTest, TestWatchOperation) { + base::ScopedTempDir temp_dir; + ASSERT_TRUE(temp_dir.CreateUniqueTempDir()); + MediaGalleryPrefId id = AddGallery(temp_dir.path()); + AddAndConfirmWatch(id); + + base::RunLoop success_loop; + ExpectGalleryChanged(&success_loop); + ASSERT_EQ( + 4, base::WriteFile(temp_dir.path().AppendASCII("fake file"), "blah", 4)); + success_loop.Run(); +} + +} // namespace component_updater diff --git a/chrome/browser/media_galleries/media_galleries_preferences.h b/chrome/browser/media_galleries/media_galleries_preferences.h index b4664c9f9d4b..283f9c2cda92 100644 --- a/chrome/browser/media_galleries/media_galleries_preferences.h +++ b/chrome/browser/media_galleries/media_galleries_preferences.h @@ -198,6 +198,10 @@ class MediaGalleriesPreferences // |gallery_id|. Returns an empty file path if the |gallery_id| is invalid. // Set |include_unpermitted_galleries| to true to get the file path of the // gallery to which this |extension| has no access permission. + // + // TODO(tommycli): Remove this function after Media Galleries API Private + // is transitioned over to public. Also, the body of the function wrong. + // It just returns the absolute path to the device, not the gallery. base::FilePath LookUpGalleryPathForExtension( MediaGalleryPrefId gallery_id, const extensions::Extension* extension, diff --git a/chrome/chrome_browser.gypi b/chrome/chrome_browser.gypi index b56373444cb7..c631ae53035b 100644 --- a/chrome/chrome_browser.gypi +++ b/chrome/chrome_browser.gypi @@ -767,6 +767,9 @@ # TODO(brettw) should this go with the webrtc sources? 'browser/media/webrtc_log_list.cc', 'browser/media/webrtc_log_list.h', + 'browser/media_galleries/gallery_watch_manager.cc', + 'browser/media_galleries/gallery_watch_manager.h', + 'browser/media_galleries/gallery_watch_manager_observer.h', 'browser/memory_details.cc', 'browser/memory_details.h', 'browser/memory_details_android.cc', diff --git a/chrome/chrome_tests_unit.gypi b/chrome/chrome_tests_unit.gypi index c87d6bcd4d99..6197000375f5 100644 --- a/chrome/chrome_tests_unit.gypi +++ b/chrome/chrome_tests_unit.gypi @@ -1087,9 +1087,9 @@ 'browser/media/webrtc_rtp_dump_handler_unittest.cc', 'browser/media/webrtc_rtp_dump_writer_unittest.cc', 'browser/media_galleries/fileapi/native_media_file_util_unittest.cc', + 'browser/media_galleries/gallery_watch_manager_unittest.cc', 'browser/media_galleries/linux/mtp_device_object_enumerator_unittest.cc', 'browser/media_galleries/mac/mtp_device_delegate_impl_mac_unittest.mm', - 'browser/media_galleries/win/mtp_device_delegate_impl_win_unittest.cc', 'browser/media_galleries/media_file_system_registry_unittest.cc', 'browser/media_galleries/media_folder_finder_unittest.cc', 'browser/media_galleries/media_galleries_dialog_controller_mock.cc', @@ -1101,6 +1101,7 @@ 'browser/media_galleries/media_galleries_preferences_unittest.cc', 'browser/media_galleries/media_galleries_scan_result_controller_unittest.cc', 'browser/media_galleries/media_scan_manager_unittest.cc', + 'browser/media_galleries/win/mtp_device_delegate_impl_win_unittest.cc', 'browser/media_galleries/win/mtp_device_object_enumerator_unittest.cc', 'browser/metrics/chrome_metrics_service_accessor_unittest.cc', 'browser/metrics/cloned_install_detector_unittest.cc', -- 2.11.4.GIT