diff options
author | Torne (Richard Coles) <torne@google.com> | 2013-05-29 14:40:03 +0100 |
---|---|---|
committer | Torne (Richard Coles) <torne@google.com> | 2013-05-29 14:40:03 +0100 |
commit | 90dce4d38c5ff5333bea97d859d4e484e27edf0c (patch) | |
tree | 9c51c7dd97d24b15befa97a3482c51851e5383a1 /apps | |
parent | 1515035f5917d10d363b0888a3615d581ad8b83f (diff) | |
download | chromium_org-90dce4d38c5ff5333bea97d859d4e484e27edf0c.tar.gz |
Merge from Chromium at DEPS revision r202854
This commit was generated by merge_to_master.py.
Change-Id: Idca323f71ef844a9e04f454d4f070b1e398f2deb
Diffstat (limited to 'apps')
30 files changed, 2088 insertions, 238 deletions
@@ -1,6 +1,7 @@ include_rules = [ "+base", "+content", + "+components/browser_context_keyed_service", "+ui", "+win8", # Temporary allowed includes. diff --git a/apps/app_restore_service.cc b/apps/app_restore_service.cc index f8932fbc01..88ac811840 100644 --- a/apps/app_restore_service.cc +++ b/apps/app_restore_service.cc @@ -4,13 +4,15 @@ #include "apps/app_restore_service.h" +#include "apps/saved_files_service.h" #include "chrome/browser/extensions/api/app_runtime/app_runtime_api.h" -#include "chrome/browser/extensions/api/file_handlers/app_file_handler_util.h" #include "chrome/browser/extensions/event_router.h" #include "chrome/browser/extensions/extension_host.h" +#include "chrome/browser/extensions/extension_prefs.h" #include "chrome/browser/extensions/extension_service.h" #include "chrome/browser/extensions/extension_system.h" #include "chrome/browser/extensions/platform_app_launcher.h" +#include "chrome/browser/ui/extensions/shell_window.h" #include "chrome/common/chrome_notification_types.h" #include "chrome/common/extensions/extension.h" #include "chrome/common/extensions/extension_set.h" @@ -55,6 +57,7 @@ AppRestoreService::AppRestoreService(Profile* profile) registrar_.Add( this, chrome::NOTIFICATION_APP_TERMINATING, content::NotificationService::AllSources()); + StartObservingShellWindows(); } void AppRestoreService::HandleStartup(bool should_restore_apps) { @@ -67,13 +70,16 @@ void AppRestoreService::HandleStartup(bool should_restore_apps) { it != extensions->end(); ++it) { const Extension* extension = *it; if (extension_prefs->IsExtensionRunning(extension->id())) { - std::vector<SavedFileEntry> file_entries; - extensions::app_file_handler_util::GetSavedFileEntries(extension_prefs, - extension->id(), - &file_entries); RecordAppStop(extension->id()); - if (should_restore_apps) - RestoreApp(*it, file_entries); + // If we are not restoring apps (e.g., because it is a clean restart), and + // the app does not have retain permission, explicitly clear the retained + // entries queue. + if (should_restore_apps) { + RestoreApp(*it); + } else { + SavedFilesService::Get(profile_)->ClearQueueIfNoRetainPermission( + extension); + } } } } @@ -102,11 +108,28 @@ void AppRestoreService::Observe(int type, // Stop listening to NOTIFICATION_EXTENSION_HOST_DESTROYED in particular // as all extension hosts will be destroyed as a result of shutdown. registrar_.RemoveAll(); + // Stop listening to the ShellWindowRegistry for window closes, because + // all windows will be closed as a result of shutdown. + StopObservingShellWindows(); break; } } } +void AppRestoreService::OnShellWindowAdded(ShellWindow* shell_window) { + RecordIfAppHasWindows(shell_window->extension_id()); +} + +void AppRestoreService::OnShellWindowIconChanged(ShellWindow* shell_window) { +} + +void AppRestoreService::OnShellWindowRemoved(ShellWindow* shell_window) { + RecordIfAppHasWindows(shell_window->extension_id()); +} + +void AppRestoreService::Shutdown() { + StopObservingShellWindows(); +} void AppRestoreService::RecordAppStart(const std::string& extension_id) { ExtensionPrefs* extension_prefs = @@ -118,16 +141,46 @@ void AppRestoreService::RecordAppStop(const std::string& extension_id) { ExtensionPrefs* extension_prefs = ExtensionSystem::Get(profile_)->extension_service()->extension_prefs(); extension_prefs->SetExtensionRunning(extension_id, false); - extensions::app_file_handler_util::ClearSavedFileEntries( - extension_prefs, extension_id); } -void AppRestoreService::RestoreApp( - const Extension* extension, - const std::vector<SavedFileEntry>& file_entries) { - extensions::RestartPlatformAppWithFileEntries(profile_, - extension, - file_entries); +void AppRestoreService::RecordIfAppHasWindows( + const std::string& id) { + ExtensionService* extension_service = + ExtensionSystem::Get(profile_)->extension_service(); + ExtensionPrefs* extension_prefs = extension_service->extension_prefs(); + + // If the extension isn't running then we will already have recorded whether + // it had windows or not. + if (!extension_prefs->IsExtensionRunning(id)) + return; + + extensions::ShellWindowRegistry* shell_window_registry = + extensions::ShellWindowRegistry::Factory::GetForProfile( + profile_, false /* create */); + if (!shell_window_registry) + return; + bool has_windows = !shell_window_registry->GetShellWindowsForApp(id).empty(); + extension_prefs->SetHasWindows(id, has_windows); +} + +void AppRestoreService::RestoreApp(const Extension* extension) { + extensions::RestartPlatformApp(profile_, extension); +} + +void AppRestoreService::StartObservingShellWindows() { + extensions::ShellWindowRegistry* shell_window_registry = + extensions::ShellWindowRegistry::Factory::GetForProfile( + profile_, false /* create */); + if (shell_window_registry) + shell_window_registry->AddObserver(this); +} + +void AppRestoreService::StopObservingShellWindows() { + extensions::ShellWindowRegistry* shell_window_registry = + extensions::ShellWindowRegistry::Factory::GetForProfile( + profile_, false /* create */); + if (shell_window_registry) + shell_window_registry->RemoveObserver(this); } } // namespace apps diff --git a/apps/app_restore_service.h b/apps/app_restore_service.h index f27c83cade..65d3de2e58 100644 --- a/apps/app_restore_service.h +++ b/apps/app_restore_service.h @@ -8,28 +8,23 @@ #include <string> #include <vector> -#include "chrome/browser/profiles/profile_keyed_service.h" +#include "chrome/browser/extensions/shell_window_registry.h" +#include "components/browser_context_keyed_service/browser_context_keyed_service.h" #include "content/public/browser/notification_observer.h" #include "content/public/browser/notification_registrar.h" namespace extensions { class Extension; - -namespace app_file_handler_util { -struct SavedFileEntry; -} - } class Profile; -using extensions::app_file_handler_util::SavedFileEntry; - namespace apps { // Tracks what apps need to be restarted when the browser restarts. -class AppRestoreService : public ProfileKeyedService, - public content::NotificationObserver { +class AppRestoreService : public BrowserContextKeyedService, + public content::NotificationObserver, + public extensions::ShellWindowRegistry::Observer { public: // Returns true if apps should be restored on the current platform, given // whether this new browser process launched due to a restart. @@ -47,11 +42,22 @@ class AppRestoreService : public ProfileKeyedService, const content::NotificationSource& source, const content::NotificationDetails& details) OVERRIDE; + // extensions::ShellWindowRegistry::Observer. + virtual void OnShellWindowAdded(ShellWindow* shell_window) OVERRIDE; + virtual void OnShellWindowIconChanged(ShellWindow* shell_window) OVERRIDE; + virtual void OnShellWindowRemoved(ShellWindow* shell_window) OVERRIDE; + + // BrowserContextKeyedService. + virtual void Shutdown() OVERRIDE; + void RecordAppStart(const std::string& extension_id); void RecordAppStop(const std::string& extension_id); - void RestoreApp( - const extensions::Extension* extension, - const std::vector<SavedFileEntry>& file_entries); + void RecordIfAppHasWindows(const std::string& id); + + void RestoreApp(const extensions::Extension* extension); + + void StartObservingShellWindows(); + void StopObservingShellWindows(); content::NotificationRegistrar registrar_; Profile* profile_; diff --git a/apps/app_restore_service_browsertest.cc b/apps/app_restore_service_browsertest.cc index a335617e75..cf832fedf6 100644 --- a/apps/app_restore_service_browsertest.cc +++ b/apps/app_restore_service_browsertest.cc @@ -4,17 +4,14 @@ #include "apps/app_restore_service.h" #include "apps/app_restore_service_factory.h" -#include "chrome/browser/extensions/api/file_handlers/app_file_handler_util.h" +#include "apps/saved_files_service.h" #include "chrome/browser/extensions/api/file_system/file_system_api.h" #include "chrome/browser/extensions/extension_prefs.h" -#include "chrome/browser/extensions/extension_service.h" -#include "chrome/browser/extensions/extension_system.h" #include "chrome/browser/extensions/extension_test_message_listener.h" #include "chrome/browser/extensions/platform_app_browsertest_util.h" #include "chrome/common/extensions/extension.h" #include "content/public/test/test_utils.h" -using extensions::app_file_handler_util::SavedFileEntry; using extensions::Extension; using extensions::ExtensionPrefs; using extensions::ExtensionSystem; @@ -35,9 +32,7 @@ IN_PROC_BROWSER_TEST_F(PlatformAppBrowserTest, RunningAppsAreRecorded) { const Extension* extension = LoadExtension( test_data_dir_.AppendASCII("platform_apps/restart_test")); ASSERT_TRUE(extension); - ExtensionService* extension_service = - ExtensionSystem::Get(browser()->profile())->extension_service(); - ExtensionPrefs* extension_prefs = extension_service->extension_prefs(); + ExtensionPrefs* extension_prefs = ExtensionPrefs::Get(browser()->profile()); // App is running. ASSERT_TRUE(extension_prefs->IsExtensionRunning(extension->id())); @@ -82,21 +77,15 @@ IN_PROC_BROWSER_TEST_F(PlatformAppBrowserTest, FileAccessIsSavedToPrefs) { ASSERT_TRUE(extension); file_written_listener.WaitUntilSatisfied(); - ExtensionPrefs* extension_prefs = - ExtensionSystem::Get(browser()->profile())->extension_prefs(); + SavedFilesService* saved_files_service = SavedFilesService::Get(profile()); - // Record the file entries in prefs because when the app gets suspended it - // will have them all cleared. - std::vector<SavedFileEntry> file_entries; - extensions::app_file_handler_util::GetSavedFileEntries( - extension_prefs, extension->id(), &file_entries); + std::vector<SavedFileEntry> file_entries = + saved_files_service->GetAllFileEntries(extension->id()); // One for the read-only file entry and one for the writable file entry. ASSERT_EQ(2u, file_entries.size()); extension_suspended.Wait(); - file_entries.clear(); - extensions::app_file_handler_util::GetSavedFileEntries( - extension_prefs, extension->id(), &file_entries); + file_entries = saved_files_service->GetAllFileEntries(extension->id()); // File entries should be cleared when the extension is suspended. ASSERT_TRUE(file_entries.empty()); } @@ -127,13 +116,10 @@ IN_PROC_BROWSER_TEST_F(PlatformAppBrowserTest, FileAccessIsRestored) { file_written_listener.WaitUntilSatisfied(); ExtensionPrefs* extension_prefs = - ExtensionSystem::Get(browser()->profile())->extension_prefs(); - // Record the file entries in prefs because when the app gets suspended it - // will have them all cleared. - std::vector<SavedFileEntry> file_entries; - extensions::app_file_handler_util::GetSavedFileEntries(extension_prefs, - extension->id(), - &file_entries); + ExtensionPrefs::Get(browser()->profile()); + SavedFilesService* saved_files_service = SavedFilesService::Get(profile()); + std::vector<SavedFileEntry> file_entries = + saved_files_service->GetAllFileEntries(extension->id()); extension_suspended.Wait(); // Simulate a restart by populating the preferences as if the browser didn't @@ -141,8 +127,8 @@ IN_PROC_BROWSER_TEST_F(PlatformAppBrowserTest, FileAccessIsRestored) { extension_prefs->SetExtensionRunning(extension->id(), true); for (std::vector<SavedFileEntry>::const_iterator it = file_entries.begin(); it != file_entries.end(); ++it) { - extensions::app_file_handler_util::AddSavedFileEntry( - extension_prefs, extension->id(), it->id, it->path, it->writable); + saved_files_service->RegisterFileEntry( + extension->id(), it->id, it->path, it->writable); } apps::AppRestoreServiceFactory::GetForProfile(browser()->profile())-> diff --git a/apps/app_restore_service_factory.cc b/apps/app_restore_service_factory.cc index 71dd80b582..ffd31a701c 100644 --- a/apps/app_restore_service_factory.cc +++ b/apps/app_restore_service_factory.cc @@ -5,22 +5,16 @@ #include "apps/app_restore_service_factory.h" #include "apps/app_restore_service.h" +#include "chrome/browser/extensions/shell_window_registry.h" #include "chrome/browser/profiles/profile.h" -#include "chrome/browser/profiles/profile_dependency_manager.h" +#include "components/browser_context_keyed_service/browser_context_dependency_manager.h" namespace apps { // static AppRestoreService* AppRestoreServiceFactory::GetForProfile(Profile* profile) { return static_cast<AppRestoreService*>( - GetInstance()->GetServiceForProfile(profile, true)); -} - -// static -void AppRestoreServiceFactory::ResetForProfile(Profile* profile) { - AppRestoreServiceFactory* factory = GetInstance(); - factory->ProfileShutdown(profile); - factory->ProfileDestroyed(profile); + GetInstance()->GetServiceForBrowserContext(profile, true)); } AppRestoreServiceFactory* AppRestoreServiceFactory::GetInstance() { @@ -28,19 +22,21 @@ AppRestoreServiceFactory* AppRestoreServiceFactory::GetInstance() { } AppRestoreServiceFactory::AppRestoreServiceFactory() - : ProfileKeyedServiceFactory("AppRestoreService", - ProfileDependencyManager::GetInstance()) { + : BrowserContextKeyedServiceFactory( + "AppRestoreService", + BrowserContextDependencyManager::GetInstance()) { + DependsOn(extensions::ShellWindowRegistry::Factory::GetInstance()); } AppRestoreServiceFactory::~AppRestoreServiceFactory() { } -ProfileKeyedService* AppRestoreServiceFactory::BuildServiceInstanceFor( +BrowserContextKeyedService* AppRestoreServiceFactory::BuildServiceInstanceFor( content::BrowserContext* profile) const { return new AppRestoreService(static_cast<Profile*>(profile)); } -bool AppRestoreServiceFactory::ServiceIsCreatedWithProfile() const { +bool AppRestoreServiceFactory::ServiceIsCreatedWithBrowserContext() const { return true; } diff --git a/apps/app_restore_service_factory.h b/apps/app_restore_service_factory.h index b84c585a64..1c0d438905 100644 --- a/apps/app_restore_service_factory.h +++ b/apps/app_restore_service_factory.h @@ -6,7 +6,7 @@ #define APPS_APP_RESTORE_SERVICE_FACTORY_H_ #include "base/memory/singleton.h" -#include "chrome/browser/profiles/profile_keyed_service_factory.h" +#include "components/browser_context_keyed_service/browser_context_keyed_service_factory.h" class Profile; @@ -17,12 +17,10 @@ class AppRestoreService; // Singleton that owns all AppRestoreServices and associates them with // Profiles. Listens for the Profile's destruction notification and cleans up // the associated AppRestoreService. -class AppRestoreServiceFactory : public ProfileKeyedServiceFactory { +class AppRestoreServiceFactory : public BrowserContextKeyedServiceFactory { public: static AppRestoreService* GetForProfile(Profile* profile); - static void ResetForProfile(Profile* profile); - static AppRestoreServiceFactory* GetInstance(); private: @@ -31,10 +29,10 @@ class AppRestoreServiceFactory : public ProfileKeyedServiceFactory { AppRestoreServiceFactory(); virtual ~AppRestoreServiceFactory(); - // ProfileKeyedServiceFactory: - virtual ProfileKeyedService* BuildServiceInstanceFor( + // BrowserContextKeyedServiceFactory: + virtual BrowserContextKeyedService* BuildServiceInstanceFor( content::BrowserContext* profile) const OVERRIDE; - virtual bool ServiceIsCreatedWithProfile() const OVERRIDE; + virtual bool ServiceIsCreatedWithBrowserContext() const OVERRIDE; }; } // namespace apps diff --git a/apps/app_shim/OWNERS b/apps/app_shim/OWNERS index c046a3267d..13c6f49f97 100644 --- a/apps/app_shim/OWNERS +++ b/apps/app_shim/OWNERS @@ -1 +1 @@ -jeremya@chromium.org +tapted@chromium.org diff --git a/apps/app_shim/app_shim_handler_mac.cc b/apps/app_shim/app_shim_handler_mac.cc index 18cca92426..6076557710 100644 --- a/apps/app_shim/app_shim_handler_mac.cc +++ b/apps/app_shim/app_shim_handler_mac.cc @@ -25,7 +25,7 @@ class AppShimHandlerRegistry { if (it != handlers_.end()) return it->second; - return NULL; + return default_handler_; } bool SetForAppMode(const std::string& app_mode_id, AppShimHandler* handler) { @@ -36,14 +36,20 @@ class AppShimHandlerRegistry { return inserted_or_removed; } + void SetDefaultHandler(AppShimHandler* handler) { + DCHECK_NE(default_handler_ == NULL, handler == NULL); + default_handler_ = handler; + } + private: friend struct DefaultSingletonTraits<AppShimHandlerRegistry>; typedef std::map<std::string, AppShimHandler*> HandlerMap; - AppShimHandlerRegistry() {} + AppShimHandlerRegistry() : default_handler_(NULL) {} ~AppShimHandlerRegistry() {} HandlerMap handlers_; + AppShimHandler* default_handler_; DISALLOW_COPY_AND_ASSIGN(AppShimHandlerRegistry); }; @@ -67,4 +73,9 @@ AppShimHandler* AppShimHandler::GetForAppMode(const std::string& app_mode_id) { return AppShimHandlerRegistry::GetInstance()->GetForAppMode(app_mode_id); } +// static +void AppShimHandler::SetDefaultHandler(AppShimHandler* handler) { + AppShimHandlerRegistry::GetInstance()->SetDefaultHandler(handler); +} + } // namespace apps diff --git a/apps/app_shim/app_shim_handler_mac.h b/apps/app_shim/app_shim_handler_mac.h index 6a694c0a0a..507fe5f841 100644 --- a/apps/app_shim/app_shim_handler_mac.h +++ b/apps/app_shim/app_shim_handler_mac.h @@ -7,6 +7,8 @@ #include <string> +class Profile; + namespace apps { // Registrar, and interface for services that can handle interactions with OSX @@ -18,6 +20,10 @@ class AppShimHandler { // Invoked when the app is closed in the browser process. virtual void OnAppClosed() = 0; + // Allows the handler to determine which app this host corresponds to. + virtual Profile* GetProfile() const = 0; + virtual std::string GetAppId() const = 0; + protected: virtual ~Host() {} }; @@ -29,10 +35,15 @@ class AppShimHandler { // Remove a handler for an |app_mode_id|. static void RemoveHandler(const std::string& app_mode_id); - // Returns the handler registered for the given |app_mode_id|, or NULL if none - // is registered. + // Returns the handler registered for the given |app_mode_id|. If there is + // none registered, it returns the default handler or NULL if there is no + // default handler. static AppShimHandler* GetForAppMode(const std::string& app_mode_id); + // Sets the default handler to return when there is no app-specific handler. + // Setting this to NULL removes the default handler. + static void SetDefaultHandler(AppShimHandler* handler); + // Invoked by the shim host when the shim process is launched. The handler // must return true if successful, or false to indicate back to the shim // process that it should close. diff --git a/apps/app_shim/app_shim_host_mac.cc b/apps/app_shim/app_shim_host_mac.cc index bfe28f9ac4..ad63e93038 100644 --- a/apps/app_shim/app_shim_host_mac.cc +++ b/apps/app_shim/app_shim_host_mac.cc @@ -10,16 +10,9 @@ #include "base/files/file_path.h" #include "base/logging.h" #include "chrome/browser/browser_process.h" -#include "chrome/browser/extensions/extension_host.h" -#include "chrome/browser/extensions/extension_service.h" -#include "chrome/browser/extensions/extension_system.h" -#include "chrome/browser/extensions/shell_window_registry.h" #include "chrome/browser/profiles/profile_manager.h" -#include "chrome/browser/ui/extensions/application_launch.h" -#include "chrome/browser/ui/extensions/shell_window.h" -#include "chrome/common/extensions/extension_constants.h" +#include "content/public/browser/browser_thread.h" #include "ipc/ipc_channel_proxy.h" -#include "ui/base/cocoa/focus_window_set.h" AppShimHost::AppShimHost() : channel_(NULL), profile_(NULL) { @@ -36,7 +29,16 @@ void AppShimHost::ServeChannel(const IPC::ChannelHandle& handle) { DCHECK(CalledOnValidThread()); DCHECK(!channel_.get()); channel_.reset(new IPC::ChannelProxy(handle, IPC::Channel::MODE_SERVER, this, - BrowserThread::GetMessageLoopProxyForThread(BrowserThread::IO))); + content::BrowserThread::GetMessageLoopProxyForThread( + content::BrowserThread::IO))); +} + +Profile* AppShimHost::GetProfile() const { + return profile_; +} + +std::string AppShimHost::GetAppId() const { + return app_id_; } bool AppShimHost::OnMessageReceived(const IPC::Message& message) { @@ -62,57 +64,25 @@ bool AppShimHost::Send(IPC::Message* message) { void AppShimHost::OnLaunchApp(std::string profile_dir, std::string app_id) { DCHECK(CalledOnValidThread()); + DCHECK(!profile_); + if (profile_) { + // Only one app launch message per channel. + Send(new AppShimMsg_LaunchApp_Done(false)); + return; + } + + profile_ = FetchProfileForDirectory(profile_dir); app_id_ = app_id; apps::AppShimHandler* handler = apps::AppShimHandler::GetForAppMode(app_id_); - bool success = - handler ? handler->OnShimLaunch(this) : LaunchAppImpl(profile_dir); + bool success = handler && handler->OnShimLaunch(this); Send(new AppShimMsg_LaunchApp_Done(success)); } void AppShimHost::OnFocus() { DCHECK(CalledOnValidThread()); apps::AppShimHandler* handler = apps::AppShimHandler::GetForAppMode(app_id_); - if (handler) { + if (handler) handler->OnShimFocus(this); - return; - } - - if (!profile_) - return; - extensions::ShellWindowRegistry* registry = - extensions::ShellWindowRegistry::Get(profile_); - const std::set<ShellWindow*> windows = - registry->GetShellWindowsForApp(app_id_); - std::set<gfx::NativeWindow> native_windows; - for (std::set<ShellWindow*>::const_iterator i = windows.begin(); - i != windows.end(); - ++i) { - native_windows.insert((*i)->GetNativeWindow()); - } - ui::FocusWindowSet(native_windows); -} - -bool AppShimHost::LaunchAppImpl(const std::string& profile_dir) { - DCHECK(CalledOnValidThread()); - if (profile_) { - // Only one app launch message per channel. - return false; - } - if (!extensions::Extension::IdIsValid(app_id_)) { - LOG(ERROR) << "Bad app ID from app shim launch message."; - return false; - } - Profile* profile = FetchProfileForDirectory(profile_dir); - if (!profile) - return false; - - if (!LaunchApp(profile)) - return false; - - profile_ = profile; - registrar_.Add(this, chrome::NOTIFICATION_EXTENSION_HOST_DESTROYED, - content::Source<Profile>(profile_)); - return true; } Profile* AppShimHost::FetchProfileForDirectory(const std::string& profile_dir) { @@ -138,50 +108,6 @@ Profile* AppShimHost::FetchProfileForDirectory(const std::string& profile_dir) { return profile; } -bool AppShimHost::LaunchApp(Profile* profile) { - extensions::ExtensionSystem* extension_system = - extensions::ExtensionSystem::Get(profile); - ExtensionServiceInterface* extension_service = - extension_system->extension_service(); - const extensions::Extension* extension = - extension_service->GetExtensionById( - app_id_, false); - if (!extension) { - LOG(ERROR) << "Attempted to launch nonexistent app with id '" - << app_id_ << "'."; - return false; - } - // TODO(jeremya): Handle the case that launching the app fails. Probably we - // need to watch for 'app successfully launched' or at least 'background page - // exists/was created' and time out with failure if we don't see that sign of - // life within a certain window. - chrome::AppLaunchParams params(profile, - extension, - extension_misc::LAUNCH_NONE, - NEW_WINDOW); - chrome::OpenApplication(params); - return true; -} - -void AppShimHost::Observe(int type, - const content::NotificationSource& source, - const content::NotificationDetails& details) { - DCHECK(CalledOnValidThread()); - DCHECK(content::Source<Profile>(source).ptr() == profile_); - switch (type) { - case chrome::NOTIFICATION_EXTENSION_HOST_DESTROYED: { - extensions::ExtensionHost* extension_host = - content::Details<extensions::ExtensionHost>(details).ptr(); - if (app_id_ == extension_host->extension_id()) - Close(); - break; - } - default: - NOTREACHED() << "Unexpected notification sent."; - break; - } -} - void AppShimHost::OnAppClosed() { Close(); } diff --git a/apps/app_shim/app_shim_host_mac.h b/apps/app_shim/app_shim_host_mac.h index 438832ce22..e68380c314 100644 --- a/apps/app_shim/app_shim_host_mac.h +++ b/apps/app_shim/app_shim_host_mac.h @@ -10,8 +10,6 @@ #include "apps/app_shim/app_shim_handler_mac.h" #include "base/memory/scoped_ptr.h" #include "base/threading/non_thread_safe.h" -#include "content/public/browser/notification_observer.h" -#include "content/public/browser/notification_registrar.h" #include "ipc/ipc_listener.h" #include "ipc/ipc_sender.h" @@ -30,7 +28,6 @@ class Message; class AppShimHost : public IPC::Listener, public IPC::Sender, public apps::AppShimHandler::Host, - public content::NotificationObserver, public base::NonThreadSafe { public: AppShimHost(); @@ -42,12 +39,9 @@ class AppShimHost : public IPC::Listener, void ServeChannel(const IPC::ChannelHandle& handle); protected: - const std::string& app_id() const { return app_id_; } - const Profile* profile() const { return profile_; } // Used internally; virtual so they can be mocked for testing. virtual Profile* FetchProfileForDirectory(const std::string& profile_dir); - virtual bool LaunchApp(Profile* profile); // IPC::Listener implementation. virtual bool OnMessageReceived(const IPC::Message& message) OVERRIDE; @@ -68,17 +62,10 @@ class AppShimHost : public IPC::Listener, // Cmd+Tabbed to it.) void OnFocus(); - bool LaunchAppImpl(const std::string& profile_dir); - - // The AppShimHost listens to the NOTIFICATION_EXTENSION_HOST_DESTROYED - // message to detect when the app closes. When that happens, the AppShimHost - // closes the channel, which causes the app shim process to quit. - virtual void Observe(int type, - const content::NotificationSource& source, - const content::NotificationDetails& details) OVERRIDE; - - // apps::AppShimHandler::Host override: + // apps::AppShimHandler::Host overrides: virtual void OnAppClosed() OVERRIDE; + virtual Profile* GetProfile() const OVERRIDE; + virtual std::string GetAppId() const OVERRIDE; // Closes the channel and destroys the AppShimHost. void Close(); @@ -86,7 +73,6 @@ class AppShimHost : public IPC::Listener, scoped_ptr<IPC::ChannelProxy> channel_; std::string app_id_; Profile* profile_; - content::NotificationRegistrar registrar_; }; #endif // CHROME_BROWSER_WEB_APPLICATIONS_APP_SHIM_HOST_MAC_H_ diff --git a/apps/app_shim/app_shim_host_mac_unittest.cc b/apps/app_shim/app_shim_host_mac_unittest.cc index 476b2ceb43..f9a8e4dcc7 100644 --- a/apps/app_shim/app_shim_host_mac_unittest.cc +++ b/apps/app_shim/app_shim_host_mac_unittest.cc @@ -5,6 +5,7 @@ #include "apps/app_shim/app_shim_host_mac.h" #include "apps/app_shim/app_shim_messages.h" +#include "base/basictypes.h" #include "base/memory/scoped_vector.h" #include "chrome/test/base/testing_profile.h" #include "ipc/ipc_message.h" @@ -31,18 +32,9 @@ class TestingAppShimHost : public AppShimHost { fails_launch_ = fails_launch; } - const std::string& GetAppId() const { - return app_id(); - } - - const Profile* GetProfile() const { - return profile(); - } - protected: virtual Profile* FetchProfileForDirectory(const std::string& profile_dir) OVERRIDE; - virtual bool LaunchApp(Profile* profile) OVERRIDE; virtual bool Send(IPC::Message* message) OVERRIDE; private: @@ -77,10 +69,6 @@ Profile* TestingAppShimHost::FetchProfileForDirectory( return fails_profile_ ? NULL : test_profile_; } -bool TestingAppShimHost::LaunchApp(Profile* profile) { - return !fails_launch_; -} - class AppShimHostTest : public testing::Test, public apps::AppShimHandler { public: @@ -117,6 +105,7 @@ class AppShimHostTest : public testing::Test, private: virtual void SetUp() OVERRIDE { + testing::Test::SetUp(); profile_.reset(new TestingProfile); host_.reset(new TestingAppShimHost(profile())); } @@ -132,19 +121,12 @@ const char kTestProfileDir[] = "Default"; } // namespace -TEST_F(AppShimHostTest, TestLaunchApp) { - host()->ReceiveMessage( - new AppShimHostMsg_LaunchApp(kTestProfileDir, kTestAppId)); - ASSERT_EQ(kTestAppId, host()->GetAppId()); - ASSERT_EQ(profile(), host()->GetProfile()); - ASSERT_TRUE(LaunchWasSuccessful()); -} - TEST_F(AppShimHostTest, TestLaunchAppWithHandler) { apps::AppShimHandler::RegisterHandler(kTestAppId, this); EXPECT_TRUE(host()->ReceiveMessage( new AppShimHostMsg_LaunchApp(kTestProfileDir, kTestAppId))); - EXPECT_EQ(kTestAppId, host()->GetAppId()); + EXPECT_EQ(kTestAppId, + implicit_cast<apps::AppShimHandler::Host*>(host())->GetAppId()); EXPECT_TRUE(LaunchWasSuccessful()); EXPECT_EQ(1, launch_count_); EXPECT_EQ(0, focus_count_); diff --git a/apps/app_shim/app_shim_host_manager_mac.h b/apps/app_shim/app_shim_host_manager_mac.h index 13283ee980..45d881c56e 100644 --- a/apps/app_shim/app_shim_host_manager_mac.h +++ b/apps/app_shim/app_shim_host_manager_mac.h @@ -5,9 +5,8 @@ #ifndef CHROME_BROWSER_WEB_APPLICATIONS_APP_SHIM_HOST_MANAGER_MAC_H_ #define CHROME_BROWSER_WEB_APPLICATIONS_APP_SHIM_HOST_MANAGER_MAC_H_ -#include "base/mac/scoped_cftyperef.h" +#include "apps/app_shim/extension_app_shim_handler_mac.h" #include "base/memory/weak_ptr.h" -#include "base/threading/thread.h" #include "ipc/ipc_channel_factory.h" // The AppShimHostManager receives connections from app shims on a UNIX @@ -34,6 +33,8 @@ class AppShimHostManager scoped_ptr<IPC::ChannelFactory> factory_; + apps::ExtensionAppShimHandler extension_app_shim_handler_; + DISALLOW_COPY_AND_ASSIGN(AppShimHostManager); }; diff --git a/apps/app_shim/app_shim_host_manager_mac.mm b/apps/app_shim/app_shim_host_manager_mac.mm index 6133b9be38..049ba5b4fe 100644 --- a/apps/app_shim/app_shim_host_manager_mac.mm +++ b/apps/app_shim/app_shim_host_manager_mac.mm @@ -4,6 +4,7 @@ #include "apps/app_shim/app_shim_host_manager_mac.h" +#include "apps/app_shim/app_shim_handler_mac.h" #include "apps/app_shim/app_shim_host_mac.h" #include "base/bind.h" #include "base/files/file_path.h" @@ -27,6 +28,7 @@ void CreateAppShimHost(const IPC::ChannelHandle& handle) { AppShimHostManager::AppShimHostManager() { DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); + apps::AppShimHandler::SetDefaultHandler(&extension_app_shim_handler_); BrowserThread::PostTask( BrowserThread::FILE, FROM_HERE, base::Bind(&AppShimHostManager::InitOnFileThread, @@ -34,6 +36,7 @@ AppShimHostManager::AppShimHostManager() { } AppShimHostManager::~AppShimHostManager() { + apps::AppShimHandler::SetDefaultHandler(NULL); } void AppShimHostManager::InitOnFileThread() { diff --git a/apps/app_shim/extension_app_shim_handler_mac.cc b/apps/app_shim/extension_app_shim_handler_mac.cc new file mode 100644 index 0000000000..9d31d694c0 --- /dev/null +++ b/apps/app_shim/extension_app_shim_handler_mac.cc @@ -0,0 +1,130 @@ +// Copyright 2013 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 "apps/app_shim/extension_app_shim_handler_mac.h" + +#include "apps/app_shim/app_shim_messages.h" +#include "base/files/file_path.h" +#include "base/logging.h" +#include "chrome/browser/extensions/extension_host.h" +#include "chrome/browser/extensions/extension_service.h" +#include "chrome/browser/extensions/extension_system.h" +#include "chrome/browser/extensions/shell_window_registry.h" +#include "chrome/browser/ui/extensions/application_launch.h" +#include "chrome/browser/ui/extensions/shell_window.h" +#include "ui/base/cocoa/focus_window_set.h" + +namespace apps { + +ExtensionAppShimHandler::ExtensionAppShimHandler() {} + +ExtensionAppShimHandler::~ExtensionAppShimHandler() { + for (HostMap::iterator it = hosts_.begin(); it != hosts_.end(); ) { + // Increment the iterator first as OnAppClosed may call back to OnShimClose + // and invalidate the iterator. + it++->second->OnAppClosed(); + } +} + +bool ExtensionAppShimHandler::OnShimLaunch(Host* host) { + Profile* profile = host->GetProfile(); + DCHECK(profile); + + const std::string& app_id = host->GetAppId(); + if (!extensions::Extension::IdIsValid(app_id)) { + LOG(ERROR) << "Bad app ID from app shim launch message."; + return false; + } + + if (!LaunchApp(profile, app_id)) + return false; + + // The first host to claim this (profile, app_id) becomes the main host. + // For any others, we launch the app but return false. + if (!hosts_.insert(make_pair(make_pair(profile, app_id), host)).second) + return false; + + if (!registrar_.IsRegistered( + this, chrome::NOTIFICATION_EXTENSION_HOST_DESTROYED, + content::Source<Profile>(profile))) { + registrar_.Add(this, chrome::NOTIFICATION_EXTENSION_HOST_DESTROYED, + content::Source<Profile>(profile)); + } + + return true; +} + +void ExtensionAppShimHandler::OnShimClose(Host* host) { + HostMap::iterator it = hosts_.find(make_pair(host->GetProfile(), + host->GetAppId())); + // Any hosts other than the main host will still call OnShimClose, so ignore + // them. + if (it != hosts_.end() && it->second == host) + hosts_.erase(it); +} + +void ExtensionAppShimHandler::OnShimFocus(Host* host) { + if (!host->GetProfile()) + return; + + extensions::ShellWindowRegistry* registry = + extensions::ShellWindowRegistry::Get(host->GetProfile()); + const extensions::ShellWindowRegistry::ShellWindowList windows = + registry->GetShellWindowsForApp(host->GetAppId()); + std::set<gfx::NativeWindow> native_windows; + for (extensions::ShellWindowRegistry::const_iterator i = windows.begin(); + i != windows.end(); ++i) { + native_windows.insert((*i)->GetNativeWindow()); + } + ui::FocusWindowSet(native_windows); +} + +bool ExtensionAppShimHandler::LaunchApp(Profile* profile, + const std::string& app_id) { + extensions::ExtensionSystem* extension_system = + extensions::ExtensionSystem::Get(profile); + ExtensionServiceInterface* extension_service = + extension_system->extension_service(); + const extensions::Extension* extension = + extension_service->GetExtensionById(app_id, false); + if (!extension) { + LOG(ERROR) << "Attempted to launch nonexistent app with id '" + << app_id << "'."; + return false; + } + // TODO(jeremya): Handle the case that launching the app fails. Probably we + // need to watch for 'app successfully launched' or at least 'background page + // exists/was created' and time out with failure if we don't see that sign of + // life within a certain window. + chrome::OpenApplication(chrome::AppLaunchParams( + profile, extension, NEW_FOREGROUND_TAB)); + return true; +} + +void ExtensionAppShimHandler::Observe( + int type, + const content::NotificationSource& source, + const content::NotificationDetails& details) { + Profile* profile = content::Source<Profile>(source).ptr(); + switch (type) { + case chrome::NOTIFICATION_EXTENSION_HOST_DESTROYED: { + extensions::ExtensionHost* extension_host = + content::Details<extensions::ExtensionHost>(details).ptr(); + CloseShim(profile, extension_host->extension_id()); + break; + } + default: + NOTREACHED(); // Unexpected notification. + break; + } +} + +void ExtensionAppShimHandler::CloseShim(Profile* profile, + const std::string& app_id) { + HostMap::const_iterator it = hosts_.find(make_pair(profile, app_id)); + if (it != hosts_.end()) + it->second->OnAppClosed(); +} + +} // namespace apps diff --git a/apps/app_shim/extension_app_shim_handler_mac.h b/apps/app_shim/extension_app_shim_handler_mac.h new file mode 100644 index 0000000000..ea9a8c7eee --- /dev/null +++ b/apps/app_shim/extension_app_shim_handler_mac.h @@ -0,0 +1,60 @@ +// Copyright 2013 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 APPS_APP_SHIM_EXTENSION_APP_SHIM_HANDLER_H_ +#define APPS_APP_SHIM_EXTENSION_APP_SHIM_HANDLER_H_ + +#include <map> +#include <string> + +#include "apps/app_shim/app_shim_handler_mac.h" +#include "content/public/browser/notification_observer.h" +#include "content/public/browser/notification_registrar.h" + +class Profile; + +namespace apps { + +// This app shim handler that handles events for app shims that correspond to an +// extension. +class ExtensionAppShimHandler : public AppShimHandler, + public content::NotificationObserver { + public: + ExtensionAppShimHandler(); + virtual ~ExtensionAppShimHandler(); + + // AppShimHandler overrides: + virtual bool OnShimLaunch(Host* host) OVERRIDE; + virtual void OnShimClose(Host* host) OVERRIDE; + virtual void OnShimFocus(Host* host) OVERRIDE; + + protected: + typedef std::map<std::pair<Profile*, std::string>, AppShimHandler::Host*> + HostMap; + + // Exposed for testing. + HostMap& hosts() { return hosts_; } + content::NotificationRegistrar& registrar() { return registrar_; } + + private: + virtual bool LaunchApp(Profile* profile, const std::string& app_id); + + // Listen to the NOTIFICATION_EXTENSION_HOST_DESTROYED message to detect when + // an app closes. When that happens, call OnAppClosed on the relevant + // AppShimHandler::Host which causes the app shim process to quit. + // The host will call OnShimClose on this. + virtual void Observe(int type, + const content::NotificationSource& source, + const content::NotificationDetails& details) OVERRIDE; + + void CloseShim(Profile* profile, const std::string& app_id); + + HostMap hosts_; + + content::NotificationRegistrar registrar_; +}; + +} // namespace apps + +#endif // APPS_APP_SHIM_EXTENSION_APP_SHIM_HANDLER_H_ diff --git a/apps/app_shim/extension_app_shim_handler_mac_unittest.cc b/apps/app_shim/extension_app_shim_handler_mac_unittest.cc new file mode 100644 index 0000000000..dbbbd13c80 --- /dev/null +++ b/apps/app_shim/extension_app_shim_handler_mac_unittest.cc @@ -0,0 +1,139 @@ +// Copyright 2013 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 "apps/app_shim/extension_app_shim_handler_mac.h" + +#include "apps/app_shim/app_shim_host_mac.h" +#include "base/memory/scoped_ptr.h" +#include "chrome/common/chrome_notification_types.h" +#include "chrome/test/base/testing_profile.h" +#include "content/public/browser/notification_service.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace apps { + +class TestingExtensionAppShimHandler : public ExtensionAppShimHandler { + public: + explicit TestingExtensionAppShimHandler() : fails_launch_(false) {} + virtual ~TestingExtensionAppShimHandler() {} + + void set_fails_launch(bool fails_launch) { + fails_launch_ = fails_launch; + } + + AppShimHandler::Host* FindHost(Profile* profile, + const std::string& app_id) { + HostMap::const_iterator it = hosts().find(make_pair(profile, app_id)); + return it == hosts().end() ? NULL : it->second; + } + + content::NotificationRegistrar& GetRegistrar() { return registrar(); } + + protected: + virtual bool LaunchApp(Profile* profile, const std::string& app_id) OVERRIDE { + return !fails_launch_; + } + + private: + bool fails_launch_; + + DISALLOW_COPY_AND_ASSIGN(TestingExtensionAppShimHandler); +}; + +const char kTestAppIdA[] = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; +const char kTestAppIdB[] = "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"; + +class FakeHost : public apps::AppShimHandler::Host { + public: + FakeHost(Profile* profile, + std::string app_id, + TestingExtensionAppShimHandler* handler) + : profile_(profile), + app_id_(app_id), + handler_(handler), + close_count_(0) {} + + virtual void OnAppClosed() OVERRIDE { + handler_->OnShimClose(this); + ++close_count_; + } + virtual Profile* GetProfile() const OVERRIDE { return profile_; } + virtual std::string GetAppId() const OVERRIDE { return app_id_; } + + int close_count() { return close_count_; } + + private: + Profile* profile_; + std::string app_id_; + TestingExtensionAppShimHandler* handler_; + int close_count_; + + DISALLOW_COPY_AND_ASSIGN(FakeHost); +}; + +class ExtensionAppShimHandlerTest : public testing::Test { + protected: + ExtensionAppShimHandlerTest() + : handler_(new TestingExtensionAppShimHandler), + host_aa_(&profile_a_, kTestAppIdA, handler_.get()), + host_ab_(&profile_a_, kTestAppIdB, handler_.get()), + host_bb_(&profile_b_, kTestAppIdB, handler_.get()), + host_aa_duplicate_(&profile_a_, kTestAppIdA, handler_.get()) {} + + scoped_ptr<TestingExtensionAppShimHandler> handler_; + TestingProfile profile_a_; + TestingProfile profile_b_; + FakeHost host_aa_; + FakeHost host_ab_; + FakeHost host_bb_; + FakeHost host_aa_duplicate_; + + private: + DISALLOW_COPY_AND_ASSIGN(ExtensionAppShimHandlerTest); +}; + +TEST_F(ExtensionAppShimHandlerTest, LaunchAndCloseShim) { + // If launch fails, the host is not added to the map. + handler_->set_fails_launch(true); + EXPECT_EQ(false, handler_->OnShimLaunch(&host_aa_)); + EXPECT_FALSE(handler_->FindHost(&profile_a_, kTestAppIdA)); + + // Normal startup. + handler_->set_fails_launch(false); + EXPECT_EQ(true, handler_->OnShimLaunch(&host_aa_)); + EXPECT_EQ(&host_aa_, handler_->FindHost(&profile_a_, kTestAppIdA)); + EXPECT_TRUE(handler_->GetRegistrar().IsRegistered( + handler_.get(), + chrome::NOTIFICATION_EXTENSION_HOST_DESTROYED, + content::Source<Profile>(&profile_a_))); + EXPECT_EQ(true, handler_->OnShimLaunch(&host_ab_)); + EXPECT_EQ(&host_ab_, handler_->FindHost(&profile_a_, kTestAppIdB)); + EXPECT_EQ(true, handler_->OnShimLaunch(&host_bb_)); + EXPECT_EQ(&host_bb_, handler_->FindHost(&profile_b_, kTestAppIdB)); + EXPECT_TRUE(handler_->GetRegistrar().IsRegistered( + handler_.get(), + chrome::NOTIFICATION_EXTENSION_HOST_DESTROYED, + content::Source<Profile>(&profile_b_))); + + // Starting and closing a second host does nothing. + EXPECT_EQ(false, handler_->OnShimLaunch(&host_aa_duplicate_)); + EXPECT_EQ(&host_aa_, handler_->FindHost(&profile_a_, kTestAppIdA)); + handler_->OnShimClose(&host_aa_duplicate_); + EXPECT_EQ(&host_aa_, handler_->FindHost(&profile_a_, kTestAppIdA)); + + // Normal close. + handler_->OnShimClose(&host_aa_); + EXPECT_FALSE(handler_->FindHost(&profile_a_, kTestAppIdA)); + + // Closing the second host afterward does nothing. + handler_->OnShimClose(&host_aa_duplicate_); + EXPECT_FALSE(handler_->FindHost(&profile_a_, kTestAppIdA)); + + // Destruction sends OnAppClose to remaining hosts. + handler_.reset(); + EXPECT_EQ(1, host_ab_.close_count()); + EXPECT_EQ(1, host_bb_.close_count()); +} + +} // namespace apps diff --git a/apps/apps.gypi b/apps/apps.gypi index 4867f73349..92a677e29f 100644 --- a/apps/apps.gypi +++ b/apps/apps.gypi @@ -36,12 +36,20 @@ 'app_shim/app_shim_host_mac.h', 'app_shim/app_shim_host_manager_mac.h', 'app_shim/app_shim_host_manager_mac.mm', + 'app_shim/extension_app_shim_handler_mac.h', + 'app_shim/extension_app_shim_handler_mac.cc', 'field_trial_names.cc', 'field_trial_names.h', 'pref_names.cc', 'pref_names.h', 'prefs.cc', 'prefs.h', + 'saved_files_service.cc', + 'saved_files_service.h', + 'saved_files_service_factory.cc', + 'saved_files_service_factory.h', + 'shell_window_geometry_cache.cc', + 'shell_window_geometry_cache.h', 'shortcut_manager.cc', 'shortcut_manager.h', 'shortcut_manager_factory.cc', diff --git a/apps/saved_files_service.cc b/apps/saved_files_service.cc new file mode 100644 index 0000000000..c509c083ea --- /dev/null +++ b/apps/saved_files_service.cc @@ -0,0 +1,441 @@ +// Copyright 2013 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 "apps/saved_files_service.h" + +#include <algorithm> + +#include "apps/saved_files_service_factory.h" +#include "base/basictypes.h" +#include "base/hash_tables.h" +#include "base/value_conversions.h" +#include "chrome/browser/extensions/extension_host.h" +#include "chrome/browser/extensions/extension_prefs.h" +#include "chrome/browser/extensions/extension_service.h" +#include "chrome/browser/extensions/extension_system.h" +#include "chrome/common/extensions/permissions/api_permission.h" +#include "chrome/common/extensions/permissions/permission_set.h" + +namespace apps { + +using extensions::APIPermission; +using extensions::Extension; +using extensions::ExtensionHost; +using extensions::ExtensionPrefs; + +namespace { + +// Preference keys + +// The file entries that the app has permission to access. +const char kFileEntries[] = "file_entries"; + +// The path to a file entry that the app had permission to access. +const char kFileEntryPath[] = "path"; + +// Whether or not the app had write access to a file entry. +const char kFileEntryWritable[] = "writable"; + +// The sequence number in the LRU of the file entry. +const char kFileEntrySequenceNumber[] = "sequence_number"; + +const size_t kMaxSavedFileEntries = 500; +const int kMaxSequenceNumber = kint32max; + +// These might be different to the constant values in tests. +size_t g_max_saved_file_entries = kMaxSavedFileEntries; +int g_max_sequence_number = kMaxSequenceNumber; + +// Persists a SavedFileEntry in ExtensionPrefs. +void AddSavedFileEntry(ExtensionPrefs* prefs, + const std::string& extension_id, + const SavedFileEntry& file_entry) { + ExtensionPrefs::ScopedDictionaryUpdate update( + prefs, extension_id, kFileEntries); + DictionaryValue* file_entries = update.Get(); + if (!file_entries) + file_entries = update.Create(); + DCHECK(!file_entries->GetDictionaryWithoutPathExpansion(file_entry.id, NULL)); + + DictionaryValue* file_entry_dict = new DictionaryValue(); + file_entry_dict->Set(kFileEntryPath, CreateFilePathValue(file_entry.path)); + file_entry_dict->SetBoolean(kFileEntryWritable, file_entry.writable); + file_entry_dict->SetInteger(kFileEntrySequenceNumber, + file_entry.sequence_number); + file_entries->SetWithoutPathExpansion(file_entry.id, file_entry_dict); +} + +// Updates the sequence_number of a SavedFileEntry persisted in ExtensionPrefs. +void UpdateSavedFileEntry(ExtensionPrefs* prefs, + const std::string& extension_id, + const SavedFileEntry& file_entry) { + ExtensionPrefs::ScopedDictionaryUpdate update( + prefs, extension_id, kFileEntries); + DictionaryValue* file_entries = update.Get(); + DCHECK(file_entries); + DictionaryValue* file_entry_dict = NULL; + file_entries->GetDictionaryWithoutPathExpansion(file_entry.id, + &file_entry_dict); + DCHECK(file_entry_dict); + file_entry_dict->SetInteger(kFileEntrySequenceNumber, + file_entry.sequence_number); +} + +// Removes a SavedFileEntry from ExtensionPrefs. +void RemoveSavedFileEntry(ExtensionPrefs* prefs, + const std::string& extension_id, + const std::string& file_entry_id) { + ExtensionPrefs::ScopedDictionaryUpdate update( + prefs, extension_id, kFileEntries); + DictionaryValue* file_entries = update.Get(); + if (!file_entries) + file_entries = update.Create(); + file_entries->RemoveWithoutPathExpansion(file_entry_id, NULL); +} + +// Clears all SavedFileEntry for the app from ExtensionPrefs. +void ClearSavedFileEntries(ExtensionPrefs* prefs, + const std::string& extension_id) { + prefs->UpdateExtensionPref(extension_id, kFileEntries, NULL); +} + +// Returns all SavedFileEntries for the app. +std::vector<SavedFileEntry> GetSavedFileEntries( + ExtensionPrefs* prefs, + const std::string& extension_id) { + std::vector<SavedFileEntry> result; + const DictionaryValue* file_entries = NULL; + if (!prefs->ReadPrefAsDictionary(extension_id, kFileEntries, &file_entries)) + return result; + + for (DictionaryValue::Iterator it(*file_entries); !it.IsAtEnd(); + it.Advance()) { + const DictionaryValue* file_entry = NULL; + if (!it.value().GetAsDictionary(&file_entry)) + continue; + const base::Value* path_value; + if (!file_entry->Get(kFileEntryPath, &path_value)) + continue; + base::FilePath file_path; + if (!GetValueAsFilePath(*path_value, &file_path)) + continue; + bool writable = false; + if (!file_entry->GetBoolean(kFileEntryWritable, &writable)) + continue; + int sequence_number = 0; + if (!file_entry->GetInteger(kFileEntrySequenceNumber, &sequence_number)) + continue; + if (!sequence_number) + continue; + result.push_back( + SavedFileEntry(it.key(), file_path, writable, sequence_number)); + } + return result; +} + +} // namespace + +SavedFileEntry::SavedFileEntry() : writable(false), sequence_number(0) {} + +SavedFileEntry::SavedFileEntry(const std::string& id, + const base::FilePath& path, + bool writable, + int sequence_number) + : id(id), + path(path), + writable(writable), + sequence_number(sequence_number) {} + +class SavedFilesService::SavedFiles { + public: + SavedFiles(Profile* profile, const std::string& extension_id); + ~SavedFiles(); + + void RegisterFileEntry(const std::string& id, + const base::FilePath& file_path, + bool writable); + void EnqueueFileEntry(const std::string& id); + bool IsRegistered(const std::string& id) const; + const SavedFileEntry* GetFileEntry(const std::string& id) const; + std::vector<SavedFileEntry> GetAllFileEntries() const; + + private: + // Compacts sequence numbers if the largest sequence number is + // g_max_sequence_number. Outside of testing, it is set to kint32max, so this + // will almost never do any real work. + void MaybeCompactSequenceNumbers(); + + void LoadSavedFileEntriesFromPreferences(); + + Profile* profile_; + const std::string extension_id_; + + // Contains all file entries that have been registered, keyed by ID. Owns + // values. + base::hash_map<std::string, SavedFileEntry*> registered_file_entries_; + STLValueDeleter<base::hash_map<std::string, SavedFileEntry*> > + registered_file_entries_deleter_; + + // The queue of file entries that have been retained, keyed by + // sequence_number. Values are a subset of values in registered_file_entries_. + // This should be kept in sync with file entries stored in extension prefs. + std::map<int, SavedFileEntry*> saved_file_lru_; + + DISALLOW_COPY_AND_ASSIGN(SavedFiles); +}; + +// static +SavedFilesService* SavedFilesService::Get(Profile* profile) { + return SavedFilesServiceFactory::GetForProfile(profile); +} + +SavedFilesService::SavedFilesService(Profile* profile) + : extension_id_to_saved_files_deleter_(&extension_id_to_saved_files_), + profile_(profile) { + registrar_.Add(this, + chrome::NOTIFICATION_EXTENSION_HOST_DESTROYED, + content::NotificationService::AllSources()); + registrar_.Add(this, + chrome::NOTIFICATION_APP_TERMINATING, + content::NotificationService::AllSources()); +} + +SavedFilesService::~SavedFilesService() {} + +void SavedFilesService::Observe(int type, + const content::NotificationSource& source, + const content::NotificationDetails& details) { + switch (type) { + case chrome::NOTIFICATION_EXTENSION_HOST_DESTROYED: { + ExtensionHost* host = content::Details<ExtensionHost>(details).ptr(); + const Extension* extension = host->extension(); + if (extension) { + ClearQueueIfNoRetainPermission(extension); + Clear(extension->id()); + } + break; + } + + case chrome::NOTIFICATION_APP_TERMINATING: { + // Stop listening to NOTIFICATION_EXTENSION_HOST_DESTROYED in particular + // as all extension hosts will be destroyed as a result of shutdown. + registrar_.RemoveAll(); + break; + } + } +} + +void SavedFilesService::RegisterFileEntry(const std::string& extension_id, + const std::string& id, + const base::FilePath& file_path, + bool writable) { + GetOrInsert(extension_id)->RegisterFileEntry(id, file_path, writable); +} + +void SavedFilesService::EnqueueFileEntry(const std::string& extension_id, + const std::string& id) { + GetOrInsert(extension_id)->EnqueueFileEntry(id); +} + +std::vector<SavedFileEntry> SavedFilesService::GetAllFileEntries( + const std::string& extension_id) { + return GetOrInsert(extension_id)->GetAllFileEntries(); +} + +bool SavedFilesService::IsRegistered(const std::string& extension_id, + const std::string& id) { + return GetOrInsert(extension_id)->IsRegistered(id); +} + +const SavedFileEntry* SavedFilesService::GetFileEntry( + const std::string& extension_id, + const std::string& id) { + return GetOrInsert(extension_id)->GetFileEntry(id); +} + +void SavedFilesService::ClearQueueIfNoRetainPermission( + const Extension* extension) { + if (!extension->GetActivePermissions()->HasAPIPermission( + APIPermission::kFileSystemRetainFiles)) { + ClearSavedFileEntries(ExtensionPrefs::Get(profile_), extension->id()); + Clear(extension->id()); + } +} + +SavedFilesService::SavedFiles* SavedFilesService::GetOrInsert( + const std::string& extension_id) { + std::map<std::string, SavedFiles*>::iterator it = + extension_id_to_saved_files_.find(extension_id); + if (it != extension_id_to_saved_files_.end()) + return it->second; + + SavedFiles* saved_files = new SavedFiles(profile_, extension_id); + extension_id_to_saved_files_.insert( + std::make_pair(extension_id, saved_files)); + return saved_files; +} + +void SavedFilesService::Clear(const std::string& extension_id) { + std::map<std::string, SavedFiles*>::iterator it = + extension_id_to_saved_files_.find(extension_id); + if (it != extension_id_to_saved_files_.end()) { + delete it->second; + extension_id_to_saved_files_.erase(it); + } +} + +SavedFilesService::SavedFiles::SavedFiles(Profile* profile, + const std::string& extension_id) + : profile_(profile), + extension_id_(extension_id), + registered_file_entries_deleter_(®istered_file_entries_) { + LoadSavedFileEntriesFromPreferences(); +} + +SavedFilesService::SavedFiles::~SavedFiles() {} + +void SavedFilesService::SavedFiles::RegisterFileEntry( + const std::string& id, + const base::FilePath& file_path, + bool writable) { + if (ContainsKey(registered_file_entries_, id)) + return; + + registered_file_entries_.insert( + std::make_pair(id, new SavedFileEntry(id, file_path, writable, 0))); +} + +void SavedFilesService::SavedFiles::EnqueueFileEntry(const std::string& id) { + base::hash_map<std::string, SavedFileEntry*>::iterator it = + registered_file_entries_.find(id); + DCHECK(it != registered_file_entries_.end()); + + SavedFileEntry* file_entry = it->second; + int old_sequence_number = file_entry->sequence_number; + if (!saved_file_lru_.empty()) { + // Get the sequence number after the last file entry in the LRU. + std::map<int, SavedFileEntry*>::reverse_iterator it = + saved_file_lru_.rbegin(); + if (it->second == file_entry) + return; + + file_entry->sequence_number = it->first + 1; + } else { + // The first sequence number is 1, as 0 means the entry is not in the LRU. + file_entry->sequence_number = 1; + } + saved_file_lru_.insert( + std::make_pair(file_entry->sequence_number, file_entry)); + ExtensionPrefs* prefs = ExtensionPrefs::Get(profile_); + if (old_sequence_number) { + saved_file_lru_.erase(old_sequence_number); + UpdateSavedFileEntry(prefs, extension_id_, *file_entry); + } else { + AddSavedFileEntry(prefs, extension_id_, *file_entry); + if (saved_file_lru_.size() > g_max_saved_file_entries) { + std::map<int, SavedFileEntry*>::iterator it = saved_file_lru_.begin(); + it->second->sequence_number = 0; + RemoveSavedFileEntry(prefs, extension_id_, it->second->id); + saved_file_lru_.erase(it); + } + } + MaybeCompactSequenceNumbers(); +} + +bool SavedFilesService::SavedFiles::IsRegistered(const std::string& id) const { + return ContainsKey(registered_file_entries_, id); +} + +const SavedFileEntry* SavedFilesService::SavedFiles::GetFileEntry( + const std::string& id) const { + base::hash_map<std::string, SavedFileEntry*>::const_iterator it = + registered_file_entries_.find(id); + if (it == registered_file_entries_.end()) + return NULL; + + return it->second; +} + +std::vector<SavedFileEntry> SavedFilesService::SavedFiles::GetAllFileEntries() + const { + std::vector<SavedFileEntry> result; + for (base::hash_map<std::string, SavedFileEntry*>::const_iterator it = + registered_file_entries_.begin(); + it != registered_file_entries_.end(); + ++it) { + result.push_back(*it->second); + } + return result; +} + +void SavedFilesService::SavedFiles::MaybeCompactSequenceNumbers() { + DCHECK_GE(g_max_sequence_number, 0); + DCHECK_GE(static_cast<size_t>(g_max_sequence_number), + g_max_saved_file_entries); + std::map<int, SavedFileEntry*>::reverse_iterator it = + saved_file_lru_.rbegin(); + if (it == saved_file_lru_.rend()) + return; + + // Only compact sequence numbers if the last entry's sequence number is the + // maximum value. This should almost never be the case. + if (it->first < g_max_sequence_number) + return; + + int sequence_number = 0; + ExtensionPrefs* prefs = ExtensionPrefs::Get(profile_); + for (std::map<int, SavedFileEntry*>::iterator it = saved_file_lru_.begin(); + it != saved_file_lru_.end(); + ++it) { + sequence_number++; + if (it->second->sequence_number == sequence_number) + continue; + + SavedFileEntry* file_entry = it->second; + file_entry->sequence_number = sequence_number; + UpdateSavedFileEntry(prefs, extension_id_, *file_entry); + saved_file_lru_.erase(it++); + // Provide the following element as an insert hint. While optimized + // insertion time with the following element as a hint is only supported by + // the spec in C++11, the implementations do support this. + it = saved_file_lru_.insert( + it, std::make_pair(file_entry->sequence_number, file_entry)); + } +} + +void SavedFilesService::SavedFiles::LoadSavedFileEntriesFromPreferences() { + ExtensionPrefs* prefs = ExtensionPrefs::Get(profile_); + std::vector<SavedFileEntry> saved_entries = + GetSavedFileEntries(prefs, extension_id_); + for (std::vector<SavedFileEntry>::iterator it = saved_entries.begin(); + it != saved_entries.end(); + ++it) { + SavedFileEntry* file_entry = new SavedFileEntry(*it); + registered_file_entries_.insert(std::make_pair(file_entry->id, file_entry)); + saved_file_lru_.insert( + std::make_pair(file_entry->sequence_number, file_entry)); + } +} + +// static +void SavedFilesService::SetMaxSequenceNumberForTest(int max_value) { + g_max_sequence_number = max_value; +} + +// static +void SavedFilesService::ClearMaxSequenceNumberForTest() { + g_max_sequence_number = kMaxSequenceNumber; +} + +// static +void SavedFilesService::SetLruSizeForTest(int size) { + g_max_saved_file_entries = size; +} + +// static +void SavedFilesService::ClearLruSizeForTest() { + g_max_saved_file_entries = kMaxSavedFileEntries; +} + +} // namespace apps diff --git a/apps/saved_files_service.h b/apps/saved_files_service.h new file mode 100644 index 0000000000..613764aaa8 --- /dev/null +++ b/apps/saved_files_service.h @@ -0,0 +1,135 @@ +// Copyright 2013 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 APPS_SAVED_FILES_SERVICE_H_ +#define APPS_SAVED_FILES_SERVICE_H_ + +#include <map> +#include <set> +#include <string> +#include <vector> + +#include "base/files/file_path.h" +#include "base/gtest_prod_util.h" +#include "base/stl_util.h" +#include "components/browser_context_keyed_service/browser_context_keyed_service.h" +#include "content/public/browser/notification_observer.h" +#include "content/public/browser/notification_registrar.h" + +class Profile; +class SavedFilesServiceUnitTest; +FORWARD_DECLARE_TEST(SavedFilesServiceUnitTest, RetainTwoFilesTest); +FORWARD_DECLARE_TEST(SavedFilesServiceUnitTest, EvictionTest); +FORWARD_DECLARE_TEST(SavedFilesServiceUnitTest, SequenceNumberCompactionTest); + +namespace extensions { +class Extension; +} + +namespace apps { + +// Represents a file entry that a user has given an app permission to +// access. Will be persisted to disk (in the Preferences file), so should remain +// serializable. +struct SavedFileEntry { + SavedFileEntry(); + + SavedFileEntry(const std::string& id, + const base::FilePath& path, + bool writable, + int sequence_number); + + // The opaque id of this file entry. + std::string id; + + // The path to a file entry that the app had permission to access. + base::FilePath path; + + // Whether or not the app had write access to a file entry. + bool writable; + + // The sequence number in the LRU of the file entry. The value 0 indicates + // that the entry is not in the LRU. + int sequence_number; +}; + +// Tracks the files that apps have retained access to both while running and +// when suspended. +class SavedFilesService : public BrowserContextKeyedService, + public content::NotificationObserver { + public: + explicit SavedFilesService(Profile* profile); + virtual ~SavedFilesService(); + + static SavedFilesService* Get(Profile* profile); + + // Registers a file entry with the saved files service, making it eligible to + // be put into the queue. File entries that are in the retained files queue at + // object construction are automatically registered. + void RegisterFileEntry(const std::string& extension_id, + const std::string& id, + const base::FilePath& file_path, + bool writable); + + // If the file with |id| is not in the queue of files to be retained + // permanently, adds the file to the back of the queue, evicting the least + // recently used entry at the front of the queue if it is full. If it is + // already present, moves it to the back of the queue. The |id| must have been + // registered. + void EnqueueFileEntry(const std::string& extension_id, const std::string& id); + + // Returns whether the file entry with the given |id| has been registered. + bool IsRegistered(const std::string& extension_id, const std::string& id); + + // Gets a borrowed pointer to the file entry with the specified |id|. Returns + // NULL if the file entry has not been registered. + const SavedFileEntry* GetFileEntry(const std::string& extension_id, + const std::string& id); + + // Returns all registered file entries. + std::vector<SavedFileEntry> GetAllFileEntries( + const std::string& extension_id); + + // Clears all retained files if the app does not have the + // fileSystem.retainFiles permission. + void ClearQueueIfNoRetainPermission(const extensions::Extension* extension); + + private: + FRIEND_TEST_ALL_PREFIXES(::SavedFilesServiceUnitTest, RetainTwoFilesTest); + FRIEND_TEST_ALL_PREFIXES(::SavedFilesServiceUnitTest, EvictionTest); + FRIEND_TEST_ALL_PREFIXES(::SavedFilesServiceUnitTest, + SequenceNumberCompactionTest); + friend class ::SavedFilesServiceUnitTest; + + // A container for the registered files for an app. + class SavedFiles; + + // content::NotificationObserver. + virtual void Observe(int type, + const content::NotificationSource& source, + const content::NotificationDetails& details) OVERRIDE; + + // Returns the SavedFiles for |extension_id|, creating it if necessary. + SavedFiles* GetOrInsert(const std::string& extension_id); + + // Clears the SavedFiles for |extension_id|. + void Clear(const std::string& extension_id); + + static void SetMaxSequenceNumberForTest(int max_value); + static void ClearMaxSequenceNumberForTest(); + static void SetLruSizeForTest(int size); + static void ClearLruSizeForTest(); + + std::map<std::string, SavedFiles*> extension_id_to_saved_files_; + STLValueDeleter<std::map<std::string, SavedFiles*> > + extension_id_to_saved_files_deleter_; + content::NotificationRegistrar registrar_; + Profile* profile_; + + DISALLOW_COPY_AND_ASSIGN(SavedFilesService); +}; + +} // namespace apps + +#endif // APPS_SAVED_FILES_SERVICE_H_ diff --git a/apps/saved_files_service_factory.cc b/apps/saved_files_service_factory.cc new file mode 100644 index 0000000000..c9047702d5 --- /dev/null +++ b/apps/saved_files_service_factory.cc @@ -0,0 +1,36 @@ +// Copyright 2013 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 "apps/saved_files_service_factory.h" + +#include "apps/saved_files_service.h" +#include "chrome/browser/profiles/profile.h" +#include "components/browser_context_keyed_service/browser_context_dependency_manager.h" + +namespace apps { + +// static +SavedFilesService* SavedFilesServiceFactory::GetForProfile(Profile* profile) { + return static_cast<SavedFilesService*>( + GetInstance()->GetServiceForBrowserContext(profile, true)); +} + +// static +SavedFilesServiceFactory* SavedFilesServiceFactory::GetInstance() { + return Singleton<SavedFilesServiceFactory>::get(); +} + +SavedFilesServiceFactory::SavedFilesServiceFactory() + : BrowserContextKeyedServiceFactory( + "SavedFilesService", + BrowserContextDependencyManager::GetInstance()) {} + +SavedFilesServiceFactory::~SavedFilesServiceFactory() {} + +BrowserContextKeyedService* SavedFilesServiceFactory::BuildServiceInstanceFor( + content::BrowserContext* profile) const { + return new SavedFilesService(static_cast<Profile*>(profile)); +} + +} // namespace apps diff --git a/apps/saved_files_service_factory.h b/apps/saved_files_service_factory.h new file mode 100644 index 0000000000..502010f091 --- /dev/null +++ b/apps/saved_files_service_factory.h @@ -0,0 +1,35 @@ +// Copyright 2013 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 APPS_SAVED_FILES_SERVICE_FACTORY_H_ +#define APPS_SAVED_FILES_SERVICE_FACTORY_H_ + +#include "base/memory/singleton.h" +#include "components/browser_context_keyed_service/browser_context_keyed_service_factory.h" + +class Profile; + +namespace apps { + +class SavedFilesService; + +// BrowserContextKeyedServiceFactory for SavedFilesService. +class SavedFilesServiceFactory : public BrowserContextKeyedServiceFactory { + public: + static SavedFilesService* GetForProfile(Profile* profile); + + static SavedFilesServiceFactory* GetInstance(); + + private: + SavedFilesServiceFactory(); + virtual ~SavedFilesServiceFactory(); + friend struct DefaultSingletonTraits<SavedFilesServiceFactory>; + + virtual BrowserContextKeyedService* BuildServiceInstanceFor( + content::BrowserContext* profile) const OVERRIDE; +}; + +} // namespace apps + +#endif // APPS_SAVED_FILES_SERVICE_FACTORY_H_ diff --git a/apps/saved_files_service_unittest.cc b/apps/saved_files_service_unittest.cc new file mode 100644 index 0000000000..4d31485426 --- /dev/null +++ b/apps/saved_files_service_unittest.cc @@ -0,0 +1,239 @@ +// Copyright 2013 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 <algorithm> + +#include "apps/saved_files_service.h" +#include "base/files/file_path.h" +#include "base/string_number_conversions.h" +#include "base/test/values_test_util.h" +#include "base/values.h" +#include "chrome/browser/extensions/extension_prefs.h" +#include "chrome/browser/extensions/extension_service.h" +#include "chrome/browser/extensions/extension_system.h" +#include "chrome/browser/extensions/test_extension_environment.h" +#include "chrome/common/extensions/extension.h" +#include "chrome/test/base/testing_profile.h" +#include "testing/gtest/include/gtest/gtest.h" + +#if !defined(OS_ANDROID) + +#define TRACE_CALL(expression) \ + do { \ + SCOPED_TRACE(#expression); \ + expression; \ + } while (0) + +using apps::SavedFileEntry; +using apps::SavedFilesService; + +namespace { + +std::string GenerateId(int i) { + return base::IntToString(i) + ":filename.ext"; +} + +} // namespace + +class SavedFilesServiceUnitTest : public testing::Test { + protected: + virtual void SetUp() OVERRIDE { + testing::Test::SetUp(); + extension_ = env_.MakeExtension(*base::test::ParseJson( + "{" + " \"app\": {" + " \"background\": {" + " \"scripts\": [\"background.js\"]" + " }" + " }," + " \"permissions\": [" + " {\"fileSystem\": [\"retainFiles\"]}" + " ]" + "}")); + service_ = SavedFilesService::Get(env_.profile()); + path_ = base::FilePath(FILE_PATH_LITERAL("filename.ext")); + } + + virtual void TearDown() OVERRIDE { + SavedFilesService::ClearMaxSequenceNumberForTest(); + SavedFilesService::ClearLruSizeForTest(); + testing::Test::TearDown(); + } + + // Check that a registered file entry has the correct value. + void CheckEntrySequenceNumber(int id, int sequence_number) { + std::string id_string = GenerateId(id); + SCOPED_TRACE(id_string); + EXPECT_TRUE(service_->IsRegistered(extension_->id(), id_string)); + const SavedFileEntry* entry = + service_->GetFileEntry(extension_->id(), id_string); + ASSERT_TRUE(entry); + EXPECT_EQ(id_string, entry->id); + EXPECT_EQ(path_, entry->path); + EXPECT_TRUE(entry->writable); + EXPECT_EQ(sequence_number, entry->sequence_number); + } + + // Check that a range of registered file entries have the correct values. + void CheckRangeEnqueuedInOrder(int start, int end) { + SavedFileEntry entry; + for (int i = start; i < end; i++) { + CheckEntrySequenceNumber(i, i + 1); + } + } + + extensions::TestExtensionEnvironment env_; + const extensions::Extension* extension_; + SavedFilesService* service_; + base::FilePath path_; +}; + +TEST_F(SavedFilesServiceUnitTest, RetainTwoFilesTest) { + service_->RegisterFileEntry(extension_->id(), GenerateId(1), path_, true); + service_->RegisterFileEntry(extension_->id(), GenerateId(2), path_, true); + service_->RegisterFileEntry(extension_->id(), GenerateId(3), path_, true); + + // Test that no entry has a sequence number. + TRACE_CALL(CheckEntrySequenceNumber(1, 0)); + TRACE_CALL(CheckEntrySequenceNumber(2, 0)); + TRACE_CALL(CheckEntrySequenceNumber(3, 0)); + + // Test that only entry #1 has a sequence number. + service_->EnqueueFileEntry(extension_->id(), GenerateId(1)); + TRACE_CALL(CheckEntrySequenceNumber(1, 1)); + TRACE_CALL(CheckEntrySequenceNumber(2, 0)); + + // Test that entry #1 has not changed sequence number because it is the most + // recently enqueued entry. + service_->EnqueueFileEntry(extension_->id(), GenerateId(1)); + TRACE_CALL(CheckEntrySequenceNumber(1, 1)); + TRACE_CALL(CheckEntrySequenceNumber(2, 0)); + + // Test that entry #1 is unchanged and entry #2 has been assigned the next + // sequence number. + service_->EnqueueFileEntry(extension_->id(), GenerateId(2)); + TRACE_CALL(CheckEntrySequenceNumber(1, 1)); + TRACE_CALL(CheckEntrySequenceNumber(2, 2)); + + // Test that both entries #1 and #2 are unchanged because #2 is the most + // recently enqueued entry. + service_->EnqueueFileEntry(extension_->id(), GenerateId(2)); + TRACE_CALL(CheckEntrySequenceNumber(1, 1)); + TRACE_CALL(CheckEntrySequenceNumber(2, 2)); + + // Test that entry #1 has been assigned the next sequence number. + service_->EnqueueFileEntry(extension_->id(), GenerateId(1)); + TRACE_CALL(CheckEntrySequenceNumber(1, 3)); + TRACE_CALL(CheckEntrySequenceNumber(2, 2)); + TRACE_CALL(CheckEntrySequenceNumber(3, 0)); + + EXPECT_FALSE(service_->IsRegistered(extension_->id(), "another id")); + SavedFileEntry entry; + EXPECT_FALSE(service_->GetFileEntry(extension_->id(), "another id")); + + // ClearQueueIfNoRetainPermission should be a no-op because the app has the + // fileSystem.retainFiles permission. + service_->ClearQueueIfNoRetainPermission(extension_); + TRACE_CALL(CheckEntrySequenceNumber(1, 3)); + TRACE_CALL(CheckEntrySequenceNumber(2, 2)); + TRACE_CALL(CheckEntrySequenceNumber(3, 0)); + + // Test that after a clear, retained file entries are unchanged, but file + // entries that have been registered but not retained are no longer + // registered. + service_->Clear(extension_->id()); + TRACE_CALL(CheckEntrySequenceNumber(1, 3)); + TRACE_CALL(CheckEntrySequenceNumber(2, 2)); + EXPECT_FALSE(service_->IsRegistered(extension_->id(), GenerateId(3))); +} + +TEST_F(SavedFilesServiceUnitTest, NoRetainFilesPermissionTest) { + extension_ = env_.MakeExtension(*base::test::ParseJson( + "{\"app\": {\"background\": {\"scripts\": [\"background.js\"]}}," + "\"permissions\": [\"fileSystem\"]}")); + service_->RegisterFileEntry(extension_->id(), GenerateId(1), path_, true); + TRACE_CALL(CheckEntrySequenceNumber(1, 0)); + SavedFileEntry entry; + service_->EnqueueFileEntry(extension_->id(), GenerateId(1)); + TRACE_CALL(CheckEntrySequenceNumber(1, 1)); + EXPECT_FALSE(service_->IsRegistered(extension_->id(), "another id")); + EXPECT_FALSE(service_->GetFileEntry(extension_->id(), "another id")); + + // ClearQueueIfNoRetainPermission should clear the queue, since the app does + // not have the "retainFiles" permission. + service_->ClearQueueIfNoRetainPermission(extension_); + std::vector<SavedFileEntry> entries = + service_->GetAllFileEntries(extension_->id()); + EXPECT_TRUE(entries.empty()); +} + +TEST_F(SavedFilesServiceUnitTest, EvictionTest) { + SavedFilesService::SetLruSizeForTest(10); + for (int i = 0; i < 10; i++) { + service_->RegisterFileEntry(extension_->id(), GenerateId(i), path_, true); + service_->EnqueueFileEntry(extension_->id(), GenerateId(i)); + } + service_->RegisterFileEntry(extension_->id(), GenerateId(10), path_, true); + + // Expect that entries 0 to 9 are in the queue, but 10 is not. + TRACE_CALL(CheckRangeEnqueuedInOrder(0, 10)); + TRACE_CALL(CheckEntrySequenceNumber(10, 0)); + service_->EnqueueFileEntry(extension_->id(), GenerateId(10)); + + // Expect that entries 1 to 10 are in the queue, but entry 0 is not. + TRACE_CALL(CheckEntrySequenceNumber(0, 0)); + TRACE_CALL(CheckRangeEnqueuedInOrder(1, 11)); + + // Check that retained entries are unchanged after a clear. + service_->Clear(extension_->id()); + SavedFileEntry entry; + EXPECT_FALSE(service_->GetFileEntry(extension_->id(), GenerateId(0))); + TRACE_CALL(CheckRangeEnqueuedInOrder(1, 11)); + + // Expect that entry 2 is now at the back of the queue, and no further entries + // have been evicted. + service_->EnqueueFileEntry(extension_->id(), GenerateId(2)); + TRACE_CALL(CheckEntrySequenceNumber(2, 12)); + TRACE_CALL(CheckRangeEnqueuedInOrder(1, 1)); + TRACE_CALL(CheckRangeEnqueuedInOrder(3, 11)); + + // Check that retained entries are unchanged after a clear. + service_->Clear(extension_->id()); + TRACE_CALL(CheckEntrySequenceNumber(2, 12)); + TRACE_CALL(CheckRangeEnqueuedInOrder(1, 1)); + TRACE_CALL(CheckRangeEnqueuedInOrder(3, 11)); +} + +TEST_F(SavedFilesServiceUnitTest, SequenceNumberCompactionTest) { + SavedFilesService::SetMaxSequenceNumberForTest(8); + SavedFilesService::SetLruSizeForTest(8); + for (int i = 0; i < 4; i++) { + service_->RegisterFileEntry(extension_->id(), GenerateId(i), path_, true); + service_->EnqueueFileEntry(extension_->id(), GenerateId(i)); + } + service_->EnqueueFileEntry(extension_->id(), GenerateId(2)); + service_->EnqueueFileEntry(extension_->id(), GenerateId(3)); + service_->EnqueueFileEntry(extension_->id(), GenerateId(2)); + + // The sequence numbers should be sparse, as they have not gone over the + // limit. + TRACE_CALL(CheckEntrySequenceNumber(0, 1)); + TRACE_CALL(CheckEntrySequenceNumber(1, 2)); + TRACE_CALL(CheckEntrySequenceNumber(2, 7)); + TRACE_CALL(CheckEntrySequenceNumber(3, 6)); + service_->Clear(extension_->id()); + TRACE_CALL(CheckEntrySequenceNumber(0, 1)); + TRACE_CALL(CheckEntrySequenceNumber(1, 2)); + TRACE_CALL(CheckEntrySequenceNumber(2, 7)); + TRACE_CALL(CheckEntrySequenceNumber(3, 6)); + + // This should push the sequence number to the limit of 8, and trigger a + // sequence number compaction. Expect that the sequence numbers are + // contiguous from 1 to 4. + service_->EnqueueFileEntry(extension_->id(), GenerateId(3)); + TRACE_CALL(CheckRangeEnqueuedInOrder(0, 4)); + service_->Clear(extension_->id()); + TRACE_CALL(CheckRangeEnqueuedInOrder(0, 4)); +} +#endif diff --git a/apps/shell_window_geometry_cache.cc b/apps/shell_window_geometry_cache.cc new file mode 100644 index 0000000000..19901d9b93 --- /dev/null +++ b/apps/shell_window_geometry_cache.cc @@ -0,0 +1,276 @@ +// Copyright (c) 2012 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 "apps/shell_window_geometry_cache.h" + +#include "base/bind.h" +#include "base/stl_util.h" +#include "base/strings/string_number_conversions.h" +#include "chrome/browser/extensions/extension_prefs.h" +#include "chrome/browser/extensions/extension_prefs_factory.h" +#include "chrome/browser/profiles/incognito_helpers.h" +#include "chrome/browser/profiles/profile.h" +#include "chrome/common/chrome_notification_types.h" +#include "chrome/common/extensions/extension.h" +#include "components/browser_context_keyed_service/browser_context_dependency_manager.h" +#include "content/public/browser/notification_service.h" +#include "content/public/browser/notification_types.h" + +namespace { + +// The timeout in milliseconds before we'll persist window geometry to the +// StateStore. +const int kSyncTimeoutMilliseconds = 1000; + +} // namespace + +namespace apps { + +ShellWindowGeometryCache::ShellWindowGeometryCache( + Profile* profile, extensions::ExtensionPrefs* prefs) + : prefs_(prefs), + sync_delay_(base::TimeDelta::FromMilliseconds(kSyncTimeoutMilliseconds)) { + registrar_.Add(this, chrome::NOTIFICATION_EXTENSION_LOADED, + content::Source<Profile>(profile)); + registrar_.Add(this, chrome::NOTIFICATION_EXTENSION_UNLOADED, + content::Source<Profile>(profile)); +} + +ShellWindowGeometryCache::~ShellWindowGeometryCache() { +} + +// static +ShellWindowGeometryCache* ShellWindowGeometryCache::Get( + content::BrowserContext* context) { + return Factory::GetForContext(context, true /* create */); +} + +void ShellWindowGeometryCache::SaveGeometry( + const std::string& extension_id, + const std::string& window_id, + const gfx::Rect& bounds, + ui::WindowShowState window_state) { + ExtensionData& extension_data = cache_[extension_id]; + + // If we don't have any unsynced changes and this is a duplicate of what's + // already in the cache, just ignore it. + if (extension_data[window_id].bounds == bounds && + extension_data[window_id].window_state == window_state && + !ContainsKey(unsynced_extensions_, extension_id)) + return; + + base::Time now = base::Time::Now(); + + extension_data[window_id].bounds = bounds; + extension_data[window_id].window_state = window_state; + extension_data[window_id].last_change = now; + + if (extension_data.size() > kMaxCachedWindows) { + ExtensionData::iterator oldest = extension_data.end(); + // Too many windows in the cache, find the oldest one to remove. + for (ExtensionData::iterator it = extension_data.begin(); + it != extension_data.end(); ++it) { + // Don't expunge the window that was just added. + if (it->first == window_id) continue; + + // If time is in the future, reset it to now to minimize weirdness. + if (it->second.last_change > now) + it->second.last_change = now; + + if (oldest == extension_data.end() || + it->second.last_change < oldest->second.last_change) + oldest = it; + } + extension_data.erase(oldest); + } + + unsynced_extensions_.insert(extension_id); + + // We don't use Reset() because the timer may not yet be running. + // (In that case Stop() is a no-op.) + sync_timer_.Stop(); + sync_timer_.Start(FROM_HERE, sync_delay_, this, + &ShellWindowGeometryCache::SyncToStorage); +} + +void ShellWindowGeometryCache::SyncToStorage() { + std::set<std::string> tosync; + tosync.swap(unsynced_extensions_); + for (std::set<std::string>::const_iterator it = tosync.begin(), + eit = tosync.end(); it != eit; ++it) { + const std::string& extension_id = *it; + const ExtensionData& extension_data = cache_[extension_id]; + + scoped_ptr<base::DictionaryValue> dict(new base::DictionaryValue); + for (ExtensionData::const_iterator it = extension_data.begin(), + eit = extension_data.end(); it != eit; ++it) { + base::DictionaryValue* value = new base::DictionaryValue; + const gfx::Rect& bounds = it->second.bounds; + value->SetInteger("x", bounds.x()); + value->SetInteger("y", bounds.y()); + value->SetInteger("w", bounds.width()); + value->SetInteger("h", bounds.height()); + value->SetInteger("state", it->second.window_state); + value->SetString( + "ts", base::Int64ToString(it->second.last_change.ToInternalValue())); + dict->SetWithoutPathExpansion(it->first, value); + } + prefs_->SetGeometryCache(extension_id, dict.Pass()); + } +} + +bool ShellWindowGeometryCache::GetGeometry( + const std::string& extension_id, + const std::string& window_id, + gfx::Rect* bounds, + ui::WindowShowState* window_state) const { + + std::map<std::string, ExtensionData>::const_iterator + extension_data_it = cache_.find(extension_id); + + // Not in the map means loading data for the extension didn't finish yet. + if (extension_data_it == cache_.end()) + return false; + + ExtensionData::const_iterator window_data = extension_data_it->second.find( + window_id); + + if (window_data == extension_data_it->second.end()) + return false; + + if (bounds) + *bounds = window_data->second.bounds; + if (window_state) + *window_state = window_data->second.window_state; + return true; +} + +void ShellWindowGeometryCache::Shutdown() { + SyncToStorage(); +} + +void ShellWindowGeometryCache::Observe( + int type, const content::NotificationSource& source, + const content::NotificationDetails& details) { + switch (type) { + case chrome::NOTIFICATION_EXTENSION_LOADED: { + std::string extension_id = + content::Details<const extensions::Extension>(details).ptr()->id(); + OnExtensionLoaded(extension_id); + break; + } + case chrome::NOTIFICATION_EXTENSION_UNLOADED: { + std::string extension_id = + content::Details<const extensions::UnloadedExtensionInfo>(details). + ptr()->extension->id(); + OnExtensionUnloaded(extension_id); + break; + } + default: + NOTREACHED(); + return; + } +} + +void ShellWindowGeometryCache::SetSyncDelayForTests(int timeout_ms) { + sync_delay_ = base::TimeDelta::FromMilliseconds(timeout_ms); +} + +void ShellWindowGeometryCache::OnExtensionLoaded( + const std::string& extension_id) { + ExtensionData& extension_data = cache_[extension_id]; + + const base::DictionaryValue* stored_windows = + prefs_->GetGeometryCache(extension_id); + if (!stored_windows) + return; + + for (base::DictionaryValue::Iterator it(*stored_windows); !it.IsAtEnd(); + it.Advance()) { + // If the cache already contains geometry for this window, don't + // overwrite that information since it is probably the result of an + // application starting up very quickly. + const std::string& window_id = it.key(); + ExtensionData::iterator cached_window = extension_data.find(window_id); + if (cached_window == extension_data.end()) { + const base::DictionaryValue* stored_window; + if (it.value().GetAsDictionary(&stored_window)) { + WindowData& window_data = extension_data[it.key()]; + + int i; + if (stored_window->GetInteger("x", &i)) + window_data.bounds.set_x(i); + if (stored_window->GetInteger("y", &i)) + window_data.bounds.set_y(i); + if (stored_window->GetInteger("w", &i)) + window_data.bounds.set_width(i); + if (stored_window->GetInteger("h", &i)) + window_data.bounds.set_height(i); + if (stored_window->GetInteger("state", &i)) { + window_data.window_state = + static_cast<ui::WindowShowState>(i); + } + std::string ts_as_string; + if (stored_window->GetString("ts", &ts_as_string)) { + int64 ts; + if (base::StringToInt64(ts_as_string, &ts)) { + window_data.last_change = base::Time::FromInternalValue(ts); + } + } + } + } + } +} + +void ShellWindowGeometryCache::OnExtensionUnloaded( + const std::string& extension_id) { + SyncToStorage(); + cache_.erase(extension_id); +} + +/////////////////////////////////////////////////////////////////////////////// +// Factory boilerplate + +// static +ShellWindowGeometryCache* ShellWindowGeometryCache::Factory::GetForContext( + content::BrowserContext* context, bool create) { + return static_cast<ShellWindowGeometryCache*>( + GetInstance()->GetServiceForBrowserContext(context, create)); +} + +ShellWindowGeometryCache::Factory* +ShellWindowGeometryCache::Factory::GetInstance() { + return Singleton<ShellWindowGeometryCache::Factory>::get(); +} + +ShellWindowGeometryCache::Factory::Factory() + : BrowserContextKeyedServiceFactory( + "ShellWindowGeometryCache", + BrowserContextDependencyManager::GetInstance()) { + DependsOn(extensions::ExtensionPrefsFactory::GetInstance()); +} + +ShellWindowGeometryCache::Factory::~Factory() { +} + +BrowserContextKeyedService* +ShellWindowGeometryCache::Factory::BuildServiceInstanceFor( + content::BrowserContext* context) const { + Profile* profile = Profile::FromBrowserContext(context); + return new ShellWindowGeometryCache( + profile, + extensions::ExtensionPrefs::Get(profile)); +} + +bool ShellWindowGeometryCache::Factory::ServiceIsNULLWhileTesting() const { + return false; +} + +content::BrowserContext* +ShellWindowGeometryCache::Factory::GetBrowserContextToUse( + content::BrowserContext* context) const { + return chrome::GetBrowserContextRedirectedInIncognito(context); +} + +} // namespace apps diff --git a/apps/shell_window_geometry_cache.h b/apps/shell_window_geometry_cache.h new file mode 100644 index 0000000000..91ea132a5b --- /dev/null +++ b/apps/shell_window_geometry_cache.h @@ -0,0 +1,137 @@ +// Copyright (c) 2012 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_EXTENSIONS_SHELL_WINDOW_GEOMETRY_CACHE_H_ +#define CHROME_BROWSER_EXTENSIONS_SHELL_WINDOW_GEOMETRY_CACHE_H_ + +#include <map> +#include <set> +#include <string> + +#include "base/memory/scoped_ptr.h" +#include "base/memory/singleton.h" +#include "base/time.h" +#include "base/timer.h" +#include "base/values.h" +#include "components/browser_context_keyed_service/browser_context_keyed_service.h" +#include "components/browser_context_keyed_service/browser_context_keyed_service_factory.h" +#include "content/public/browser/notification_observer.h" +#include "content/public/browser/notification_registrar.h" +#include "ui/base/ui_base_types.h" +#include "ui/gfx/rect.h" + +class Profile; + +namespace extensions { +class ExtensionPrefs; +} + +namespace apps { + +// A cache for persisted geometry of shell windows, both to not have to wait +// for IO when creating a new window, and to not cause IO on every window +// geometry change. +class ShellWindowGeometryCache + : public BrowserContextKeyedService, + public content::NotificationObserver { + public: + class Factory : public BrowserContextKeyedServiceFactory { + public: + static ShellWindowGeometryCache* GetForContext( + content::BrowserContext* context, + bool create); + + static Factory* GetInstance(); + private: + friend struct DefaultSingletonTraits<Factory>; + + Factory(); + virtual ~Factory(); + + // BrowserContextKeyedServiceFactory + virtual BrowserContextKeyedService* BuildServiceInstanceFor( + content::BrowserContext* context) const OVERRIDE; + virtual bool ServiceIsNULLWhileTesting() const OVERRIDE; + virtual content::BrowserContext* GetBrowserContextToUse( + content::BrowserContext* context) const OVERRIDE; + }; + + ShellWindowGeometryCache(Profile* profile, + extensions::ExtensionPrefs* prefs); + + virtual ~ShellWindowGeometryCache(); + + // Returns the instance for the given browsing context. + static ShellWindowGeometryCache* Get(content::BrowserContext* context); + + // Save the geometry and state associated with |extension_id| and |window_id|. + void SaveGeometry(const std::string& extension_id, + const std::string& window_id, + const gfx::Rect& bounds, + ui::WindowShowState state); + + // Get any saved geometry and state associated with |extension_id| and + // |window_id|. If saved data exists, sets |bounds| and |state| if not NULL + // and returns true. + bool GetGeometry(const std::string& extension_id, + const std::string& window_id, + gfx::Rect* bounds, + ui::WindowShowState* state) const; + + // BrowserContextKeyedService + virtual void Shutdown() OVERRIDE; + + // Maximum number of windows we'll cache the geometry for per app. + static const size_t kMaxCachedWindows = 100; + + protected: + friend class ShellWindowGeometryCacheTest; + + // For tests, this modifies the timeout delay for saving changes from calls + // to SaveGeometry. (Note that even if this is set to 0, you still need to + // run the message loop to see the results of any SyncToStorage call). + void SetSyncDelayForTests(int timeout_ms); + + private: + // Data stored for each window. + struct WindowData { + WindowData() : window_state(ui::SHOW_STATE_DEFAULT) {} + gfx::Rect bounds; + ui::WindowShowState window_state; + base::Time last_change; + }; + + // Data stored for each extension. + typedef std::map<std::string, WindowData> ExtensionData; + + // content::NotificationObserver + virtual void Observe(int type, + const content::NotificationSource& source, + const content::NotificationDetails& details) OVERRIDE; + + void OnExtensionLoaded(const std::string& extension_id); + void OnExtensionUnloaded(const std::string& extension_id); + void SyncToStorage(); + + // Preferences storage. + extensions::ExtensionPrefs* prefs_; + + // Cached data + std::map<std::string, ExtensionData> cache_; + + // Data that still needs saving + std::set<std::string> unsynced_extensions_; + + // The timer used to save the data + base::OneShotTimer<ShellWindowGeometryCache> sync_timer_; + + // The timeout value we'll use for |sync_timer_|. + base::TimeDelta sync_delay_; + + content::NotificationRegistrar registrar_; +}; + +} // namespace apps + +#endif // CHROME_BROWSER_EXTENSIONS_SHELL_WINDOW_GEOMETRY_CACHE_H_ diff --git a/apps/shell_window_geometry_cache_unittest.cc b/apps/shell_window_geometry_cache_unittest.cc new file mode 100644 index 0000000000..255c78c535 --- /dev/null +++ b/apps/shell_window_geometry_cache_unittest.cc @@ -0,0 +1,256 @@ +// Copyright (c) 2012 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 "apps/shell_window_geometry_cache.h" +#include "base/memory/scoped_ptr.h" +#include "base/prefs/mock_pref_change_callback.h" +#include "base/strings/string_number_conversions.h" +#include "chrome/browser/extensions/extension_prefs.h" +#include "chrome/browser/extensions/test_extension_prefs.h" +#include "chrome/test/base/testing_profile.h" +#include "content/public/test/test_browser_thread.h" +#include "content/public/test/test_utils.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace { + static const char kWindowId[] = "windowid"; + static const char kWindowId2[] = "windowid2"; + + const char kWindowGeometryKey[] = "window_geometry"; +} // namespace + +using content::BrowserThread; + +namespace apps { + +// Base class for tests. +class ShellWindowGeometryCacheTest : public testing::Test { + public: + ShellWindowGeometryCacheTest() : + ui_thread_(BrowserThread::UI, &ui_message_loop_) { + prefs_.reset(new extensions::TestExtensionPrefs( + ui_message_loop_.message_loop_proxy())); + cache_.reset( + new ShellWindowGeometryCache(&profile_, prefs_->prefs())); + cache_->SetSyncDelayForTests(0); + } + + void AddGeometryAndLoadExtension( + const std::string& extension_id, + const std::string& window_id, + const gfx::Rect& bounds, + ui::WindowShowState state); + + // Spins the UI threads' message loops to make sure any task + // posted to sync the geometry to the value store gets a chance to run. + void WaitForSync(); + + void LoadExtension(const std::string& extension_id); + void UnloadExtension(const std::string& extension_id); + + protected: + TestingProfile profile_; + MessageLoopForUI ui_message_loop_; + content::TestBrowserThread ui_thread_; + scoped_ptr<extensions::TestExtensionPrefs> prefs_; + scoped_ptr<ShellWindowGeometryCache> cache_; +}; + +void ShellWindowGeometryCacheTest::AddGeometryAndLoadExtension( + const std::string& extension_id, + const std::string& window_id, + const gfx::Rect& bounds, + ui::WindowShowState state) { + scoped_ptr<base::DictionaryValue> dict(new base::DictionaryValue); + base::DictionaryValue* value = new base::DictionaryValue; + value->SetInteger("x", bounds.x()); + value->SetInteger("y", bounds.y()); + value->SetInteger("w", bounds.width()); + value->SetInteger("h", bounds.height()); + value->SetInteger("state", state); + dict->SetWithoutPathExpansion(window_id, value); + prefs_->prefs()->SetGeometryCache(extension_id, dict.Pass()); + LoadExtension(extension_id); +} + +void ShellWindowGeometryCacheTest::WaitForSync() { + content::RunAllPendingInMessageLoop(); +} + +void ShellWindowGeometryCacheTest::LoadExtension( + const std::string& extension_id) { + cache_->OnExtensionLoaded(extension_id); + WaitForSync(); +} + +void ShellWindowGeometryCacheTest::UnloadExtension( + const std::string& extension_id) { + cache_->OnExtensionUnloaded(extension_id); + WaitForSync(); +} + +// Test getting geometry from an empty store. +TEST_F(ShellWindowGeometryCacheTest, GetGeometryEmptyStore) { + const std::string extension_id = prefs_->AddExtensionAndReturnId("ext1"); + ASSERT_FALSE(cache_->GetGeometry(extension_id, kWindowId, NULL, NULL)); +} + +// Test getting geometry for an unknown extension. +TEST_F(ShellWindowGeometryCacheTest, GetGeometryUnkownExtension) { + const std::string extension_id1 = prefs_->AddExtensionAndReturnId("ext1"); + const std::string extension_id2 = prefs_->AddExtensionAndReturnId("ext2"); + AddGeometryAndLoadExtension(extension_id1, kWindowId, + gfx::Rect(4, 5, 31, 43), + ui::SHOW_STATE_DEFAULT); + ASSERT_FALSE(cache_->GetGeometry(extension_id2, kWindowId, NULL, NULL)); +} + +// Test getting geometry for an unknown window in a known extension. +TEST_F(ShellWindowGeometryCacheTest, GetGeometryUnkownWindow) { + const std::string extension_id = prefs_->AddExtensionAndReturnId("ext1"); + AddGeometryAndLoadExtension(extension_id, kWindowId, + gfx::Rect(4, 5, 31, 43), + ui::SHOW_STATE_DEFAULT); + ASSERT_FALSE(cache_->GetGeometry(extension_id, kWindowId2, NULL, NULL)); +} + +// Test that loading geometry and state from the store works correctly. +TEST_F(ShellWindowGeometryCacheTest, GetGeometryAndStateFromStore) { + const std::string extension_id = prefs_->AddExtensionAndReturnId("ext1"); + gfx::Rect bounds(4, 5, 31, 43); + ui::WindowShowState state = ui::SHOW_STATE_NORMAL; + AddGeometryAndLoadExtension(extension_id, kWindowId, bounds, state); + gfx::Rect new_bounds; + ui::WindowShowState new_state = ui::SHOW_STATE_DEFAULT; + ASSERT_TRUE(cache_->GetGeometry( + extension_id, kWindowId, &new_bounds, &new_state)); + ASSERT_EQ(bounds, new_bounds); + ASSERT_EQ(state, new_state); +} + +// Test saving geometry and state to the cache and state store, and reading +// it back. +TEST_F(ShellWindowGeometryCacheTest, SaveGeometryAndStateToStore) { + const std::string extension_id = prefs_->AddExtensionAndReturnId("ext1"); + const std::string window_id(kWindowId); + + // inform cache of extension + LoadExtension(extension_id); + + // update geometry stored in cache + gfx::Rect bounds(4, 5, 31, 43); + ui::WindowShowState state = ui::SHOW_STATE_NORMAL; + cache_->SaveGeometry(extension_id, window_id, bounds, state); + + // make sure that immediately reading back geometry works + gfx::Rect new_bounds; + ui::WindowShowState new_state = ui::SHOW_STATE_DEFAULT; + ASSERT_TRUE(cache_->GetGeometry( + extension_id, window_id, &new_bounds, &new_state)); + ASSERT_EQ(bounds, new_bounds); + ASSERT_EQ(state, new_state); + + // unload extension to force cache to save data to the state store + UnloadExtension(extension_id); + + // check if geometry got stored correctly in the state store + const base::DictionaryValue* dict = + prefs_->prefs()->GetGeometryCache(extension_id); + ASSERT_TRUE(dict); + + ASSERT_TRUE(dict->HasKey(window_id)); + int v; + ASSERT_TRUE(dict->GetInteger(window_id + ".x", &v)); + ASSERT_EQ(bounds.x(), v); + ASSERT_TRUE(dict->GetInteger(window_id + ".y", &v)); + ASSERT_EQ(bounds.y(), v); + ASSERT_TRUE(dict->GetInteger(window_id + ".w", &v)); + ASSERT_EQ(bounds.width(), v); + ASSERT_TRUE(dict->GetInteger(window_id + ".h", &v)); + ASSERT_EQ(bounds.height(), v); + ASSERT_TRUE(dict->GetInteger(window_id + ".state", &v)); + ASSERT_EQ(state, v); + + // check to make sure cache indeed doesn't know about this extension anymore + ASSERT_FALSE(cache_->GetGeometry( + extension_id, window_id, &new_bounds, &new_state)); + + // reload extension + LoadExtension(extension_id); + // and make sure the geometry got reloaded properly too + ASSERT_TRUE(cache_->GetGeometry( + extension_id, window_id, &new_bounds, &new_state)); + ASSERT_EQ(bounds, new_bounds); + ASSERT_EQ(state, new_state); +} + +// Tests that we won't do writes to the state store for SaveGeometry calls +// which don't change the state we already have. +TEST_F(ShellWindowGeometryCacheTest, NoDuplicateWrites) { + using testing::_; + using testing::Mock; + + const std::string extension_id = prefs_->AddExtensionAndReturnId("ext1"); + gfx::Rect bounds1(100, 200, 300, 400); + gfx::Rect bounds2(200, 400, 600, 800); + gfx::Rect bounds2_duplicate(200, 400, 600, 800); + + MockPrefChangeCallback observer(prefs_->pref_service()); + PrefChangeRegistrar registrar; + registrar.Init(prefs_->pref_service()); + registrar.Add("extensions.settings", observer.GetCallback()); + + // Write the first bounds - it should do > 0 writes. + EXPECT_CALL(observer, OnPreferenceChanged(_)); + cache_->SaveGeometry(extension_id, kWindowId, bounds1, + ui::SHOW_STATE_DEFAULT); + WaitForSync(); + Mock::VerifyAndClearExpectations(&observer); + + // Write a different bounds - it should also do > 0 writes. + EXPECT_CALL(observer, OnPreferenceChanged(_)); + cache_->SaveGeometry(extension_id, kWindowId, bounds2, + ui::SHOW_STATE_DEFAULT); + WaitForSync(); + Mock::VerifyAndClearExpectations(&observer); + + // Write a different state - it should also do > 0 writes. + EXPECT_CALL(observer, OnPreferenceChanged(_)); + cache_->SaveGeometry(extension_id, kWindowId, bounds2, + ui::SHOW_STATE_NORMAL); + WaitForSync(); + Mock::VerifyAndClearExpectations(&observer); + + // Write a bounds and state that's a duplicate of what we already have. + // This should not do any writes. + EXPECT_CALL(observer, OnPreferenceChanged(_)).Times(0); + cache_->SaveGeometry(extension_id, kWindowId, bounds2_duplicate, + ui::SHOW_STATE_NORMAL); + WaitForSync(); + Mock::VerifyAndClearExpectations(&observer); +} + +// Tests that no more than kMaxCachedWindows windows will be cached. +TEST_F(ShellWindowGeometryCacheTest, MaxWindows) { + const std::string extension_id = prefs_->AddExtensionAndReturnId("ext1"); + // inform cache of extension + LoadExtension(extension_id); + + gfx::Rect bounds(4, 5, 31, 43); + for (size_t i = 0; i < ShellWindowGeometryCache::kMaxCachedWindows + 1; ++i) { + std::string window_id = "window_" + base::IntToString(i); + cache_->SaveGeometry(extension_id, window_id, bounds, + ui::SHOW_STATE_DEFAULT); + } + + // The first added window should no longer have cached geometry. + EXPECT_FALSE(cache_->GetGeometry(extension_id, "window_0", NULL, NULL)); + // All other windows should still exist. + for (size_t i = 1; i < ShellWindowGeometryCache::kMaxCachedWindows + 1; ++i) { + std::string window_id = "window_" + base::IntToString(i); + EXPECT_TRUE(cache_->GetGeometry(extension_id, window_id, NULL, NULL)); + } +} + +} // namespace extensions diff --git a/apps/shortcut_manager.cc b/apps/shortcut_manager.cc index df31a797a3..2bdccb5fc2 100644 --- a/apps/shortcut_manager.cc +++ b/apps/shortcut_manager.cc @@ -6,6 +6,8 @@ #include "base/bind.h" #include "base/compiler_specific.h" +#include "base/string16.h" +#include "base/utf_string_conversions.h" #include "chrome/browser/shell_integration.h" #include "chrome/browser/ui/web_applications/web_app_ui.h" #include "chrome/browser/web_applications/web_app.h" @@ -61,7 +63,9 @@ void ShortcutManager::Observe(int type, base::Callback<void(const ShellIntegration::ShortcutInfo&)> create_or_update; if (installed_info->is_update) { - create_or_update = base::Bind(&web_app::UpdateAllShortcuts); + string16 old_title = UTF8ToUTF16(installed_info->old_name); + create_or_update = base::Bind(&web_app::UpdateAllShortcuts, + old_title); } else { create_or_update = base::Bind(&CreateShortcutsInApplicationsMenu); } diff --git a/apps/shortcut_manager.h b/apps/shortcut_manager.h index a1efa01a79..a0efb822c1 100644 --- a/apps/shortcut_manager.h +++ b/apps/shortcut_manager.h @@ -6,8 +6,8 @@ #define APPS_SHORTCUT_MANAGER_H_ #include "base/memory/weak_ptr.h" -#include "chrome/browser/profiles/profile_keyed_service.h" #include "chrome/common/extensions/extension.h" +#include "components/browser_context_keyed_service/browser_context_keyed_service.h" #include "content/public/browser/notification_observer.h" #include "content/public/browser/notification_registrar.h" @@ -16,7 +16,7 @@ class Profile; namespace apps { // This class manages the installation of shortcuts for platform apps. -class ShortcutManager : public ProfileKeyedService, +class ShortcutManager : public BrowserContextKeyedService, public content::NotificationObserver { public: explicit ShortcutManager(Profile* profile); diff --git a/apps/shortcut_manager_factory.cc b/apps/shortcut_manager_factory.cc index b76b16dfba..f59e57584a 100644 --- a/apps/shortcut_manager_factory.cc +++ b/apps/shortcut_manager_factory.cc @@ -6,21 +6,14 @@ #include "apps/shortcut_manager.h" #include "chrome/browser/profiles/profile.h" -#include "chrome/browser/profiles/profile_dependency_manager.h" +#include "components/browser_context_keyed_service/browser_context_dependency_manager.h" namespace apps { // static ShortcutManager* ShortcutManagerFactory::GetForProfile(Profile* profile) { return static_cast<ShortcutManager*>( - GetInstance()->GetServiceForProfile(profile, true)); -} - -// static -void ShortcutManagerFactory::ResetForProfile(Profile* profile) { - ShortcutManagerFactory* factory = GetInstance(); - factory->ProfileShutdown(profile); - factory->ProfileDestroyed(profile); + GetInstance()->GetServiceForBrowserContext(profile, true)); } ShortcutManagerFactory* ShortcutManagerFactory::GetInstance() { @@ -28,19 +21,20 @@ ShortcutManagerFactory* ShortcutManagerFactory::GetInstance() { } ShortcutManagerFactory::ShortcutManagerFactory() - : ProfileKeyedServiceFactory("ShortcutManager", - ProfileDependencyManager::GetInstance()) { + : BrowserContextKeyedServiceFactory( + "ShortcutManager", + BrowserContextDependencyManager::GetInstance()) { } ShortcutManagerFactory::~ShortcutManagerFactory() { } -ProfileKeyedService* ShortcutManagerFactory::BuildServiceInstanceFor( +BrowserContextKeyedService* ShortcutManagerFactory::BuildServiceInstanceFor( content::BrowserContext* profile) const { return new ShortcutManager(static_cast<Profile*>(profile)); } -bool ShortcutManagerFactory::ServiceIsCreatedWithProfile() const { +bool ShortcutManagerFactory::ServiceIsCreatedWithBrowserContext() const { return true; } diff --git a/apps/shortcut_manager_factory.h b/apps/shortcut_manager_factory.h index bed2136c5d..8b41f062d2 100644 --- a/apps/shortcut_manager_factory.h +++ b/apps/shortcut_manager_factory.h @@ -5,10 +5,12 @@ #ifndef APPS_SHORTCUT_MANAGER_FACTORY_H_ #define APPS_SHORTCUT_MANAGER_FACTORY_H_ -#include "chrome/browser/profiles/profile_keyed_service_factory.h" +#include "components/browser_context_keyed_service/browser_context_keyed_service_factory.h" template<typename Type> struct DefaultSingletonTraits; +class Profile; + namespace apps { class ShortcutManager; @@ -17,12 +19,10 @@ class ShortcutManager; // Profiles. Listens for the Profile's destruction notification and cleans up // the associated ShortcutManager. // ShortcutManagers should not exist in incognito profiles. -class ShortcutManagerFactory : public ProfileKeyedServiceFactory { +class ShortcutManagerFactory : public BrowserContextKeyedServiceFactory { public: static ShortcutManager* GetForProfile(Profile* profile); - static void ResetForProfile(Profile* profile); - static ShortcutManagerFactory* GetInstance(); private: @@ -31,10 +31,10 @@ class ShortcutManagerFactory : public ProfileKeyedServiceFactory { ShortcutManagerFactory(); virtual ~ShortcutManagerFactory(); - // ProfileKeyedServiceFactory: - virtual ProfileKeyedService* BuildServiceInstanceFor( + // BrowserContextKeyedServiceFactory: + virtual BrowserContextKeyedService* BuildServiceInstanceFor( content::BrowserContext* profile) const OVERRIDE; - virtual bool ServiceIsCreatedWithProfile() const OVERRIDE; + virtual bool ServiceIsCreatedWithBrowserContext() const OVERRIDE; }; } // namespace apps |