diff options
author | sammc@chromium.org <sammc@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2013-05-25 14:09:24 +0000 |
---|---|---|
committer | sammc@chromium.org <sammc@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2013-05-25 14:09:24 +0000 |
commit | 961745f2cca727fe6144a28fd16ca536dbd94768 (patch) | |
tree | 0437c54836347b13227b0a45824cdbadefc0f063 /apps | |
parent | 1b6a233a4f2af1a5c02c99f0c8958b14d3efafbb (diff) | |
download | chromium_src-961745f2cca727fe6144a28fd16ca536dbd94768.zip chromium_src-961745f2cca727fe6144a28fd16ca536dbd94768.tar.gz chromium_src-961745f2cca727fe6144a28fd16ca536dbd94768.tar.bz2 |
Add support for persistent file access in apps.
This replaces chrome.fileSystem.getEntry(By)Id with retainEntry,
restoreEntry and isRestorable, which enable restoring file access after
an app is closed for apps with the fileSystem.retainFiles permission.
Retained files are evicted by LRU, with a maximum of 500 retained files.
BUG=224684
Review URL: https://chromiumcodereview.appspot.com/14607023
git-svn-id: svn://svn.chromium.org/chrome/trunk/src@202276 0039d316-1c4b-4281-b951-d872f2087c98
Diffstat (limited to 'apps')
-rw-r--r-- | apps/app_restore_service.cc | 27 | ||||
-rw-r--r-- | apps/app_restore_service.h | 11 | ||||
-rw-r--r-- | apps/app_restore_service_browsertest.cc | 30 | ||||
-rw-r--r-- | apps/apps.gypi | 4 | ||||
-rw-r--r-- | apps/saved_files_service.cc | 441 | ||||
-rw-r--r-- | apps/saved_files_service.h | 135 | ||||
-rw-r--r-- | apps/saved_files_service_factory.cc | 36 | ||||
-rw-r--r-- | apps/saved_files_service_factory.h | 35 | ||||
-rw-r--r-- | apps/saved_files_service_unittest.cc | 239 |
9 files changed, 913 insertions, 45 deletions
diff --git a/apps/app_restore_service.cc b/apps/app_restore_service.cc index a567811..88ac811 100644 --- a/apps/app_restore_service.cc +++ b/apps/app_restore_service.cc @@ -4,8 +4,8 @@ #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" @@ -70,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); + } } } } @@ -138,8 +141,6 @@ 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::RecordIfAppHasWindows( @@ -162,12 +163,8 @@ void AppRestoreService::RecordIfAppHasWindows( extension_prefs->SetHasWindows(id, has_windows); } -void AppRestoreService::RestoreApp( - const Extension* extension, - const std::vector<SavedFileEntry>& file_entries) { - extensions::RestartPlatformAppWithFileEntries(profile_, - extension, - file_entries); +void AppRestoreService::RestoreApp(const Extension* extension) { + extensions::RestartPlatformApp(profile_, extension); } void AppRestoreService::StartObservingShellWindows() { diff --git a/apps/app_restore_service.h b/apps/app_restore_service.h index 97dcd88..65d3de2 100644 --- a/apps/app_restore_service.h +++ b/apps/app_restore_service.h @@ -15,17 +15,10 @@ 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. @@ -61,9 +54,7 @@ class AppRestoreService : public BrowserContextKeyedService, void RecordAppStop(const std::string& extension_id); void RecordIfAppHasWindows(const std::string& id); - void RestoreApp( - const extensions::Extension* extension, - const std::vector<SavedFileEntry>& file_entries); + void RestoreApp(const extensions::Extension* extension); void StartObservingShellWindows(); void StopObservingShellWindows(); diff --git a/apps/app_restore_service_browsertest.cc b/apps/app_restore_service_browsertest.cc index ce800a4..cf832fe 100644 --- a/apps/app_restore_service_browsertest.cc +++ b/apps/app_restore_service_browsertest.cc @@ -4,7 +4,7 @@ #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_test_message_listener.h" @@ -12,7 +12,6 @@ #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; @@ -78,21 +77,15 @@ IN_PROC_BROWSER_TEST_F(PlatformAppBrowserTest, FileAccessIsSavedToPrefs) { ASSERT_TRUE(extension); file_written_listener.WaitUntilSatisfied(); - ExtensionPrefs* extension_prefs = - ExtensionPrefs::Get(browser()->profile()); + 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()); } @@ -124,12 +117,9 @@ IN_PROC_BROWSER_TEST_F(PlatformAppBrowserTest, FileAccessIsRestored) { ExtensionPrefs* extension_prefs = ExtensionPrefs::Get(browser()->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); + 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 @@ -137,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/apps.gypi b/apps/apps.gypi index 6ba1011..f6bfa6c 100644 --- a/apps/apps.gypi +++ b/apps/apps.gypi @@ -42,6 +42,10 @@ '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', diff --git a/apps/saved_files_service.cc b/apps/saved_files_service.cc new file mode 100644 index 0000000..c509c08 --- /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 0000000..613764a --- /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 0000000..c904770 --- /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 0000000..502010f --- /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 0000000..4d31485 --- /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 |