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/bookmarks/bookmark_prompt_controller.h" 6 7 #include "base/bind.h" 8 #include "base/metrics/field_trial.h" 9 #include "base/metrics/histogram.h" 10 #include "base/prefs/pref_service.h" 11 #include "chrome/browser/bookmarks/bookmark_model.h" 12 #include "chrome/browser/bookmarks/bookmark_model_factory.h" 13 #include "chrome/browser/bookmarks/bookmark_prompt_prefs.h" 14 #include "chrome/browser/browser_process.h" 15 #include "chrome/browser/defaults.h" 16 #include "chrome/browser/history/history_service.h" 17 #include "chrome/browser/history/history_service_factory.h" 18 #include "chrome/browser/ui/browser.h" 19 #include "chrome/browser/ui/browser_finder.h" 20 #include "chrome/browser/ui/browser_list.h" 21 #include "chrome/browser/ui/browser_window.h" 22 #include "chrome/browser/ui/tabs/tab_strip_model.h" 23 #include "chrome/common/chrome_version_info.h" 24 #include "chrome/common/metrics/variations/variation_ids.h" 25 #include "chrome/common/pref_names.h" 26 #include "components/variations/variations_associated_data.h" 27 #include "content/public/browser/notification_service.h" 28 #include "content/public/browser/notification_types.h" 29 #include "content/public/browser/web_contents.h" 30 31 using content::WebContents; 32 33 namespace { 34 35 const char kBookmarkPromptTrialName[] = "BookmarkPrompt"; 36 const char kBookmarkPromptDefaultGroup[] = "Disabled"; 37 const char kBookmarkPromptControlGroup[] = "Control"; 38 const char kBookmarkPromptExperimentGroup[] = "Experiment"; 39 40 // This enum is used for the BookmarkPrompt.DisabledReason histogram. 41 enum PromptDisabledReason { 42 PROMPT_DISABLED_REASON_BY_IMPRESSION_COUNT, 43 PROMPT_DISABLED_REASON_BY_MANUAL, 44 45 PROMPT_DISABLED_REASON_LIMIT, // Keep this last. 46 }; 47 48 // This enum represents reason why we display bookmark prompt and for the 49 // BookmarkPrompt.DisplayReason histogram. 50 enum PromptDisplayReason { 51 PROMPT_DISPLAY_REASON_NOT_DISPLAY, // We don't display the prompt. 52 PROMPT_DISPLAY_REASON_PERMANENT, 53 PROMPT_DISPLAY_REASON_SESSION, 54 55 PROMPT_DISPLAY_REASON_LIMIT, // Keep this last. 56 }; 57 58 // We enable bookmark prompt experiment for users who have profile created 59 // before |install_date| until |expiration_date|. 60 struct ExperimentDateRange { 61 base::Time::Exploded install_date; 62 base::Time::Exploded expiration_date; 63 }; 64 65 bool CanShowBookmarkPrompt(Browser* browser) { 66 BookmarkPromptPrefs prefs(browser->profile()->GetPrefs()); 67 if (!prefs.IsBookmarkPromptEnabled()) 68 return false; 69 return prefs.GetPromptImpressionCount() < 70 BookmarkPromptController::kMaxPromptImpressionCount; 71 } 72 73 const ExperimentDateRange* GetExperimentDateRange() { 74 switch (chrome::VersionInfo::GetChannel()) { 75 case chrome::VersionInfo::CHANNEL_BETA: 76 case chrome::VersionInfo::CHANNEL_DEV: { 77 // Experiment date range for M26 Beta/Dev 78 static const ExperimentDateRange kBetaAndDevRange = { 79 { 2013, 3, 0, 1, 0, 0, 0, 0 }, // Mar 1, 2013 80 { 2013, 4, 0, 1, 0, 0, 0, 0 }, // Apr 1, 2013 81 }; 82 return &kBetaAndDevRange; 83 } 84 case chrome::VersionInfo::CHANNEL_CANARY: { 85 // Experiment date range for M26 Canary. 86 static const ExperimentDateRange kCanaryRange = { 87 { 2013, 1, 0, 17, 0, 0, 0, 0 }, // Jan 17, 2013 88 { 2013, 2, 0, 18, 0, 0, 0, 0 }, // Feb 17, 2013 89 }; 90 return &kCanaryRange; 91 } 92 case chrome::VersionInfo::CHANNEL_STABLE: { 93 // Experiment date range for M26 Stable. 94 static const ExperimentDateRange kStableRange = { 95 { 2013, 4, 0, 5, 0, 0, 0, 0 }, // Apr 5, 2013 96 { 2013, 5, 0, 5, 0, 0, 0, 0 }, // May 5, 2013 97 }; 98 return &kStableRange; 99 } 100 default: 101 return NULL; 102 } 103 } 104 105 bool IsActiveWebContents(Browser* browser, WebContents* web_contents) { 106 if (!browser->window()->IsActive()) 107 return false; 108 return browser->tab_strip_model()->GetActiveWebContents() == web_contents; 109 } 110 111 bool IsBookmarked(Browser* browser, const GURL& url) { 112 BookmarkModel* model = BookmarkModelFactory::GetForProfile( 113 browser->profile()); 114 return model && model->IsBookmarked(url); 115 } 116 117 bool IsEligiblePageTransitionForBookmarkPrompt( 118 content::PageTransition type) { 119 if (!content::PageTransitionIsMainFrame(type)) 120 return false; 121 122 const content::PageTransition core_type = 123 PageTransitionStripQualifier(type); 124 125 if (core_type == content::PAGE_TRANSITION_RELOAD) 126 return false; 127 128 const int32 qualifier = content::PageTransitionGetQualifier(type); 129 return !(qualifier & content::PAGE_TRANSITION_FORWARD_BACK); 130 } 131 132 // CheckPromptTriger returns prompt display reason based on |visits|. 133 PromptDisplayReason CheckPromptTriger(const history::VisitVector& visits) { 134 const base::Time now = base::Time::Now(); 135 // We assume current visit is already in history database. Although, this 136 // assumption may be false. We'll display prompt next time. 137 int visit_permanent_count = 0; 138 int visit_session_count = 0; 139 for (history::VisitVector::const_iterator it = visits.begin(); 140 it != visits.end(); ++it) { 141 if (!IsEligiblePageTransitionForBookmarkPrompt(it->transition)) 142 continue; 143 ++visit_permanent_count; 144 if ((now - it->visit_time) <= base::TimeDelta::FromDays(1)) 145 ++visit_session_count; 146 } 147 148 if (visit_permanent_count == 149 BookmarkPromptController::kVisitCountForPermanentTrigger) 150 return PROMPT_DISPLAY_REASON_PERMANENT; 151 152 if (visit_session_count == 153 BookmarkPromptController::kVisitCountForSessionTrigger) 154 return PROMPT_DISPLAY_REASON_SESSION; 155 156 return PROMPT_DISPLAY_REASON_NOT_DISPLAY; 157 } 158 159 } // namespace 160 161 // BookmarkPromptController 162 163 // When impression count is greater than |kMaxPromptImpressionCount|, we 164 // don't display bookmark prompt anymore. 165 const int BookmarkPromptController::kMaxPromptImpressionCount = 5; 166 167 // When user visited the URL 10 times, we show the bookmark prompt. 168 const int BookmarkPromptController::kVisitCountForPermanentTrigger = 10; 169 170 // When user visited the URL 3 times last 24 hours, we show the bookmark 171 // prompt. 172 const int BookmarkPromptController::kVisitCountForSessionTrigger = 3; 173 174 BookmarkPromptController::BookmarkPromptController() 175 : browser_(NULL), 176 web_contents_(NULL) { 177 DCHECK(browser_defaults::bookmarks_enabled); 178 BrowserList::AddObserver(this); 179 } 180 181 BookmarkPromptController::~BookmarkPromptController() { 182 BrowserList::RemoveObserver(this); 183 SetBrowser(NULL); 184 } 185 186 // static 187 void BookmarkPromptController::AddedBookmark(Browser* browser, 188 const GURL& url) { 189 BookmarkPromptController* controller = 190 g_browser_process->bookmark_prompt_controller(); 191 if (controller) 192 controller->AddedBookmarkInternal(browser, url); 193 } 194 195 // static 196 void BookmarkPromptController::ClosingBookmarkPrompt() { 197 BookmarkPromptController* controller = 198 g_browser_process->bookmark_prompt_controller(); 199 if (controller) 200 controller->ClosingBookmarkPromptInternal(); 201 } 202 203 // static 204 void BookmarkPromptController::DisableBookmarkPrompt( 205 PrefService* prefs) { 206 UMA_HISTOGRAM_ENUMERATION("BookmarkPrompt.DisabledReason", 207 PROMPT_DISABLED_REASON_BY_MANUAL, 208 PROMPT_DISABLED_REASON_LIMIT); 209 BookmarkPromptPrefs prompt_prefs(prefs); 210 prompt_prefs.DisableBookmarkPrompt(); 211 } 212 213 // Enable bookmark prompt controller feature for 1% of new users for one month 214 // on canary. We'll change the date for stable channel once release date fixed. 215 // static 216 bool BookmarkPromptController::IsEnabled() { 217 // If manually create field trial available, we use it. 218 const std::string manual_group_name = base::FieldTrialList::FindFullName( 219 "BookmarkPrompt"); 220 if (!manual_group_name.empty()) 221 return manual_group_name == kBookmarkPromptExperimentGroup; 222 223 const ExperimentDateRange* date_range = GetExperimentDateRange(); 224 if (!date_range) 225 return false; 226 227 scoped_refptr<base::FieldTrial> trial( 228 base::FieldTrialList::FactoryGetFieldTrial( 229 kBookmarkPromptTrialName, 100, kBookmarkPromptDefaultGroup, 230 date_range->expiration_date.year, 231 date_range->expiration_date.month, 232 date_range->expiration_date.day_of_month, 233 base::FieldTrial::ONE_TIME_RANDOMIZED, 234 NULL)); 235 trial->AppendGroup(kBookmarkPromptControlGroup, 10); 236 trial->AppendGroup(kBookmarkPromptExperimentGroup, 10); 237 238 chrome_variations::AssociateGoogleVariationID( 239 chrome_variations::GOOGLE_UPDATE_SERVICE, 240 kBookmarkPromptTrialName, kBookmarkPromptDefaultGroup, 241 chrome_variations::BOOKMARK_PROMPT_TRIAL_DEFAULT); 242 chrome_variations::AssociateGoogleVariationID( 243 chrome_variations::GOOGLE_UPDATE_SERVICE, 244 kBookmarkPromptTrialName, kBookmarkPromptControlGroup, 245 chrome_variations::BOOKMARK_PROMPT_TRIAL_CONTROL); 246 chrome_variations::AssociateGoogleVariationID( 247 chrome_variations::GOOGLE_UPDATE_SERVICE, 248 kBookmarkPromptTrialName, kBookmarkPromptExperimentGroup, 249 chrome_variations::BOOKMARK_PROMPT_TRIAL_EXPERIMENT); 250 251 const base::Time start_date = base::Time::FromLocalExploded( 252 date_range->install_date); 253 const int64 install_time = 254 g_browser_process->local_state()->GetInt64(prefs::kInstallDate); 255 // This must be called after the pref is initialized. 256 DCHECK(install_time); 257 const base::Time install_date = base::Time::FromTimeT(install_time); 258 259 if (install_date < start_date) { 260 trial->Disable(); 261 return false; 262 } 263 return trial->group_name() == kBookmarkPromptExperimentGroup; 264 } 265 266 void BookmarkPromptController::ActiveTabChanged(WebContents* old_contents, 267 WebContents* new_contents, 268 int index, 269 int reason) { 270 SetWebContents(new_contents); 271 } 272 273 void BookmarkPromptController::AddedBookmarkInternal(Browser* browser, 274 const GURL& url) { 275 if (browser == browser_ && url == last_prompted_url_) { 276 last_prompted_url_ = GURL::EmptyGURL(); 277 UMA_HISTOGRAM_TIMES("BookmarkPrompt.AddedBookmark", 278 base::Time::Now() - last_prompted_time_); 279 } 280 } 281 282 void BookmarkPromptController::ClosingBookmarkPromptInternal() { 283 UMA_HISTOGRAM_TIMES("BookmarkPrompt.DisplayDuration", 284 base::Time::Now() - last_prompted_time_); 285 } 286 287 void BookmarkPromptController::Observe( 288 int type, 289 const content::NotificationSource&, 290 const content::NotificationDetails&) { 291 DCHECK_EQ(type, content::NOTIFICATION_LOAD_COMPLETED_MAIN_FRAME); 292 query_url_consumer_.CancelAllRequests(); 293 if (!CanShowBookmarkPrompt(browser_)) 294 return; 295 296 const GURL url = web_contents_->GetURL(); 297 if (!HistoryService::CanAddURL(url) || IsBookmarked(browser_, url)) 298 return; 299 300 HistoryService* history_service = HistoryServiceFactory::GetForProfile( 301 browser_->profile(), 302 Profile::IMPLICIT_ACCESS); 303 if (!history_service) 304 return; 305 306 query_url_start_time_ = base::Time::Now(); 307 history_service->QueryURL( 308 url, true, &query_url_consumer_, 309 base::Bind(&BookmarkPromptController::OnDidQueryURL, 310 base::Unretained(this))); 311 } 312 313 void BookmarkPromptController::OnBrowserRemoved(Browser* browser) { 314 if (browser_ == browser) 315 SetBrowser(NULL); 316 } 317 318 void BookmarkPromptController::OnBrowserSetLastActive(Browser* browser) { 319 if (browser && browser->type() == Browser::TYPE_TABBED && 320 !browser->profile()->IsOffTheRecord() && 321 browser->CanSupportWindowFeature(Browser::FEATURE_LOCATIONBAR) && 322 CanShowBookmarkPrompt(browser)) 323 SetBrowser(browser); 324 else 325 SetBrowser(NULL); 326 } 327 328 void BookmarkPromptController::OnDidQueryURL( 329 CancelableRequestProvider::Handle handle, 330 bool success, 331 const history::URLRow* url_row, 332 history::VisitVector* visits) { 333 if (!success) 334 return; 335 336 const GURL url = web_contents_->GetURL(); 337 if (url_row->url() != url) { 338 // The URL of web_contents_ is changed during QueryURL call. This is an 339 // edge case but can be happened. 340 return; 341 } 342 343 UMA_HISTOGRAM_TIMES("BookmarkPrompt.QueryURLDuration", 344 base::Time::Now() - query_url_start_time_); 345 346 if (!browser_->SupportsWindowFeature(Browser::FEATURE_LOCATIONBAR) || 347 !CanShowBookmarkPrompt(browser_) || 348 !IsActiveWebContents(browser_, web_contents_) || 349 IsBookmarked(browser_, url)) 350 return; 351 352 PromptDisplayReason reason = CheckPromptTriger(*visits); 353 UMA_HISTOGRAM_ENUMERATION("BookmarkPrompt.DisplayReason", 354 reason, 355 PROMPT_DISPLAY_REASON_LIMIT); 356 if (reason == PROMPT_DISPLAY_REASON_NOT_DISPLAY) 357 return; 358 359 BookmarkPromptPrefs prefs(browser_->profile()->GetPrefs()); 360 prefs.IncrementPromptImpressionCount(); 361 if (prefs.GetPromptImpressionCount() == kMaxPromptImpressionCount) { 362 UMA_HISTOGRAM_ENUMERATION("BookmarkPrompt.DisabledReason", 363 PROMPT_DISABLED_REASON_BY_IMPRESSION_COUNT, 364 PROMPT_DISABLED_REASON_LIMIT); 365 prefs.DisableBookmarkPrompt(); 366 } 367 last_prompted_time_ = base::Time::Now(); 368 last_prompted_url_ = web_contents_->GetURL(); 369 browser_->window()->ShowBookmarkPrompt(); 370 } 371 372 void BookmarkPromptController::SetBrowser(Browser* browser) { 373 if (browser_ == browser) 374 return; 375 if (browser_) 376 browser_->tab_strip_model()->RemoveObserver(this); 377 browser_ = browser; 378 if (browser_) 379 browser_->tab_strip_model()->AddObserver(this); 380 SetWebContents(browser_ ? browser_->tab_strip_model()->GetActiveWebContents() 381 : NULL); 382 } 383 384 void BookmarkPromptController::SetWebContents(WebContents* web_contents) { 385 if (web_contents_) { 386 last_prompted_url_ = GURL::EmptyGURL(); 387 query_url_consumer_.CancelAllRequests(); 388 registrar_.Remove( 389 this, content::NOTIFICATION_LOAD_COMPLETED_MAIN_FRAME, 390 content::Source<WebContents>(web_contents_)); 391 } 392 web_contents_ = web_contents; 393 if (web_contents_) { 394 registrar_.Add(this, content::NOTIFICATION_LOAD_COMPLETED_MAIN_FRAME, 395 content::Source<WebContents>(web_contents_)); 396 } 397 } 398