| // Copyright (c) 2012 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "chrome/browser/ui/bookmarks/bookmark_prompt_controller.h" |
| |
| #include "base/bind.h" |
| #include "base/metrics/field_trial.h" |
| #include "base/metrics/histogram.h" |
| #include "base/prefs/pref_service.h" |
| #include "chrome/browser/bookmarks/bookmark_model.h" |
| #include "chrome/browser/bookmarks/bookmark_model_factory.h" |
| #include "chrome/browser/bookmarks/bookmark_prompt_prefs.h" |
| #include "chrome/browser/browser_process.h" |
| #include "chrome/browser/defaults.h" |
| #include "chrome/browser/history/history_service.h" |
| #include "chrome/browser/history/history_service_factory.h" |
| #include "chrome/browser/ui/browser.h" |
| #include "chrome/browser/ui/browser_finder.h" |
| #include "chrome/browser/ui/browser_list.h" |
| #include "chrome/browser/ui/browser_window.h" |
| #include "chrome/browser/ui/tabs/tab_strip_model.h" |
| #include "chrome/common/chrome_version_info.h" |
| #include "chrome/common/metrics/variations/variation_ids.h" |
| #include "chrome/common/pref_names.h" |
| #include "components/variations/variations_associated_data.h" |
| #include "content/public/browser/notification_service.h" |
| #include "content/public/browser/notification_types.h" |
| #include "content/public/browser/web_contents.h" |
| |
| using content::WebContents; |
| |
| namespace { |
| |
| const char kBookmarkPromptTrialName[] = "BookmarkPrompt"; |
| const char kBookmarkPromptDefaultGroup[] = "Disabled"; |
| const char kBookmarkPromptControlGroup[] = "Control"; |
| const char kBookmarkPromptExperimentGroup[] = "Experiment"; |
| |
| // This enum is used for the BookmarkPrompt.DisabledReason histogram. |
| enum PromptDisabledReason { |
| PROMPT_DISABLED_REASON_BY_IMPRESSION_COUNT, |
| PROMPT_DISABLED_REASON_BY_MANUAL, |
| |
| PROMPT_DISABLED_REASON_LIMIT, // Keep this last. |
| }; |
| |
| // This enum represents reason why we display bookmark prompt and for the |
| // BookmarkPrompt.DisplayReason histogram. |
| enum PromptDisplayReason { |
| PROMPT_DISPLAY_REASON_NOT_DISPLAY, // We don't display the prompt. |
| PROMPT_DISPLAY_REASON_PERMANENT, |
| PROMPT_DISPLAY_REASON_SESSION, |
| |
| PROMPT_DISPLAY_REASON_LIMIT, // Keep this last. |
| }; |
| |
| // We enable bookmark prompt experiment for users who have profile created |
| // before |install_date| until |expiration_date|. |
| struct ExperimentDateRange { |
| base::Time::Exploded install_date; |
| base::Time::Exploded expiration_date; |
| }; |
| |
| bool CanShowBookmarkPrompt(Browser* browser) { |
| BookmarkPromptPrefs prefs(browser->profile()->GetPrefs()); |
| if (!prefs.IsBookmarkPromptEnabled()) |
| return false; |
| return prefs.GetPromptImpressionCount() < |
| BookmarkPromptController::kMaxPromptImpressionCount; |
| } |
| |
| const ExperimentDateRange* GetExperimentDateRange() { |
| switch (chrome::VersionInfo::GetChannel()) { |
| case chrome::VersionInfo::CHANNEL_BETA: |
| case chrome::VersionInfo::CHANNEL_DEV: { |
| // Experiment date range for M26 Beta/Dev |
| static const ExperimentDateRange kBetaAndDevRange = { |
| { 2013, 3, 0, 1, 0, 0, 0, 0 }, // Mar 1, 2013 |
| { 2013, 4, 0, 1, 0, 0, 0, 0 }, // Apr 1, 2013 |
| }; |
| return &kBetaAndDevRange; |
| } |
| case chrome::VersionInfo::CHANNEL_CANARY: { |
| // Experiment date range for M26 Canary. |
| static const ExperimentDateRange kCanaryRange = { |
| { 2013, 1, 0, 17, 0, 0, 0, 0 }, // Jan 17, 2013 |
| { 2013, 2, 0, 18, 0, 0, 0, 0 }, // Feb 17, 2013 |
| }; |
| return &kCanaryRange; |
| } |
| case chrome::VersionInfo::CHANNEL_STABLE: { |
| // Experiment date range for M26 Stable. |
| static const ExperimentDateRange kStableRange = { |
| { 2013, 4, 0, 5, 0, 0, 0, 0 }, // Apr 5, 2013 |
| { 2013, 5, 0, 5, 0, 0, 0, 0 }, // May 5, 2013 |
| }; |
| return &kStableRange; |
| } |
| default: |
| return NULL; |
| } |
| } |
| |
| bool IsActiveWebContents(Browser* browser, WebContents* web_contents) { |
| if (!browser->window()->IsActive()) |
| return false; |
| return browser->tab_strip_model()->GetActiveWebContents() == web_contents; |
| } |
| |
| bool IsBookmarked(Browser* browser, const GURL& url) { |
| BookmarkModel* model = BookmarkModelFactory::GetForProfile( |
| browser->profile()); |
| return model && model->IsBookmarked(url); |
| } |
| |
| bool IsEligiblePageTransitionForBookmarkPrompt( |
| content::PageTransition type) { |
| if (!content::PageTransitionIsMainFrame(type)) |
| return false; |
| |
| const content::PageTransition core_type = |
| PageTransitionStripQualifier(type); |
| |
| if (core_type == content::PAGE_TRANSITION_RELOAD) |
| return false; |
| |
| const int32 qualifier = content::PageTransitionGetQualifier(type); |
| return !(qualifier & content::PAGE_TRANSITION_FORWARD_BACK); |
| } |
| |
| // CheckPromptTriger returns prompt display reason based on |visits|. |
| PromptDisplayReason CheckPromptTriger(const history::VisitVector& visits) { |
| const base::Time now = base::Time::Now(); |
| // We assume current visit is already in history database. Although, this |
| // assumption may be false. We'll display prompt next time. |
| int visit_permanent_count = 0; |
| int visit_session_count = 0; |
| for (history::VisitVector::const_iterator it = visits.begin(); |
| it != visits.end(); ++it) { |
| if (!IsEligiblePageTransitionForBookmarkPrompt(it->transition)) |
| continue; |
| ++visit_permanent_count; |
| if ((now - it->visit_time) <= base::TimeDelta::FromDays(1)) |
| ++visit_session_count; |
| } |
| |
| if (visit_permanent_count == |
| BookmarkPromptController::kVisitCountForPermanentTrigger) |
| return PROMPT_DISPLAY_REASON_PERMANENT; |
| |
| if (visit_session_count == |
| BookmarkPromptController::kVisitCountForSessionTrigger) |
| return PROMPT_DISPLAY_REASON_SESSION; |
| |
| return PROMPT_DISPLAY_REASON_NOT_DISPLAY; |
| } |
| |
| } // namespace |
| |
| // BookmarkPromptController |
| |
| // When impression count is greater than |kMaxPromptImpressionCount|, we |
| // don't display bookmark prompt anymore. |
| const int BookmarkPromptController::kMaxPromptImpressionCount = 5; |
| |
| // When user visited the URL 10 times, we show the bookmark prompt. |
| const int BookmarkPromptController::kVisitCountForPermanentTrigger = 10; |
| |
| // When user visited the URL 3 times last 24 hours, we show the bookmark |
| // prompt. |
| const int BookmarkPromptController::kVisitCountForSessionTrigger = 3; |
| |
| BookmarkPromptController::BookmarkPromptController() |
| : browser_(NULL), |
| web_contents_(NULL) { |
| DCHECK(browser_defaults::bookmarks_enabled); |
| BrowserList::AddObserver(this); |
| } |
| |
| BookmarkPromptController::~BookmarkPromptController() { |
| BrowserList::RemoveObserver(this); |
| SetBrowser(NULL); |
| } |
| |
| // static |
| void BookmarkPromptController::AddedBookmark(Browser* browser, |
| const GURL& url) { |
| BookmarkPromptController* controller = |
| g_browser_process->bookmark_prompt_controller(); |
| if (controller) |
| controller->AddedBookmarkInternal(browser, url); |
| } |
| |
| // static |
| void BookmarkPromptController::ClosingBookmarkPrompt() { |
| BookmarkPromptController* controller = |
| g_browser_process->bookmark_prompt_controller(); |
| if (controller) |
| controller->ClosingBookmarkPromptInternal(); |
| } |
| |
| // static |
| void BookmarkPromptController::DisableBookmarkPrompt( |
| PrefService* prefs) { |
| UMA_HISTOGRAM_ENUMERATION("BookmarkPrompt.DisabledReason", |
| PROMPT_DISABLED_REASON_BY_MANUAL, |
| PROMPT_DISABLED_REASON_LIMIT); |
| BookmarkPromptPrefs prompt_prefs(prefs); |
| prompt_prefs.DisableBookmarkPrompt(); |
| } |
| |
| // Enable bookmark prompt controller feature for 1% of new users for one month |
| // on canary. We'll change the date for stable channel once release date fixed. |
| // static |
| bool BookmarkPromptController::IsEnabled() { |
| // If manually create field trial available, we use it. |
| const std::string manual_group_name = base::FieldTrialList::FindFullName( |
| "BookmarkPrompt"); |
| if (!manual_group_name.empty()) |
| return manual_group_name == kBookmarkPromptExperimentGroup; |
| |
| const ExperimentDateRange* date_range = GetExperimentDateRange(); |
| if (!date_range) |
| return false; |
| |
| scoped_refptr<base::FieldTrial> trial( |
| base::FieldTrialList::FactoryGetFieldTrial( |
| kBookmarkPromptTrialName, 100, kBookmarkPromptDefaultGroup, |
| date_range->expiration_date.year, |
| date_range->expiration_date.month, |
| date_range->expiration_date.day_of_month, |
| base::FieldTrial::ONE_TIME_RANDOMIZED, |
| NULL)); |
| trial->AppendGroup(kBookmarkPromptControlGroup, 10); |
| trial->AppendGroup(kBookmarkPromptExperimentGroup, 10); |
| |
| chrome_variations::AssociateGoogleVariationID( |
| chrome_variations::GOOGLE_UPDATE_SERVICE, |
| kBookmarkPromptTrialName, kBookmarkPromptDefaultGroup, |
| chrome_variations::BOOKMARK_PROMPT_TRIAL_DEFAULT); |
| chrome_variations::AssociateGoogleVariationID( |
| chrome_variations::GOOGLE_UPDATE_SERVICE, |
| kBookmarkPromptTrialName, kBookmarkPromptControlGroup, |
| chrome_variations::BOOKMARK_PROMPT_TRIAL_CONTROL); |
| chrome_variations::AssociateGoogleVariationID( |
| chrome_variations::GOOGLE_UPDATE_SERVICE, |
| kBookmarkPromptTrialName, kBookmarkPromptExperimentGroup, |
| chrome_variations::BOOKMARK_PROMPT_TRIAL_EXPERIMENT); |
| |
| const base::Time start_date = base::Time::FromLocalExploded( |
| date_range->install_date); |
| const int64 install_time = |
| g_browser_process->local_state()->GetInt64(prefs::kInstallDate); |
| // This must be called after the pref is initialized. |
| DCHECK(install_time); |
| const base::Time install_date = base::Time::FromTimeT(install_time); |
| |
| if (install_date < start_date) { |
| trial->Disable(); |
| return false; |
| } |
| return trial->group_name() == kBookmarkPromptExperimentGroup; |
| } |
| |
| void BookmarkPromptController::ActiveTabChanged(WebContents* old_contents, |
| WebContents* new_contents, |
| int index, |
| int reason) { |
| SetWebContents(new_contents); |
| } |
| |
| void BookmarkPromptController::AddedBookmarkInternal(Browser* browser, |
| const GURL& url) { |
| if (browser == browser_ && url == last_prompted_url_) { |
| last_prompted_url_ = GURL::EmptyGURL(); |
| UMA_HISTOGRAM_TIMES("BookmarkPrompt.AddedBookmark", |
| base::Time::Now() - last_prompted_time_); |
| } |
| } |
| |
| void BookmarkPromptController::ClosingBookmarkPromptInternal() { |
| UMA_HISTOGRAM_TIMES("BookmarkPrompt.DisplayDuration", |
| base::Time::Now() - last_prompted_time_); |
| } |
| |
| void BookmarkPromptController::Observe( |
| int type, |
| const content::NotificationSource&, |
| const content::NotificationDetails&) { |
| DCHECK_EQ(type, content::NOTIFICATION_LOAD_COMPLETED_MAIN_FRAME); |
| query_url_consumer_.CancelAllRequests(); |
| if (!CanShowBookmarkPrompt(browser_)) |
| return; |
| |
| const GURL url = web_contents_->GetURL(); |
| if (!HistoryService::CanAddURL(url) || IsBookmarked(browser_, url)) |
| return; |
| |
| HistoryService* history_service = HistoryServiceFactory::GetForProfile( |
| browser_->profile(), |
| Profile::IMPLICIT_ACCESS); |
| if (!history_service) |
| return; |
| |
| query_url_start_time_ = base::Time::Now(); |
| history_service->QueryURL( |
| url, true, &query_url_consumer_, |
| base::Bind(&BookmarkPromptController::OnDidQueryURL, |
| base::Unretained(this))); |
| } |
| |
| void BookmarkPromptController::OnBrowserRemoved(Browser* browser) { |
| if (browser_ == browser) |
| SetBrowser(NULL); |
| } |
| |
| void BookmarkPromptController::OnBrowserSetLastActive(Browser* browser) { |
| if (browser && browser->type() == Browser::TYPE_TABBED && |
| !browser->profile()->IsOffTheRecord() && |
| browser->CanSupportWindowFeature(Browser::FEATURE_LOCATIONBAR) && |
| CanShowBookmarkPrompt(browser)) |
| SetBrowser(browser); |
| else |
| SetBrowser(NULL); |
| } |
| |
| void BookmarkPromptController::OnDidQueryURL( |
| CancelableRequestProvider::Handle handle, |
| bool success, |
| const history::URLRow* url_row, |
| history::VisitVector* visits) { |
| if (!success) |
| return; |
| |
| const GURL url = web_contents_->GetURL(); |
| if (url_row->url() != url) { |
| // The URL of web_contents_ is changed during QueryURL call. This is an |
| // edge case but can be happened. |
| return; |
| } |
| |
| UMA_HISTOGRAM_TIMES("BookmarkPrompt.QueryURLDuration", |
| base::Time::Now() - query_url_start_time_); |
| |
| if (!browser_->SupportsWindowFeature(Browser::FEATURE_LOCATIONBAR) || |
| !CanShowBookmarkPrompt(browser_) || |
| !IsActiveWebContents(browser_, web_contents_) || |
| IsBookmarked(browser_, url)) |
| return; |
| |
| PromptDisplayReason reason = CheckPromptTriger(*visits); |
| UMA_HISTOGRAM_ENUMERATION("BookmarkPrompt.DisplayReason", |
| reason, |
| PROMPT_DISPLAY_REASON_LIMIT); |
| if (reason == PROMPT_DISPLAY_REASON_NOT_DISPLAY) |
| return; |
| |
| BookmarkPromptPrefs prefs(browser_->profile()->GetPrefs()); |
| prefs.IncrementPromptImpressionCount(); |
| if (prefs.GetPromptImpressionCount() == kMaxPromptImpressionCount) { |
| UMA_HISTOGRAM_ENUMERATION("BookmarkPrompt.DisabledReason", |
| PROMPT_DISABLED_REASON_BY_IMPRESSION_COUNT, |
| PROMPT_DISABLED_REASON_LIMIT); |
| prefs.DisableBookmarkPrompt(); |
| } |
| last_prompted_time_ = base::Time::Now(); |
| last_prompted_url_ = web_contents_->GetURL(); |
| browser_->window()->ShowBookmarkPrompt(); |
| } |
| |
| void BookmarkPromptController::SetBrowser(Browser* browser) { |
| if (browser_ == browser) |
| return; |
| if (browser_) |
| browser_->tab_strip_model()->RemoveObserver(this); |
| browser_ = browser; |
| if (browser_) |
| browser_->tab_strip_model()->AddObserver(this); |
| SetWebContents(browser_ ? browser_->tab_strip_model()->GetActiveWebContents() |
| : NULL); |
| } |
| |
| void BookmarkPromptController::SetWebContents(WebContents* web_contents) { |
| if (web_contents_) { |
| last_prompted_url_ = GURL::EmptyGURL(); |
| query_url_consumer_.CancelAllRequests(); |
| registrar_.Remove( |
| this, content::NOTIFICATION_LOAD_COMPLETED_MAIN_FRAME, |
| content::Source<WebContents>(web_contents_)); |
| } |
| web_contents_ = web_contents; |
| if (web_contents_) { |
| registrar_.Add(this, content::NOTIFICATION_LOAD_COMPLETED_MAIN_FRAME, |
| content::Source<WebContents>(web_contents_)); |
| } |
| } |