Commit e967c552 authored by sauski's avatar sauski Committed by Commit Bot

HaTS Next: Add web dialog to deliver web based survey

To support delivery of HaTS Next surveys, a new web dialog is created
which connects to the Hats Next Chrome specific website. The dialog
responds to events provided by the survey for managing dialog lifetime.

For example the survey dialog is only shown when the HaTS webpage has
confirmed that the user is eligible for a survey.

Bug: 1110888
Change-Id: I323c662e41331cab47f05424de53906169b42f23
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2246695Reviewed-by: default avatarTheodore Olsauskas-Warren <sauski@chromium.org>
Reviewed-by: default avatarBret Sepulveda <bsep@chromium.org>
Commit-Queue: Theodore Olsauskas-Warren <sauski@google.com>
Cr-Commit-Position: refs/heads/master@{#795911}
parent 6e578df0
...@@ -2639,6 +2639,8 @@ static_library("ui") { ...@@ -2639,6 +2639,8 @@ static_library("ui") {
"views/close_bubble_on_tab_activation_helper.h", "views/close_bubble_on_tab_activation_helper.h",
"views/hats/hats_bubble_view.cc", "views/hats/hats_bubble_view.cc",
"views/hats/hats_bubble_view.h", "views/hats/hats_bubble_view.h",
"views/hats/hats_next_web_dialog.cc",
"views/hats/hats_next_web_dialog.h",
"views/hats/hats_web_dialog.cc", "views/hats/hats_web_dialog.cc",
"views/hats/hats_web_dialog.h", "views/hats/hats_web_dialog.h",
"views/profiles/incognito_menu_view.cc", "views/profiles/incognito_menu_view.cc",
......
...@@ -51,6 +51,8 @@ constexpr char kHatsSurveyEnSiteID[] = "en_site_id"; ...@@ -51,6 +51,8 @@ constexpr char kHatsSurveyEnSiteID[] = "en_site_id";
constexpr double kHatsSurveyProbabilityDefault = 0; constexpr double kHatsSurveyProbabilityDefault = 0;
constexpr char kHatsSurveyEnSiteIDDefault[] = "bhej2dndhpc33okm6xexsbyv4y"; constexpr char kHatsSurveyEnSiteIDDefault[] = "bhej2dndhpc33okm6xexsbyv4y";
constexpr char kHatsNextSurveyTriggerIDDefault[] =
"zishSVViB0kPN8UwQ150VGjBKuBP";
constexpr base::TimeDelta kMinimumTimeBetweenSurveyStarts = constexpr base::TimeDelta kMinimumTimeBetweenSurveyStarts =
base::TimeDelta::FromDays(60); base::TimeDelta::FromDays(60);
...@@ -151,12 +153,16 @@ HatsService::HatsService(Profile* profile) : profile_(profile) { ...@@ -151,12 +153,16 @@ HatsService::HatsService(Profile* profile) : profile_(profile) {
.Get())); .Get()));
} }
// Ensure a default survey exists (for demo purpose). // Ensure a default survey exists (for demo purpose).
auto* default_survey_id =
base::FeatureList::IsEnabled(
features::kHappinessTrackingSurveysForDesktopMigration)
? kHatsNextSurveyTriggerIDDefault
: kHatsSurveyEnSiteIDDefault;
if (survey_configs_by_triggers_.find(kHatsSurveyTriggerSatisfaction) == if (survey_configs_by_triggers_.find(kHatsSurveyTriggerSatisfaction) ==
survey_configs_by_triggers_.end()) { survey_configs_by_triggers_.end()) {
survey_configs_by_triggers_.emplace( survey_configs_by_triggers_.emplace(
kHatsSurveyTriggerSatisfaction, kHatsSurveyTriggerSatisfaction,
SurveyConfig(kHatsSurveyProbabilityDefault, SurveyConfig(kHatsSurveyProbabilityDefault, default_survey_id));
kHatsSurveyEnSiteIDDefault));
} }
} }
...@@ -421,14 +427,23 @@ void HatsService::CheckSurveyStatusAndMaybeShow(Browser* browser, ...@@ -421,14 +427,23 @@ void HatsService::CheckSurveyStatusAndMaybeShow(Browser* browser,
DCHECK(survey_configs_by_triggers_.find(trigger) != DCHECK(survey_configs_by_triggers_.find(trigger) !=
survey_configs_by_triggers_.end()); survey_configs_by_triggers_.end());
if (!checker_) if (base::FeatureList::IsEnabled(
checker_ = std::make_unique<HatsSurveyStatusChecker>(profile_); features::kHappinessTrackingSurveysForDesktopMigration)) {
checker_->CheckSurveyStatus( // Bypass the checker for showing HaTS Next surveys as the survey website
survey_configs_by_triggers_[trigger].en_site_id_, // itself will determine eligibility. This is communicated via updates to
base::BindOnce(&HatsService::ShowSurvey, weak_ptr_factory_.GetWeakPtr(), // HatsNextWebDialog::OnSurveyStateUpdateReceived.
browser, trigger), browser->window()->ShowHatsBubble(
base::BindOnce(&HatsService::OnSurveyStatusError, survey_configs_by_triggers_[trigger].en_site_id_);
weak_ptr_factory_.GetWeakPtr(), trigger)); } else {
if (!checker_)
checker_ = std::make_unique<HatsSurveyStatusChecker>(profile_);
checker_->CheckSurveyStatus(
survey_configs_by_triggers_[trigger].en_site_id_,
base::BindOnce(&HatsService::ShowSurvey, weak_ptr_factory_.GetWeakPtr(),
browser, trigger),
base::BindOnce(&HatsService::OnSurveyStatusError,
weak_ptr_factory_.GetWeakPtr(), trigger));
}
} }
void HatsService::ShowSurvey(Browser* browser, const std::string& trigger) { void HatsService::ShowSurvey(Browser* browser, const std::string& trigger) {
......
...@@ -14,6 +14,7 @@ ...@@ -14,6 +14,7 @@
#include "chrome/browser/ui/test/test_browser_dialog.h" #include "chrome/browser/ui/test/test_browser_dialog.h"
#include "chrome/browser/ui/views/frame/browser_view.h" #include "chrome/browser/ui/views/frame/browser_view.h"
#include "chrome/browser/ui/views/hats/hats_bubble_view.h" #include "chrome/browser/ui/views/hats/hats_bubble_view.h"
#include "chrome/browser/ui/views/hats/hats_next_web_dialog.h"
#include "chrome/browser/ui/views/hats/hats_web_dialog.h" #include "chrome/browser/ui/views/hats/hats_web_dialog.h"
#include "chrome/common/chrome_paths.h" #include "chrome/common/chrome_paths.h"
#include "components/content_settings/core/browser/host_content_settings_map.h" #include "components/content_settings/core/browser/host_content_settings_map.h"
...@@ -178,3 +179,91 @@ IN_PROC_BROWSER_TEST_F(HatsWebDialogBrowserTest, Cookies) { ...@@ -178,3 +179,91 @@ IN_PROC_BROWSER_TEST_F(HatsWebDialogBrowserTest, Cookies) {
settings_map->GetContentSetting( settings_map->GetContentSetting(
url2, url2, ContentSettingsType::COOKIES, std::string())); url2, url2, ContentSettingsType::COOKIES, std::string()));
} }
class MockHatsNextWebDialog : public HatsNextWebDialog {
public:
MockHatsNextWebDialog(Browser* browser,
const std::string& trigger_id,
const GURL& hats_survey_url,
const base::TimeDelta& timeout)
: HatsNextWebDialog(browser, trigger_id, hats_survey_url, timeout) {}
MOCK_METHOD0(ShowWidget, void());
MOCK_METHOD0(CloseWidget, void());
};
typedef InProcessBrowserTest HatsNextWebDialogBrowserTest;
// Test that the web dialog correctly receives change to history state that
// indicates a survey is ready to be shown.
IN_PROC_BROWSER_TEST_F(HatsNextWebDialogBrowserTest, SurveyLoaded) {
ASSERT_TRUE(embedded_test_server()->Start());
auto* dialog = new MockHatsNextWebDialog(
browser(), "load_for_testing",
embedded_test_server()->GetURL("/hats/hats_next_mock.html"),
base::TimeDelta::FromSeconds(100));
// The hats_next_mock.html will provide a state update to the dialog to
// indicate that the survey has been loaded.
base::RunLoop run_loop;
EXPECT_CALL(*dialog, ShowWidget)
.WillOnce(testing::Invoke([dialog, &run_loop]() {
EXPECT_FALSE(dialog->IsWaitingForSurveyForTesting());
run_loop.Quit();
}));
run_loop.Run();
}
// Test that the web dialog correctly receives change to history state that
// indicates the survey window should be closed.
IN_PROC_BROWSER_TEST_F(HatsNextWebDialogBrowserTest, SurveyClosed) {
ASSERT_TRUE(embedded_test_server()->Start());
auto* dialog = new MockHatsNextWebDialog(
browser(), "close_for_testing",
embedded_test_server()->GetURL("/hats/hats_next_mock.html"),
base::TimeDelta::FromSeconds(100));
// The hats_next_mock.html will provide a state update to the dialog to
// indicate that the survey window should be closed.
base::RunLoop run_loop;
EXPECT_CALL(*dialog, CloseWidget).WillOnce([&run_loop]() {
run_loop.Quit();
});
run_loop.Run();
}
// Test that if the survey does not indicate it is ready for display before the
// timeout the widget is closed.
IN_PROC_BROWSER_TEST_F(HatsNextWebDialogBrowserTest, SurveyTimeout) {
ASSERT_TRUE(embedded_test_server()->Start());
auto* dialog = new MockHatsNextWebDialog(
browser(), "invalid_test",
embedded_test_server()->GetURL("/hats/non_existent.html"),
base::TimeDelta::FromMilliseconds(1));
base::RunLoop run_loop;
EXPECT_CALL(*dialog, CloseWidget).WillOnce([&run_loop]() {
run_loop.Quit();
});
run_loop.Run();
}
IN_PROC_BROWSER_TEST_F(HatsNextWebDialogBrowserTest, UnknownURLFragment) {
ASSERT_TRUE(embedded_test_server()->Start());
// Check that providing an unknown URL fragment results in the dialog being
// closed.
auto* dialog = new MockHatsNextWebDialog(
browser(), "invalid_url_fragment_for_testing",
embedded_test_server()->GetURL("/hats/hats_next_mock.html"),
base::TimeDelta::FromSeconds(100));
base::RunLoop run_loop;
EXPECT_CALL(*dialog, CloseWidget).WillOnce([&run_loop]() {
run_loop.Quit();
});
run_loop.Run();
}
...@@ -13,7 +13,9 @@ ...@@ -13,7 +13,9 @@
#include "chrome/browser/ui/views/frame/app_menu_button.h" #include "chrome/browser/ui/views/frame/app_menu_button.h"
#include "chrome/browser/ui/views/frame/browser_view.h" #include "chrome/browser/ui/views/frame/browser_view.h"
#include "chrome/browser/ui/views/frame/toolbar_button_provider.h" #include "chrome/browser/ui/views/frame/toolbar_button_provider.h"
#include "chrome/browser/ui/views/hats/hats_next_web_dialog.h"
#include "chrome/browser/ui/views/hats/hats_web_dialog.h" #include "chrome/browser/ui/views/hats/hats_web_dialog.h"
#include "chrome/common/chrome_features.h"
#include "chrome/grit/chromium_strings.h" #include "chrome/grit/chromium_strings.h"
#include "chrome/grit/generated_resources.h" #include "chrome/grit/generated_resources.h"
#include "chrome/grit/theme_resources.h" #include "chrome/grit/theme_resources.h"
...@@ -70,7 +72,13 @@ void HatsBubbleView::ShowOnContentReady(Browser* browser, ...@@ -70,7 +72,13 @@ void HatsBubbleView::ShowOnContentReady(Browser* browser,
// The bubble will only show after the survey content is retrieved. // The bubble will only show after the survey content is retrieved.
// If it fails due to no internet connection or any other reason, the bubble // If it fails due to no internet connection or any other reason, the bubble
// will not show. // will not show.
HatsWebDialog::Create(browser, site_id); if (base::FeatureList::IsEnabled(
features::kHappinessTrackingSurveysForDesktopMigration)) {
// Self deleting on close.
new HatsNextWebDialog(browser, site_id);
} else {
HatsWebDialog::Create(browser, site_id);
}
} }
void HatsBubbleView::Show(Browser* browser, void HatsBubbleView::Show(Browser* browser,
......
// Copyright 2020 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/views/hats/hats_next_web_dialog.h"
#include "chrome/browser/ui/browser_dialogs.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/profiles/profile_destroyer.h"
#include "chrome/browser/ui/views/frame/app_menu_button.h"
#include "chrome/browser/ui/views/frame/browser_view.h"
#include "chrome/browser/ui/views/frame/toolbar_button_provider.h"
#include "chrome/browser/ui/views/hats/hats_bubble_view.h"
#include "chrome/browser/ui/webui/chrome_web_contents_handler.h"
#include "chrome/common/chrome_features.h"
#include "chrome/common/chrome_isolated_world_ids.h"
#include "components/constrained_window/constrained_window_views.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/navigation_handle.h"
#include "content/public/browser/render_frame_host.h"
#include "net/base/url_util.h"
#include "ui/base/ui_base_types.h"
#include "ui/views/bubble/bubble_dialog_delegate_view.h"
#include "ui/views/bubble/bubble_frame_view.h"
#include "ui/views/controls/webview/web_dialog_view.h"
#include "ui/views/layout/fill_layout.h"
// A thin wrapper that forwards the reference part of the URL associated with
// navigation events to the enclosing web dialog.
class HatsNextWebDialog::WebContentsObserver
: public content::WebContentsObserver {
public:
WebContentsObserver(content::WebContents* contents, HatsNextWebDialog* dialog)
: content::WebContentsObserver(contents), dialog_(dialog) {}
// content::WebContentsObserver overrides.
void DidStartNavigation(
content::NavigationHandle* navigation_handle) override {
if (navigation_handle->IsSameDocument() &&
navigation_handle->IsRendererInitiated()) {
dialog_->OnSurveyStateUpdateReceived(navigation_handle->GetURL().ref());
}
}
private:
HatsNextWebDialog* dialog_;
};
HatsNextWebDialog::HatsNextWebDialog(Browser* browser,
const std::string& trigger_id)
: HatsNextWebDialog(
browser,
trigger_id,
GURL("https://storage.googleapis.com/chrome_hats/index.html"),
base::TimeDelta::FromSeconds(10)) {}
ui::ModalType HatsNextWebDialog::GetDialogModalType() const {
return ui::MODAL_TYPE_NONE;
}
base::string16 HatsNextWebDialog::GetDialogTitle() const {
return base::string16();
}
GURL HatsNextWebDialog::GetDialogContentURL() const {
GURL param_url =
net::AppendQueryParameter(hats_survey_url_, "trigger_id", trigger_id_);
if (base::FeatureList::IsEnabled(
features::kHappinessTrackingSurveysForDesktopDemo)) {
param_url = net::AppendQueryParameter(param_url, "enable_testing", "true");
}
return param_url;
}
void HatsNextWebDialog::GetWebUIMessageHandlers(
std::vector<content::WebUIMessageHandler*>* handlers) const {}
void HatsNextWebDialog::GetDialogSize(gfx::Size* size) const {}
bool HatsNextWebDialog::CanResizeDialog() const {
return false;
}
std::string HatsNextWebDialog::GetDialogArgs() const {
return std::string();
}
void HatsNextWebDialog::OnDialogClosed(const std::string& json_retval) {}
void HatsNextWebDialog::OnCloseContents(content::WebContents* source,
bool* out_close_dialog) {
*out_close_dialog = true;
}
bool HatsNextWebDialog::ShouldShowCloseButton() const {
return false;
}
bool HatsNextWebDialog::ShouldShowDialogTitle() const {
return false;
}
bool HatsNextWebDialog::HandleContextMenu(
content::RenderFrameHost* render_frame_host,
const content::ContextMenuParams& params) {
return true;
}
gfx::Size HatsNextWebDialog::CalculatePreferredSize() const {
// Default width/height of the dialog in screen size, these values are derived
// from the size of the HaTS HTML component displayed by this dialog.
constexpr int kDefaultHatsDialogWidth = 363;
constexpr int kDefaultHatsDialogHeight = 440;
return gfx::Size(kDefaultHatsDialogWidth, kDefaultHatsDialogHeight);
}
void HatsNextWebDialog::OnProfileWillBeDestroyed(Profile* profile) {
DCHECK_EQ(profile, otr_profile_);
otr_profile_ = nullptr;
}
HatsNextWebDialog::HatsNextWebDialog(Browser* browser,
const std::string& trigger_id,
const GURL& hats_survey_url,
const base::TimeDelta& timeout)
: BubbleDialogDelegateView(BrowserView::GetBrowserViewForBrowser(browser)
->toolbar_button_provider()
->GetAppMenuButton(),
views::BubbleBorder::TOP_RIGHT),
otr_profile_(browser->profile()->GetOffTheRecordProfile(
Profile::OTRProfileID::CreateUnique("HaTSNext:WebDialog"))),
trigger_id_(trigger_id),
hats_survey_url_(hats_survey_url),
timeout_(timeout),
close_bubble_helper_(this, browser) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
otr_profile_->AddObserver(this);
set_close_on_deactivate(false);
SetButtons(ui::DIALOG_BUTTON_NONE);
SetLayoutManager(std::make_unique<views::FillLayout>());
auto* web_view = AddChildView(std::make_unique<views::WebDialogView>(
otr_profile_, this, std::make_unique<ChromeWebContentsHandler>(),
/* use_dialog_frame */ true));
widget_ = views::BubbleDialogDelegateView::CreateBubble(this);
web_contents_observer_ =
std::make_unique<WebContentsObserver>(web_view->web_contents(), this);
loading_timer_.Start(FROM_HERE, timeout_,
base::BindOnce(&HatsNextWebDialog::CloseWidget,
weak_factory_.GetWeakPtr()));
}
HatsNextWebDialog::~HatsNextWebDialog() {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
if (otr_profile_) {
otr_profile_->RemoveObserver(this);
ProfileDestroyer::DestroyProfileWhenAppropriate(otr_profile_);
}
}
void HatsNextWebDialog::OnSurveyStateUpdateReceived(std::string state) {
loading_timer_.AbandonAndStop();
if (state == "loaded") {
ShowWidget();
} else if (state == "close") {
CloseWidget();
} else {
LOG(ERROR) << "Unknown state provided in URL fragment by HaTS survey:"
<< state;
CloseWidget();
}
}
void HatsNextWebDialog::SetHatsSurveyURLforTesting(GURL url) {
hats_survey_url_ = url;
}
void HatsNextWebDialog::ShowWidget() {
widget_->Show();
}
void HatsNextWebDialog::CloseWidget() {
widget_->Close();
}
bool HatsNextWebDialog::IsWaitingForSurveyForTesting() {
return loading_timer_.IsRunning();
}
// Copyright 2020 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.
#ifndef CHROME_BROWSER_UI_VIEWS_HATS_HATS_NEXT_WEB_DIALOG_H_
#define CHROME_BROWSER_UI_VIEWS_HATS_HATS_NEXT_WEB_DIALOG_H_
#include "chrome/browser/profiles/profile_observer.h"
#include "chrome/browser/ui/views/close_bubble_on_tab_activation_helper.h"
#include "content/public/browser/web_contents_observer.h"
#include "ui/views/bubble/bubble_dialog_delegate_view.h"
#include "ui/views/controls/webview/web_dialog_view.h"
#include "ui/views/window/dialog_delegate.h"
#include "ui/web_dialogs/web_dialog_delegate.h"
class Browser;
class Profile;
namespace views {
class Widget;
} // namespace views
// A dialog for displaying a Happiness Tracking Survey (HaTS) NEXT survey to
// the user. The dialog presents a WebContents which connects to a publicly
// accessible, Chrome specific, webpage which is responsible for displaying the
// survey to users. The webpage has additional logic to provide information to
// this dialog via URL fragments, such as whether a survey is ready to be shown
// to the user.
class HatsNextWebDialog : public ui::WebDialogDelegate,
public views::BubbleDialogDelegateView,
public content::WebContentsDelegate,
public ProfileObserver {
public:
HatsNextWebDialog(Browser* browser, const std::string& trigger_id);
~HatsNextWebDialog() override;
HatsNextWebDialog(const HatsNextWebDialog&) = delete;
HatsNextWebDialog& operator=(const HatsNextWebDialog&) = delete;
// ui::WebDialogDelegate:
ui::ModalType GetDialogModalType() const override;
base::string16 GetDialogTitle() const override;
GURL GetDialogContentURL() const override;
void GetWebUIMessageHandlers(
std::vector<content::WebUIMessageHandler*>* handlers) const override;
void GetDialogSize(gfx::Size* size) const override;
bool CanResizeDialog() const override;
std::string GetDialogArgs() const override;
void OnDialogClosed(const std::string& json_retval) override;
void OnCloseContents(content::WebContents* source,
bool* out_close_dialog) override;
bool ShouldShowCloseButton() const override;
bool ShouldShowDialogTitle() const override;
bool HandleContextMenu(content::RenderFrameHost* render_frame_host,
const content::ContextMenuParams& params) override;
// BubbleDialogDelegateView:
gfx::Size CalculatePreferredSize() const override;
// ProfileObserver:
void OnProfileWillBeDestroyed(Profile* profile) override;
protected:
FRIEND_TEST_ALL_PREFIXES(HatsNextWebDialogBrowserTest, SurveyLoaded);
HatsNextWebDialog(Browser* browser,
const std::string& trigger_id,
const GURL& hats_survey_url_,
const base::TimeDelta& timeout);
class WebContentsObserver;
// Fired by the observer when the survey page has pushed state to the window
// via URL fragments.
void OnSurveyStateUpdateReceived(std::string state);
// Provides mechanism to override URL requested by the dialog. Must be called
// before CreateWebDialog() to take effect.
void SetHatsSurveyURLforTesting(GURL url);
// Displays the widget to the user, called when the dialog believes a survey
// ready for display. Virtual to allow mocking in tests.
virtual void ShowWidget();
// Called by the dialog to close the widget due to timeout or the survey being
// closed. Virtual to allow mocking in tests.
virtual void CloseWidget();
// Returns whether the dialog is still waiting for the survey to load.
bool IsWaitingForSurveyForTesting();
private:
// A timer to prevent unresponsive loading of survey dialog.
base::OneShotTimer loading_timer_;
// The off-the-record profile used for browsing to the Chrome HaTS webpage.
Profile* otr_profile_;
// The HaTS Next survey trigger ID that is provided to the HaTS webpage.
const std::string& trigger_id_;
views::Widget* widget_ = nullptr;
std::unique_ptr<WebContentsObserver> web_contents_observer_;
GURL hats_survey_url_;
base::TimeDelta timeout_;
CloseBubbleOnTabActivationHelper close_bubble_helper_;
base::WeakPtrFactory<HatsNextWebDialog> weak_factory_{this};
};
#endif // CHROME_BROWSER_UI_VIEWS_HATS_HATS_NEXT_WEB_DIALOG_H_
<!doctype html>
<html>
<head>
<script>
const params = new URLSearchParams(window.location.search);
if (params.get('trigger_id') == "load_for_testing") {
history.pushState('', '', '#loaded');
}
if (params.get('trigger_id') == "close_for_testing") {
history.pushState('', '', '#close');
}
if (params.get('trigger_id') == "invalid_url_fragment_for_testing") {
history.pushState('', '', '#foo');
}
</script>
</head>
<body>
</body>
</html>
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment