diff options
author | xiyuan@chromium.org <xiyuan@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2014-06-19 04:10:13 +0000 |
---|---|---|
committer | xiyuan@chromium.org <xiyuan@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2014-06-19 04:10:13 +0000 |
commit | c30bda26c225a049e105db1c1164833032c0682f (patch) | |
tree | c50de680ba0da6415d9710ef90dbd8fef3f98501 /chrome/browser/apps | |
parent | 494eb70be9ba547f417387fdde551c2ca785904f (diff) | |
download | chromium_src-c30bda26c225a049e105db1c1164833032c0682f.zip chromium_src-c30bda26c225a049e105db1c1164833032c0682f.tar.gz chromium_src-c30bda26c225a049e105db1c1164833032c0682f.tar.bz2 |
app_list: Drive app integration.
- DriveAppProvider to map Drive apps to chrome apps or local url apps;
- DriveAppMapping to track the mapped chrome apps;
- DriveServiceBridge to wrap DriveAPIService and DriveAppRegistry to
provide the user Drive apps info;
- Put feature behind "--enable-drive-apps-in-app-list";
BUG=358791,345066
TEST=DriveAppProviderTest.*:DriveAppMappingTest.*
Review URL: https://codereview.chromium.org/308003005
git-svn-id: svn://svn.chromium.org/chrome/trunk/src@278265 0039d316-1c4b-4281-b951-d872f2087c98
Diffstat (limited to 'chrome/browser/apps')
-rw-r--r-- | chrome/browser/apps/drive/OWNERS | 8 | ||||
-rw-r--r-- | chrome/browser/apps/drive/drive_app_converter.cc | 213 | ||||
-rw-r--r-- | chrome/browser/apps/drive/drive_app_converter.h | 74 | ||||
-rw-r--r-- | chrome/browser/apps/drive/drive_app_converter_browsertest.cc | 159 | ||||
-rw-r--r-- | chrome/browser/apps/drive/drive_app_mapping.cc | 127 | ||||
-rw-r--r-- | chrome/browser/apps/drive/drive_app_mapping.h | 47 | ||||
-rw-r--r-- | chrome/browser/apps/drive/drive_app_mapping_unittest.cc | 116 | ||||
-rw-r--r-- | chrome/browser/apps/drive/drive_app_provider.cc | 263 | ||||
-rw-r--r-- | chrome/browser/apps/drive/drive_app_provider.h | 96 | ||||
-rw-r--r-- | chrome/browser/apps/drive/drive_app_provider_browsertest.cc | 453 | ||||
-rw-r--r-- | chrome/browser/apps/drive/drive_service_bridge.cc | 138 | ||||
-rw-r--r-- | chrome/browser/apps/drive/drive_service_bridge.h | 35 |
12 files changed, 1729 insertions, 0 deletions
diff --git a/chrome/browser/apps/drive/OWNERS b/chrome/browser/apps/drive/OWNERS new file mode 100644 index 0000000..061c003 --- /dev/null +++ b/chrome/browser/apps/drive/OWNERS @@ -0,0 +1,8 @@ +benwells@chromium.org +calamity@chromium.org +stevenjb@chromium.org +tapted@chromium.org +xiyuan@chromium.org + +# Drive API +kinaba@chromium.org diff --git a/chrome/browser/apps/drive/drive_app_converter.cc b/chrome/browser/apps/drive/drive_app_converter.cc new file mode 100644 index 0000000..592622d --- /dev/null +++ b/chrome/browser/apps/drive/drive_app_converter.cc @@ -0,0 +1,213 @@ +// Copyright 2014 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "chrome/browser/apps/drive/drive_app_converter.h" + +#include <algorithm> +#include <set> + +#include "base/logging.h" +#include "base/memory/ref_counted.h" +#include "base/strings/utf_string_conversions.h" +#include "chrome/browser/extensions/crx_installer.h" +#include "chrome/browser/extensions/install_tracker.h" +#include "chrome/browser/image_decoder.h" +#include "chrome/browser/profiles/profile.h" +#include "content/public/browser/browser_thread.h" +#include "extensions/browser/extension_system.h" +#include "extensions/common/constants.h" +#include "net/base/load_flags.h" +#include "net/http/http_status_code.h" +#include "net/url_request/url_fetcher.h" +#include "net/url_request/url_fetcher_delegate.h" +#include "net/url_request/url_request_status.h" +#include "third_party/skia/include/core/SkBitmap.h" + +using content::BrowserThread; + +// IconFetcher downloads |icon_url| using |converter|'s profile. The icon +// url is passed from a DriveAppInfo and should follow icon url definition +// in Drive API: +// https://developers.google.com/drive/v2/reference/apps#resource +// Each icon url represents a single image associated with a certain size. +class DriveAppConverter::IconFetcher : public net::URLFetcherDelegate, + public ImageDecoder::Delegate { + public: + IconFetcher(DriveAppConverter* converter, + const GURL& icon_url, + int expected_size) + : converter_(converter), + icon_url_(icon_url), + expected_size_(expected_size) {} + virtual ~IconFetcher() { + if (image_decoder_.get()) + image_decoder_->set_delegate(NULL); + } + + void Start() { + fetcher_.reset( + net::URLFetcher::Create(icon_url_, net::URLFetcher::GET, this)); + fetcher_->SetRequestContext(converter_->profile_->GetRequestContext()); + fetcher_->SetLoadFlags(net::LOAD_DO_NOT_SAVE_COOKIES); + fetcher_->Start(); + } + + const GURL& icon_url() const { return icon_url_; } + const SkBitmap& icon() const { return icon_; } + + private: + // net::URLFetcherDelegate overrides: + virtual void OnURLFetchComplete(const net::URLFetcher* source) OVERRIDE { + CHECK_EQ(fetcher_.get(), source); + scoped_ptr<net::URLFetcher> fetcher(fetcher_.Pass()); + + if (!fetcher->GetStatus().is_success() || + fetcher->GetResponseCode() != net::HTTP_OK) { + converter_->OnIconFetchComplete(this); + return; + } + + std::string unsafe_icon_data; + fetcher->GetResponseAsString(&unsafe_icon_data); + + image_decoder_ = + new ImageDecoder(this, unsafe_icon_data, ImageDecoder::DEFAULT_CODEC); + image_decoder_->Start( + BrowserThread::GetMessageLoopProxyForThread(BrowserThread::UI)); + } + + // ImageDecoder::Delegate overrides: + virtual void OnImageDecoded(const ImageDecoder* decoder, + const SkBitmap& decoded_image) OVERRIDE { + if (decoded_image.width() == expected_size_) + icon_ = decoded_image; + converter_->OnIconFetchComplete(this); + } + + virtual void OnDecodeImageFailed(const ImageDecoder* decoder) OVERRIDE { + converter_->OnIconFetchComplete(this); + } + + DriveAppConverter* converter_; + const GURL icon_url_; + const int expected_size_; + + scoped_ptr<net::URLFetcher> fetcher_; + scoped_refptr<ImageDecoder> image_decoder_; + SkBitmap icon_; + + DISALLOW_COPY_AND_ASSIGN(IconFetcher); +}; + +DriveAppConverter::DriveAppConverter(Profile* profile, + const drive::DriveAppInfo& drive_app_info, + const FinishedCallback& finished_callback) + : profile_(profile), + drive_app_info_(drive_app_info), + extension_(NULL), + is_new_install_(false), + finished_callback_(finished_callback) { + DCHECK(profile_); +} + +DriveAppConverter::~DriveAppConverter() { + PostInstallCleanUp(); +} + +void DriveAppConverter::Start() { + DCHECK(!IsStarted()); + + if (drive_app_info_.app_name.empty() || + !drive_app_info_.create_url.is_valid()) { + finished_callback_.Run(this, false); + return; + } + + web_app_.title = base::UTF8ToUTF16(drive_app_info_.app_name); + web_app_.app_url = drive_app_info_.create_url; + + const std::set<int> allowed_sizes(extension_misc::kExtensionIconSizes, + extension_misc::kExtensionIconSizes + + extension_misc::kNumExtensionIconSizes); + std::set<int> pending_sizes; + for (size_t i = 0; i < drive_app_info_.app_icons.size(); ++i) { + const int icon_size = drive_app_info_.app_icons[i].first; + if (allowed_sizes.find(icon_size) == allowed_sizes.end() || + pending_sizes.find(icon_size) != pending_sizes.end()) { + continue; + } + + pending_sizes.insert(icon_size); + const GURL& icon_url = drive_app_info_.app_icons[i].second; + IconFetcher* fetcher = new IconFetcher(this, icon_url, icon_size); + fetchers_.push_back(fetcher); // Pass ownership to |fetchers|. + fetcher->Start(); + } + + if (fetchers_.empty()) + StartInstall(); +} + +bool DriveAppConverter::IsStarted() const { + return !fetchers_.empty() || crx_installer_; +} + +bool DriveAppConverter::IsInstalling(const std::string& app_id) const { + return crx_installer_ && crx_installer_->extension() && + crx_installer_->extension()->id() == app_id; +} + +void DriveAppConverter::OnIconFetchComplete(const IconFetcher* fetcher) { + const SkBitmap& icon = fetcher->icon(); + if (!icon.empty() && icon.width() != 0) { + WebApplicationInfo::IconInfo icon_info; + icon_info.url = fetcher->icon_url(); + icon_info.data = icon; + icon_info.width = icon.width(); + icon_info.height = icon.height(); + web_app_.icons.push_back(icon_info); + } + + fetchers_.erase(std::find(fetchers_.begin(), fetchers_.end(), fetcher)); + + if (fetchers_.empty()) + StartInstall(); +} + +void DriveAppConverter::StartInstall() { + DCHECK(!crx_installer_); + crx_installer_ = extensions::CrxInstaller::CreateSilent( + extensions::ExtensionSystem::Get(profile_)->extension_service()); + // The converted url app should not be syncable. Drive apps go with the user's + // account and url apps will be created when needed. Syncing those apps could + // hit an edge case where the synced url apps become orphans when the user has + // corresponding chrome apps. + crx_installer_->set_do_not_sync(true); + + extensions::InstallTracker::Get(profile_)->AddObserver(this); + crx_installer_->InstallWebApp(web_app_); +} + +void DriveAppConverter::PostInstallCleanUp() { + if (!crx_installer_) + return; + + extensions::InstallTracker::Get(profile_)->RemoveObserver(this); + crx_installer_ = NULL; +} + +void DriveAppConverter::OnFinishCrxInstall(const std::string& extension_id, + bool success) { + if (!crx_installer_->extension() || + crx_installer_->extension()->id() != extension_id) { + return; + } + + extension_ = crx_installer_->extension(); + is_new_install_ = success && crx_installer_->current_version().empty(); + PostInstallCleanUp(); + + finished_callback_.Run(this, success); + // |finished_callback_| could delete this. +} diff --git a/chrome/browser/apps/drive/drive_app_converter.h b/chrome/browser/apps/drive/drive_app_converter.h new file mode 100644 index 0000000..772ea1e --- /dev/null +++ b/chrome/browser/apps/drive/drive_app_converter.h @@ -0,0 +1,74 @@ +// Copyright 2014 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef CHROME_BROWSER_APPS_DRIVE_DRIVE_APP_CONVERTER_H_ +#define CHROME_BROWSER_APPS_DRIVE_DRIVE_APP_CONVERTER_H_ + +#include <string> + +#include "base/callback.h" +#include "base/macros.h" +#include "base/memory/ref_counted.h" +#include "base/memory/scoped_vector.h" +#include "chrome/browser/drive/drive_app_registry.h" +#include "chrome/browser/extensions/install_observer.h" +#include "chrome/common/web_application_info.h" + +class Profile; + +namespace extensions { +class CrxInstaller; +class Extension; +} + +// DriveAppConverter creates and installs a local URL app for the given +// DriveAppInfo into the given profile. +class DriveAppConverter : public extensions::InstallObserver { + public: + typedef base::Callback<void(const DriveAppConverter*, bool success)> + FinishedCallback; + + DriveAppConverter(Profile* profile, + const drive::DriveAppInfo& drive_app_info, + const FinishedCallback& finished_callback); + virtual ~DriveAppConverter(); + + void Start(); + bool IsStarted() const; + + bool IsInstalling(const std::string& app_id) const; + + const drive::DriveAppInfo& drive_app_info() const { return drive_app_info_; } + const extensions::Extension* extension() const { return extension_; } + bool is_new_install() const { return is_new_install_; } + + private: + class IconFetcher; + + // Callbacks from IconFetcher. + void OnIconFetchComplete(const IconFetcher* fetcher); + + void StartInstall(); + void PostInstallCleanUp(); + + // extensions::InstallObserver: + virtual void OnFinishCrxInstall(const std::string& extension_id, + bool success) OVERRIDE; + + Profile* profile_; + const drive::DriveAppInfo drive_app_info_; + + WebApplicationInfo web_app_; + const extensions::Extension* extension_; + bool is_new_install_; + + ScopedVector<IconFetcher> fetchers_; + scoped_refptr<extensions::CrxInstaller> crx_installer_; + + FinishedCallback finished_callback_; + + DISALLOW_COPY_AND_ASSIGN(DriveAppConverter); +}; + +#endif // CHROME_BROWSER_APPS_DRIVE_DRIVE_APP_CONVERTER_H_ diff --git a/chrome/browser/apps/drive/drive_app_converter_browsertest.cc b/chrome/browser/apps/drive/drive_app_converter_browsertest.cc new file mode 100644 index 0000000..ce28ba4 --- /dev/null +++ b/chrome/browser/apps/drive/drive_app_converter_browsertest.cc @@ -0,0 +1,159 @@ +// Copyright 2014 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "chrome/browser/apps/drive/drive_app_converter.h" + +#include <utility> + +#include "base/bind.h" +#include "base/memory/scoped_ptr.h" +#include "base/path_service.h" +#include "base/version.h" +#include "chrome/browser/extensions/extension_browsertest.h" +#include "chrome/browser/extensions/extension_service.h" +#include "chrome/browser/extensions/extension_util.h" +#include "chrome/common/chrome_paths.h" +#include "chrome/common/extensions/extension_constants.h" +#include "chrome/common/extensions/manifest_handlers/app_launch_info.h" +#include "content/public/test/test_utils.h" +#include "extensions/browser/extension_system.h" +#include "extensions/common/extension.h" +#include "extensions/common/manifest_handlers/icons_handler.h" +#include "extensions/common/permissions/permission_set.h" +#include "extensions/common/permissions/permissions_data.h" +#include "net/test/embedded_test_server/embedded_test_server.h" +#include "testing/gtest/include/gtest/gtest.h" + +using extensions::AppLaunchInfo; +using extensions::Extension; +using extensions::ExtensionRegistry; + +namespace { + +const char kAppName[] = "Test drive app"; +const char kAppUrl[] = "http://foobar.com/drive_app"; + +} // namespace + +class DriveAppConverterTest : public ExtensionBrowserTest { + public: + DriveAppConverterTest() {} + virtual ~DriveAppConverterTest() {} + + // ExtensionBrowserTest: + virtual void SetUpOnMainThread() OVERRIDE { + ExtensionBrowserTest::SetUpOnMainThread(); + + base::FilePath test_data_dir; + PathService::Get(chrome::DIR_TEST_DATA, &test_data_dir); + embedded_test_server()->ServeFilesFromDirectory(test_data_dir); + ASSERT_TRUE(embedded_test_server()->InitializeAndWaitUntilReady()); + } + + void InstallAndWaitFinish(const drive::DriveAppInfo& drive_app) { + runner_ = new content::MessageLoopRunner; + + converter_.reset(new DriveAppConverter( + profile(), + drive_app, + base::Bind(&DriveAppConverterTest::ConverterFinished, + base::Unretained(this)))); + converter_->Start(); + + runner_->Run(); + } + + GURL GetTestUrl(const std::string& path) { + return embedded_test_server()->base_url().Resolve(path); + } + + drive::DriveAppInfo GetTestDriveApp() { + // Define four icons. icon1.png is 16x16 and good to use. icon2.png is + // 16x16 but claims to be 32x32 and should be dropped. icon3.png is 66x66 + // and not a valid extension icon size and should be dropped too. The forth + // one is icon2.png with 16x16 but should be ignored because 16x16 already + // has icon1.png as its resource. + drive::DriveAppInfo::IconList app_icons; + app_icons.push_back(std::make_pair(16, GetTestUrl("extensions/icon1.png"))); + app_icons.push_back(std::make_pair(32, GetTestUrl("extensions/icon2.png"))); + app_icons.push_back(std::make_pair(66, GetTestUrl("extensions/icon3.png"))); + app_icons.push_back(std::make_pair(16, GetTestUrl("extensions/icon2.png"))); + + drive::DriveAppInfo::IconList document_icons; + + return drive::DriveAppInfo("fake_drive_app_id", + "fake_product_id", + app_icons, + document_icons, + kAppName, + GURL(kAppUrl), + true); + } + + const DriveAppConverter* converter() const { return converter_.get(); } + + private: + void ConverterFinished(const DriveAppConverter* converter, bool success) { + if (runner_) + runner_->Quit(); + } + + scoped_ptr<DriveAppConverter> converter_; + scoped_refptr<content::MessageLoopRunner> runner_; + + DISALLOW_COPY_AND_ASSIGN(DriveAppConverterTest); +}; + +IN_PROC_BROWSER_TEST_F(DriveAppConverterTest, GoodApp) { + InstallAndWaitFinish(GetTestDriveApp()); + + const Extension* app = converter()->extension(); + ASSERT_TRUE(app != NULL); + EXPECT_EQ(kAppName, app->name()); + EXPECT_TRUE(app->is_hosted_app()); + EXPECT_TRUE(app->from_bookmark()); + EXPECT_EQ(GURL(kAppUrl), AppLaunchInfo::GetLaunchWebURL(app)); + EXPECT_EQ(extensions::LAUNCH_CONTAINER_TAB, + AppLaunchInfo::GetLaunchContainer(app)); + EXPECT_EQ(0u, app->permissions_data()->active_permissions()->apis().size()); + EXPECT_EQ(1u, extensions::IconsInfo::GetIcons(app).map().size()); + + const Extension* installed = extensions::ExtensionSystem::Get(profile()) + ->extension_service() + ->GetInstalledExtension(app->id()); + EXPECT_EQ(app, installed); + EXPECT_FALSE(extensions::util::ShouldSyncApp(app, profile())); +} + +IN_PROC_BROWSER_TEST_F(DriveAppConverterTest, BadApp) { + drive::DriveAppInfo no_name = GetTestDriveApp(); + no_name.app_name.clear(); + InstallAndWaitFinish(no_name); + EXPECT_TRUE(converter()->extension() == NULL); + + drive::DriveAppInfo no_url = GetTestDriveApp(); + no_url.create_url = GURL(); + InstallAndWaitFinish(no_url); + EXPECT_TRUE(converter()->extension() == NULL); +} + +IN_PROC_BROWSER_TEST_F(DriveAppConverterTest, InstallTwice) { + InstallAndWaitFinish(GetTestDriveApp()); + const Extension* first_install = converter()->extension(); + ASSERT_TRUE(first_install != NULL); + EXPECT_TRUE(converter()->is_new_install()); + const std::string first_install_id = first_install->id(); + const base::Version first_install_version(first_install->VersionString()); + ASSERT_TRUE(first_install_version.IsValid()); + + InstallAndWaitFinish(GetTestDriveApp()); + const Extension* second_install = converter()->extension(); + ASSERT_TRUE(second_install != NULL); + EXPECT_FALSE(converter()->is_new_install()); + + // Two different app instances. + ASSERT_NE(first_install, second_install); + EXPECT_EQ(first_install_id, second_install->id()); + EXPECT_GE(second_install->version()->CompareTo(first_install_version), 0); +} diff --git a/chrome/browser/apps/drive/drive_app_mapping.cc b/chrome/browser/apps/drive/drive_app_mapping.cc new file mode 100644 index 0000000..ae95310 --- /dev/null +++ b/chrome/browser/apps/drive/drive_app_mapping.cc @@ -0,0 +1,127 @@ +// Copyright 2014 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "chrome/browser/apps/drive/drive_app_mapping.h" + +#include "base/prefs/pref_service.h" +#include "base/prefs/scoped_user_pref_update.h" +#include "base/values.h" +#include "chrome/common/pref_names.h" +#include "components/pref_registry/pref_registry_syncable.h" + +namespace { + +// Key for a string value that holds the mapped chrome app id. +const char kKeyChromeApp[] = "chrome_app"; + +// Key for a boolean value of whether the mapped chrome app is auto generated. +// Default is false. +const char kKeyGenerated[] = "generated"; + +scoped_ptr<base::DictionaryValue> CreateInfoDict( + const std::string& chrome_app_id, + bool generated) { + scoped_ptr<base::DictionaryValue> dict(new base::DictionaryValue); + dict->SetStringWithoutPathExpansion(kKeyChromeApp, chrome_app_id); + + // Only writes non-default value. + if (generated) + dict->SetBooleanWithoutPathExpansion(kKeyGenerated, true); + return dict.Pass(); +} + +} // namespace + +DriveAppMapping::DriveAppMapping(PrefService* prefs) : prefs_(prefs) { +} + +DriveAppMapping::~DriveAppMapping() { +} + +// static +void DriveAppMapping::RegisterProfilePrefs( + user_prefs::PrefRegistrySyncable* registry) { + registry->RegisterDictionaryPref( + prefs::kAppLauncherDriveAppMapping, + user_prefs::PrefRegistrySyncable::UNSYNCABLE_PREF); +} + +void DriveAppMapping::Add(const std::string& drive_app_id, + const std::string& chrome_app_id, + bool generated) { + DictionaryPrefUpdate update(prefs_, prefs::kAppLauncherDriveAppMapping); + update->SetWithoutPathExpansion( + drive_app_id, CreateInfoDict(chrome_app_id, generated).release()); +} + +void DriveAppMapping::Remove(const std::string& drive_app_id) { + DictionaryPrefUpdate update(prefs_, prefs::kAppLauncherDriveAppMapping); + update->RemoveWithoutPathExpansion(drive_app_id, NULL); +} + +std::string DriveAppMapping::GetChromeApp( + const std::string& drive_app_id) const { + const base::DictionaryValue* mapping = + prefs_->GetDictionary(prefs::kAppLauncherDriveAppMapping); + const base::DictionaryValue* info_dict; + std::string chrome_app_id; + if (mapping->GetDictionaryWithoutPathExpansion(drive_app_id, &info_dict) && + info_dict->GetStringWithoutPathExpansion(kKeyChromeApp, &chrome_app_id)) { + return chrome_app_id; + } + + return std::string(); +} + +std::string DriveAppMapping::GetDriveApp( + const std::string& chrome_app_id) const { + const base::DictionaryValue* mapping = + prefs_->GetDictionary(prefs::kAppLauncherDriveAppMapping); + for (base::DictionaryValue::Iterator it(*mapping); !it.IsAtEnd(); + it.Advance()) { + const base::DictionaryValue* info_dict; + std::string value_string; + DCHECK(it.value().IsType(base::Value::TYPE_DICTIONARY)); + if (it.value().GetAsDictionary(&info_dict) && + info_dict->GetStringWithoutPathExpansion(kKeyChromeApp, + &value_string) && + value_string == chrome_app_id) { + return it.key(); + } + } + return std::string(); +} + +bool DriveAppMapping::IsChromeAppGenerated( + const std::string& chrome_app_id) const { + const base::DictionaryValue* mapping = + prefs_->GetDictionary(prefs::kAppLauncherDriveAppMapping); + for (base::DictionaryValue::Iterator it(*mapping); !it.IsAtEnd(); + it.Advance()) { + const base::DictionaryValue* info_dict; + std::string value_string; + bool generated; + DCHECK(it.value().IsType(base::Value::TYPE_DICTIONARY)); + if (it.value().GetAsDictionary(&info_dict) && + info_dict->GetStringWithoutPathExpansion(kKeyChromeApp, + &value_string) && + value_string == chrome_app_id && + info_dict->GetBooleanWithoutPathExpansion(kKeyGenerated, &generated)) { + return generated; + } + } + + return false; +} + +std::set<std::string> DriveAppMapping::GetDriveAppIds() const { + const base::DictionaryValue* mapping = + prefs_->GetDictionary(prefs::kAppLauncherDriveAppMapping); + std::set<std::string> keys; + for (base::DictionaryValue::Iterator it(*mapping); !it.IsAtEnd(); + it.Advance()) { + keys.insert(it.key()); + } + return keys; +} diff --git a/chrome/browser/apps/drive/drive_app_mapping.h b/chrome/browser/apps/drive/drive_app_mapping.h new file mode 100644 index 0000000..be3c1ee --- /dev/null +++ b/chrome/browser/apps/drive/drive_app_mapping.h @@ -0,0 +1,47 @@ +// Copyright 2014 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef CHROME_BROWSER_APPS_DRIVE_DRIVE_APP_MAPPING_H_ +#define CHROME_BROWSER_APPS_DRIVE_DRIVE_APP_MAPPING_H_ + +#include <set> +#include <string> + +#include "base/macros.h" + +namespace user_prefs { +class PrefRegistrySyncable; +} + +class PrefService; + +// DriveAppMapping tracks the mapping between Drive apps and corresponding +// Chrome apps. The data is backed by kAppLauncherDriveAppMapping pref. +class DriveAppMapping { + public: + explicit DriveAppMapping(PrefService* prefs); + ~DriveAppMapping(); + + static void RegisterProfilePrefs(user_prefs::PrefRegistrySyncable* registry); + + // Adds a mapping from |drive_app_id| to |chrome_app_id|. |generated| + // represents whether the corresponding Chrome app is generated. + void Add(const std::string& drive_app_id, + const std::string& chrome_app_id, + bool generated); + void Remove(const std::string& drive_app_id); + + std::string GetChromeApp(const std::string& drive_app_id) const; + std::string GetDriveApp(const std::string& chrome_app_id) const; + bool IsChromeAppGenerated(const std::string& chrome_app_id) const; + + std::set<std::string> GetDriveAppIds() const; + + private: + PrefService* prefs_; + + DISALLOW_COPY_AND_ASSIGN(DriveAppMapping); +}; + +#endif // CHROME_BROWSER_APPS_DRIVE_DRIVE_APP_MAPPING_H_ diff --git a/chrome/browser/apps/drive/drive_app_mapping_unittest.cc b/chrome/browser/apps/drive/drive_app_mapping_unittest.cc new file mode 100644 index 0000000..a1f8949 --- /dev/null +++ b/chrome/browser/apps/drive/drive_app_mapping_unittest.cc @@ -0,0 +1,116 @@ +// Copyright 2014 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "chrome/browser/apps/drive/drive_app_mapping.h" + +#include "base/macros.h" +#include "base/memory/scoped_ptr.h" +#include "chrome/test/base/testing_pref_service_syncable.h" +#include "testing/gtest/include/gtest/gtest.h" + +class DriveAppMappingTest : public testing::Test { + public: + DriveAppMappingTest() {} + virtual ~DriveAppMappingTest() {} + + // testing::Test: + virtual void SetUp() OVERRIDE { + pref_service_.reset(new TestingPrefServiceSyncable); + DriveAppMapping::RegisterProfilePrefs(pref_service_->registry()); + + mapping_.reset(new DriveAppMapping(pref_service_.get())); + } + + DriveAppMapping* mapping() { return mapping_.get(); } + + private: + scoped_ptr<TestingPrefServiceSyncable> pref_service_; + scoped_ptr<DriveAppMapping> mapping_; + + DISALLOW_COPY_AND_ASSIGN(DriveAppMappingTest); +}; + +TEST_F(DriveAppMappingTest, Empty) { + EXPECT_EQ("", mapping()->GetChromeApp("")); + EXPECT_EQ("", mapping()->GetDriveApp("")); + EXPECT_EQ("", mapping()->GetChromeApp("non-existent-drive-app")); + EXPECT_EQ("", mapping()->GetDriveApp("non-existent-chrome-app")); + EXPECT_EQ(0u, mapping()->GetDriveAppIds().size()); +} + +TEST_F(DriveAppMappingTest, Add) { + std::set<std::string> drive_app_ids; + + // Add one. + mapping()->Add("drive", "chrome", false); + EXPECT_EQ("chrome", mapping()->GetChromeApp("drive")); + EXPECT_EQ("drive", mapping()->GetDriveApp("chrome")); + EXPECT_FALSE(mapping()->IsChromeAppGenerated("chrome")); + drive_app_ids = mapping()->GetDriveAppIds(); + EXPECT_EQ(1u, drive_app_ids.size()); + EXPECT_EQ(1u, drive_app_ids.count("drive")); + + // Overwrite previous mapping if added under the same key. + mapping()->Add("drive", "another-chrome-app", true); + EXPECT_EQ("", mapping()->GetDriveApp("chrome")); + EXPECT_FALSE(mapping()->IsChromeAppGenerated("chrome")); + EXPECT_EQ("another-chrome-app", mapping()->GetChromeApp("drive")); + EXPECT_EQ("drive", mapping()->GetDriveApp("another-chrome-app")); + EXPECT_TRUE(mapping()->IsChromeAppGenerated("another-chrome-app")); + drive_app_ids = mapping()->GetDriveAppIds(); + EXPECT_EQ(1u, drive_app_ids.size()); + EXPECT_EQ(1u, drive_app_ids.count("drive")); + + // Add another one. + mapping()->Add("drive-1", "chrome-1", false); + EXPECT_EQ("chrome-1", mapping()->GetChromeApp("drive-1")); + EXPECT_EQ("drive-1", mapping()->GetDriveApp("chrome-1")); + drive_app_ids = mapping()->GetDriveAppIds(); + EXPECT_EQ(2u, drive_app_ids.size()); + EXPECT_EQ(1u, drive_app_ids.count("drive")); + EXPECT_EQ(1u, drive_app_ids.count("drive-1")); + + // Previous mapping should not be affected by new mapping. + EXPECT_EQ("another-chrome-app", mapping()->GetChromeApp("drive")); + EXPECT_EQ("drive", mapping()->GetDriveApp("another-chrome-app")); + EXPECT_TRUE(mapping()->IsChromeAppGenerated("another-chrome-app")); + + // Non-existent value returns empty string. + EXPECT_EQ("", mapping()->GetChromeApp("non-existent-drive-app")); + EXPECT_EQ("", mapping()->GetDriveApp("non-existent-chrome-app")); +} + +TEST_F(DriveAppMappingTest, Remove) { + std::set<std::string> drive_app_ids; + + // Prepare data. + mapping()->Add("drive-1", "chrome-1", false); + mapping()->Add("drive-2", "chrome-2", false); + drive_app_ids = mapping()->GetDriveAppIds(); + EXPECT_EQ(2u, drive_app_ids.size()); + + // Remove non-existent. + mapping()->Remove("non-existent-drive-app"); + drive_app_ids = mapping()->GetDriveAppIds(); + EXPECT_EQ(2u, drive_app_ids.size()); + + // Remove one. + EXPECT_EQ("chrome-1", mapping()->GetChromeApp("drive-1")); + mapping()->Remove("drive-1"); + EXPECT_EQ("", mapping()->GetChromeApp("drive-1")); + drive_app_ids = mapping()->GetDriveAppIds(); + EXPECT_EQ(1u, drive_app_ids.size()); + + // Remove it again has no effect. + mapping()->Remove("drive-1"); + drive_app_ids = mapping()->GetDriveAppIds(); + EXPECT_EQ(1u, drive_app_ids.size()); + + // Remove another one. + EXPECT_EQ("chrome-2", mapping()->GetChromeApp("drive-2")); + mapping()->Remove("drive-2"); + EXPECT_EQ("", mapping()->GetChromeApp("drive-2")); + drive_app_ids = mapping()->GetDriveAppIds(); + EXPECT_EQ(0u, drive_app_ids.size()); +} diff --git a/chrome/browser/apps/drive/drive_app_provider.cc b/chrome/browser/apps/drive/drive_app_provider.cc new file mode 100644 index 0000000..2bde9fe --- /dev/null +++ b/chrome/browser/apps/drive/drive_app_provider.cc @@ -0,0 +1,263 @@ +// Copyright 2014 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "chrome/browser/apps/drive/drive_app_provider.h" + +#include <vector> + +#include "base/bind.h" +#include "base/bind_helpers.h" +#include "base/logging.h" +#include "base/message_loop/message_loop.h" +#include "base/stl_util.h" +#include "chrome/browser/apps/drive/drive_app_converter.h" +#include "chrome/browser/apps/drive/drive_app_mapping.h" +#include "chrome/browser/apps/drive/drive_service_bridge.h" +#include "chrome/browser/drive/drive_app_registry.h" +#include "chrome/browser/extensions/extension_service.h" +#include "chrome/browser/profiles/profile.h" +#include "chrome/common/extensions/manifest_handlers/app_launch_info.h" +#include "extensions/browser/extension_registry.h" +#include "extensions/browser/extension_registry_factory.h" +#include "extensions/browser/extension_system.h" +#include "extensions/common/extension.h" + +using extensions::Extension; +using extensions::ExtensionRegistry; + +namespace { + +void IgnoreUninstallResult(google_apis::GDataErrorCode) { +} + +} // namespace + +DriveAppProvider::DriveAppProvider(Profile* profile) + : profile_(profile), + service_bridge_(DriveServiceBridge::Create(profile).Pass()), + mapping_(new DriveAppMapping(profile->GetPrefs())), + weak_ptr_factory_(this) { + service_bridge_->GetAppRegistry()->AddObserver(this); + ExtensionRegistry::Get(profile_)->AddObserver(this); +} + +DriveAppProvider::~DriveAppProvider() { + ExtensionRegistry::Get(profile_)->RemoveObserver(this); + service_bridge_->GetAppRegistry()->RemoveObserver(this); +} + +// static +void DriveAppProvider::AppendDependsOnFactories( + std::set<BrowserContextKeyedServiceFactory*>* factories) { + factories->insert(extensions::ExtensionRegistryFactory::GetInstance()); + DriveServiceBridge::AppendDependsOnFactories(factories); +} + +void DriveAppProvider::SetDriveServiceBridgeForTest( + scoped_ptr<DriveServiceBridge> test_bridge) { + service_bridge_->GetAppRegistry()->RemoveObserver(this); + service_bridge_ = test_bridge.Pass(); + service_bridge_->GetAppRegistry()->AddObserver(this); +} + +void DriveAppProvider::UpdateMappingAndExtensionSystem( + const std::string& drive_app_id, + const Extension* new_app, + bool is_new_app_generated) { + const std::string& new_chrome_app_id = new_app->id(); + + const std::string existing_chrome_app_id = + mapping_->GetChromeApp(drive_app_id); + if (existing_chrome_app_id == new_chrome_app_id) + return; + + const bool is_existing_app_generated = + mapping_->IsChromeAppGenerated(existing_chrome_app_id); + mapping_->Add(drive_app_id, new_chrome_app_id, is_new_app_generated); + + const Extension* existing_app = + ExtensionRegistry::Get(profile_)->GetExtensionById( + existing_chrome_app_id, ExtensionRegistry::EVERYTHING); + if (existing_app && is_existing_app_generated) { + extensions::ExtensionSystem::Get(profile_) + ->extension_service() + ->UninstallExtension(existing_chrome_app_id, false, NULL); + } +} + +void DriveAppProvider::ProcessDeferredOnExtensionInstalled( + const std::string drive_app_id, + const std::string chrome_app_id) { + const Extension* app = ExtensionRegistry::Get(profile_)->GetExtensionById( + chrome_app_id, ExtensionRegistry::EVERYTHING); + if (!app) + return; + + UpdateMappingAndExtensionSystem(drive_app_id, app, false); +} + +void DriveAppProvider::SchedulePendingConverters() { + if (pending_converters_.empty()) + return; + + if (!pending_converters_.front()->IsStarted()) + pending_converters_.front()->Start(); +} + +void DriveAppProvider::OnLocalAppConverted(const DriveAppConverter* converter, + bool success) { + DCHECK_EQ(pending_converters_.front(), converter); + + if (success) { + const bool was_generated = + mapping_->GetDriveApp(converter->extension()->id()) == + converter->drive_app_info().app_id && + mapping_->IsChromeAppGenerated(converter->extension()->id()); + UpdateMappingAndExtensionSystem( + converter->drive_app_info().app_id, + converter->extension(), + converter->is_new_install() || was_generated); + } else { + LOG(WARNING) << "Failed to convert drive app to web app, " + << "drive app id= " << converter->drive_app_info().app_id + << ", name=" << converter->drive_app_info().app_name; + } + + pending_converters_.erase(pending_converters_.begin()); + SchedulePendingConverters(); +} + +bool DriveAppProvider::IsMappedUrlAppUpToDate( + const drive::DriveAppInfo& drive_app) const { + const std::string& url_app_id = mapping_->GetChromeApp(drive_app.app_id); + if (url_app_id.empty()) + return false; + + const Extension* url_app = ExtensionRegistry::Get(profile_)->GetExtensionById( + url_app_id, ExtensionRegistry::EVERYTHING); + if (!url_app) + return false; + DCHECK(url_app->is_hosted_app() && url_app->from_bookmark()); + + return drive_app.app_name == url_app->name() && + drive_app.create_url == + extensions::AppLaunchInfo::GetLaunchWebURL(url_app); +} + +void DriveAppProvider::AddOrUpdateDriveApp( + const drive::DriveAppInfo& drive_app) { + const Extension* chrome_app = + ExtensionRegistry::Get(profile_)->GetExtensionById( + drive_app.product_id, ExtensionRegistry::EVERYTHING); + if (chrome_app) { + UpdateMappingAndExtensionSystem(drive_app.app_id, chrome_app, false); + return; + } + + if (IsMappedUrlAppUpToDate(drive_app)) + return; + + ScopedVector<DriveAppConverter>::iterator it = pending_converters_.begin(); + while (it != pending_converters_.end()) { + if (!(*it)->IsStarted() && + (*it)->drive_app_info().app_id == drive_app.app_id) { + it = pending_converters_.erase(it); + } else { + ++it; + } + } + + pending_converters_.push_back( + new DriveAppConverter(profile_, + drive_app, + base::Bind(&DriveAppProvider::OnLocalAppConverted, + base::Unretained(this)))); +} + +void DriveAppProvider::ProcessRemovedDriveApp(const std::string& drive_app_id) { + const std::string chrome_app_id = mapping_->GetChromeApp(drive_app_id); + const bool is_generated = mapping_->IsChromeAppGenerated(chrome_app_id); + mapping_->Remove(drive_app_id); + + if (chrome_app_id.empty() || !is_generated) + return; + + const Extension* existing_app = + ExtensionRegistry::Get(profile_) + ->GetExtensionById(chrome_app_id, ExtensionRegistry::EVERYTHING); + if (!existing_app) + return; + + extensions::ExtensionSystem::Get(profile_) + ->extension_service() + ->UninstallExtension(chrome_app_id, false, NULL); +} + +void DriveAppProvider::OnDriveAppRegistryUpdated() { + service_bridge_->GetAppRegistry()->GetAppList(&drive_apps_); + + IdSet current_ids; + for (size_t i = 0; i < drive_apps_.size(); ++i) + current_ids.insert(drive_apps_[i].app_id); + + const IdSet existing_ids = mapping_->GetDriveAppIds(); + const IdSet ids_to_remove = + base::STLSetDifference<IdSet>(existing_ids, current_ids); + for (IdSet::const_iterator it = ids_to_remove.begin(); + it != ids_to_remove.end(); + ++it) { + ProcessRemovedDriveApp(*it); + } + + for (size_t i = 0; i < drive_apps_.size(); ++i) { + AddOrUpdateDriveApp(drive_apps_[i]); + } + SchedulePendingConverters(); +} + +void DriveAppProvider::OnExtensionInstalled( + content::BrowserContext* browser_context, + const Extension* extension) { + // Bail if the |extension| is installed from a converter. The post install + // processing will be handled in OnLocalAppConverted. + if (!pending_converters_.empty() && + pending_converters_.front()->IsInstalling(extension->id())) { + return; + } + + // Only user installed app reaches here. If it is mapped, make sure it is not + // tagged as generated. + const std::string drive_app_id = mapping_->GetDriveApp(extension->id()); + if (!drive_app_id.empty() && + mapping_->IsChromeAppGenerated(extension->id())) { + mapping_->Add(drive_app_id, extension->id(), false); + return; + } + + for (size_t i = 0; i < drive_apps_.size(); ++i) { + if (drive_apps_[i].product_id == extension->id()) { + // Defer the processing because it touches the extensions system and + // it is better to let the current task finish to avoid unexpected + // incomplete status. + base::MessageLoop::current()->PostTask( + FROM_HERE, + base::Bind(&DriveAppProvider::ProcessDeferredOnExtensionInstalled, + weak_ptr_factory_.GetWeakPtr(), + drive_apps_[i].app_id, + extension->id())); + return; + } + } +} + +void DriveAppProvider::OnExtensionUninstalled( + content::BrowserContext* browser_context, + const Extension* extension) { + std::string drive_app_id = mapping_->GetDriveApp(extension->id()); + if (drive_app_id.empty()) + return; + + service_bridge_->GetAppRegistry()->UninstallApp( + drive_app_id, base::Bind(&IgnoreUninstallResult)); +} diff --git a/chrome/browser/apps/drive/drive_app_provider.h b/chrome/browser/apps/drive/drive_app_provider.h new file mode 100644 index 0000000..12a5e21 --- /dev/null +++ b/chrome/browser/apps/drive/drive_app_provider.h @@ -0,0 +1,96 @@ +// Copyright 2014 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef CHROME_BROWSER_APPS_DRIVE_DRIVE_APP_PROVIDER_H_ +#define CHROME_BROWSER_APPS_DRIVE_DRIVE_APP_PROVIDER_H_ + +#include <set> +#include <string> +#include <vector> + +#include "base/macros.h" +#include "base/memory/scoped_ptr.h" +#include "base/memory/scoped_vector.h" +#include "base/memory/weak_ptr.h" +#include "chrome/browser/drive/drive_app_registry_observer.h" +#include "extensions/browser/extension_registry_observer.h" + +namespace drive { +struct DriveAppInfo; +} + +class BrowserContextKeyedServiceFactory; +class DriveAppConverter; +class DriveAppMapping; +class DriveServiceBridge; +class ExtensionService; +class Profile; + +// DriveAppProvider is the integration point for Drive apps. It ensures each +// Drive app has a corresponding Chrome app in the extension system. If there +// is no matching Chrome app, a local URL app would be created. The class +// processes app changes from both DriveAppRegistry and extension system to +// keep the two in sync. +class DriveAppProvider : public drive::DriveAppRegistryObserver, + public extensions::ExtensionRegistryObserver { + public: + explicit DriveAppProvider(Profile* profile); + virtual ~DriveAppProvider(); + + // Appends PKS factories this class depends on. + static void AppendDependsOnFactories( + std::set<BrowserContextKeyedServiceFactory*>* factories); + + void SetDriveServiceBridgeForTest(scoped_ptr<DriveServiceBridge> test_bridge); + + private: + friend class DriveAppProviderTest; + + typedef std::set<std::string> IdSet; + typedef std::vector<drive::DriveAppInfo> DriveAppInfos; + + // Maps |drive_app_id| to |new_app|'s id in mapping. Auto uninstall existing + // mapped app if it is an URL app. + void UpdateMappingAndExtensionSystem(const std::string& drive_app_id, + const extensions::Extension* new_app, + bool is_new_app_generated); + + // Deferred processing of relevant extension installed message. + void ProcessDeferredOnExtensionInstalled(const std::string drive_app_id, + const std::string chrome_app_id); + + void SchedulePendingConverters(); + void OnLocalAppConverted(const DriveAppConverter* converter, bool success); + + bool IsMappedUrlAppUpToDate(const drive::DriveAppInfo& drive_app) const; + + void AddOrUpdateDriveApp(const drive::DriveAppInfo& drive_app); + void ProcessRemovedDriveApp(const std::string& drive_app_id); + + // drive::DriveAppRegistryObserver overrides: + virtual void OnDriveAppRegistryUpdated() OVERRIDE; + + // extensions::ExtensionRegistryObserver overrides: + virtual void OnExtensionInstalled( + content::BrowserContext* browser_context, + const extensions::Extension* extension) OVERRIDE; + virtual void OnExtensionUninstalled( + content::BrowserContext* browser_context, + const extensions::Extension* extension) OVERRIDE; + + Profile* profile_; + + scoped_ptr<DriveServiceBridge> service_bridge_; + scoped_ptr<DriveAppMapping> mapping_; + DriveAppInfos drive_apps_; + + // Tracks the pending web app convertions. + ScopedVector<DriveAppConverter> pending_converters_; + + base::WeakPtrFactory<DriveAppProvider> weak_ptr_factory_; + + DISALLOW_COPY_AND_ASSIGN(DriveAppProvider); +}; + +#endif // CHROME_BROWSER_APPS_DRIVE_DRIVE_APP_PROVIDER_H_ diff --git a/chrome/browser/apps/drive/drive_app_provider_browsertest.cc b/chrome/browser/apps/drive/drive_app_provider_browsertest.cc new file mode 100644 index 0000000..4165a19 --- /dev/null +++ b/chrome/browser/apps/drive/drive_app_provider_browsertest.cc @@ -0,0 +1,453 @@ +// Copyright 2014 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "chrome/browser/apps/drive/drive_app_provider.h" + +#include "base/logging.h" +#include "base/macros.h" +#include "base/memory/scoped_ptr.h" +#include "base/path_service.h" +#include "base/strings/utf_string_conversions.h" +#include "base/timer/timer.h" +#include "chrome/browser/apps/drive/drive_app_mapping.h" +#include "chrome/browser/apps/drive/drive_service_bridge.h" +#include "chrome/browser/drive/drive_app_registry.h" +#include "chrome/browser/drive/fake_drive_service.h" +#include "chrome/browser/extensions/crx_installer.h" +#include "chrome/browser/extensions/extension_browsertest.h" +#include "chrome/browser/extensions/install_tracker.h" +#include "chrome/common/chrome_paths.h" +#include "chrome/common/extensions/manifest_handlers/app_launch_info.h" +#include "chrome/common/web_application_info.h" +#include "content/public/test/test_utils.h" +#include "extensions/browser/extension_registry.h" +#include "extensions/browser/extension_system.h" + +using extensions::AppLaunchInfo; +using extensions::Extension; +using extensions::ExtensionRegistry; + +namespace { + +const char kDriveAppId[] = "drive_app_id"; +const char kDriveAppName[] = "Fake Drive App"; +const char kLaunchUrl[] = "http://example.com/drive"; + +// App id of hosted_app.crx. +const char kChromeAppId[] = "kbmnembihfiondgfjekmnmcbddelicoi"; + +// Stub drive service bridge. +class TestDriveServiceBridge : public DriveServiceBridge { + public: + explicit TestDriveServiceBridge(drive::DriveAppRegistry* registry) + : registry_(registry) {} + virtual ~TestDriveServiceBridge() {} + + virtual drive::DriveAppRegistry* GetAppRegistry() OVERRIDE { + return registry_; + } + + private: + drive::DriveAppRegistry* registry_; + + DISALLOW_COPY_AND_ASSIGN(TestDriveServiceBridge); +}; + +} // namespace + +class DriveAppProviderTest : public ExtensionBrowserTest, + public extensions::InstallObserver { + public: + DriveAppProviderTest() {} + virtual ~DriveAppProviderTest() {} + + // ExtensionBrowserTest: + virtual void SetUpOnMainThread() OVERRIDE { + ExtensionBrowserTest::SetUpOnMainThread(); + + fake_drive_service_.reset(new drive::FakeDriveService); + fake_drive_service_->LoadAppListForDriveApi("drive/applist_empty.json"); + apps_registry_.reset( + new drive::DriveAppRegistry(fake_drive_service_.get())); + + provider_.reset(new DriveAppProvider(profile())); + provider_->SetDriveServiceBridgeForTest( + make_scoped_ptr(new TestDriveServiceBridge(apps_registry_.get())) + .PassAs<DriveServiceBridge>()); + } + + virtual void CleanUpOnMainThread() OVERRIDE { + provider_.reset(); + apps_registry_.reset(); + fake_drive_service_.reset(); + + ExtensionBrowserTest::CleanUpOnMainThread(); + } + + const Extension* InstallChromeApp(int expected_change) { + base::FilePath test_data_path; + if (!PathService::Get(chrome::DIR_TEST_DATA, &test_data_path)) { + ADD_FAILURE(); + return NULL; + } + test_data_path = + test_data_path.AppendASCII("extensions").AppendASCII("hosted_app.crx"); + const Extension* extension = + InstallExtension(test_data_path, expected_change); + return extension; + } + + void RefreshDriveAppRegistry() { + apps_registry_->Update(); + content::RunAllPendingInMessageLoop(); + } + + void WaitForPendingDriveAppConverters() { + DCHECK(!runner_); + + if (provider_->pending_converters_.empty()) + return; + + runner_ = new content::MessageLoopRunner; + + pending_drive_app_converter_check_timer_.Start( + FROM_HERE, + base::TimeDelta::FromMilliseconds(50), + base::Bind(&DriveAppProviderTest::OnPendingDriveAppConverterCheckTimer, + base::Unretained(this))); + + runner_->Run(); + + pending_drive_app_converter_check_timer_.Stop(); + runner_ = NULL; + } + + void InstallUserUrlApp(const std::string& url) { + DCHECK(!runner_); + runner_ = new content::MessageLoopRunner; + + WebApplicationInfo web_app; + web_app.title = base::ASCIIToUTF16("User installed Url app"); + web_app.app_url = GURL(url); + + scoped_refptr<extensions::CrxInstaller> crx_installer = + extensions::CrxInstaller::CreateSilent( + extensions::ExtensionSystem::Get(profile())->extension_service()); + crx_installer->set_creation_flags(Extension::FROM_BOOKMARK); + extensions::InstallTracker::Get(profile())->AddObserver(this); + crx_installer->InstallWebApp(web_app); + + runner_->Run(); + runner_ = NULL; + extensions::InstallTracker::Get(profile())->RemoveObserver(this); + + content::RunAllPendingInMessageLoop(); + } + + bool HasPendingConverters() const { + return !provider_->pending_converters_.empty(); + } + + drive::FakeDriveService* fake_drive_service() { + return fake_drive_service_.get(); + } + DriveAppProvider* provider() { return provider_.get(); } + DriveAppMapping* mapping() { return provider_->mapping_.get(); } + + private: + void OnPendingDriveAppConverterCheckTimer() { + if (!HasPendingConverters()) + runner_->Quit(); + } + + // extensions::InstallObserver + virtual void OnFinishCrxInstall(const std::string& extension_id, + bool success) OVERRIDE { + runner_->Quit(); + } + + scoped_ptr<drive::FakeDriveService> fake_drive_service_; + scoped_ptr<drive::DriveAppRegistry> apps_registry_; + scoped_ptr<DriveAppProvider> provider_; + + base::RepeatingTimer<DriveAppProviderTest> + pending_drive_app_converter_check_timer_; + scoped_refptr<content::MessageLoopRunner> runner_; + + DISALLOW_COPY_AND_ASSIGN(DriveAppProviderTest); +}; + +// A Drive app maps to an existing Chrome app that has a matching id. +// Uninstalling the chrome app would also disconnect the drive app. +IN_PROC_BROWSER_TEST_F(DriveAppProviderTest, ExistingChromeApp) { + // Prepare an existing chrome app. + const Extension* chrome_app = InstallChromeApp(1); + ASSERT_TRUE(chrome_app); + + // Prepare a Drive app that matches the chrome app id. + fake_drive_service()->AddApp( + kDriveAppId, kDriveAppName, chrome_app->id(), kLaunchUrl); + RefreshDriveAppRegistry(); + EXPECT_FALSE(HasPendingConverters()); + + // The Drive app should use the matching chrome app. + EXPECT_EQ(chrome_app->id(), mapping()->GetChromeApp(kDriveAppId)); + EXPECT_FALSE(mapping()->IsChromeAppGenerated(chrome_app->id())); + + // Unintalling chrome app should disconnect the Drive app on server. + EXPECT_TRUE(fake_drive_service()->HasApp(kDriveAppId)); + UninstallExtension(chrome_app->id()); + EXPECT_FALSE(fake_drive_service()->HasApp(kDriveAppId)); +} + +// A Drive app creates an URL app when no matching Chrome app presents. +IN_PROC_BROWSER_TEST_F(DriveAppProviderTest, CreateUrlApp) { + // Prepare a Drive app with no underlying chrome app. + fake_drive_service()->AddApp(kDriveAppId, kDriveAppName, "", kLaunchUrl); + RefreshDriveAppRegistry(); + WaitForPendingDriveAppConverters(); + + // An Url app should be created. + const Extension* chrome_app = + ExtensionRegistry::Get(profile())->GetExtensionById( + mapping()->GetChromeApp(kDriveAppId), ExtensionRegistry::EVERYTHING); + ASSERT_TRUE(chrome_app); + EXPECT_EQ(kDriveAppName, chrome_app->name()); + EXPECT_TRUE(chrome_app->is_hosted_app()); + EXPECT_TRUE(chrome_app->from_bookmark()); + EXPECT_EQ(GURL(kLaunchUrl), AppLaunchInfo::GetLaunchWebURL(chrome_app)); + + EXPECT_EQ(chrome_app->id(), mapping()->GetChromeApp(kDriveAppId)); + EXPECT_TRUE(mapping()->IsChromeAppGenerated(chrome_app->id())); + + // Unintalling the chrome app should disconnect the Drive app on server. + EXPECT_TRUE(fake_drive_service()->HasApp(kDriveAppId)); + UninstallExtension(chrome_app->id()); + EXPECT_FALSE(fake_drive_service()->HasApp(kDriveAppId)); +} + +// A matching Chrome app replaces the created URL app. +IN_PROC_BROWSER_TEST_F(DriveAppProviderTest, MatchingChromeAppInstalled) { + // Prepare a Drive app that matches the not-yet-installed kChromeAppId. + fake_drive_service()->AddApp( + kDriveAppId, kDriveAppName, kChromeAppId, kLaunchUrl); + RefreshDriveAppRegistry(); + WaitForPendingDriveAppConverters(); + + // An Url app should be created. + const Extension* url_app = + ExtensionRegistry::Get(profile())->GetExtensionById( + mapping()->GetChromeApp(kDriveAppId), ExtensionRegistry::EVERYTHING); + EXPECT_TRUE(url_app->is_hosted_app()); + EXPECT_TRUE(url_app->from_bookmark()); + + const std::string url_app_id = url_app->id(); + EXPECT_NE(kChromeAppId, url_app_id); + EXPECT_EQ(url_app_id, mapping()->GetChromeApp(kDriveAppId)); + EXPECT_TRUE(mapping()->IsChromeAppGenerated(url_app_id)); + + // Installs a chrome app with matching id. + InstallChromeApp(0); + + // The Drive app should be mapped to chrome app. + EXPECT_EQ(kChromeAppId, mapping()->GetChromeApp(kDriveAppId)); + EXPECT_FALSE(mapping()->IsChromeAppGenerated(kChromeAppId)); + + // Url app should be auto uninstalled. + EXPECT_FALSE(ExtensionRegistry::Get(profile())->GetExtensionById( + url_app_id, ExtensionRegistry::EVERYTHING)); +} + +// Tests that the corresponding URL app is uninstalled when a Drive app is +// disconnected. +IN_PROC_BROWSER_TEST_F(DriveAppProviderTest, + DisconnectDriveAppUninstallUrlApp) { + // Prepare a Drive app that matches the not-yet-installed kChromeAppId. + fake_drive_service()->AddApp( + kDriveAppId, kDriveAppName, kChromeAppId, kLaunchUrl); + RefreshDriveAppRegistry(); + WaitForPendingDriveAppConverters(); + + // Url app is created. + const std::string url_app_id = mapping()->GetChromeApp(kDriveAppId); + EXPECT_TRUE(ExtensionRegistry::Get(profile())->GetExtensionById( + url_app_id, ExtensionRegistry::EVERYTHING)); + + fake_drive_service()->RemoveAppByProductId(kChromeAppId); + RefreshDriveAppRegistry(); + + // Url app is auto uninstalled. + EXPECT_FALSE(ExtensionRegistry::Get(profile())->GetExtensionById( + url_app_id, ExtensionRegistry::EVERYTHING)); +} + +// Tests that the matching Chrome app is preserved when a Drive app is +// disconnected. +IN_PROC_BROWSER_TEST_F(DriveAppProviderTest, + DisconnectDriveAppPreserveChromeApp) { + // Prepare an existing chrome app. + const Extension* chrome_app = InstallChromeApp(1); + ASSERT_TRUE(chrome_app); + + // Prepare a Drive app that matches the chrome app id. + fake_drive_service()->AddApp( + kDriveAppId, kDriveAppName, kChromeAppId, kLaunchUrl); + RefreshDriveAppRegistry(); + EXPECT_FALSE(HasPendingConverters()); + + fake_drive_service()->RemoveAppByProductId(kChromeAppId); + RefreshDriveAppRegistry(); + + // Chrome app is still present after the Drive app is disconnected. + EXPECT_TRUE(ExtensionRegistry::Get(profile())->GetExtensionById( + kChromeAppId, ExtensionRegistry::EVERYTHING)); +} + +// The "generated" flag of an app should stay across Drive app conversion. +IN_PROC_BROWSER_TEST_F(DriveAppProviderTest, KeepGeneratedFlagBetweenUpdates) { + // Prepare a Drive app with no underlying chrome app. + fake_drive_service()->AddApp( + kDriveAppId, kDriveAppName, kChromeAppId, kLaunchUrl); + RefreshDriveAppRegistry(); + WaitForPendingDriveAppConverters(); + + const std::string url_app_id = mapping()->GetChromeApp(kDriveAppId); + EXPECT_TRUE(mapping()->IsChromeAppGenerated(url_app_id)); + + // Change name to trigger an update. + const char kChangedName[] = "Changed name"; + fake_drive_service()->RemoveAppByProductId(kChromeAppId); + fake_drive_service()->AddApp( + kDriveAppId, kChangedName, kChromeAppId, kLaunchUrl); + RefreshDriveAppRegistry(); + WaitForPendingDriveAppConverters(); + + // It should still map to the same url app id and tagged as generated. + EXPECT_EQ(url_app_id, mapping()->GetChromeApp(kDriveAppId)); + EXPECT_TRUE(mapping()->IsChromeAppGenerated(url_app_id)); +} + +// A new URL app replaces the existing one and keeps existing// position when a +// Drive app changes its name or URL. +IN_PROC_BROWSER_TEST_F(DriveAppProviderTest, DriveAppChanged) { + // Prepare a Drive app with no underlying chrome app. + fake_drive_service()->AddApp( + kDriveAppId, kDriveAppName, kChromeAppId, kLaunchUrl); + RefreshDriveAppRegistry(); + WaitForPendingDriveAppConverters(); + + // An Url app should be created. + const std::string url_app_id = mapping()->GetChromeApp(kDriveAppId); + const Extension* url_app = + ExtensionRegistry::Get(profile()) + ->GetExtensionById(url_app_id, ExtensionRegistry::EVERYTHING); + ASSERT_TRUE(url_app); + EXPECT_EQ(kDriveAppName, url_app->name()); + EXPECT_TRUE(url_app->is_hosted_app()); + EXPECT_TRUE(url_app->from_bookmark()); + EXPECT_EQ(GURL(kLaunchUrl), AppLaunchInfo::GetLaunchWebURL(url_app)); + EXPECT_TRUE(mapping()->IsChromeAppGenerated(url_app_id)); + + // Register the Drive app with a different name and URL. + const char kAnotherName[] = "Another drive app name"; + const char kAnotherLaunchUrl[] = "http://example.com/another_end_point"; + fake_drive_service()->RemoveAppByProductId(kChromeAppId); + fake_drive_service()->AddApp( + kDriveAppId, kAnotherName, kChromeAppId, kAnotherLaunchUrl); + RefreshDriveAppRegistry(); + WaitForPendingDriveAppConverters(); + + // Old URL app should be auto uninstalled. + url_app = ExtensionRegistry::Get(profile()) + ->GetExtensionById(url_app_id, ExtensionRegistry::EVERYTHING); + EXPECT_FALSE(url_app); + + // New URL app should be used. + const std::string new_url_app_id = mapping()->GetChromeApp(kDriveAppId); + EXPECT_NE(new_url_app_id, url_app_id); + EXPECT_TRUE(mapping()->IsChromeAppGenerated(new_url_app_id)); + + const Extension* new_url_app = + ExtensionRegistry::Get(profile()) + ->GetExtensionById(new_url_app_id, ExtensionRegistry::EVERYTHING); + ASSERT_TRUE(new_url_app); + EXPECT_EQ(kAnotherName, new_url_app->name()); + EXPECT_TRUE(new_url_app->is_hosted_app()); + EXPECT_TRUE(new_url_app->from_bookmark()); + EXPECT_EQ(GURL(kAnotherLaunchUrl), + AppLaunchInfo::GetLaunchWebURL(new_url_app)); +} + +// An existing URL app is not changed when underlying drive app data (name and +// URL) is not changed. +IN_PROC_BROWSER_TEST_F(DriveAppProviderTest, NoChange) { + // Prepare one Drive app. + fake_drive_service()->AddApp( + kDriveAppId, kDriveAppName, kChromeAppId, kLaunchUrl); + RefreshDriveAppRegistry(); + WaitForPendingDriveAppConverters(); + + const std::string url_app_id = mapping()->GetChromeApp(kDriveAppId); + const Extension* url_app = + ExtensionRegistry::Get(profile()) + ->GetExtensionById(url_app_id, ExtensionRegistry::EVERYTHING); + + // Refresh with no actual change. + RefreshDriveAppRegistry(); + EXPECT_FALSE(HasPendingConverters()); + + // Url app should remain unchanged. + const std::string new_url_app_id = mapping()->GetChromeApp(kDriveAppId); + EXPECT_EQ(new_url_app_id, url_app_id); + + const Extension* new_url_app = + ExtensionRegistry::Get(profile()) + ->GetExtensionById(new_url_app_id, ExtensionRegistry::EVERYTHING); + EXPECT_EQ(url_app, new_url_app); +} + +// User installed url app before Drive app conversion should not be tagged +// as generated and not auto uninstalled. +IN_PROC_BROWSER_TEST_F(DriveAppProviderTest, UserInstalledBeforeDriveApp) { + InstallUserUrlApp(kLaunchUrl); + + fake_drive_service()->AddApp( + kDriveAppId, kDriveAppName, kChromeAppId, kLaunchUrl); + RefreshDriveAppRegistry(); + WaitForPendingDriveAppConverters(); + + const std::string url_app_id = mapping()->GetChromeApp(kDriveAppId); + EXPECT_FALSE(mapping()->IsChromeAppGenerated(url_app_id)); + + fake_drive_service()->RemoveAppByProductId(kChromeAppId); + RefreshDriveAppRegistry(); + + // Url app is still present after the Drive app is disconnected. + EXPECT_TRUE(ExtensionRegistry::Get(profile())->GetExtensionById( + url_app_id, ExtensionRegistry::EVERYTHING)); +} + +// Similar to UserInstalledBeforeDriveApp but test the case where user +// installation happens after Drive app conversion. +IN_PROC_BROWSER_TEST_F(DriveAppProviderTest, UserInstalledAfterDriveApp) { + fake_drive_service()->AddApp( + kDriveAppId, kDriveAppName, kChromeAppId, kLaunchUrl); + RefreshDriveAppRegistry(); + WaitForPendingDriveAppConverters(); + + // Drive app converted and tagged as generated. + const std::string url_app_id = mapping()->GetChromeApp(kDriveAppId); + EXPECT_TRUE(mapping()->IsChromeAppGenerated(url_app_id)); + + // User installation resets the generated flag. + InstallUserUrlApp(kLaunchUrl); + EXPECT_FALSE(mapping()->IsChromeAppGenerated(url_app_id)); + + fake_drive_service()->RemoveAppByProductId(kChromeAppId); + RefreshDriveAppRegistry(); + + // Url app is still present after the Drive app is disconnected. + EXPECT_TRUE(ExtensionRegistry::Get(profile())->GetExtensionById( + url_app_id, ExtensionRegistry::EVERYTHING)); +} diff --git a/chrome/browser/apps/drive/drive_service_bridge.cc b/chrome/browser/apps/drive/drive_service_bridge.cc new file mode 100644 index 0000000..207fc35 --- /dev/null +++ b/chrome/browser/apps/drive/drive_service_bridge.cc @@ -0,0 +1,138 @@ +// Copyright 2014 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "chrome/browser/apps/drive/drive_service_bridge.h" + +#include <string> + +#include "base/logging.h" +#include "chrome/browser/drive/drive_api_service.h" +#include "chrome/browser/drive/drive_app_registry.h" +#include "chrome/browser/drive/drive_notification_manager.h" +#include "chrome/browser/drive/drive_notification_manager_factory.h" +#include "chrome/browser/drive/drive_notification_observer.h" +#include "chrome/browser/profiles/profile.h" +#include "chrome/browser/signin/profile_oauth2_token_service_factory.h" +#include "chrome/browser/signin/signin_manager_factory.h" +#include "components/signin/core/browser/profile_oauth2_token_service.h" +#include "components/signin/core/browser/signin_manager.h" +#include "content/public/browser/browser_thread.h" + +namespace { + +// Hosts DriveAPIService and DriveAppRegistry. +// TODO(xiyuan): Optimize to leverage chromeos::DriveIntegrationService. +class DriveServiceBridgeImpl : public DriveServiceBridge, + public drive::DriveServiceObserver, + public drive::DriveNotificationObserver { + public: + explicit DriveServiceBridgeImpl(Profile* profile); + virtual ~DriveServiceBridgeImpl(); + + void Initialize(); + + // DriveServiceBridge: + virtual drive::DriveAppRegistry* GetAppRegistry() OVERRIDE; + + // drive::DriveServiceObserver: + virtual void OnReadyToSendRequests() OVERRIDE; + + // drive::DriveNotificationObserver: + virtual void OnNotificationReceived() OVERRIDE; + virtual void OnPushNotificationEnabled(bool enabled) OVERRIDE; + + private: + Profile* profile_; + scoped_ptr<drive::DriveServiceInterface> drive_service_; + scoped_ptr<drive::DriveAppRegistry> drive_app_registry_; + + DISALLOW_COPY_AND_ASSIGN(DriveServiceBridgeImpl); +}; + +DriveServiceBridgeImpl::DriveServiceBridgeImpl(Profile* profile) + : profile_(profile) { + DCHECK(profile_); +} + +DriveServiceBridgeImpl::~DriveServiceBridgeImpl() { + drive::DriveNotificationManager* drive_notification_manager = + drive::DriveNotificationManagerFactory::FindForBrowserContext(profile_); + if (drive_notification_manager) + drive_notification_manager->RemoveObserver(this); + + drive_service_->RemoveObserver(this); + + drive_app_registry_.reset(); + drive_service_.reset(); +} + +void DriveServiceBridgeImpl::Initialize() { + scoped_refptr<base::SequencedWorkerPool> worker_pool( + content::BrowserThread::GetBlockingPool()); + scoped_refptr<base::SequencedTaskRunner> drive_task_runner( + worker_pool->GetSequencedTaskRunnerWithShutdownBehavior( + worker_pool->GetSequenceToken(), + base::SequencedWorkerPool::SKIP_ON_SHUTDOWN)); + + ProfileOAuth2TokenService* token_service = + ProfileOAuth2TokenServiceFactory::GetForProfile(profile_); + drive_service_.reset(new drive::DriveAPIService( + token_service, + profile_->GetRequestContext(), + drive_task_runner.get(), + GURL(google_apis::DriveApiUrlGenerator::kBaseUrlForProduction), + GURL(google_apis::DriveApiUrlGenerator::kBaseDownloadUrlForProduction), + GURL(google_apis::GDataWapiUrlGenerator::kBaseUrlForProduction), + std::string() /* custom_user_agent */)); + SigninManagerBase* signin_manager = + SigninManagerFactory::GetForProfile(profile_); + drive_service_->Initialize(signin_manager->GetAuthenticatedAccountId()); + drive_service_->AddObserver(this); + + drive::DriveNotificationManager* drive_notification_manager = + drive::DriveNotificationManagerFactory::GetForBrowserContext(profile_); + if (drive_notification_manager) + drive_notification_manager->AddObserver(this); + + drive_app_registry_.reset(new drive::DriveAppRegistry(drive_service_.get())); + if (drive_service_->CanSendRequest()) + drive_app_registry_->Update(); +} + +drive::DriveAppRegistry* DriveServiceBridgeImpl::GetAppRegistry() { + return drive_app_registry_.get(); +} + +void DriveServiceBridgeImpl::OnReadyToSendRequests() { + drive_app_registry_->Update(); +} + +void DriveServiceBridgeImpl::OnNotificationReceived() { + if (drive_service_->CanSendRequest()) + drive_app_registry_->Update(); +} + +void DriveServiceBridgeImpl::OnPushNotificationEnabled(bool enabled) { + if (enabled && drive_service_->CanSendRequest()) + drive_app_registry_->Update(); +} + +} // namespace + +// static +scoped_ptr<DriveServiceBridge> DriveServiceBridge::Create(Profile* profile) { + scoped_ptr<DriveServiceBridgeImpl> bridge( + new DriveServiceBridgeImpl(profile)); + bridge->Initialize(); + return bridge.PassAs<DriveServiceBridge>(); +} + +// static +void DriveServiceBridge::AppendDependsOnFactories( + std::set<BrowserContextKeyedServiceFactory*>* factories) { + DCHECK(factories); + factories->insert(ProfileOAuth2TokenServiceFactory::GetInstance()); + factories->insert(SigninManagerFactory::GetInstance()); + factories->insert(drive::DriveNotificationManagerFactory::GetInstance()); +} diff --git a/chrome/browser/apps/drive/drive_service_bridge.h b/chrome/browser/apps/drive/drive_service_bridge.h new file mode 100644 index 0000000..7bc1ea4 --- /dev/null +++ b/chrome/browser/apps/drive/drive_service_bridge.h @@ -0,0 +1,35 @@ +// Copyright 2014 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef CHROME_BROWSER_APPS_DRIVE_DRIVE_SERVICE_BRIDGE_H_ +#define CHROME_BROWSER_APPS_DRIVE_DRIVE_SERVICE_BRIDGE_H_ + +#include <set> + +#include "base/memory/scoped_ptr.h" + +namespace drive { +class DriveAppRegistry; +} + +class BrowserContextKeyedServiceFactory; +class Profile; + +// An interface to access Drive service for a given profile. +class DriveServiceBridge { + public: + virtual ~DriveServiceBridge() {} + + // Factory to create an instance of DriveServiceBridge. + static scoped_ptr<DriveServiceBridge> Create(Profile* profile); + + // Appends PKS factories this class depends on. + static void AppendDependsOnFactories( + std::set<BrowserContextKeyedServiceFactory*>* factories); + + // Returns the DriveAppRegistery to use. The ownership is not transferred. + virtual drive::DriveAppRegistry* GetAppRegistry() = 0; +}; + +#endif // CHROME_BROWSER_APPS_DRIVE_DRIVE_SERVICE_BRIDGE_H_ |