Added support for pending extensions to ExtensionsService and
ExtensionUpdater.  This is needed for theme syncing.

Basically a pending extension is an (id, update_url) pair.  This change
makes it so that one can pass pending extensions to the extension service
and they will be installed if necessary on the next auto-update cycle.

BUG=32414
TEST=unittests, trybots, in-progress theme syncing change

Review URL: http://codereview.chromium.org/1232003

git-svn-id: svn://svn.chromium.org/chrome/trunk/src@42711 0039d316-1c4b-4281-b951-d872f2087c98
diff --git a/chrome/browser/extensions/extension_updater.cc b/chrome/browser/extensions/extension_updater.cc
index de1ca96c..80621f5 100644
--- a/chrome/browser/extensions/extension_updater.cc
+++ b/chrome/browser/extensions/extension_updater.cc
@@ -378,7 +378,7 @@
     safe_parser->Start();
   } else {
     // TODO(asargent) Do exponential backoff here. (http://crbug.com/12546).
-    LOG(INFO) << "Failed to fetch manifst '" << url.possibly_invalid_spec() <<
+    LOG(INFO) << "Failed to fetch manifest '" << url.possibly_invalid_spec() <<
         "' response code:" << response_code;
   }
   manifest_fetcher_.reset();
@@ -540,59 +540,110 @@
   ScheduleNextCheck(TimeDelta::FromSeconds(frequency_seconds_));
 }
 
-void ExtensionUpdater::CheckNow() {
-  // List of data on fetches we're going to do. We limit the number of
-  // extensions grouped together in one batch to avoid running into the limits
-  // on the length of http GET requests, so there might be multiple
-  // ManifestFetchData* objects with the same base_url.
-  std::multimap<GURL, ManifestFetchData*> fetches;
+namespace {
 
-  int no_url_count = 0;
-  int google_url_count = 0;
-  int other_url_count = 0;
-  int theme_count = 0;
+struct URLStats {
+  URLStats()
+      : no_url_count(0),
+        google_url_count(0),
+        other_url_count(0),
+        theme_count(0) {}
 
-  const ExtensionList* extensions = service_->extensions();
-  for (ExtensionList::const_iterator iter = extensions->begin();
-       iter != extensions->end(); ++iter) {
-    Extension* extension = (*iter);
+  void ReportStats() const {
+    UMA_HISTOGRAM_COUNTS_100("Extensions.UpdateCheckExtensions",
+                             google_url_count + other_url_count -
+                             theme_count);
+    UMA_HISTOGRAM_COUNTS_100("Extensions.UpdateCheckTheme",
+                             theme_count);
+    UMA_HISTOGRAM_COUNTS_100("Extensions.UpdateCheckGoogleUrl",
+                             google_url_count);
+    UMA_HISTOGRAM_COUNTS_100("Extensions.UpdateCheckOtherUrl",
+                             other_url_count);
+    UMA_HISTOGRAM_COUNTS_100("Extensions.UpdateCheckNoUrl",
+                             no_url_count);
+  }
 
-    // Only internal and external extensions can be autoupdated.
-    if (extension->location() != Extension::INTERNAL &&
-        !Extension::IsExternalLocation(extension->location())) {
-      continue;
+  int no_url_count, google_url_count, other_url_count, theme_count;
+};
+
+class ManifestFetchesBuilder {
+ public:
+  explicit ManifestFetchesBuilder(ExtensionUpdateService* service)
+      : service_(service) {
+    DCHECK(service_);
+  }
+
+  void AddExtension(const Extension& extension) {
+    AddExtensionData(extension.location(),
+                     extension.id(),
+                     *extension.version(),
+                     extension.converted_from_user_script(),
+                     extension.IsTheme(),
+                     extension.update_url());
+  }
+
+  void AddPendingExtension(const std::string& id,
+                           const PendingExtensionInfo& info) {
+    AddExtensionData(Extension::INTERNAL, id, info.version,
+                     false, info.is_theme, info.update_url);
+  }
+
+  const URLStats& url_stats() const { return url_stats_; }
+
+  // Caller takes ownership of the returned ManifestFetchData
+  // objects.  Clears all counters.
+  std::vector<ManifestFetchData*> GetFetches() {
+    std::vector<ManifestFetchData*> fetches;
+    fetches.reserve(fetches_.size());
+    for (std::multimap<GURL, ManifestFetchData*>::iterator it =
+             fetches_.begin(); it != fetches_.end(); ++it) {
+      fetches.push_back(it->second);
     }
+    fetches_.clear();
+    url_stats_ = URLStats();
+    return fetches;
+  }
 
-    const GURL& update_url = extension->update_url();
+ private:
+  void AddExtensionData(Extension::Location location,
+                        const std::string& id,
+                        const Version& version,
+                        bool converted_from_user_script,
+                        bool is_theme,
+                        const GURL& update_url) {
+    // Only internal and external extensions can be autoupdated.
+    if (location != Extension::INTERNAL &&
+        !Extension::IsExternalLocation(location)) {
+      return;
+    }
 
     // Collect histogram data and skip extensions with no update url.
     if (update_url.DomainIs("google.com")) {
-      google_url_count++;
-    } else if (update_url.is_empty() || extension->id().empty()) {
+      url_stats_.google_url_count++;
+    } else if (update_url.is_empty() || id.empty()) {
       // TODO(asargent) when a default URL is added, make sure to update
       // the total histogram below.  Also, make sure to skip extensions that
       // are "converted_from_user_script".
-      if (!extension->converted_from_user_script())
-        no_url_count++;
-      continue;
+      if (!converted_from_user_script)
+        url_stats_.no_url_count++;
+      return;
     } else {
-      other_url_count++;
+      url_stats_.other_url_count++;
     }
-    if (extension->IsTheme())
-      theme_count++;
+    if (is_theme)
+      url_stats_.theme_count++;
 
     DCHECK(update_url.is_valid());
 
     ManifestFetchData* fetch = NULL;
     std::multimap<GURL, ManifestFetchData*>::iterator existing_iter =
-        fetches.find(update_url);
+        fetches_.find(update_url);
 
     // Find or create a ManifestFetchData to add this extension to.
-    std::string id = extension->id();
-    std::string version = extension->VersionString();
-    int ping_days = CalculatePingDays(extension->id());
-    while (existing_iter != fetches.end()) {
-      if (existing_iter->second->AddExtension(id, version, ping_days)) {
+    int ping_days = CalculatePingDays(id);
+    while (existing_iter != fetches_.end()) {
+      if (existing_iter->second->AddExtension(id, version.GetString(),
+                                              ping_days)) {
         fetch = existing_iter->second;
         break;
       }
@@ -600,12 +651,58 @@
     }
     if (!fetch) {
       fetch = new ManifestFetchData(update_url);
-      fetches.insert(std::pair<GURL, ManifestFetchData*>(update_url, fetch));
-      bool added = fetch->AddExtension(id, version, ping_days);
+      fetches_.insert(std::pair<GURL, ManifestFetchData*>(update_url, fetch));
+      bool added = fetch->AddExtension(id, version.GetString(), ping_days);
       DCHECK(added);
     }
   }
 
+  // Calculates the value to use for the ping days parameter in manifest
+  // fetches for a given extension.
+  int CalculatePingDays(const std::string& extension_id) {
+    int days = ManifestFetchData::kNeverPinged;
+    Time last_ping_day = service_->LastPingDay(extension_id);
+    if (!last_ping_day.is_null()) {
+      days = (Time::Now() - last_ping_day).InDays();
+    }
+    return days;
+  }
+
+  ExtensionUpdateService* service_;
+
+  // List of data on fetches we're going to do. We limit the number of
+  // extensions grouped together in one batch to avoid running into the limits
+  // on the length of http GET requests, so there might be multiple
+  // ManifestFetchData* objects with the same base_url.
+  std::multimap<GURL, ManifestFetchData*> fetches_;
+
+  URLStats url_stats_;
+
+  DISALLOW_COPY_AND_ASSIGN(ManifestFetchesBuilder);
+};
+
+}  // namespace
+
+void ExtensionUpdater::CheckNow() {
+  ManifestFetchesBuilder fetches_builder(service_);
+
+  const ExtensionList* extensions = service_->extensions();
+  for (ExtensionList::const_iterator iter = extensions->begin();
+       iter != extensions->end(); ++iter) {
+    fetches_builder.AddExtension(**iter);
+  }
+
+  const PendingExtensionMap& pending_extensions =
+      service_->pending_extensions();
+  for (PendingExtensionMap::const_iterator iter = pending_extensions.begin();
+       iter != pending_extensions.end(); ++iter) {
+    fetches_builder.AddPendingExtension(iter->first, iter->second);
+  }
+
+  fetches_builder.url_stats().ReportStats();
+
+  std::vector<ManifestFetchData*> fetches(fetches_builder.GetFetches());
+
   // Start a fetch of the blacklist if needed.
   if (blacklist_checks_enabled_ && service_->HasInstalledExtensions()) {
     ManifestFetchData* blacklist_fetch =
@@ -616,36 +713,16 @@
   }
 
   // Now start fetching regular extension updates
-  std::multimap<GURL, ManifestFetchData*>::iterator i = fetches.begin();
-  while (i != fetches.end()) {
-    ManifestFetchData *fetch = i->second;
-
+  for (std::vector<ManifestFetchData*>::const_iterator it = fetches.begin();
+       it != fetches.end(); ++it) {
     // StartUpdateCheck makes sure the url isn't already downloading or
     // scheduled, so we don't need to check before calling it. Ownership of
     // fetch is transferred here.
-    StartUpdateCheck(fetch);
-    fetches.erase(i++);
+    StartUpdateCheck(*it);
   }
-
-  UMA_HISTOGRAM_COUNTS_100("Extensions.UpdateCheckExtensions",
-                           google_url_count + other_url_count - theme_count);
-  UMA_HISTOGRAM_COUNTS_100("Extensions.UpdateCheckTheme",
-                           theme_count);
-  UMA_HISTOGRAM_COUNTS_100("Extensions.UpdateCheckGoogleUrl",
-                           google_url_count);
-  UMA_HISTOGRAM_COUNTS_100("Extensions.UpdateCheckOtherUrl",
-                           other_url_count);
-  UMA_HISTOGRAM_COUNTS_100("Extensions.UpdateCheckNoUrl",
-                           no_url_count);
-}
-
-int ExtensionUpdater::CalculatePingDays(const std::string& extension_id) {
-  int days = ManifestFetchData::kNeverPinged;
-  Time last_ping_day = service_->LastPingDay(extension_id);
-  if (!last_ping_day.is_null()) {
-    days = (Time::Now() - last_ping_day).InDays();
-  }
-  return days;
+  // We don't want to use fetches after this since StartUpdateCheck()
+  // takes ownership of its argument.
+  fetches.clear();
 }
 
 bool ExtensionUpdater::GetExistingVersion(const std::string& id,
@@ -655,6 +732,12 @@
       WideToASCII(prefs_->GetString(kExtensionBlacklistUpdateVersion));
     return true;
   }
+  PendingExtensionMap::const_iterator it =
+      service_->pending_extensions().find(id);
+  if (it != service_->pending_extensions().end()) {
+    *version = it->second.version.GetString();
+    return true;
+  }
   Extension* extension = service_->GetExtensionById(id, false);
   if (!extension) {
     return false;
diff --git a/chrome/browser/extensions/extension_updater.h b/chrome/browser/extensions/extension_updater.h
index ae7c4881..bd48af8e 100644
--- a/chrome/browser/extensions/extension_updater.h
+++ b/chrome/browser/extensions/extension_updater.h
@@ -198,10 +198,6 @@
   void HandleManifestResults(const ManifestFetchData& fetch_data,
                              const UpdateManifest::Results& results);
 
-  // Calculates the value to use for the ping days parameter in manifest
-  // fetches for a given extension.
-  int CalculatePingDays(const std::string& extension_id);
-
   // Determines the version of an existing extension.
   // Returns true on success and false on failures.
   bool GetExistingVersion(const std::string& id, std::string* version);
diff --git a/chrome/browser/extensions/extension_updater_unittest.cc b/chrome/browser/extensions/extension_updater_unittest.cc
index 8368364..b613bee1 100644
--- a/chrome/browser/extensions/extension_updater_unittest.cc
+++ b/chrome/browser/extensions/extension_updater_unittest.cc
@@ -6,9 +6,11 @@
 
 #include "base/file_util.h"
 #include "base/rand_util.h"
+#include "base/scoped_ptr.h"
 #include "base/stl_util-inl.h"
 #include "base/string_util.h"
 #include "base/thread.h"
+#include "base/version.h"
 #include "chrome/browser/chrome_thread.h"
 #include "chrome/browser/extensions/extension_updater.h"
 #include "chrome/browser/extensions/extensions_service.h"
@@ -50,6 +52,11 @@
     return NULL;
   }
 
+  virtual const PendingExtensionMap& pending_extensions() const {
+    EXPECT_TRUE(false);
+    return pending_extensions_;
+  }
+
   virtual void UpdateExtension(const std::string& id,
                                const FilePath& extension_path,
                                const GURL& download_url) {
@@ -85,6 +92,9 @@
     return Time();
   }
 
+ protected:
+  PendingExtensionMap pending_extensions_;
+
  private:
   std::map<std::string, Time> last_ping_days_;
   DISALLOW_COPY_AND_ASSIGN(MockService);
@@ -140,6 +150,22 @@
   }
 }
 
+// Creates test pending extensions and inserts them into list. The
+// name and version are all based on their index.
+void CreateTestPendingExtensions(int count, const GURL& update_url,
+                                 PendingExtensionMap* pending_extensions) {
+  for (int i = 1; i <= count; i++) {
+    bool is_theme = (i % 2) == 0;
+    const bool kInstallSilently = true;
+    scoped_ptr<Version> version(
+        Version::GetVersionFromString(StringPrintf("%d.0.0.0", i)));
+    ASSERT_TRUE(version.get());
+    (*pending_extensions)[StringPrintf("extension%i", i)] =
+        PendingExtensionInfo(update_url, *version,
+                             is_theme, kInstallSilently);
+  }
+}
+
 class ServiceForManifestTests : public MockService {
  public:
   ServiceForManifestTests() : has_installed_extensions_(false) {}
@@ -158,10 +184,19 @@
 
   virtual const ExtensionList* extensions() const { return &extensions_; }
 
+  virtual const PendingExtensionMap& pending_extensions() const {
+    return pending_extensions_;
+  }
+
   void set_extensions(ExtensionList extensions) {
     extensions_ = extensions;
   }
 
+  void set_pending_extensions(
+      const PendingExtensionMap& pending_extensions) {
+    pending_extensions_ = pending_extensions;
+  }
+
   virtual bool HasInstalledExtensions() {
     return has_installed_extensions_;
   }
@@ -185,11 +220,20 @@
     download_url_ = download_url;
   }
 
+  virtual const PendingExtensionMap& pending_extensions() const {
+    return pending_extensions_;
+  }
+
   virtual Extension* GetExtensionById(const std::string& id, bool) {
     last_inquired_extension_id_ = id;
     return NULL;
   }
 
+  void set_pending_extensions(
+      const PendingExtensionMap& pending_extensions) {
+    pending_extensions_ = pending_extensions;
+  }
+
   const std::string& extension_id() { return extension_id_; }
   const FilePath& install_path() { return install_path_; }
   const GURL& download_url() { return download_url_; }
@@ -272,13 +316,19 @@
     results->list.push_back(result);
   }
 
-  static void TestExtensionUpdateCheckRequests() {
+  static void TestExtensionUpdateCheckRequests(bool pending) {
     // Create an extension with an update_url.
     ServiceForManifestTests service;
-    ExtensionList tmp;
     std::string update_url("http://foo.com/bar");
-    CreateTestExtensions(1, &tmp, &update_url);
-    service.set_extensions(tmp);
+    ExtensionList extensions;
+    PendingExtensionMap pending_extensions;
+    if (pending) {
+      CreateTestPendingExtensions(1, GURL(update_url), &pending_extensions);
+      service.set_pending_extensions(pending_extensions);
+    } else {
+      CreateTestExtensions(1, &extensions, &update_url);
+      service.set_extensions(extensions);
+    }
 
     // Setup and start the updater.
     MessageLoop message_loop;
@@ -316,11 +366,18 @@
                                                UnescapeRule::URL_SPECIAL_CHARS);
     std::map<std::string, std::string> params;
     ExtractParameters(decoded, &params);
-    EXPECT_EQ(tmp[0]->id(), params["id"]);
-    EXPECT_EQ(tmp[0]->VersionString(), params["v"]);
+    if (pending) {
+      EXPECT_EQ(pending_extensions.begin()->first, params["id"]);
+      EXPECT_EQ("1.0.0.0", params["v"]);
+    } else {
+      EXPECT_EQ(extensions[0]->id(), params["id"]);
+      EXPECT_EQ(extensions[0]->VersionString(), params["v"]);
+    }
     EXPECT_EQ("", params["uc"]);
 
-    STLDeleteElements(&tmp);
+    if (!pending) {
+      STLDeleteElements(&extensions);
+    }
   }
 
   static void TestBlacklistUpdateCheckRequests() {
@@ -415,6 +472,37 @@
     STLDeleteElements(&tmp);
   }
 
+  static void TestDetermineUpdatesPending() {
+    // Create a set of test extensions
+    ServiceForManifestTests service;
+    PendingExtensionMap pending_extensions;
+    CreateTestPendingExtensions(3, GURL(), &pending_extensions);
+    service.set_pending_extensions(pending_extensions);
+
+    MessageLoop message_loop;
+    ScopedTempPrefService prefs;
+    scoped_refptr<ExtensionUpdater> updater =
+        new ExtensionUpdater(&service, prefs.get(), kUpdateFrequencySecs);
+
+    ManifestFetchData fetch_data(GURL("http://localhost/foo"));
+    UpdateManifest::Results updates;
+    for (PendingExtensionMap::const_iterator it = pending_extensions.begin();
+         it != pending_extensions.end(); ++it) {
+      fetch_data.AddExtension(it->first,
+                              it->second.version.GetString(),
+                              ManifestFetchData::kNeverPinged);
+      AddParseResult(it->first,
+                     "1.1", "http://localhost/e1_1.1.crx", &updates);
+    }
+    std::vector<int> updateable =
+        updater->DetermineUpdates(fetch_data, updates);
+    // Only the first one is updateable.
+    EXPECT_EQ(1u, updateable.size());
+    for (std::vector<int>::size_type i = 0; i < updateable.size(); ++i) {
+      EXPECT_EQ(static_cast<int>(i), updateable[i]);
+    }
+  }
+
   static void TestMultipleManifestDownloading() {
     MessageLoop ui_loop;
     ChromeThread ui_thread(ChromeThread::UI, &ui_loop);
@@ -479,7 +567,7 @@
     xmlCleanupGlobals();
   }
 
-  static void TestSingleExtensionDownloading() {
+  static void TestSingleExtensionDownloading(bool pending) {
     MessageLoop ui_loop;
     ChromeThread ui_thread(ChromeThread::UI, &ui_loop);
     ChromeThread file_thread(ChromeThread::FILE);
@@ -499,9 +587,19 @@
 
     std::string id = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
     std::string hash = "";
-    std::string version = "0.0.1";
+    scoped_ptr<Version> version(Version::GetVersionFromString("0.0.1"));
+    ASSERT_TRUE(version.get());
+    updater->FetchUpdatedExtension(id, test_url, hash, version->GetString());
 
-    updater->FetchUpdatedExtension(id, test_url, hash, version);
+    if (pending) {
+      const bool kIsTheme = false;
+      const bool kInstallSilently = true;
+      PendingExtensionMap pending_extensions;
+      pending_extensions[id] =
+          PendingExtensionInfo(test_url, *version,
+                               kIsTheme, kInstallSilently);
+      service.set_pending_extensions(pending_extensions);
+    }
 
     // Call back the ExtensionUpdater with a 200 response and some test data
     std::string extension_data("whatever");
@@ -756,7 +854,11 @@
 // subclasses where friendship with ExtenionUpdater is not inherited.
 
 TEST(ExtensionUpdaterTest, TestExtensionUpdateCheckRequests) {
-  ExtensionUpdaterTest::TestExtensionUpdateCheckRequests();
+  ExtensionUpdaterTest::TestExtensionUpdateCheckRequests(false);
+}
+
+TEST(ExtensionUpdaterTest, TestExtensionUpdateCheckRequestsPending) {
+  ExtensionUpdaterTest::TestExtensionUpdateCheckRequests(true);
 }
 
 // This test is disabled on Mac, see http://crbug.com/26035.
@@ -768,12 +870,20 @@
   ExtensionUpdaterTest::TestDetermineUpdates();
 }
 
+TEST(ExtensionUpdaterTest, TestDetermineUpdatesPending) {
+  ExtensionUpdaterTest::TestDetermineUpdatesPending();
+}
+
 TEST(ExtensionUpdaterTest, TestMultipleManifestDownloading) {
   ExtensionUpdaterTest::TestMultipleManifestDownloading();
 }
 
 TEST(ExtensionUpdaterTest, TestSingleExtensionDownloading) {
-  ExtensionUpdaterTest::TestSingleExtensionDownloading();
+  ExtensionUpdaterTest::TestSingleExtensionDownloading(false);
+}
+
+TEST(ExtensionUpdaterTest, TestSingleExtensionDownloadingPending) {
+  ExtensionUpdaterTest::TestSingleExtensionDownloading(true);
 }
 
 // This test is disabled on Mac, see http://crbug.com/26035.
diff --git a/chrome/browser/extensions/extensions_service.cc b/chrome/browser/extensions/extensions_service.cc
index 28ba10c..6807f6a 100644
--- a/chrome/browser/extensions/extensions_service.cc
+++ b/chrome/browser/extensions/extensions_service.cc
@@ -12,6 +12,7 @@
 #include "base/string_util.h"
 #include "base/time.h"
 #include "base/values.h"
+#include "base/version.h"
 #include "chrome/browser/browser_process.h"
 #include "chrome/browser/chrome_thread.h"
 #include "chrome/browser/debugger/devtools_manager.h"
@@ -97,6 +98,21 @@
 
 }  // namespace
 
+PendingExtensionInfo::PendingExtensionInfo(const GURL& update_url,
+                                           const Version& version,
+                                           bool is_theme,
+                                           bool install_silently)
+    : update_url(update_url),
+      version(version),
+      is_theme(is_theme),
+      install_silently(install_silently) {}
+
+PendingExtensionInfo::PendingExtensionInfo()
+    : update_url(),
+      version(),
+      is_theme(false),
+      install_silently(false) {}
+
 // ExtensionsService.
 
 const char* ExtensionsService::kInstallDirectoryName = "Extensions";
@@ -203,19 +219,41 @@
   installer->InstallCrx(extension_path);
 }
 
+namespace {
+  // TODO(akalin): Put this somewhere where both crx_installer.cc and
+  // this file can use it.
+  void DeleteFileHelper(const FilePath& path, bool recursive) {
+    DCHECK(ChromeThread::CurrentlyOn(ChromeThread::FILE));
+    file_util::Delete(path, recursive);
+  }
+}  // namespace
+
 void ExtensionsService::UpdateExtension(const std::string& id,
                                         const FilePath& extension_path,
                                         const GURL& download_url) {
-  if (!GetExtensionByIdInternal(id, true, true)) {
-    LOG(WARNING) << "Will not update extension " << id << " because it is not "
-                 << "installed";
+  PendingExtensionMap::const_iterator it = pending_extensions_.find(id);
+  if ((it == pending_extensions_.end()) &&
+      !GetExtensionByIdInternal(id, true, true)) {
+    LOG(WARNING) << "Will not update extension " << id
+                 << " because it is not installed or pending";
+    // Delete extension_path since we're not creating a CrxInstaller
+    // that would do it for us.
+    ChromeThread::PostTask(
+        ChromeThread::FILE, FROM_HERE,
+        NewRunnableFunction(&DeleteFileHelper, extension_path, false));
     return;
   }
 
+  // We want a silent install only for non-pending extensions and
+  // pending extensions that have install_silently set.
+  ExtensionInstallUI* client =
+      ((it == pending_extensions_.end()) || it->second.install_silently) ?
+      NULL : new ExtensionInstallUI(profile_);
+
   scoped_refptr<CrxInstaller> installer(
       new CrxInstaller(install_directory_,
                        this,  // frontend
-                       NULL));  // no client (silent install)
+                       client));
   installer->set_expected_id(id);
   installer->set_delete_source(true);
   installer->set_force_web_origin_to_download_url(true);
@@ -223,6 +261,23 @@
   installer->InstallCrx(extension_path);
 }
 
+void ExtensionsService::AddPendingExtension(
+    const std::string& id, const GURL& update_url,
+    const Version& version, bool is_theme, bool install_silently) {
+  if (GetExtensionByIdInternal(id, true, true)) {
+    return;
+  }
+  AddPendingExtensionInternal(id, update_url, version,
+                              is_theme, install_silently);
+}
+
+void ExtensionsService::AddPendingExtensionInternal(
+    const std::string& id, const GURL& update_url,
+    const Version& version, bool is_theme, bool install_silently) {
+  pending_extensions_[id] =
+      PendingExtensionInfo(update_url, version, is_theme, install_silently);
+}
+
 void ExtensionsService::ReloadExtension(const std::string& extension_id) {
   FilePath path;
   Extension* current_extension = GetExtensionById(extension_id, false);
@@ -823,6 +878,22 @@
 
 void ExtensionsService::OnExtensionInstalled(Extension* extension,
                                              bool allow_privilege_increase) {
+  PendingExtensionMap::iterator it =
+      pending_extensions_.find(extension->id());
+  if (it != pending_extensions_.end() &&
+      (it->second.is_theme != extension->IsTheme())) {
+    LOG(WARNING) << "Not installing pending extension " << extension->id()
+                 << " with is_theme = " << extension->IsTheme()
+                 << "; expected is_theme = " << it->second.is_theme;
+    // Delete the extension directory since we're not going to load
+    // it.
+    ChromeThread::PostTask(
+        ChromeThread::FILE, FROM_HERE,
+        NewRunnableFunction(&DeleteFileHelper, extension->path(), true));
+    delete extension;
+    return;
+  }
+
   extension_prefs_->OnExtensionInstalled(extension);
 
   // If the extension is a theme, tell the profile (and therefore ThemeProvider)
@@ -841,6 +912,11 @@
 
   // Also load the extension.
   OnExtensionLoaded(extension, allow_privilege_increase);
+
+  // Erase any pending extension.
+  if (it != pending_extensions_.end()) {
+    pending_extensions_.erase(it);
+  }
 }
 
 void ExtensionsService::OnExtensionOverinstallAttempted(const std::string& id) {
diff --git a/chrome/browser/extensions/extensions_service.h b/chrome/browser/extensions/extensions_service.h
index 413454c..256fa37ba 100644
--- a/chrome/browser/extensions/extensions_service.h
+++ b/chrome/browser/extensions/extensions_service.h
@@ -29,6 +29,7 @@
 #include "chrome/common/notification_observer.h"
 #include "chrome/common/notification_registrar.h"
 #include "chrome/common/extensions/extension.h"
+#include "testing/gtest/include/gtest/gtest_prod.h"
 
 class Browser;
 class ExtensionsServiceBackend;
@@ -39,6 +40,29 @@
 class Profile;
 class ResourceDispatcherHost;
 class SiteInstance;
+class Version;
+
+// A pending extension is an extension that hasn't been installed yet
+// and is intended to be installed in the next auto-update cycle.  The
+// update URL of a pending extension may be blank, in which case a
+// default one is assumed.
+struct PendingExtensionInfo {
+  PendingExtensionInfo(const GURL& update_url,
+                       const Version& version,
+                       bool is_theme,
+                       bool install_silently);
+
+  PendingExtensionInfo();
+
+  GURL update_url;
+  Version version;
+  bool is_theme;
+  bool install_silently;
+};
+
+// A PendingExtensionMap is a map from IDs of pending extensions to
+// their info.
+typedef std::map<std::string, PendingExtensionInfo> PendingExtensionMap;
 
 // This is an interface class to encapsulate the dependencies that
 // ExtensionUpdater has on ExtensionsService. This allows easy mocking.
@@ -46,6 +70,7 @@
  public:
   virtual ~ExtensionUpdateService() {}
   virtual const ExtensionList* extensions() const = 0;
+  virtual const PendingExtensionMap& pending_extensions() const = 0;
   virtual void UpdateExtension(const std::string& id, const FilePath& path,
                                const GURL& download_url) = 0;
   virtual Extension* GetExtensionById(const std::string& id,
@@ -115,6 +140,11 @@
     return &disabled_extensions_;
   }
 
+  // Gets the set of pending extensions.
+  virtual const PendingExtensionMap& pending_extensions() const {
+    return pending_extensions_;
+  }
+
   // Registers an extension to be loaded as a component extension.
   void register_component_extension(const ComponentExtensionInfo& info) {
     component_extension_manifests_.push_back(info);
@@ -159,6 +189,16 @@
                                const FilePath& extension_path,
                                const GURL& download_url);
 
+  // If an extension with the given id is already installed,
+  // does nothing.  Otherwise, the extension will be installed in the
+  // next auto-update cycle.
+  //
+  // TODO(akalin): Make sure that all the places that check for
+  // existing versions also consult the pending extension info.
+  void AddPendingExtension(
+      const std::string& id, const GURL& update_url,
+      const Version& version, bool is_theme, bool install_silently);
+
   // Reloads the specified extension.
   void ReloadExtension(const std::string& extension_id);
 
@@ -302,6 +342,12 @@
                                       bool include_enabled,
                                       bool include_disabled);
 
+  // Like AddPendingExtension() but assumes an extension with the same
+  // id is not already installed.
+  void AddPendingExtensionInternal(
+      const std::string& id, const GURL& update_url,
+      const Version& version, bool is_theme, bool install_silently);
+
   // Handles sending notification that |extension| was loaded.
   void NotifyExtensionLoaded(Extension* extension);
 
@@ -326,6 +372,9 @@
   // The list of installed extensions that have been disabled.
   ExtensionList disabled_extensions_;
 
+  // The set of pending extensions.
+  PendingExtensionMap pending_extensions_;
+
   // The full path to the directory where extensions are installed.
   FilePath install_directory_;
 
@@ -370,6 +419,8 @@
   typedef std::vector<ComponentExtensionInfo> RegisteredComponentExtensions;
   RegisteredComponentExtensions component_extension_manifests_;
 
+  FRIEND_TEST(ExtensionsServiceTest, UpdatePendingExtensionAlreadyInstalled);
+
   DISALLOW_COPY_AND_ASSIGN(ExtensionsService);
 };
 
diff --git a/chrome/browser/extensions/extensions_service_unittest.cc b/chrome/browser/extensions/extensions_service_unittest.cc
index ff9a2b6..c475884 100644
--- a/chrome/browser/extensions/extensions_service_unittest.cc
+++ b/chrome/browser/extensions/extensions_service_unittest.cc
@@ -13,9 +13,11 @@
 #include "base/json/json_reader.h"
 #include "base/message_loop.h"
 #include "base/path_service.h"
+#include "base/scoped_ptr.h"
 #include "base/string16.h"
 #include "base/string_util.h"
 #include "base/task.h"
+#include "base/version.h"
 #include "chrome/browser/extensions/crx_installer.h"
 #include "chrome/browser/extensions/extension_creator.h"
 #include "chrome/browser/extensions/extensions_service.h"
@@ -398,7 +400,8 @@
   }
 
   void UpdateExtension(const std::string& id, const FilePath& in_path,
-                       bool should_succeed, bool expect_report_on_failure) {
+                       bool should_succeed, bool should_install,
+                       bool expect_report_on_failure) {
     ASSERT_TRUE(file_util::PathExists(in_path));
 
     // We need to copy this to a temporary location because Update() will delete
@@ -413,7 +416,7 @@
 
     if (should_succeed) {
       EXPECT_EQ(0u, errors.size()) << path.value();
-      EXPECT_EQ(1u, service_->extensions()->size());
+      EXPECT_EQ(should_install ? 1u : 0u, service_->extensions()->size());
     } else {
       if (expect_report_on_failure) {
         EXPECT_EQ(1u, errors.size()) << path.value();
@@ -1048,7 +1051,7 @@
   ASSERT_EQ(good_crx, good->id());
 
   path = extensions_path.AppendASCII("good2.crx");
-  UpdateExtension(good_crx, path, true, true);
+  UpdateExtension(good_crx, path, true, true, true);
   ASSERT_EQ("1.0.0.1", loaded_[0]->version()->GetString());
 }
 
@@ -1060,7 +1063,7 @@
   extensions_path = extensions_path.AppendASCII("extensions");
 
   FilePath path = extensions_path.AppendASCII("good.crx");
-  service_->UpdateExtension(good_crx, path, GURL());
+  UpdateExtension(good_crx, path, true, false, true);
   loop_.RunAllPending();
 
   ASSERT_EQ(0u, service_->extensions()->size());
@@ -1084,7 +1087,7 @@
 
   // Change path from good2.crx -> good.crx
   path = extensions_path.AppendASCII("good.crx");
-  UpdateExtension(good_crx, path, false, true);
+  UpdateExtension(good_crx, path, false, false, true);
   ASSERT_EQ("1.0.0.1", service_->extensions()->at(0)->VersionString());
 }
 
@@ -1100,7 +1103,141 @@
   InstallExtension(path, true);
   Extension* good = service_->extensions()->at(0);
   ASSERT_EQ(good_crx, good->id());
-  UpdateExtension(good_crx, path, false, false);
+  UpdateExtension(good_crx, path, false, false, false);
+}
+
+// Test adding a pending extension.
+TEST_F(ExtensionsServiceTest, AddPendingExtension) {
+  InitializeEmptyExtensionsService();
+
+  const std::string kFakeId("fake-id");
+  const GURL kFakeUpdateURL("http:://fake.update/url");
+  scoped_ptr<Version> fake_version(Version::GetVersionFromString("4.3.2.1"));
+  ASSERT_TRUE(fake_version.get());
+  const bool kFakeIsTheme(false);
+  const bool kFakeInstallSilently(true);
+
+  service_->AddPendingExtension(kFakeId, kFakeUpdateURL,
+                                *fake_version, kFakeIsTheme,
+                                kFakeInstallSilently);
+  PendingExtensionMap::const_iterator it =
+      service_->pending_extensions().find(kFakeId);
+  ASSERT_TRUE(it != service_->pending_extensions().end());
+  EXPECT_EQ(kFakeUpdateURL, it->second.update_url);
+  EXPECT_EQ(kFakeIsTheme, it->second.is_theme);
+  EXPECT_EQ(kFakeInstallSilently, it->second.install_silently);
+  EXPECT_TRUE(it->second.version.Equals(*fake_version));
+}
+
+// Test adding a pending extension for one that is already installed.
+TEST_F(ExtensionsServiceTest, AddPendingExtensionAlreadyInstalled) {
+  InitializeEmptyExtensionsService();
+
+  FilePath extensions_path;
+  ASSERT_TRUE(PathService::Get(chrome::DIR_TEST_DATA, &extensions_path));
+  extensions_path = extensions_path.AppendASCII("extensions");
+  FilePath path = extensions_path.AppendASCII("good.crx");
+  InstallExtension(path, true);
+  Extension* good = service_->extensions()->at(0);
+  const bool kInstallSilently(true);
+
+  service_->AddPendingExtension(good->id(), good->update_url(),
+                                *good->version(), good->IsTheme(),
+                                kInstallSilently);
+  EXPECT_FALSE(ContainsKey(service_->pending_extensions(), good->id()));
+}
+
+namespace {
+const char kGoodId[] = "ldnnhddmnhbkjipkidpdiheffobcpfmf";
+const char kGoodUpdateURL[] = "http://good.update/url";
+const bool kGoodIsTheme = false;
+const bool kGoodInstallSilently = true;
+const char kGoodVersion[] = "1.2.3.4";
+}  // namespace
+
+// Test updating a pending extension.
+TEST_F(ExtensionsServiceTest, UpdatePendingExtension) {
+  InitializeEmptyExtensionsService();
+  scoped_ptr<Version> good_version(
+      Version::GetVersionFromString(kGoodVersion));
+  ASSERT_TRUE(good_version.get());
+  service_->AddPendingExtension(kGoodId, GURL(kGoodUpdateURL),
+                                *good_version, kGoodIsTheme,
+                                kGoodInstallSilently);
+  EXPECT_TRUE(ContainsKey(service_->pending_extensions(), kGoodId));
+
+  FilePath extensions_path;
+  ASSERT_TRUE(PathService::Get(chrome::DIR_TEST_DATA, &extensions_path));
+  extensions_path = extensions_path.AppendASCII("extensions");
+  FilePath path = extensions_path.AppendASCII("good.crx");
+  UpdateExtension(kGoodId, path, true, true, false);
+
+  EXPECT_FALSE(ContainsKey(service_->pending_extensions(), kGoodId));
+}
+
+// TODO(akalin): Test updating a pending extension non-silently once
+// we can mock out ExtensionInstallUI and inject our version into
+// UpdateExtension().
+
+// Test updating a pending extension with wrong is_theme.
+TEST_F(ExtensionsServiceTest, UpdatePendingExtensionWrongIsTheme) {
+  InitializeEmptyExtensionsService();
+  scoped_ptr<Version> good_version(
+      Version::GetVersionFromString(kGoodVersion));
+  ASSERT_TRUE(good_version.get());
+  // Add pending extension with a flipped is_theme.
+  service_->AddPendingExtension(kGoodId, GURL(kGoodUpdateURL),
+                                *good_version, !kGoodIsTheme,
+                                kGoodInstallSilently);
+  EXPECT_TRUE(ContainsKey(service_->pending_extensions(), kGoodId));
+
+  FilePath extensions_path;
+  ASSERT_TRUE(PathService::Get(chrome::DIR_TEST_DATA, &extensions_path));
+  extensions_path = extensions_path.AppendASCII("extensions");
+  FilePath path = extensions_path.AppendASCII("good.crx");
+  UpdateExtension(kGoodId, path, true, false, false);
+
+  // TODO(akalin): Figure out how to check that the extensions
+  // directory is cleaned up properly in OnExtensionInstalled().
+
+  EXPECT_TRUE(ContainsKey(service_->pending_extensions(), kGoodId));
+}
+
+// Test updating a pending extension for one that is not pending.
+TEST_F(ExtensionsServiceTest, UpdatePendingExtensionNotPending) {
+  InitializeEmptyExtensionsService();
+
+  FilePath extensions_path;
+  ASSERT_TRUE(PathService::Get(chrome::DIR_TEST_DATA, &extensions_path));
+  extensions_path = extensions_path.AppendASCII("extensions");
+  FilePath path = extensions_path.AppendASCII("good.crx");
+  UpdateExtension(kGoodId, path, true, false, false);
+
+  EXPECT_FALSE(ContainsKey(service_->pending_extensions(), kGoodId));
+}
+
+// Test updating a pending extension for one that is already
+// installed.
+TEST_F(ExtensionsServiceTest, UpdatePendingExtensionAlreadyInstalled) {
+  InitializeEmptyExtensionsService();
+
+  FilePath extensions_path;
+  ASSERT_TRUE(PathService::Get(chrome::DIR_TEST_DATA, &extensions_path));
+  extensions_path = extensions_path.AppendASCII("extensions");
+  FilePath path = extensions_path.AppendASCII("good.crx");
+  InstallExtension(path, true);
+  ASSERT_EQ(1u, service_->extensions()->size());
+  Extension* good = service_->extensions()->at(0);
+
+  // Use AddPendingExtensionInternal() as AddPendingExtension() would
+  // balk.
+  service_->AddPendingExtensionInternal(
+      good->id(), good->update_url(), *good->version(),
+      good->IsTheme(), kGoodInstallSilently);
+
+  UpdateExtension(good->id(), path, true, true, false);
+
+  EXPECT_TRUE(ContainsKey(service_->pending_extensions(), kGoodId));
 }
 
 // Test pref settings for blacklist and unblacklist extensions.
@@ -1144,7 +1281,7 @@
   InstallExtension(path, true);
   Extension* good = service_->extensions()->at(0);
   EXPECT_EQ(good_crx, good->id());
-  UpdateExtension(good_crx, path, false, false);
+  UpdateExtension(good_crx, path, false, false, false);
 
   std::vector<std::string> blacklist;
   blacklist.push_back(good_crx);