1 // Copyright (c) 2012 The Chromium Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style license that can be 3 // found in the LICENSE file. 4 5 #include "chrome/browser/ui/webui/ntp/android/promo_handler.h" 6 7 #include "base/logging.h" 8 #include "base/memory/ref_counted_memory.h" 9 #include "base/metrics/histogram.h" 10 #include "base/prefs/pref_service.h" 11 #include "base/strings/string_number_conversions.h" 12 #include "base/strings/string_util.h" 13 #include "base/strings/utf_string_conversions.h" 14 #include "chrome/browser/android/intent_helper.h" 15 #include "chrome/browser/chrome_notification_types.h" 16 #include "chrome/browser/profiles/profile.h" 17 #include "chrome/browser/profiles/profile_manager.h" 18 #include "chrome/browser/signin/signin_manager.h" 19 #include "chrome/browser/sync/glue/synced_session.h" 20 #include "chrome/browser/sync/open_tabs_ui_delegate.h" 21 #include "chrome/browser/sync/profile_sync_service.h" 22 #include "chrome/browser/sync/profile_sync_service_factory.h" 23 #include "chrome/browser/web_resource/notification_promo.h" 24 #include "chrome/browser/web_resource/notification_promo_mobile_ntp.h" 25 #include "chrome/browser/web_resource/promo_resource_service.h" 26 #include "chrome/common/pref_names.h" 27 #include "components/user_prefs/pref_registry_syncable.h" 28 #include "content/public/browser/browser_thread.h" 29 #include "content/public/browser/notification_service.h" 30 #include "content/public/browser/web_contents.h" 31 32 using content::BrowserThread; 33 34 namespace { 35 36 // Promotion impression types for the NewTabPage.MobilePromo histogram. 37 // Should be kept in sync with the values in histograms.xml 38 enum PromoImpressionBuckets { 39 PROMO_IMPRESSION_MOST_VISITED = 0, 40 PROMO_IMPRESSION_OPEN_TABS = 1, 41 PROMO_IMPRESSION_SYNC_PROMO = 2, 42 PROMO_IMPRESSION_SEND_EMAIL_CLICKED = 3, 43 PROMO_IMPRESSION_CLOSE_PROMO_CLICKED = 4, 44 PROMO_IMPRESSION_BUCKET_BOUNDARY = 5 45 }; 46 47 // Helper to record an impression in NewTabPage.MobilePromo histogram. 48 void RecordImpressionOnHistogram(PromoImpressionBuckets type) { 49 UMA_HISTOGRAM_ENUMERATION("NewTabPage.MobilePromo", type, 50 PROMO_IMPRESSION_BUCKET_BOUNDARY); 51 } 52 53 // Helper to ask whether the promo is active. 54 bool CanShowNotificationPromo() { 55 NotificationPromo notification_promo; 56 notification_promo.InitFromPrefs(NotificationPromo::MOBILE_NTP_SYNC_PROMO); 57 return notification_promo.CanShow(); 58 } 59 60 // Helper to send out promo resource change notification. 61 void Notify(PromoHandler* ph, chrome::NotificationType notification_type) { 62 content::NotificationService* service = 63 content::NotificationService::current(); 64 service->Notify(notification_type, 65 content::Source<PromoHandler>(ph), 66 content::NotificationService::NoDetails()); 67 } 68 69 // Replaces all formatting markup in the promo with the corresponding HTML. 70 std::string ReplaceSimpleMarkupWithHtml(std::string text) { 71 const std::string LINE_BREAK = "<br/>"; 72 const std::string SYNCGRAPHIC_IMAGE = 73 "<div class=\"promo-sync-graphic\"></div>"; 74 const std::string BEGIN_HIGHLIGHT = 75 "<div style=\"text-align: center\"><button class=\"promo-button\">"; 76 const std::string END_HIGHLIGHT = "</button></div>"; 77 const std::string BEGIN_LINK = 78 "<span style=\"color: blue; text-decoration: underline;\">"; 79 const std::string END_LINK = "</span>"; 80 const std::string BEGIN_PROMO_AREA = "<div class=\"promo-action-target\">"; 81 const std::string END_PROMO_AREA = "</div>"; 82 83 ReplaceSubstringsAfterOffset(&text, 0, "LINE_BREAK", LINE_BREAK); 84 ReplaceSubstringsAfterOffset( 85 &text, 0, "SYNCGRAPHIC_IMAGE", SYNCGRAPHIC_IMAGE); 86 ReplaceSubstringsAfterOffset(&text, 0, "BEGIN_HIGHLIGHT", BEGIN_HIGHLIGHT); 87 ReplaceSubstringsAfterOffset(&text, 0, "END_HIGHLIGHT", END_HIGHLIGHT); 88 ReplaceSubstringsAfterOffset(&text, 0, "BEGIN_LINK", BEGIN_LINK); 89 ReplaceSubstringsAfterOffset(&text, 0, "END_LINK", END_LINK); 90 return BEGIN_PROMO_AREA + text + END_PROMO_AREA; 91 } 92 93 } // namespace 94 95 PromoHandler::PromoHandler() { 96 // Watch for pref changes that cause us to need to re-inject promolines. 97 registrar_.Add(this, chrome::NOTIFICATION_PROMO_RESOURCE_STATE_CHANGED, 98 content::NotificationService::AllSources()); 99 100 // Watch for sync service updates that could cause re-injections 101 registrar_.Add(this, chrome::NOTIFICATION_SYNC_CONFIGURE_DONE, 102 content::NotificationService::AllSources()); 103 registrar_.Add(this, chrome::NOTIFICATION_FOREIGN_SESSION_UPDATED, 104 content::NotificationService::AllSources()); 105 } 106 107 PromoHandler::~PromoHandler() { 108 } 109 110 void PromoHandler::RegisterMessages() { 111 web_ui()->RegisterMessageCallback("getPromotions", 112 base::Bind(&PromoHandler::HandleGetPromotions, 113 base::Unretained(this))); 114 web_ui()->RegisterMessageCallback("recordImpression", 115 base::Bind(&PromoHandler::HandleRecordImpression, 116 base::Unretained(this))); 117 web_ui()->RegisterMessageCallback("promoActionTriggered", 118 base::Bind(&PromoHandler::HandlePromoActionTriggered, 119 base::Unretained(this))); 120 web_ui()->RegisterMessageCallback("promoDisabled", 121 base::Bind(&PromoHandler::HandlePromoDisabled, 122 base::Unretained(this))); 123 } 124 125 // static 126 void PromoHandler::RegisterProfilePrefs( 127 user_prefs::PrefRegistrySyncable* registry) { 128 registry->RegisterBooleanPref( 129 prefs::kNtpPromoDesktopSessionFound, 130 false, 131 user_prefs::PrefRegistrySyncable::UNSYNCABLE_PREF); 132 } 133 134 void PromoHandler::Observe(int type, 135 const content::NotificationSource& source, 136 const content::NotificationDetails& details) { 137 if (chrome::NOTIFICATION_PROMO_RESOURCE_STATE_CHANGED == type || 138 chrome::NOTIFICATION_SYNC_CONFIGURE_DONE == type || 139 chrome::NOTIFICATION_FOREIGN_SESSION_UPDATED == type) { 140 // A change occurred to one of the preferences we care about 141 CheckDesktopSessions(); 142 InjectPromoDecorations(); 143 } else { 144 NOTREACHED() << "Unknown pref changed."; 145 } 146 } 147 148 void PromoHandler::HandlePromoSendEmail(const base::ListValue* args) { 149 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); 150 Profile* profile = Profile::FromBrowserContext( 151 web_ui()->GetWebContents()->GetBrowserContext()); 152 if (!profile) 153 return; 154 155 base::string16 data_subject, data_body, data_inv; 156 if (!args || args->GetSize() < 3) { 157 DVLOG(1) << "promoSendEmail: expected three args, got " 158 << (args ? args->GetSize() : 0); 159 return; 160 } 161 162 args->GetString(0, &data_subject); 163 args->GetString(1, &data_body); 164 args->GetString(2, &data_inv); 165 if (data_inv.empty() || (data_subject.empty() && data_body.empty())) 166 return; 167 168 std::string data_email; 169 ProfileSyncService* service = 170 ProfileSyncServiceFactory::GetInstance()->GetForProfile(profile); 171 if (service && service->signin()) 172 data_email = service->signin()->GetAuthenticatedUsername(); 173 174 chrome::android::SendEmail( 175 base::UTF8ToUTF16(data_email), data_subject, data_body, data_inv, 176 base::string16()); 177 RecordImpressionOnHistogram(PROMO_IMPRESSION_SEND_EMAIL_CLICKED); 178 } 179 180 void PromoHandler::HandlePromoActionTriggered(const base::ListValue* /*args*/) { 181 if (!CanShowNotificationPromo()) 182 return; 183 184 NotificationPromoMobileNtp promo; 185 if (!promo.InitFromPrefs()) 186 return; 187 188 if (promo.action_type() == "ACTION_EMAIL") 189 HandlePromoSendEmail(promo.action_args()); 190 } 191 192 void PromoHandler::HandlePromoDisabled(const base::ListValue* /*args*/) { 193 if (!CanShowNotificationPromo()) 194 return; 195 196 NotificationPromo::HandleClosed(NotificationPromo::MOBILE_NTP_SYNC_PROMO); 197 RecordImpressionOnHistogram(PROMO_IMPRESSION_CLOSE_PROMO_CLICKED); 198 199 content::NotificationService* service = 200 content::NotificationService::current(); 201 service->Notify(chrome::NOTIFICATION_PROMO_RESOURCE_STATE_CHANGED, 202 content::Source<PromoHandler>(this), 203 content::NotificationService::NoDetails()); 204 } 205 206 void PromoHandler::HandleGetPromotions(const base::ListValue* /*args*/) { 207 CheckDesktopSessions(); 208 InjectPromoDecorations(); 209 } 210 211 void PromoHandler::HandleRecordImpression(const base::ListValue* args) { 212 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); 213 DCHECK(args && !args->empty()); 214 RecordPromotionImpression(UTF16ToASCII(ExtractStringValue(args))); 215 } 216 217 void PromoHandler::InjectPromoDecorations() { 218 DictionaryValue result; 219 if (FetchPromotion(&result)) 220 web_ui()->CallJavascriptFunction("ntp.setPromotions", result); 221 else 222 web_ui()->CallJavascriptFunction("ntp.clearPromotions"); 223 } 224 225 void PromoHandler::RecordPromotionImpression(const std::string& id) { 226 // Update number of views a promotion has received and trigger refresh 227 // if it exceeded max_views set for the promotion. 228 if (NotificationPromo::HandleViewed( 229 NotificationPromo::MOBILE_NTP_SYNC_PROMO)) { 230 Notify(this, chrome::NOTIFICATION_PROMO_RESOURCE_STATE_CHANGED); 231 } 232 233 if (id == "most_visited") 234 RecordImpressionOnHistogram(PROMO_IMPRESSION_MOST_VISITED); 235 else if (id == "open_tabs") 236 RecordImpressionOnHistogram(PROMO_IMPRESSION_OPEN_TABS); 237 else if (id == "sync_promo") 238 RecordImpressionOnHistogram(PROMO_IMPRESSION_SYNC_PROMO); 239 else 240 NOTREACHED() << "Unknown promotion impression: " << id; 241 } 242 243 bool PromoHandler::FetchPromotion(DictionaryValue* result) { 244 DCHECK(result != NULL); 245 if (!CanShowNotificationPromo()) 246 return false; 247 248 NotificationPromoMobileNtp promo; 249 if (!promo.InitFromPrefs()) 250 return false; 251 252 DCHECK(!promo.text().empty()); 253 if (!DoesChromePromoMatchCurrentSync( 254 promo.requires_sync(), promo.requires_mobile_only_sync())) { 255 return false; 256 } 257 258 result->SetBoolean("promoIsAllowed", true); 259 result->SetBoolean("promoIsAllowedOnMostVisited", 260 promo.show_on_most_visited()); 261 result->SetBoolean("promoIsAllowedOnOpenTabs", promo.show_on_open_tabs()); 262 result->SetBoolean("promoIsAllowedAsVC", promo.show_as_virtual_computer()); 263 result->SetString("promoVCTitle", promo.virtual_computer_title()); 264 result->SetString("promoVCLastSynced", promo.virtual_computer_lastsync()); 265 result->SetString("promoMessage", ReplaceSimpleMarkupWithHtml(promo.text())); 266 result->SetString("promoMessageLong", 267 ReplaceSimpleMarkupWithHtml(promo.text_long())); 268 return true; 269 } 270 271 bool PromoHandler::DoesChromePromoMatchCurrentSync( 272 bool promo_requires_sync, 273 bool promo_requires_no_active_desktop_sync_sessions) { 274 Profile* profile = Profile::FromWebUI(web_ui()); 275 if (!profile) 276 return false; 277 278 // If the promo doesn't require any sync, the requirements are fulfilled. 279 if (!promo_requires_sync) 280 return true; 281 282 // The promo requires the sync; check that the sync service is active. 283 ProfileSyncService* service = 284 ProfileSyncServiceFactory::GetInstance()->GetForProfile(profile); 285 if (!service || !service->ShouldPushChanges()) 286 return false; 287 288 // If the promo doesn't have specific requirements for the sync, it matches. 289 if (!promo_requires_no_active_desktop_sync_sessions) 290 return true; 291 292 // If the promo requires mobile-only sync, 293 // check that no desktop sessions are found. 294 PrefService* prefs = profile->GetPrefs(); 295 return !prefs || !prefs->GetBoolean(prefs::kNtpPromoDesktopSessionFound); 296 } 297 298 void PromoHandler::CheckDesktopSessions() { 299 Profile* profile = Profile::FromWebUI(web_ui()); 300 if (!profile) 301 return; 302 303 // Check if desktop sessions have already been found. 304 PrefService* prefs = profile->GetPrefs(); 305 if (!prefs || prefs->GetBoolean(prefs::kNtpPromoDesktopSessionFound)) 306 return; 307 308 // Check if the sync is currently active. 309 ProfileSyncService* service = 310 ProfileSyncServiceFactory::GetInstance()->GetForProfile(profile); 311 if (!service || !service->ShouldPushChanges()) 312 return; 313 314 // Check if the sync has any open sessions. 315 browser_sync::OpenTabsUIDelegate* open_tabs = 316 service->GetOpenTabsUIDelegate(); 317 if (!open_tabs) 318 return; 319 320 // Let's see if there are no desktop sessions. 321 std::vector<const browser_sync::SyncedSession*> sessions; 322 ListValue session_list; 323 if (!open_tabs->GetAllForeignSessions(&sessions)) 324 return; 325 326 for (size_t i = 0; i < sessions.size(); ++i) { 327 const browser_sync::SyncedSession::DeviceType device_type = 328 sessions[i]->device_type; 329 if (device_type == browser_sync::SyncedSession::TYPE_WIN || 330 device_type == browser_sync::SyncedSession::TYPE_MACOSX || 331 device_type == browser_sync::SyncedSession::TYPE_LINUX) { 332 // Found a desktop session: write out the pref. 333 prefs->SetBoolean(prefs::kNtpPromoDesktopSessionFound, true); 334 return; 335 } 336 } 337 } 338