Home | History | Annotate | Download | only in bookmarks
      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