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 31using content::WebContents; 32 33namespace { 34 35const char kBookmarkPromptTrialName[] = "BookmarkPrompt"; 36const char kBookmarkPromptDefaultGroup[] = "Disabled"; 37const char kBookmarkPromptControlGroup[] = "Control"; 38const char kBookmarkPromptExperimentGroup[] = "Experiment"; 39 40// This enum is used for the BookmarkPrompt.DisabledReason histogram. 41enum 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. 50enum 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|. 60struct ExperimentDateRange { 61 base::Time::Exploded install_date; 62 base::Time::Exploded expiration_date; 63}; 64 65bool 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 73const 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 105bool 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 111bool IsBookmarked(Browser* browser, const GURL& url) { 112 BookmarkModel* model = BookmarkModelFactory::GetForProfile( 113 browser->profile()); 114 return model && model->IsBookmarked(url); 115} 116 117bool 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|. 133PromptDisplayReason 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. 165const int BookmarkPromptController::kMaxPromptImpressionCount = 5; 166 167// When user visited the URL 10 times, we show the bookmark prompt. 168const int BookmarkPromptController::kVisitCountForPermanentTrigger = 10; 169 170// When user visited the URL 3 times last 24 hours, we show the bookmark 171// prompt. 172const int BookmarkPromptController::kVisitCountForSessionTrigger = 3; 173 174BookmarkPromptController::BookmarkPromptController() 175 : browser_(NULL), 176 web_contents_(NULL) { 177 DCHECK(browser_defaults::bookmarks_enabled); 178 BrowserList::AddObserver(this); 179} 180 181BookmarkPromptController::~BookmarkPromptController() { 182 BrowserList::RemoveObserver(this); 183 SetBrowser(NULL); 184} 185 186// static 187void 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 196void BookmarkPromptController::ClosingBookmarkPrompt() { 197 BookmarkPromptController* controller = 198 g_browser_process->bookmark_prompt_controller(); 199 if (controller) 200 controller->ClosingBookmarkPromptInternal(); 201} 202 203// static 204void 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 216bool 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 266void BookmarkPromptController::ActiveTabChanged(WebContents* old_contents, 267 WebContents* new_contents, 268 int index, 269 int reason) { 270 SetWebContents(new_contents); 271} 272 273void 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 282void BookmarkPromptController::ClosingBookmarkPromptInternal() { 283 UMA_HISTOGRAM_TIMES("BookmarkPrompt.DisplayDuration", 284 base::Time::Now() - last_prompted_time_); 285} 286 287void 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 313void BookmarkPromptController::OnBrowserRemoved(Browser* browser) { 314 if (browser_ == browser) 315 SetBrowser(NULL); 316} 317 318void 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 328void 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 372void 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 384void 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