| // 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. |
| // |
| // This code glues the RLZ library DLL with Chrome. It allows Chrome to work |
| // with or without the DLL being present. If the DLL is not present the |
| // functions do nothing and just return false. |
| |
| #include "components/rlz/rlz_tracker.h" |
| |
| #include <algorithm> |
| #include <utility> |
| |
| #include "base/bind.h" |
| #include "base/message_loop/message_loop.h" |
| #include "base/sequenced_task_runner.h" |
| #include "base/strings/string_util.h" |
| #include "base/strings/stringprintf.h" |
| #include "base/strings/utf_string_conversions.h" |
| #include "base/task_scheduler/post_task.h" |
| #include "base/task_scheduler/task_traits.h" |
| #include "base/trace_event/trace_event.h" |
| #include "build/build_config.h" |
| #include "components/rlz/rlz_tracker_delegate.h" |
| |
| namespace rlz { |
| namespace { |
| |
| // Maximum and minimum delay for financial ping we would allow to be set through |
| // master preferences. Somewhat arbitrary, may need to be adjusted in future. |
| const base::TimeDelta kMaxInitDelay = base::TimeDelta::FromSeconds(200); |
| const base::TimeDelta kMinInitDelay = base::TimeDelta::FromSeconds(20); |
| |
| void RecordProductEvents(bool first_run, |
| bool is_google_default_search, |
| bool is_google_homepage, |
| bool is_google_in_startpages, |
| bool already_ran, |
| bool omnibox_used, |
| bool homepage_used, |
| bool app_list_used) { |
| TRACE_EVENT0("RLZ", "RecordProductEvents"); |
| // Record the installation of chrome. We call this all the time but the rlz |
| // lib should ignore all but the first one. |
| rlz_lib::RecordProductEvent(rlz_lib::CHROME, |
| RLZTracker::ChromeOmnibox(), |
| rlz_lib::INSTALL); |
| #if !defined(OS_IOS) |
| rlz_lib::RecordProductEvent(rlz_lib::CHROME, |
| RLZTracker::ChromeHomePage(), |
| rlz_lib::INSTALL); |
| rlz_lib::RecordProductEvent(rlz_lib::CHROME, |
| RLZTracker::ChromeAppList(), |
| rlz_lib::INSTALL); |
| #endif // !defined(OS_IOS) |
| |
| if (!already_ran) { |
| // Do the initial event recording if is the first run or if we have an |
| // empty rlz which means we haven't got a chance to do it. |
| char omnibox_rlz[rlz_lib::kMaxRlzLength + 1]; |
| if (!rlz_lib::GetAccessPointRlz(RLZTracker::ChromeOmnibox(), omnibox_rlz, |
| rlz_lib::kMaxRlzLength)) { |
| omnibox_rlz[0] = 0; |
| } |
| |
| // Record if google is the initial search provider and/or home page. |
| if ((first_run || omnibox_rlz[0] == 0) && is_google_default_search) { |
| rlz_lib::RecordProductEvent(rlz_lib::CHROME, |
| RLZTracker::ChromeOmnibox(), |
| rlz_lib::SET_TO_GOOGLE); |
| } |
| |
| #if !defined(OS_IOS) |
| char homepage_rlz[rlz_lib::kMaxRlzLength + 1]; |
| if (!rlz_lib::GetAccessPointRlz(RLZTracker::ChromeHomePage(), homepage_rlz, |
| rlz_lib::kMaxRlzLength)) { |
| homepage_rlz[0] = 0; |
| } |
| |
| if ((first_run || homepage_rlz[0] == 0) && |
| (is_google_homepage || is_google_in_startpages)) { |
| rlz_lib::RecordProductEvent(rlz_lib::CHROME, |
| RLZTracker::ChromeHomePage(), |
| rlz_lib::SET_TO_GOOGLE); |
| } |
| |
| char app_list_rlz[rlz_lib::kMaxRlzLength + 1]; |
| if (!rlz_lib::GetAccessPointRlz(RLZTracker::ChromeAppList(), app_list_rlz, |
| rlz_lib::kMaxRlzLength)) { |
| app_list_rlz[0] = 0; |
| } |
| |
| // Record if google is the initial search provider and/or home page. |
| if ((first_run || app_list_rlz[0] == 0) && is_google_default_search) { |
| rlz_lib::RecordProductEvent(rlz_lib::CHROME, |
| RLZTracker::ChromeAppList(), |
| rlz_lib::SET_TO_GOOGLE); |
| } |
| #endif // !defined(OS_IOS) |
| } |
| |
| // Record first user interaction with the omnibox. We call this all the |
| // time but the rlz lib should ingore all but the first one. |
| if (omnibox_used) { |
| rlz_lib::RecordProductEvent(rlz_lib::CHROME, |
| RLZTracker::ChromeOmnibox(), |
| rlz_lib::FIRST_SEARCH); |
| } |
| |
| #if !defined(OS_IOS) |
| // Record first user interaction with the home page. We call this all the |
| // time but the rlz lib should ingore all but the first one. |
| if (homepage_used || is_google_in_startpages) { |
| rlz_lib::RecordProductEvent(rlz_lib::CHROME, |
| RLZTracker::ChromeHomePage(), |
| rlz_lib::FIRST_SEARCH); |
| } |
| |
| // Record first user interaction with the app list. We call this all the |
| // time but the rlz lib should ingore all but the first one. |
| if (app_list_used) { |
| rlz_lib::RecordProductEvent(rlz_lib::CHROME, |
| RLZTracker::ChromeAppList(), |
| rlz_lib::FIRST_SEARCH); |
| } |
| #endif // !defined(OS_IOS) |
| } |
| |
| bool SendFinancialPing(const std::string& brand, |
| const base::string16& lang, |
| const base::string16& referral) { |
| rlz_lib::AccessPoint points[] = {RLZTracker::ChromeOmnibox(), |
| #if !defined(OS_IOS) |
| RLZTracker::ChromeHomePage(), |
| RLZTracker::ChromeAppList(), |
| #endif |
| rlz_lib::NO_ACCESS_POINT}; |
| std::string lang_ascii(base::UTF16ToASCII(lang)); |
| std::string referral_ascii(base::UTF16ToASCII(referral)); |
| std::string product_signature; |
| #if defined(OS_CHROMEOS) |
| product_signature = "chromeos"; |
| #else |
| product_signature = "chrome"; |
| #endif |
| return rlz_lib::SendFinancialPing(rlz_lib::CHROME, points, |
| product_signature.c_str(), |
| brand.c_str(), referral_ascii.c_str(), |
| lang_ascii.c_str(), false, true); |
| } |
| |
| } // namespace |
| |
| RLZTracker* RLZTracker::tracker_ = nullptr; |
| |
| // static |
| RLZTracker* RLZTracker::GetInstance() { |
| return tracker_ ? tracker_ : base::Singleton<RLZTracker>::get(); |
| } |
| |
| RLZTracker::RLZTracker() |
| : first_run_(false), |
| send_ping_immediately_(false), |
| is_google_default_search_(false), |
| is_google_homepage_(false), |
| is_google_in_startpages_(false), |
| already_ran_(false), |
| omnibox_used_(false), |
| homepage_used_(false), |
| app_list_used_(false), |
| min_init_delay_(kMinInitDelay), |
| background_task_runner_(base::CreateSequencedTaskRunnerWithTraits( |
| {base::TaskShutdownBehavior::SKIP_ON_SHUTDOWN, base::MayBlock(), |
| base::WithBaseSyncPrimitives(), base::TaskPriority::BACKGROUND})) { |
| DETACH_FROM_SEQUENCE(sequence_checker_); |
| } |
| |
| RLZTracker::~RLZTracker() { |
| } |
| |
| // static |
| void RLZTracker::SetRlzDelegate(std::unique_ptr<RLZTrackerDelegate> delegate) { |
| RLZTracker* tracker = GetInstance(); |
| if (!tracker->delegate_) { |
| // RLZTracker::SetRlzDelegate is called at Profile creation time which can |
| // happens multiple time on ChromeOS, so do nothing if the delegate already |
| // exists. |
| tracker->SetDelegate(std::move(delegate)); |
| } |
| } |
| |
| void RLZTracker::SetDelegate(std::unique_ptr<RLZTrackerDelegate> delegate) { |
| DCHECK(delegate); |
| DCHECK(!delegate_); |
| delegate_ = std::move(delegate); |
| } |
| |
| // static |
| bool RLZTracker::InitRlzDelayed(bool first_run, |
| bool send_ping_immediately, |
| base::TimeDelta delay, |
| bool is_google_default_search, |
| bool is_google_homepage, |
| bool is_google_in_startpages) { |
| return GetInstance()->Init(first_run, send_ping_immediately, delay, |
| is_google_default_search, is_google_homepage, |
| is_google_in_startpages); |
| } |
| |
| bool RLZTracker::Init(bool first_run, |
| bool send_ping_immediately, |
| base::TimeDelta delay, |
| bool is_google_default_search, |
| bool is_google_homepage, |
| bool is_google_in_startpages) { |
| DCHECK(delegate_) << "RLZTracker used before initialization"; |
| first_run_ = first_run; |
| is_google_default_search_ = is_google_default_search; |
| is_google_homepage_ = is_google_homepage; |
| is_google_in_startpages_ = is_google_in_startpages; |
| send_ping_immediately_ = send_ping_immediately; |
| |
| // Enable zero delays for testing. |
| if (delegate_->ShouldEnableZeroDelayForTesting()) |
| EnableZeroDelayForTesting(); |
| |
| delay = std::min(kMaxInitDelay, std::max(min_init_delay_, delay)); |
| |
| if (delegate_->GetBrand(&brand_) && !delegate_->IsBrandOrganic(brand_)) { |
| // Register for notifications from the omnibox so that we can record when |
| // the user performs a first search. |
| delegate_->SetOmniboxSearchCallback( |
| base::Bind(&RLZTracker::RecordFirstSearch, base::Unretained(this), |
| ChromeOmnibox())); |
| |
| #if !defined(OS_IOS) |
| // Register for notifications from navigations, to see if the user has used |
| // the home page. |
| delegate_->SetHomepageSearchCallback( |
| base::Bind(&RLZTracker::RecordFirstSearch, base::Unretained(this), |
| ChromeHomePage())); |
| #endif |
| } |
| delegate_->GetReactivationBrand(&reactivation_brand_); |
| |
| // Could be null; don't run if so. RLZ will try again next restart. |
| net::URLRequestContextGetter* context_getter = delegate_->GetRequestContext(); |
| if (context_getter) { |
| rlz_lib::SetURLRequestContext(context_getter); |
| ScheduleDelayedInit(delay); |
| } |
| |
| #if !defined(OS_IOS) |
| // Prime the RLZ cache for the home page access point so that its avaiable |
| // for the startup page if needed (i.e., when the startup page is set to |
| // the home page). |
| GetAccessPointRlz(ChromeHomePage(), nullptr); |
| #endif // !defined(OS_IOS) |
| |
| return true; |
| } |
| |
| void RLZTracker::Cleanup() { |
| rlz_cache_.clear(); |
| if (delegate_) |
| delegate_->Cleanup(); |
| } |
| |
| void RLZTracker::ScheduleDelayedInit(base::TimeDelta delay) { |
| DCHECK(delegate_) << "RLZTracker used before initialization"; |
| // The RLZTracker is a singleton object that outlives any runnable tasks |
| // that will be queued up. |
| background_task_runner_->PostDelayedTask( |
| FROM_HERE, base::Bind(&RLZTracker::DelayedInit, base::Unretained(this)), |
| delay); |
| } |
| |
| void RLZTracker::DelayedInit() { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| DCHECK(delegate_) << "RLZTracker used before initialization"; |
| bool schedule_ping = false; |
| |
| // For organic brandcodes do not use rlz at all. Empty brandcode usually |
| // means a chromium install. This is ok. |
| if (!delegate_->IsBrandOrganic(brand_)) { |
| RecordProductEvents(first_run_, is_google_default_search_, |
| is_google_homepage_, is_google_in_startpages_, |
| already_ran_, omnibox_used_, homepage_used_, |
| app_list_used_); |
| schedule_ping = true; |
| } |
| |
| // If chrome has been reactivated, record the events for this brand |
| // as well. |
| if (!delegate_->IsBrandOrganic(reactivation_brand_)) { |
| rlz_lib::SupplementaryBranding branding(reactivation_brand_.c_str()); |
| RecordProductEvents(first_run_, is_google_default_search_, |
| is_google_homepage_, is_google_in_startpages_, |
| already_ran_, omnibox_used_, homepage_used_, |
| app_list_used_); |
| schedule_ping = true; |
| } |
| |
| already_ran_ = true; |
| |
| if (schedule_ping) |
| ScheduleFinancialPing(); |
| } |
| |
| void RLZTracker::ScheduleFinancialPing() { |
| DCHECK(delegate_) << "RLZTracker used before initialization"; |
| background_task_runner_->PostTask( |
| FROM_HERE, base::Bind(&RLZTracker::PingNowImpl, base::Unretained(this))); |
| } |
| |
| void RLZTracker::PingNowImpl() { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| DCHECK(delegate_) << "RLZTracker used before initialization"; |
| TRACE_EVENT0("RLZ", "RLZTracker::PingNowImpl"); |
| base::string16 lang; |
| delegate_->GetLanguage(&lang); |
| if (lang.empty()) |
| lang = base::ASCIIToUTF16("en"); |
| base::string16 referral; |
| delegate_->GetReferral(&referral); |
| |
| if (!delegate_->IsBrandOrganic(brand_) && |
| SendFinancialPing(brand_, lang, referral)) { |
| delegate_->ClearReferral(); |
| |
| { |
| base::AutoLock lock(cache_lock_); |
| rlz_cache_.clear(); |
| } |
| |
| // Prime the RLZ cache for the access points we are interested in. |
| GetAccessPointRlz(RLZTracker::ChromeOmnibox(), nullptr); |
| #if !defined(OS_IOS) |
| GetAccessPointRlz(RLZTracker::ChromeHomePage(), nullptr); |
| GetAccessPointRlz(RLZTracker::ChromeAppList(), nullptr); |
| #endif // !defined(OS_IOS) |
| } |
| |
| if (!delegate_->IsBrandOrganic(reactivation_brand_)) { |
| rlz_lib::SupplementaryBranding branding(reactivation_brand_.c_str()); |
| SendFinancialPing(reactivation_brand_, lang, referral); |
| } |
| } |
| |
| bool RLZTracker::SendFinancialPing(const std::string& brand, |
| const base::string16& lang, |
| const base::string16& referral) { |
| return ::rlz::SendFinancialPing(brand, lang, referral); |
| } |
| |
| // static |
| bool RLZTracker::RecordProductEvent(rlz_lib::Product product, |
| rlz_lib::AccessPoint point, |
| rlz_lib::Event event_id) { |
| // This method is called during unit tests while the RLZTracker has not been |
| // initialized, so check for the presence of a delegate and exit if there is |
| // none registered. |
| RLZTracker* tracker = GetInstance(); |
| return !tracker->delegate_ ? false : tracker->RecordProductEventImpl( |
| product, point, event_id); |
| } |
| |
| bool RLZTracker::RecordProductEventImpl(rlz_lib::Product product, |
| rlz_lib::AccessPoint point, |
| rlz_lib::Event event_id) { |
| DCHECK(delegate_) << "RLZTracker used before initialization"; |
| // Make sure we don't access disk outside of the I/O thread. |
| // In such case we repost the task on the right thread and return error. |
| if (ScheduleRecordProductEvent(product, point, event_id)) |
| return true; |
| |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| |
| bool ret = rlz_lib::RecordProductEvent(product, point, event_id); |
| |
| // If chrome has been reactivated, record the event for this brand as well. |
| if (!reactivation_brand_.empty()) { |
| rlz_lib::SupplementaryBranding branding(reactivation_brand_.c_str()); |
| ret &= rlz_lib::RecordProductEvent(product, point, event_id); |
| } |
| |
| return ret; |
| } |
| |
| bool RLZTracker::ScheduleRecordProductEvent(rlz_lib::Product product, |
| rlz_lib::AccessPoint point, |
| rlz_lib::Event event_id) { |
| DCHECK(delegate_) << "RLZTracker used before initialization"; |
| if (!delegate_->IsOnUIThread()) |
| return false; |
| |
| background_task_runner_->PostTask( |
| FROM_HERE, base::Bind(base::IgnoreResult(&RLZTracker::RecordProductEvent), |
| product, point, event_id)); |
| return true; |
| } |
| |
| void RLZTracker::RecordFirstSearch(rlz_lib::AccessPoint point) { |
| DCHECK(delegate_) << "RLZTracker used before initialization"; |
| // Make sure we don't access disk outside of the I/O thread. |
| // In such case we repost the task on the right thread and return error. |
| if (ScheduleRecordFirstSearch(point)) |
| return; |
| |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| |
| bool* record_used = GetAccessPointRecord(point); |
| |
| // Try to record event now, else set the flag to try later when we |
| // attempt the ping. |
| if (!RecordProductEvent(rlz_lib::CHROME, point, rlz_lib::FIRST_SEARCH)) { |
| *record_used = true; |
| } else if (send_ping_immediately_ && point == ChromeOmnibox()) { |
| ScheduleDelayedInit(base::TimeDelta()); |
| } |
| } |
| |
| bool RLZTracker::ScheduleRecordFirstSearch(rlz_lib::AccessPoint point) { |
| DCHECK(delegate_) << "RLZTracker used before initialization"; |
| if (!delegate_->IsOnUIThread()) |
| return false; |
| background_task_runner_->PostTask(FROM_HERE, |
| base::Bind(&RLZTracker::RecordFirstSearch, |
| base::Unretained(this), point)); |
| return true; |
| } |
| |
| bool* RLZTracker::GetAccessPointRecord(rlz_lib::AccessPoint point) { |
| if (point == ChromeOmnibox()) |
| return &omnibox_used_; |
| #if !defined(OS_IOS) |
| if (point == ChromeHomePage()) |
| return &homepage_used_; |
| if (point == ChromeAppList()) |
| return &app_list_used_; |
| #endif // !defined(OS_IOS) |
| NOTREACHED(); |
| return nullptr; |
| } |
| |
| // static |
| std::string RLZTracker::GetAccessPointHttpHeader(rlz_lib::AccessPoint point) { |
| TRACE_EVENT0("RLZ", "RLZTracker::GetAccessPointHttpHeader"); |
| std::string extra_headers; |
| base::string16 rlz_string; |
| RLZTracker::GetAccessPointRlz(point, &rlz_string); |
| if (!rlz_string.empty()) { |
| return base::StringPrintf("X-Rlz-String: %s\r\n", |
| base::UTF16ToUTF8(rlz_string).c_str()); |
| } |
| |
| return extra_headers; |
| } |
| |
| // GetAccessPointRlz() caches RLZ strings for all access points. If we had |
| // a successful ping, then we update the cached value. |
| // static |
| bool RLZTracker::GetAccessPointRlz(rlz_lib::AccessPoint point, |
| base::string16* rlz) { |
| // This method is called during unit tests while the RLZTracker has not been |
| // initialized, so check for the presence of a delegate and exit if there is |
| // none registered. |
| TRACE_EVENT0("RLZ", "RLZTracker::GetAccessPointRlz"); |
| RLZTracker* tracker = GetInstance(); |
| return !tracker->delegate_ ? false |
| : tracker->GetAccessPointRlzImpl(point, rlz); |
| } |
| |
| // GetAccessPointRlz() caches RLZ strings for all access points. If we had |
| // a successful ping, then we update the cached value. |
| bool RLZTracker::GetAccessPointRlzImpl(rlz_lib::AccessPoint point, |
| base::string16* rlz) { |
| DCHECK(delegate_) << "RLZTracker used before initialization"; |
| // If the RLZ string for the specified access point is already cached, |
| // simply return its value. |
| { |
| base::AutoLock lock(cache_lock_); |
| if (rlz_cache_.find(point) != rlz_cache_.end()) { |
| if (rlz) |
| *rlz = rlz_cache_[point]; |
| return true; |
| } |
| } |
| |
| // Make sure we don't access disk outside of the I/O thread. |
| // In such case we repost the task on the right thread and return error. |
| if (ScheduleGetAccessPointRlz(point)) |
| return false; |
| |
| char str_rlz[rlz_lib::kMaxRlzLength + 1]; |
| if (!rlz_lib::GetAccessPointRlz(point, str_rlz, rlz_lib::kMaxRlzLength)) |
| return false; |
| |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| |
| base::string16 rlz_local(base::ASCIIToUTF16(str_rlz)); |
| if (rlz) |
| *rlz = rlz_local; |
| |
| base::AutoLock lock(cache_lock_); |
| rlz_cache_[point] = rlz_local; |
| return true; |
| } |
| |
| bool RLZTracker::ScheduleGetAccessPointRlz(rlz_lib::AccessPoint point) { |
| DCHECK(delegate_) << "RLZTracker used before initialization"; |
| if (!delegate_->IsOnUIThread()) |
| return false; |
| |
| base::string16* not_used = nullptr; |
| background_task_runner_->PostTask( |
| FROM_HERE, base::Bind(base::IgnoreResult(&RLZTracker::GetAccessPointRlz), |
| point, not_used)); |
| return true; |
| } |
| |
| #if defined(OS_CHROMEOS) |
| // static |
| void RLZTracker::ClearRlzState() { |
| RLZTracker* tracker = GetInstance(); |
| if (tracker->delegate_) |
| tracker->ClearRlzStateImpl(); |
| } |
| |
| void RLZTracker::ClearRlzStateImpl() { |
| DCHECK(delegate_) << "RLZTracker used before initialization"; |
| if (ScheduleClearRlzState()) |
| return; |
| |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| rlz_lib::ClearAllProductEvents(rlz_lib::CHROME); |
| } |
| |
| bool RLZTracker::ScheduleClearRlzState() { |
| DCHECK(delegate_) << "RLZTracker used before initialization"; |
| if (!delegate_->IsOnUIThread()) |
| return false; |
| |
| background_task_runner_->PostTask( |
| FROM_HERE, |
| base::Bind(&RLZTracker::ClearRlzStateImpl, base::Unretained(this))); |
| return true; |
| } |
| #endif |
| |
| // static |
| void RLZTracker::CleanupRlz() { |
| GetInstance()->Cleanup(); |
| rlz_lib::SetURLRequestContext(nullptr); |
| } |
| |
| // static |
| void RLZTracker::EnableZeroDelayForTesting() { |
| GetInstance()->min_init_delay_ = base::TimeDelta(); |
| } |
| |
| #if !defined(OS_IOS) |
| // static |
| void RLZTracker::RecordAppListSearch() { |
| // This method is called during unit tests while the RLZTracker has not been |
| // initialized, so check for the presence of a delegate and exit if there is |
| // none registered. |
| RLZTracker* tracker = GetInstance(); |
| if (tracker->delegate_) |
| tracker->RecordFirstSearch(RLZTracker::ChromeAppList()); |
| } |
| #endif |
| |
| } // namespace rlz |