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