diff options
Diffstat (limited to 'chrome/browser')
-rw-r--r-- | chrome/browser/extensions/apps_promo.cc | 225 | ||||
-rw-r--r-- | chrome/browser/extensions/apps_promo.h | 100 | ||||
-rw-r--r-- | chrome/browser/extensions/apps_promo_unittest.cc | 139 | ||||
-rw-r--r-- | chrome/browser/ui/webui/ntp/app_launcher_handler.cc | 11 | ||||
-rw-r--r-- | chrome/browser/web_resource/promo_resource_service.cc | 31 | ||||
-rw-r--r-- | chrome/browser/web_resource/promo_resource_service.h | 26 | ||||
-rw-r--r-- | chrome/browser/web_resource/promo_resource_service_unittest.cc | 196 |
7 files changed, 513 insertions, 215 deletions
diff --git a/chrome/browser/extensions/apps_promo.cc b/chrome/browser/extensions/apps_promo.cc index a137bac..390b575 100644 --- a/chrome/browser/extensions/apps_promo.cc +++ b/chrome/browser/extensions/apps_promo.cc @@ -4,21 +4,44 @@ #include "chrome/browser/extensions/apps_promo.h" +#include "base/base64.h" #include "base/command_line.h" #include "base/metrics/histogram.h" #include "chrome/browser/browser_process.h" #include "chrome/browser/prefs/pref_service.h" +#include "chrome/browser/profiles/profile.h" #include "chrome/browser/ui/webui/ntp/shown_sections_handler.h" #include "chrome/common/chrome_switches.h" #include "chrome/common/extensions/extension.h" +#include "chrome/common/chrome_notification_types.h" #include "chrome/common/pref_names.h" +#include "content/common/url_constants.h" +#include "content/common/notification_service.h" +#include "net/base/load_flags.h" +#include "net/url_request/url_request_status.h" const int AppsPromo::kDefaultAppsCounterMax = 10; namespace { // The default logo for the promo. -const char kDefaultPromoLogo[] = "chrome://theme/IDR_WEBSTORE_ICON"; +const char kDefaultLogo[] = "chrome://theme/IDR_WEBSTORE_ICON"; + +// The default promo data (this is only used for testing with +// --force-apps-promo-visible). +const char kDefaultHeader[] = "Browse thousands of apps and games for Chrome"; +const char kDefaultButton[] = "Visit the Chrome Web Store"; +const char kDefaultExpire[] = "No thanks"; +const char kDefaultLink[] = "https://chrome.google.com/webstore"; + +// Http success status code. +const int kHttpSuccess = 200; + +// The match pattern for valid logo URLs. +const char kValidLogoPattern[] = "https://*.google.com/*.png"; + +// The prefix for 'data' URL images. +const char kPNGDataURLPrefix[] = "data:image/png;base64,"; // Returns the string pref at |path|, using |fallback| as the default (if there // is no pref value present). |fallback| is used for debugging in concert with @@ -35,6 +58,24 @@ std::string GetStringPref(const char* path, const std::string& fallback) { } // namespace +AppsPromo::PromoData::PromoData() {} +AppsPromo::PromoData::PromoData(const std::string& id, + const std::string& header, + const std::string& button, + const GURL& link, + const std::string& expire, + const GURL& logo, + const int user_group) + : id(id), + header(header), + button(button), + link(link), + expire(expire), + logo(logo), + user_group(user_group) {} + +AppsPromo::PromoData::~PromoData() {} + // static void AppsPromo::RegisterPrefs(PrefService* local_state) { std::string empty; @@ -44,6 +85,7 @@ void AppsPromo::RegisterPrefs(PrefService* local_state) { local_state->RegisterStringPref(prefs::kNTPWebStorePromoButton, empty); local_state->RegisterStringPref(prefs::kNTPWebStorePromoLink, empty); local_state->RegisterStringPref(prefs::kNTPWebStorePromoLogo, empty); + local_state->RegisterStringPref(prefs::kNTPWebStorePromoLogoSource, empty); local_state->RegisterStringPref(prefs::kNTPWebStorePromoExpire, empty); local_state->RegisterIntegerPref(prefs::kNTPWebStorePromoUserGroup, 0); } @@ -62,21 +104,8 @@ void AppsPromo::RegisterUserPrefs(PrefService* prefs) { std::string(), PrefService::UNSYNCABLE_PREF); prefs->RegisterBooleanPref(prefs::kNTPHideWebStorePromo, - false, - PrefService::UNSYNCABLE_PREF); -} - -// static -void AppsPromo::ClearPromo() { - PrefService* local_state = g_browser_process->local_state(); - local_state->ClearPref(prefs::kNTPWebStoreEnabled); - local_state->ClearPref(prefs::kNTPWebStorePromoId); - local_state->ClearPref(prefs::kNTPWebStorePromoHeader); - local_state->ClearPref(prefs::kNTPWebStorePromoButton); - local_state->ClearPref(prefs::kNTPWebStorePromoLink); - local_state->ClearPref(prefs::kNTPWebStorePromoLogo); - local_state->ClearPref(prefs::kNTPWebStorePromoExpire); - local_state->ClearPref(prefs::kNTPWebStorePromoUserGroup); + false, + PrefService::UNSYNCABLE_PREF); } // static @@ -100,68 +129,68 @@ bool AppsPromo::IsWebStoreSupportedForLocale() { } // static -std::string AppsPromo::GetPromoButtonText() { - return GetStringPref(prefs::kNTPWebStorePromoButton, "Click here now"); +void AppsPromo::SetWebStoreSupportedForLocale(bool supported) { + PrefService* local_state = g_browser_process->local_state(); + local_state->SetBoolean(prefs::kNTPWebStoreEnabled, supported); } // static -std::string AppsPromo::GetPromoId() { - return GetStringPref(prefs::kNTPWebStorePromoId, ""); +void AppsPromo::ClearPromo() { + PrefService* local_state = g_browser_process->local_state(); + local_state->ClearPref(prefs::kNTPWebStoreEnabled); + local_state->ClearPref(prefs::kNTPWebStorePromoId); + local_state->ClearPref(prefs::kNTPWebStorePromoHeader); + local_state->ClearPref(prefs::kNTPWebStorePromoButton); + local_state->ClearPref(prefs::kNTPWebStorePromoLink); + local_state->ClearPref(prefs::kNTPWebStorePromoLogo); + local_state->ClearPref(prefs::kNTPWebStorePromoLogoSource); + local_state->ClearPref(prefs::kNTPWebStorePromoExpire); + local_state->ClearPref(prefs::kNTPWebStorePromoUserGroup); } // static -std::string AppsPromo::GetPromoHeaderText() { - return GetStringPref(prefs::kNTPWebStorePromoHeader, "Get great apps!"); -} +AppsPromo::PromoData AppsPromo::GetPromo() { + PromoData data; + PrefService* local_state = g_browser_process->local_state(); -// static -GURL AppsPromo::GetPromoLink() { - return GURL(GetStringPref(prefs::kNTPWebStorePromoLink, - "https://chrome.google.com/webstore")); -} + data.id = GetStringPref(prefs::kNTPWebStorePromoId, ""); + data.link = GURL(GetStringPref(prefs::kNTPWebStorePromoLink, kDefaultLink)); + data.user_group = local_state->GetInteger(prefs::kNTPWebStorePromoUserGroup); + data.header = GetStringPref(prefs::kNTPWebStorePromoHeader, kDefaultHeader); + data.button = GetStringPref(prefs::kNTPWebStorePromoButton, kDefaultButton); + data.expire = GetStringPref(prefs::kNTPWebStorePromoExpire, kDefaultExpire); -// static -GURL AppsPromo::GetPromoLogo() { - PrefService* local_state = g_browser_process->local_state(); GURL logo_url(local_state->GetString(prefs::kNTPWebStorePromoLogo)); - if (logo_url.is_valid() && logo_url.SchemeIs("data")) - return logo_url; - return GURL(kDefaultPromoLogo); -} + if (logo_url.is_valid() && logo_url.SchemeIs(chrome::kDataScheme)) + data.logo = logo_url; + else + data.logo = GURL(kDefaultLogo); -// static -std::string AppsPromo::GetPromoExpireText() { - return GetStringPref(prefs::kNTPWebStorePromoExpire, "No thanks."); + return data; } // static -int AppsPromo::GetPromoUserGroup() { +void AppsPromo::SetPromo(AppsPromo::PromoData data) { PrefService* local_state = g_browser_process->local_state(); - return local_state->GetInteger(prefs::kNTPWebStorePromoUserGroup); + local_state->SetString(prefs::kNTPWebStorePromoId, data.id); + local_state->SetString(prefs::kNTPWebStorePromoButton, data.button); + local_state->SetString(prefs::kNTPWebStorePromoHeader, data.header); + local_state->SetString(prefs::kNTPWebStorePromoLink, data.link.spec()); + local_state->SetString(prefs::kNTPWebStorePromoLogo, data.logo.spec()); + local_state->SetString(prefs::kNTPWebStorePromoExpire, data.expire); + local_state->SetInteger(prefs::kNTPWebStorePromoUserGroup, data.user_group); } // static -void AppsPromo::SetPromo(const std::string& id, - const std::string& header_text, - const std::string& button_text, - const GURL& link, - const std::string& expire_text, - const GURL& logo, - const int user_group) { - PrefService* local_state = g_browser_process->local_state(); - local_state->SetString(prefs::kNTPWebStorePromoId, id); - local_state->SetString(prefs::kNTPWebStorePromoButton, button_text); - local_state->SetString(prefs::kNTPWebStorePromoHeader, header_text); - local_state->SetString(prefs::kNTPWebStorePromoLink, link.spec()); - local_state->SetString(prefs::kNTPWebStorePromoLogo, logo.spec()); - local_state->SetString(prefs::kNTPWebStorePromoExpire, expire_text); - local_state->SetInteger(prefs::kNTPWebStorePromoUserGroup, user_group); +GURL AppsPromo::GetSourcePromoLogoURL() { + return GURL(GetStringPref(prefs::kNTPWebStorePromoLogoSource, "")); } // static -void AppsPromo::SetWebStoreSupportedForLocale(bool supported) { +void AppsPromo::SetSourcePromoLogoURL(GURL logo_source) { PrefService* local_state = g_browser_process->local_state(); - local_state->SetBoolean(prefs::kNTPWebStoreEnabled, supported); + local_state->SetString(prefs::kNTPWebStorePromoLogoSource, + logo_source.spec()); } AppsPromo::AppsPromo(PrefService* prefs) @@ -246,15 +275,14 @@ void AppsPromo::ExpireDefaultApps() { } void AppsPromo::MaximizeAppsIfNecessary() { - std::string promo_id = GetPromoId(); - int maximize_setting = GetPromoUserGroup(); + PromoData promo = GetPromo(); // Maximize the apps section of the NTP if this is the first time viewing the // specific promo and the current user group is targetted. - if (GetLastPromoId() != promo_id) { - if ((maximize_setting & GetCurrentUserGroup()) != 0) + if (GetLastPromoId() != promo.id) { + if ((promo.user_group & GetCurrentUserGroup()) != 0) ShownSectionsHandler::SetShownSection(prefs_, APPS); - SetLastPromoId(promo_id); + SetLastPromoId(promo.id); } } @@ -297,3 +325,76 @@ AppsPromo::UserGroup AppsPromo::GetCurrentUserGroup() const { CHECK(last_promo_id); return last_promo_id->IsDefaultValue() ? USERS_NEW : USERS_EXISTING; } + +AppsPromoLogoFetcher::AppsPromoLogoFetcher( + Profile* profile, + AppsPromo::PromoData promo_data) + : profile_(profile), + promo_data_(promo_data) { + if (SupportsLogoURL()) { + if (HaveCachedLogo()) { + promo_data_.logo = AppsPromo::GetPromo().logo; + SavePromo(); + } else { + FetchLogo(); + } + } else { + // We only care about the source URL when this fetches the logo. + AppsPromo::SetSourcePromoLogoURL(GURL()); + SavePromo(); + } +} + +AppsPromoLogoFetcher::~AppsPromoLogoFetcher() {} + +void AppsPromoLogoFetcher::OnURLFetchComplete(const URLFetcher* source) { + std::string data; + std::string base64_data; + + CHECK(source == url_fetcher_.get()); + CHECK(source->GetResponseAsString(&data)); + + if (source->status().is_success() && + source->response_code() == kHttpSuccess && + base::Base64Encode(data, &base64_data)) { + AppsPromo::SetSourcePromoLogoURL(promo_data_.logo); + promo_data_.logo = GURL(kPNGDataURLPrefix + base64_data); + } else { + // The logo wasn't downloaded correctly or we failed to encode it in + // base64. Reset the source URL so we fetch it again next time. AppsPromo + // will revert to the default logo. + AppsPromo::SetSourcePromoLogoURL(GURL()); + } + + SavePromo(); +} + +void AppsPromoLogoFetcher::FetchLogo() { + CHECK(promo_data_.logo.scheme() == chrome::kHttpsScheme); + + url_fetcher_.reset(URLFetcher::Create( + 0, promo_data_.logo, URLFetcher::GET, this)); + url_fetcher_->set_request_context( + g_browser_process->system_request_context()); + url_fetcher_->set_load_flags(net::LOAD_DO_NOT_SEND_COOKIES | + net::LOAD_DO_NOT_SAVE_COOKIES); + url_fetcher_->Start(); +} + +bool AppsPromoLogoFetcher::HaveCachedLogo() { + return promo_data_.logo == AppsPromo::GetSourcePromoLogoURL(); +} + +void AppsPromoLogoFetcher::SavePromo() { + AppsPromo::SetPromo(promo_data_); + + NotificationService::current()->Notify( + chrome::NOTIFICATION_WEB_STORE_PROMO_LOADED, + Source<Profile>(profile_), + NotificationService::NoDetails()); +} + +bool AppsPromoLogoFetcher::SupportsLogoURL() { + URLPattern allowed_urls(URLPattern::SCHEME_HTTPS, kValidLogoPattern); + return allowed_urls.MatchesURL(promo_data_.logo); +} diff --git a/chrome/browser/extensions/apps_promo.h b/chrome/browser/extensions/apps_promo.h index 1715180..28c5b03 100644 --- a/chrome/browser/extensions/apps_promo.h +++ b/chrome/browser/extensions/apps_promo.h @@ -11,8 +11,10 @@ #include "base/gtest_prod_util.h" #include "chrome/common/extensions/extension.h" +#include "content/common/url_fetcher.h" class PrefService; +class Profile; // This encapsulates business logic for: // - Whether to show the apps promo in the launcher @@ -32,56 +34,55 @@ class AppsPromo { USERS_EXISTING = 1 << 1, }; + // Holds all the data that specifies a promo for the apps section of the NTP. + struct PromoData { + PromoData(); + PromoData(const std::string& id, + const std::string& header, + const std::string& button, + const GURL& link, + const std::string& expire, + const GURL& logo, + int user_group); + ~PromoData(); + + // See PromoResourceService::UnpackWebStoreSignal for descriptions of these + // fields. + std::string id; + std::string header; + std::string button; + GURL link; + std::string expire; + GURL logo; + int user_group; + }; + // Register our preferences. Parts of the promo content are stored in Local // State since they're independent of the user profile. static void RegisterPrefs(PrefService* local_state); static void RegisterUserPrefs(PrefService* prefs); - // Removes the current promo data. - static void ClearPromo(); - // Returns true if a promo is available for the current locale. static bool IsPromoSupportedForLocale(); // Returns true if the web store is active for the existing locale. static bool IsWebStoreSupportedForLocale(); - // Gets the ID of the current promo. - static std::string GetPromoId(); - - // Gets the text for the promo button. - static std::string GetPromoButtonText(); - - // Gets the text for the promo header. - static std::string GetPromoHeaderText(); - - // Gets the promo link. - static GURL GetPromoLink(); - - // Gets the URL of the promo logo image. - static GURL GetPromoLogo(); - - // Gets the text for the promo "hide this" link. - static std::string GetPromoExpireText(); - - // Gets the user groups for which we should maximize the promo and apps - // section. The return value is a bitwise OR of UserGroup enums. - static int GetPromoUserGroup(); - - // Called to set the current promo data. The default web store logo will be - // used if |logo| is empty or not valid. - static void SetPromo(const std::string& id, - const std::string& header_text, - const std::string& button_text, - const GURL& link, - const std::string& expire_text, - const GURL& logo, - const int user_group); - // Sets whether the web store and apps section is supported for the current // locale. static void SetWebStoreSupportedForLocale(bool supported); + // Accesses the current promo data. The default logo will be used if + // |promo_data.logo| is empty or not a valid 'data' URL. + static void ClearPromo(); + static PromoData GetPromo(); + static void SetPromo(PromoData promo_data); + + // Gets the original URL of the logo. This should only be set when the logo + // was served over HTTPS. + static GURL GetSourcePromoLogoURL(); + static void SetSourcePromoLogoURL(GURL original_url); + explicit AppsPromo(PrefService* prefs); ~AppsPromo(); @@ -145,4 +146,33 @@ class AppsPromo { DISALLOW_COPY_AND_ASSIGN(AppsPromo); }; +// Fetches logos over HTTPS, making sure we don't send cookies and that we +// cache the image until its source URL changes. +class AppsPromoLogoFetcher : public URLFetcher::Delegate { + public: + AppsPromoLogoFetcher(Profile* profile, + AppsPromo::PromoData promo_data); + virtual ~AppsPromoLogoFetcher(); + + virtual void OnURLFetchComplete(const URLFetcher* source) OVERRIDE; + + private: + // Fetches the logo and stores the result as a data URL. + void FetchLogo(); + + // Checks if the logo was downloaded previously. + bool HaveCachedLogo(); + + // Sets the apps promo based on the current data and then issues the + // WEB_STORE_PROMO_LOADED notification so open NTPs can inject the promo. + void SavePromo(); + + // Checks if the promo logo matches https://*.google.com/*.png. + bool SupportsLogoURL(); + + Profile* profile_; + AppsPromo::PromoData promo_data_; + scoped_ptr<URLFetcher> url_fetcher_; +}; + #endif // CHROME_BROWSER_EXTENSIONS_APPS_PROMO_H_ diff --git a/chrome/browser/extensions/apps_promo_unittest.cc b/chrome/browser/extensions/apps_promo_unittest.cc index 607da43..77d40ea 100644 --- a/chrome/browser/extensions/apps_promo_unittest.cc +++ b/chrome/browser/extensions/apps_promo_unittest.cc @@ -108,9 +108,10 @@ TEST_F(ExtensionAppsPromo, HappyPath) { &promo_just_expired)); // Once the promo is set, we show both the promo and app launcher. - AppsPromo::SetPromo(kPromoId, kPromoHeader, kPromoButton, - GURL(kPromoLink), kPromoExpire, GURL(""), - kPromoUserGroup); + AppsPromo::PromoData promo_data(kPromoId, kPromoHeader, kPromoButton, + GURL(kPromoLink), kPromoExpire, GURL(""), + kPromoUserGroup); + AppsPromo::SetPromo(promo_data); AppsPromo::SetWebStoreSupportedForLocale(true); EXPECT_TRUE(AppsPromo::IsPromoSupportedForLocale()); EXPECT_TRUE(apps_promo()->ShouldShowAppLauncher(installed_ids)); @@ -148,60 +149,56 @@ TEST_F(ExtensionAppsPromo, HappyPath) { // Tests get and set of promo content. TEST_F(ExtensionAppsPromo, PromoPrefs) { // Store a promo.... - AppsPromo::SetPromo(kPromoId, kPromoHeader, kPromoButton, - GURL(kPromoLink), kPromoExpire, GURL(""), - kPromoUserGroup); + AppsPromo::PromoData promo_data(kPromoId, kPromoHeader, kPromoButton, + GURL(kPromoLink), kPromoExpire, GURL(""), + kPromoUserGroup); + AppsPromo::SetPromo(promo_data); // ... then make sure AppsPromo can access it. - EXPECT_EQ(kPromoId, AppsPromo::GetPromoId()); - EXPECT_EQ(kPromoHeader, AppsPromo::GetPromoHeaderText()); - EXPECT_EQ(kPromoButton, AppsPromo::GetPromoButtonText()); - EXPECT_EQ(GURL(kPromoLink), AppsPromo::GetPromoLink()); - EXPECT_EQ(kPromoExpire, AppsPromo::GetPromoExpireText()); - EXPECT_EQ(kPromoUserGroup, AppsPromo::GetPromoUserGroup()); + AppsPromo::PromoData actual_data = AppsPromo::GetPromo(); + EXPECT_EQ(kPromoId, actual_data.id); + EXPECT_EQ(kPromoHeader, actual_data.header); + EXPECT_EQ(kPromoButton, actual_data.button); + EXPECT_EQ(GURL(kPromoLink), actual_data.link); + EXPECT_EQ(kPromoExpire, actual_data.expire); + EXPECT_EQ(kPromoUserGroup, actual_data.user_group); // The promo logo should be the default value. - EXPECT_EQ(GURL(kPromoLogo), AppsPromo::GetPromoLogo()); + EXPECT_EQ(GURL(kPromoLogo), actual_data.logo); EXPECT_TRUE(AppsPromo::IsPromoSupportedForLocale()); AppsPromo::ClearPromo(); - EXPECT_EQ("", AppsPromo::GetPromoId()); - EXPECT_EQ("", AppsPromo::GetPromoHeaderText()); - EXPECT_EQ("", AppsPromo::GetPromoButtonText()); - EXPECT_EQ(GURL(""), AppsPromo::GetPromoLink()); - EXPECT_EQ("", AppsPromo::GetPromoExpireText()); - EXPECT_EQ(AppsPromo::USERS_NONE, AppsPromo::GetPromoUserGroup()); - EXPECT_EQ(GURL(kPromoLogo), AppsPromo::GetPromoLogo()); + actual_data = AppsPromo::GetPromo(); + EXPECT_EQ("", actual_data.id); + EXPECT_EQ("", actual_data.header); + EXPECT_EQ("", actual_data.button); + EXPECT_EQ(GURL(""), actual_data.link); + EXPECT_EQ("", actual_data.expire); + EXPECT_EQ(AppsPromo::USERS_NONE, actual_data.user_group); + EXPECT_EQ(GURL(kPromoLogo), actual_data.logo); EXPECT_FALSE(AppsPromo::IsPromoSupportedForLocale()); // Make sure we can set the logo to something other than the default. std::string promo_logo = "data:image/png;base64,iVBORw0kGgoAAAN"; - AppsPromo::SetPromo(kPromoId, kPromoHeader, kPromoButton, - GURL(kPromoLink), kPromoExpire, GURL(promo_logo), - kPromoUserGroup); - EXPECT_EQ(GURL(promo_logo), AppsPromo::GetPromoLogo()); + promo_data.logo = GURL(promo_logo); + AppsPromo::SetPromo(promo_data); + EXPECT_EQ(GURL(promo_logo), AppsPromo::GetPromo().logo); EXPECT_TRUE(AppsPromo::IsPromoSupportedForLocale()); - // Verify that the default is returned instead of http or https URLs. - promo_logo = "http://google.com/logo.png"; - AppsPromo::SetPromo(kPromoId, kPromoHeader, kPromoButton, - GURL(kPromoLink), kPromoExpire, GURL(promo_logo), - kPromoUserGroup); - EXPECT_EQ(GURL(kPromoLogo), AppsPromo::GetPromoLogo()); + // Verify that the default is returned instead of HTTP or HTTPS URLs. + promo_data.logo = GURL("http://google.com/logo.png"); + AppsPromo::SetPromo(promo_data); + EXPECT_EQ(GURL(kPromoLogo), AppsPromo::GetPromo().logo); EXPECT_TRUE(AppsPromo::IsPromoSupportedForLocale()); - promo_logo = "https://google.com/logo.png"; - AppsPromo::SetPromo(kPromoId, kPromoHeader, kPromoButton, - GURL(kPromoLink), kPromoExpire, GURL(promo_logo), - kPromoUserGroup); - EXPECT_EQ(GURL(kPromoLogo), AppsPromo::GetPromoLogo()); + promo_data.logo = GURL("https://google.com/logo.png"); + AppsPromo::SetPromo(promo_data); + EXPECT_EQ(GURL(kPromoLogo), AppsPromo::GetPromo().logo); EXPECT_TRUE(AppsPromo::IsPromoSupportedForLocale()); // Try an invalid URL. - promo_logo = "sldkfjlsdn"; - AppsPromo::SetPromo(kPromoId, kPromoHeader, kPromoButton, - GURL(kPromoLink), kPromoExpire, GURL(promo_logo), - kPromoUserGroup); - EXPECT_EQ(GURL(kPromoLogo), AppsPromo::GetPromoLogo()); + promo_data.logo = GURL("sldkfjlsdn"); + AppsPromo::SetPromo(promo_data); + EXPECT_EQ(GURL(kPromoLogo), AppsPromo::GetPromo().logo); EXPECT_TRUE(AppsPromo::IsPromoSupportedForLocale()); // Try the web store supported flag. @@ -210,6 +207,11 @@ TEST_F(ExtensionAppsPromo, PromoPrefs) { EXPECT_TRUE(AppsPromo::IsWebStoreSupportedForLocale()); AppsPromo::SetWebStoreSupportedForLocale(false); EXPECT_FALSE(AppsPromo::IsWebStoreSupportedForLocale()); + + // Try setting and getting the source logo URL. + GURL expected_source("https://www.google.com/images/test.png"); + AppsPromo::SetSourcePromoLogoURL(expected_source); + EXPECT_EQ(expected_source, AppsPromo::GetSourcePromoLogoURL()); } // Tests maximizing the promo for USERS_NONE. @@ -218,16 +220,16 @@ TEST_F(ExtensionAppsPromo, UpdatePromoFocus_UsersNone) { ExpectAppsSectionMaximized(prefs(), false); // The promo shouldn't maximize for anyone. - AppsPromo::SetPromo(kPromoId, kPromoHeader, kPromoButton, - GURL(kPromoLink), kPromoExpire, GURL(""), - AppsPromo::USERS_NONE); + AppsPromo::PromoData promo_data(kPromoId, kPromoHeader, kPromoButton, + GURL(kPromoLink), kPromoExpire, GURL(""), + AppsPromo::USERS_NONE); + AppsPromo::SetPromo(promo_data); apps_promo()->MaximizeAppsIfNecessary(); ExpectAppsSectionMaximized(prefs(), false); // The promo still shouldn't maximize if we change it's ID. - AppsPromo::SetPromo("lkksdf", kPromoHeader, kPromoButton, - GURL(kPromoLink), kPromoExpire, GURL(""), - AppsPromo::USERS_NONE); + promo_data.id = "lkksdf"; + AppsPromo::SetPromo(promo_data); apps_promo()->MaximizeAppsIfNecessary(); ExpectAppsSectionMaximized(prefs(), false); } @@ -238,19 +240,17 @@ TEST_F(ExtensionAppsPromo, UpdatePromoFocus_UsersExisting) { ExpectAppsSectionMaximized(prefs(), false); // Set the promo content. - AppsPromo::SetPromo(kPromoId, kPromoHeader, kPromoButton, - GURL(kPromoLink), kPromoExpire, GURL(""), - AppsPromo::USERS_EXISTING); - + AppsPromo::PromoData promo_data(kPromoId, kPromoHeader, kPromoButton, + GURL(kPromoLink), kPromoExpire, GURL(""), + AppsPromo::USERS_EXISTING); + AppsPromo::SetPromo(promo_data); // This is a new user so the apps section shouldn't maximize. apps_promo()->MaximizeAppsIfNecessary(); ExpectAppsSectionMaximized(prefs(), false); - // Set a new promo and now it should maximize. - AppsPromo::SetPromo("lksdf", kPromoHeader, kPromoButton, - GURL(kPromoLink), kPromoExpire, GURL(""), - AppsPromo::USERS_EXISTING); + promo_data.id = "lksdf"; + AppsPromo::SetPromo(promo_data); apps_promo()->MaximizeAppsIfNecessary(); ExpectAppsSectionMaximized(prefs(), true); @@ -265,9 +265,10 @@ TEST_F(ExtensionAppsPromo, UpdatePromoFocus_UsersNew) { ExpectAppsSectionMaximized(prefs(), false); // The promo should maximize for new users. - AppsPromo::SetPromo(kPromoId, kPromoHeader, kPromoButton, - GURL(kPromoLink), kPromoExpire, GURL(""), - AppsPromo::USERS_NEW); + AppsPromo::PromoData promo_data(kPromoId, kPromoHeader, kPromoButton, + GURL(kPromoLink), kPromoExpire, GURL(""), + AppsPromo::USERS_NEW); + AppsPromo::SetPromo(promo_data); apps_promo()->MaximizeAppsIfNecessary(); ExpectAppsSectionMaximized(prefs(), true); @@ -280,9 +281,8 @@ TEST_F(ExtensionAppsPromo, UpdatePromoFocus_UsersNew) { ExpectAppsSectionMaximized(prefs(), false); // Another promo targetting new users should not maximize. - AppsPromo::SetPromo("lksdf", kPromoHeader, kPromoButton, - GURL(kPromoLink), kPromoExpire, GURL(""), - AppsPromo::USERS_NEW); + promo_data.id = "lksdf"; + AppsPromo::SetPromo(promo_data); apps_promo()->MaximizeAppsIfNecessary(); ExpectAppsSectionMaximized(prefs(), false); } @@ -293,9 +293,10 @@ TEST_F(ExtensionAppsPromo, UpdatePromoFocus_UsersAll) { ExpectAppsSectionMaximized(prefs(), false); // The apps section should maximize for all users. - AppsPromo::SetPromo(kPromoId, kPromoHeader, kPromoButton, - GURL(kPromoLink), kPromoExpire, GURL(""), - AppsPromo::USERS_NEW | AppsPromo::USERS_EXISTING); + AppsPromo::PromoData promo_data( + kPromoId, kPromoHeader, kPromoButton, GURL(kPromoLink), kPromoExpire, + GURL(""), AppsPromo::USERS_NEW | AppsPromo::USERS_EXISTING); + AppsPromo::SetPromo(promo_data); apps_promo()->MaximizeAppsIfNecessary(); ExpectAppsSectionMaximized(prefs(), true); @@ -307,9 +308,8 @@ TEST_F(ExtensionAppsPromo, UpdatePromoFocus_UsersAll) { ExpectAppsSectionMaximized(prefs(), false); // A promo with a new ID should maximize though. - AppsPromo::SetPromo("lkksdf", kPromoHeader, kPromoButton, - GURL(kPromoLink), kPromoExpire, GURL(""), - AppsPromo::USERS_NEW | AppsPromo::USERS_EXISTING); + promo_data.id = "lkksdf"; + AppsPromo::SetPromo(promo_data); apps_promo()->MaximizeAppsIfNecessary(); ExpectAppsSectionMaximized(prefs(), true); } @@ -320,9 +320,10 @@ TEST_F(ExtensionAppsPromo, PromoHiddenByPref) { // When the "hide" pref is false, the promo should still appear. prefs()->SetBoolean(prefs::kNTPHideWebStorePromo, false); - AppsPromo::SetPromo(kPromoId, kPromoHeader, kPromoButton, - GURL(kPromoLink), kPromoExpire, GURL(""), - AppsPromo::USERS_NEW | AppsPromo::USERS_EXISTING); + AppsPromo::PromoData promo_data( + kPromoId, kPromoHeader, kPromoButton, GURL(kPromoLink), kPromoExpire, + GURL(""), AppsPromo::USERS_NEW | AppsPromo::USERS_EXISTING); + AppsPromo::SetPromo(promo_data); bool just_expired; bool show_promo = apps_promo()->ShouldShowPromo( apps_promo()->old_default_apps(), &just_expired); diff --git a/chrome/browser/ui/webui/ntp/app_launcher_handler.cc b/chrome/browser/ui/webui/ntp/app_launcher_handler.cc index 9916ccd..6d08ef2 100644 --- a/chrome/browser/ui/webui/ntp/app_launcher_handler.cc +++ b/chrome/browser/ui/webui/ntp/app_launcher_handler.cc @@ -452,11 +452,12 @@ DictionaryValue* AppLauncherHandler::GetAppInfo(const Extension* extension) { } void AppLauncherHandler::FillPromoDictionary(DictionaryValue* dictionary) { - dictionary->SetString("promoHeader", AppsPromo::GetPromoHeaderText()); - dictionary->SetString("promoButton", AppsPromo::GetPromoButtonText()); - dictionary->SetString("promoLink", AppsPromo::GetPromoLink().spec()); - dictionary->SetString("promoLogo", AppsPromo::GetPromoLogo().spec()); - dictionary->SetString("promoExpire", AppsPromo::GetPromoExpireText()); + AppsPromo::PromoData data = AppsPromo::GetPromo(); + dictionary->SetString("promoHeader", data.header); + dictionary->SetString("promoButton", data.button); + dictionary->SetString("promoLink", data.link.spec()); + dictionary->SetString("promoLogo", data.logo.spec()); + dictionary->SetString("promoExpire", data.expire); } void AppLauncherHandler::HandleGetApps(const ListValue* args) { diff --git a/chrome/browser/web_resource/promo_resource_service.cc b/chrome/browser/web_resource/promo_resource_service.cc index 09b0aee..1336fd0 100644 --- a/chrome/browser/web_resource/promo_resource_service.cc +++ b/chrome/browser/web_resource/promo_resource_service.cc @@ -316,13 +316,9 @@ void PromoResourceService::UnpackWebStoreSignal( bool is_webstore_active = false; bool signal_found = false; - std::string promo_id = ""; - std::string promo_header = ""; - std::string promo_button = ""; + AppsPromo::PromoData promo_data; std::string promo_link = ""; - std::string promo_expire = ""; std::string promo_logo = ""; - int maximize_setting = 0; int target_builds = 0; if (!parsed_json.GetDictionary("topic", &topic_dict) || @@ -362,24 +358,26 @@ void PromoResourceService::UnpackWebStoreSignal( name = name.substr(split+1); split = name.find(':'); if (split == std::string::npos || - !base::StringToInt(name.substr(0, split), &maximize_setting)) + !base::StringToInt(name.substr(0, split), &promo_data.user_group)) continue; // (4) optional text that specifies a URL of a logo image promo_logo = name.substr(split+1); - if (!a_dic->GetString(kAnswerIdProperty, &promo_id) || - !a_dic->GetString(kWebStoreHeaderProperty, &promo_header) || - !a_dic->GetString(kWebStoreButtonProperty, &promo_button) || + if (!a_dic->GetString(kAnswerIdProperty, &promo_data.id) || + !a_dic->GetString(kWebStoreHeaderProperty, &promo_data.header) || + !a_dic->GetString(kWebStoreButtonProperty, &promo_data.button) || !a_dic->GetString(kWebStoreLinkProperty, &promo_link) || - !a_dic->GetString(kWebStoreExpireProperty, &promo_expire)) + !a_dic->GetString(kWebStoreExpireProperty, &promo_data.expire)) continue; if (IsThisBuildTargeted(target_builds)) { - // Store the first web store promo that targets the current build. - AppsPromo::SetPromo(promo_id, promo_header, promo_button, - GURL(promo_link), promo_expire, GURL(promo_logo), - maximize_setting); + // The downloader will set the promo prefs and send the + // NOTIFICATION_WEB_STORE_PROMO_LOADED notification. + promo_data.link = GURL(promo_link); + promo_data.logo = GURL(promo_logo); + apps_promo_logo_fetcher_.reset( + new AppsPromoLogoFetcher(profile_, promo_data)); signal_found = true; break; } @@ -392,11 +390,6 @@ void PromoResourceService::UnpackWebStoreSignal( AppsPromo::SetWebStoreSupportedForLocale(is_webstore_active); - NotificationService::current()->Notify( - chrome::NOTIFICATION_WEB_STORE_PROMO_LOADED, - Source<Profile>(profile_), - NotificationService::NoDetails()); - return; } diff --git a/chrome/browser/web_resource/promo_resource_service.h b/chrome/browser/web_resource/promo_resource_service.h index 2350418..a68e2aa 100644 --- a/chrome/browser/web_resource/promo_resource_service.h +++ b/chrome/browser/web_resource/promo_resource_service.h @@ -11,6 +11,8 @@ #include "chrome/browser/web_resource/web_resource_service.h" #include "chrome/common/chrome_version_info.h" +class AppsPromoLogoFetcher; +class PrefService; class Profile; namespace PromoResourceServiceUtil { @@ -21,8 +23,6 @@ bool CanShowPromo(Profile* profile); } // namespace PromoResourceServiceUtil -class PrefService; - // A PromoResourceService fetches data from a web resource server to be used to // dynamically change the appearance of the New Tab Page. For example, it has // been used to fetch "tips" to be displayed on the NTP, or to display @@ -52,6 +52,13 @@ class PromoResourceService FRIEND_TEST_ALL_PREFIXES(PromoResourceServiceTest, UnpackWebStoreSignal); FRIEND_TEST_ALL_PREFIXES( PromoResourceServiceTest, UnpackPartialWebStoreSignal); + FRIEND_TEST_ALL_PREFIXES( + PromoResourceServiceTest, UnpackWebStoreSignalHttpsLogo); + FRIEND_TEST_ALL_PREFIXES( + PromoResourceServiceTest, UnpackWebStoreSignalHttpsLogoError); + FRIEND_TEST_ALL_PREFIXES( + PromoResourceServiceTest, UnpackWebStoreSignalHttpLogo); + // Identifies types of Chrome builds for promo targeting. enum BuildType { @@ -168,7 +175,7 @@ class PromoResourceService // "answers": [ // { // "answer_id": "1143011", - // "name": "webstore_promo:15:", + // "name": "webstore_promo:15:1:https://www.google.com/logo.png", // "question": "Browse thousands of apps and games for Chrome.", // "inproduct_target": "Visit the Chrome Web Store", // "inproduct": "https://chrome.google.com/webstore?hl=en", @@ -184,11 +191,13 @@ class PromoResourceService // inproduct_target: the promo button text // inproduct: the promo button link // tooltip: the text for the "hide this" link on the promo - // name: starts with "webstore_promo" to identify the signal. the second + // name: starts with "webstore_promo" to identify the signal. The second // part contains the release channels targeted (bitwise or of - // BuildTypes). The third part is optional and specifies the URL of - // the logo image. In the example above, the URL is empty so the - // default webstore logo will be used. + // BuildTypes). The third part specifies what users should maximize + // the apps section of the NTP when first loading the promo (bitwise + // or of AppsPromo::UserGroup). The forth part is optional and + // specifies the URL of the logo image. If left out, the default + // webstore logo will be used. The logo can be an HTTPS or DATA URL. // answer_id: the promo's id void UnpackWebStoreSignal(const base::DictionaryValue& parsed_json); @@ -202,6 +211,9 @@ class PromoResourceService // Overrides the current Chrome release channel for testing purposes. chrome::VersionInfo::Channel channel_; + // A helper that downloads the promo logo. + scoped_ptr<AppsPromoLogoFetcher> apps_promo_logo_fetcher_; + DISALLOW_COPY_AND_ASSIGN(PromoResourceService); }; diff --git a/chrome/browser/web_resource/promo_resource_service_unittest.cc b/chrome/browser/web_resource/promo_resource_service_unittest.cc index b196b12..a69385c 100644 --- a/chrome/browser/web_resource/promo_resource_service_unittest.cc +++ b/chrome/browser/web_resource/promo_resource_service_unittest.cc @@ -15,6 +15,7 @@ #include "chrome/test/base/testing_browser_process.h" #include "chrome/test/base/testing_pref_service.h" #include "chrome/test/base/testing_profile.h" +#include "content/test/test_url_fetcher_factory.h" #include "testing/gtest/include/gtest/gtest.h" class PromoResourceServiceTest : public testing::Test { @@ -28,6 +29,7 @@ class PromoResourceServiceTest : public testing::Test { TestingProfile profile_; ScopedTestingLocalState local_state_; scoped_refptr<PromoResourceService> web_resource_service_; + MessageLoop loop_; }; // Verifies that custom dates read from a web resource server are written to @@ -131,9 +133,6 @@ TEST_F(PromoResourceServiceTest, UnpackPromoSignal) { scoped_ptr<DictionaryValue> test_json(static_cast<DictionaryValue*>( base::JSONReader::Read(json, false))); - // Initialize a message loop for this to run on. - MessageLoop loop; - // Check that prefs are set correctly. web_resource_service_->UnpackPromoSignal(*(test_json.get())); PrefService* prefs = profile_.GetPrefs(); @@ -187,22 +186,24 @@ TEST_F(PromoResourceServiceTest, UnpackWebStoreSignal) { scoped_ptr<DictionaryValue> test_json(static_cast<DictionaryValue*>( base::JSONReader::Read(json, false))); - // Initialize a message loop for this to run on. - MessageLoop loop; + // Set the source logo URL to verify that it gets cleared. + AppsPromo::SetSourcePromoLogoURL(GURL("https://www.google.com/test.png")); // Check that prefs are set correctly. web_resource_service_->UnpackWebStoreSignal(*(test_json.get())); - PrefService* prefs = profile_.GetPrefs(); - ASSERT_TRUE(prefs != NULL); - EXPECT_EQ("341252", AppsPromo::GetPromoId()); - EXPECT_EQ("The header!", AppsPromo::GetPromoHeaderText()); - EXPECT_EQ("The button label!", AppsPromo::GetPromoButtonText()); - EXPECT_EQ(GURL("http://link.com"), AppsPromo::GetPromoLink()); - EXPECT_EQ("No thanks, hide this.", AppsPromo::GetPromoExpireText()); - EXPECT_EQ(AppsPromo::USERS_NEW, AppsPromo::GetPromoUserGroup()); - EXPECT_EQ(GURL("chrome://theme/IDR_WEBSTORE_ICON"), - AppsPromo::GetPromoLogo()); + AppsPromo::PromoData actual_data = AppsPromo::GetPromo(); + EXPECT_EQ("341252", actual_data.id); + EXPECT_EQ("The header!", actual_data.header); + EXPECT_EQ("The button label!", actual_data.button); + EXPECT_EQ(GURL("http://link.com"), actual_data.link); + EXPECT_EQ("No thanks, hide this.", actual_data.expire); + EXPECT_EQ(AppsPromo::USERS_NEW, actual_data.user_group); + + // When we don't download a logo, we revert to the default and clear the + // source pref. + EXPECT_EQ(GURL("chrome://theme/IDR_WEBSTORE_ICON"), actual_data.logo); + EXPECT_EQ(GURL(""), AppsPromo::GetSourcePromoLogoURL()); } // Tests that the "web store active" flag is set even when the web store promo @@ -221,15 +222,174 @@ TEST_F(PromoResourceServiceTest, UnpackPartialWebStoreSignal) { scoped_ptr<DictionaryValue> test_json(static_cast<DictionaryValue*>( base::JSONReader::Read(json, false))); - // Initialize a message loop for this to run on. - MessageLoop loop; - // Check that prefs are set correctly. web_resource_service_->UnpackWebStoreSignal(*(test_json.get())); EXPECT_FALSE(AppsPromo::IsPromoSupportedForLocale()); EXPECT_TRUE(AppsPromo::IsWebStoreSupportedForLocale()); } +// Tests that we can successfully unpack web store signals with HTTPS +// logos. +TEST_F(PromoResourceServiceTest, UnpackWebStoreSignalHttpsLogo) { + web_resource_service_->set_channel(chrome::VersionInfo::CHANNEL_DEV); + + std::string logo_url = "https://www.google.com/image/test.png"; + std::string png_data = "!$#%,./nvl;iadh9oh82"; + std::string png_base64 = "data:image/png;base64,ISQjJSwuL252bDtpYWRoOW9oODI="; + + FakeURLFetcherFactory factory; + factory.SetFakeResponse(logo_url, png_data, true); + + std::string json = + "{ " + " \"topic\": {" + " \"answers\": [" + " {" + " \"answer_id\": \"340252\"," + " \"name\": \"webstore_promo:15:1:" + logo_url + "\"," + " \"question\": \"Header!\"," + " \"inproduct_target\": \"The button label!\"," + " \"inproduct\": \"http://link.com\"," + " \"tooltip\": \"No thanks, hide this.\"" + " }" + " ]" + " }" + "}"; + + scoped_ptr<DictionaryValue> test_json(static_cast<DictionaryValue*>( + base::JSONReader::Read(json, false))); + + // Update the promo multiple times to verify the logo is cached correctly. + for (size_t i = 0; i < 2; ++i) { + web_resource_service_->UnpackWebStoreSignal(*(test_json.get())); + + // We should only need to run the message loop the first time since the + // image is then cached. + if (i == 0) + loop_.RunAllPending(); + + // Reset this scoped_ptr to prevent a DCHECK. + web_resource_service_->apps_promo_logo_fetcher_.reset(); + + AppsPromo::PromoData actual_data = AppsPromo::GetPromo(); + EXPECT_EQ("340252", actual_data.id); + EXPECT_EQ("Header!", actual_data.header); + EXPECT_EQ("The button label!", actual_data.button); + EXPECT_EQ(GURL("http://link.com"), actual_data.link); + EXPECT_EQ("No thanks, hide this.", actual_data.expire); + EXPECT_EQ(AppsPromo::USERS_NEW, actual_data.user_group); + + // The logo should now be a base64 DATA URL. + EXPECT_EQ(GURL(png_base64), actual_data.logo); + + // And the source pref should hold the source HTTPS URL. + EXPECT_EQ(GURL(logo_url), AppsPromo::GetSourcePromoLogoURL()); + } +} + +// Tests that we revert to the default logo when the fetch fails. +TEST_F(PromoResourceServiceTest, UnpackWebStoreSignalHttpsLogoError) { + web_resource_service_->set_channel(chrome::VersionInfo::CHANNEL_DEV); + + std::string logo_url = "https://www.google.com/image/test.png"; + std::string png_data = "!$#%,./nvl;iadh9oh82"; + std::string png_base64 = "ISQjJSwuL252bDtpYWRoOW9oODI="; + + FakeURLFetcherFactory factory; + + // Have URLFetcher return a 500 error. + factory.SetFakeResponse(logo_url, png_data, false); + + std::string json = + "{ " + " \"topic\": {" + " \"answers\": [" + " {" + " \"answer_id\": \"340252\"," + " \"name\": \"webstore_promo:15:1:" + logo_url + "\"," + " \"question\": \"Header!\"," + " \"inproduct_target\": \"The button label!\"," + " \"inproduct\": \"http://link.com\"," + " \"tooltip\": \"No thanks, hide this.\"" + " }" + " ]" + " }" + "}"; + + scoped_ptr<DictionaryValue> test_json(static_cast<DictionaryValue*>( + base::JSONReader::Read(json, false))); + + web_resource_service_->UnpackWebStoreSignal(*(test_json.get())); + + loop_.RunAllPending(); + + // Reset this scoped_ptr to prevent a DCHECK. + web_resource_service_->apps_promo_logo_fetcher_.reset(); + + AppsPromo::PromoData actual_data = AppsPromo::GetPromo(); + EXPECT_EQ("340252", actual_data.id); + EXPECT_EQ("Header!", actual_data.header); + EXPECT_EQ("The button label!", actual_data.button); + EXPECT_EQ(GURL("http://link.com"), actual_data.link); + EXPECT_EQ("No thanks, hide this.", actual_data.expire); + EXPECT_EQ(AppsPromo::USERS_NEW, actual_data.user_group); + + // Logos are the default values. + EXPECT_EQ(GURL("chrome://theme/IDR_WEBSTORE_ICON"), actual_data.logo); + EXPECT_EQ(GURL(""), AppsPromo::GetSourcePromoLogoURL()); +} + +// Tests that we don't download images over HTTP. +TEST_F(PromoResourceServiceTest, UnpackWebStoreSignalHttpLogo) { + web_resource_service_->set_channel(chrome::VersionInfo::CHANNEL_DEV); + + // Use an HTTP URL. + std::string logo_url = "http://www.google.com/image/test.png"; + std::string png_data = "!$#%,./nvl;iadh9oh82"; + std::string png_base64 = "ISQjJSwuL252bDtpYWRoOW9oODI="; + + FakeURLFetcherFactory factory; + factory.SetFakeResponse(logo_url, png_data, true); + + std::string json = + "{ " + " \"topic\": {" + " \"answers\": [" + " {" + " \"answer_id\": \"340252\"," + " \"name\": \"webstore_promo:15:1:" + logo_url + "\"," + " \"question\": \"Header!\"," + " \"inproduct_target\": \"The button label!\"," + " \"inproduct\": \"http://link.com\"," + " \"tooltip\": \"No thanks, hide this.\"" + " }" + " ]" + " }" + "}"; + + scoped_ptr<DictionaryValue> test_json(static_cast<DictionaryValue*>( + base::JSONReader::Read(json, false))); + + web_resource_service_->UnpackWebStoreSignal(*(test_json.get())); + + loop_.RunAllPending(); + + // Reset this scoped_ptr to prevent a DCHECK. + web_resource_service_->apps_promo_logo_fetcher_.reset(); + + AppsPromo::PromoData actual_data = AppsPromo::GetPromo(); + EXPECT_EQ("340252", actual_data.id); + EXPECT_EQ("Header!", actual_data.header); + EXPECT_EQ("The button label!", actual_data.button); + EXPECT_EQ(GURL("http://link.com"), actual_data.link); + EXPECT_EQ("No thanks, hide this.", actual_data.expire); + EXPECT_EQ(AppsPromo::USERS_NEW, actual_data.user_group); + + // Logos should be the default values because HTTP URLs are not valid. + EXPECT_EQ(GURL("chrome://theme/IDR_WEBSTORE_ICON"), actual_data.logo); + EXPECT_EQ(GURL(""), AppsPromo::GetSourcePromoLogoURL()); +} + TEST_F(PromoResourceServiceTest, IsBuildTargeted) { // canary const chrome::VersionInfo::Channel canary = |