Commit 53c05c25 authored by Lucas Tenório's avatar Lucas Tenório Committed by Commit Bot

Implement the full Supervision Onboarding flow.

This change adds the "Details" and "All Set" pages to the flow, as well
as a class to manage the (now multiple) onboarding states.

Another change is that we stop removing some webview listeners when we
stop loading the page. This was causing the webview to never finish
loading subsequent pages, getting stuck in the first blocking listener.

Bug: 958995
Change-Id: Ib28f8d271263ffa832e05a5c6cefb7f7675b02a2
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/1630223
Commit-Queue: Lucas Tenório <ltenorio@chromium.org>
Reviewed-by: default avatarXiyuan Xia <xiyuan@chromium.org>
Reviewed-by: default avatarMichael Giuffrida <michaelpg@chromium.org>
Reviewed-by: default avatarOliver Chang <ochang@chromium.org>
Cr-Commit-Position: refs/heads/master@{#666535}
parent 9c8502c4
...@@ -1952,10 +1952,18 @@ source_set("chromeos") { ...@@ -1952,10 +1952,18 @@ source_set("chromeos") {
"smb_client/temp_file_manager.h", "smb_client/temp_file_manager.h",
"startup_settings_cache.cc", "startup_settings_cache.cc",
"startup_settings_cache.h", "startup_settings_cache.h",
"supervision/kiosk_next_flow_observer.cc",
"supervision/kiosk_next_flow_observer.h",
"supervision/onboarding_constants.cc", "supervision/onboarding_constants.cc",
"supervision/onboarding_constants.h", "supervision/onboarding_constants.h",
"supervision/onboarding_controller_impl.cc", "supervision/onboarding_controller_impl.cc",
"supervision/onboarding_controller_impl.h", "supervision/onboarding_controller_impl.h",
"supervision/onboarding_flow_model.cc",
"supervision/onboarding_flow_model.h",
"supervision/onboarding_logger.cc",
"supervision/onboarding_logger.h",
"supervision/onboarding_presenter.cc",
"supervision/onboarding_presenter.h",
"system/automatic_reboot_manager.cc", "system/automatic_reboot_manager.cc",
"system/automatic_reboot_manager.h", "system/automatic_reboot_manager.h",
"system/automatic_reboot_manager_observer.h", "system/automatic_reboot_manager_observer.h",
......
...@@ -11,7 +11,9 @@ ...@@ -11,7 +11,9 @@
#include "base/command_line.h" #include "base/command_line.h"
#include "base/run_loop.h" #include "base/run_loop.h"
#include "base/strings/string_piece.h" #include "base/strings/string_piece.h"
#include "base/strings/string_util.h"
#include "base/test/scoped_feature_list.h" #include "base/test/scoped_feature_list.h"
#include "base/threading/thread_task_runner_handle.h"
#include "chrome/browser/chromeos/login/login_wizard.h" #include "chrome/browser/chromeos/login/login_wizard.h"
#include "chrome/browser/chromeos/login/mixin_based_in_process_browser_test.h" #include "chrome/browser/chromeos/login/mixin_based_in_process_browser_test.h"
#include "chrome/browser/chromeos/login/oobe_screen.h" #include "chrome/browser/chromeos/login/oobe_screen.h"
...@@ -76,6 +78,8 @@ class FakeSupervisionServer { ...@@ -76,6 +78,8 @@ class FakeSupervisionServer {
custom_http_header_value_ = base::nullopt; custom_http_header_value_ = base::nullopt;
} }
const std::string& last_request_url() { return last_request_url_; }
size_t GetReceivedRequestsCount() const { size_t GetReceivedRequestsCount() const {
// It's safe to use the size of the access token list as a proxy to the // It's safe to use the size of the access token list as a proxy to the
// number of requests. This server asserts that all requests contain an // number of requests. This server asserts that all requests contain an
...@@ -88,8 +92,10 @@ class FakeSupervisionServer { ...@@ -88,8 +92,10 @@ class FakeSupervisionServer {
// We are not interested in other URLs hitting the server at this point. // We are not interested in other URLs hitting the server at this point.
// This will filter bogus requests like favicon fetches and stop us from // This will filter bogus requests like favicon fetches and stop us from
// handling requests that are targeting gaia. // handling requests that are targeting gaia.
if (request.relative_url != supervision::kOnboardingStartPageRelativeUrl) if (!base::StartsWith(request.relative_url, "/kids/deviceonboarding",
base::CompareCase::INSENSITIVE_ASCII)) {
return nullptr; return nullptr;
}
UpdateVerificationData(request); UpdateVerificationData(request);
auto response = std::make_unique<BasicHttpResponse>(); auto response = std::make_unique<BasicHttpResponse>();
...@@ -113,10 +119,12 @@ class FakeSupervisionServer { ...@@ -113,10 +119,12 @@ class FakeSupervisionServer {
FakeGaiaMixin::kFakeAllScopeAccessToken)); FakeGaiaMixin::kFakeAllScopeAccessToken));
received_auth_header_values_.push_back(auth_header->second); received_auth_header_values_.push_back(auth_header->second);
last_request_url_ = request.relative_url;
} }
net::EmbeddedTestServer* test_server_; net::EmbeddedTestServer* test_server_;
std::vector<std::string> received_auth_header_values_; std::vector<std::string> received_auth_header_values_;
std::string last_request_url_;
base::Optional<std::string> custom_http_header_value_ = base::nullopt; base::Optional<std::string> custom_http_header_value_ = base::nullopt;
}; };
...@@ -192,11 +200,19 @@ class SupervisionOnboardingBaseTest : public MixinBasedInProcessBrowserTest { ...@@ -192,11 +200,19 @@ class SupervisionOnboardingBaseTest : public MixinBasedInProcessBrowserTest {
void ShowScreen() { supervision_onboarding_screen_->Show(); } void ShowScreen() { supervision_onboarding_screen_->Show(); }
void WaitForScreen() { void WaitForPageWithUrl(const std::string& requested_url) {
OobeScreenWaiter screen_waiter(SupervisionOnboardingScreenView::kScreenId); // Wait for the request...
screen_waiter.set_assert_next_screen(); while (supervision_server()->last_request_url() != requested_url) {
screen_waiter.Wait(); base::RunLoop run_loop;
// Avoid RunLoop::RunUntilIdle() because this is in a loop and
// could end up being a busy loop when there are no pending tasks.
base::ThreadTaskRunnerHandle::Get()->PostDelayedTask(
FROM_HERE, run_loop.QuitClosure(),
base::TimeDelta::FromMilliseconds(100));
run_loop.Run();
}
// Now wait for the UI to be updated with the response.
test::OobeJS() test::OobeJS()
.CreateVisibilityWaiter( .CreateVisibilityWaiter(
true, {"supervision-onboarding", "supervision-onboarding-content"}) true, {"supervision-onboarding", "supervision-onboarding-content"})
...@@ -206,7 +222,7 @@ class SupervisionOnboardingBaseTest : public MixinBasedInProcessBrowserTest { ...@@ -206,7 +222,7 @@ class SupervisionOnboardingBaseTest : public MixinBasedInProcessBrowserTest {
void ClickButton(const std::string& button_id) { void ClickButton(const std::string& button_id) {
std::initializer_list<base::StringPiece> button_path = { std::initializer_list<base::StringPiece> button_path = {
"supervision-onboarding", button_id}; "supervision-onboarding", button_id};
test::OobeJS().CreateEnabledWaiter(true, button_path)->Wait(); test::OobeJS().CreateVisibilityWaiter(true, button_path)->Wait();
test::OobeJS().TapOnPath(button_path); test::OobeJS().TapOnPath(button_path);
} }
...@@ -219,6 +235,40 @@ class SupervisionOnboardingBaseTest : public MixinBasedInProcessBrowserTest { ...@@ -219,6 +235,40 @@ class SupervisionOnboardingBaseTest : public MixinBasedInProcessBrowserTest {
run_loop.Run(); run_loop.Run();
} }
// Shows the screen and navigates to the start page.
// This will also expect that we will make the correct requests to load the
// start page.
void NavigateToStartPage() {
ShowScreen();
OobeScreenWaiter screen_waiter(SupervisionOnboardingScreenView::kScreenId);
screen_waiter.set_assert_next_screen();
screen_waiter.Wait();
WaitForPageWithUrl(supervision::kOnboardingStartPageRelativeUrl);
EXPECT_EQ(1u, supervision_server()->GetReceivedRequestsCount());
}
// Navigates to the details page by first going through the start page.
void NavigateToDetailsPage() {
NavigateToStartPage();
ClickButton("supervision-onboarding-next-button");
WaitForPageWithUrl(supervision::kOnboardingDetailsPageRelativeUrl);
EXPECT_EQ(2u, supervision_server()->GetReceivedRequestsCount());
}
// Navigates to the "All Set!" page by going through the Start and Details
// pages.
void NavigateToAllSetPage() {
NavigateToDetailsPage();
ClickButton("supervision-onboarding-next-button");
WaitForPageWithUrl(supervision::kOnboardingAllSetPageRelativeUrl);
EXPECT_EQ(3u, supervision_server()->GetReceivedRequestsCount());
}
FakeSupervisionServer* supervision_server() { return &supervision_server_; } FakeSupervisionServer* supervision_server() { return &supervision_server_; }
SupervisionOnboardingScreen* supervision_onboarding_screen_; SupervisionOnboardingScreen* supervision_onboarding_screen_;
...@@ -322,22 +372,44 @@ IN_PROC_BROWSER_TEST_F(SupervisionOnboardingTest, ...@@ -322,22 +372,44 @@ IN_PROC_BROWSER_TEST_F(SupervisionOnboardingTest,
EXPECT_EQ(1u, supervision_server()->GetReceivedRequestsCount()); EXPECT_EQ(1u, supervision_server()->GetReceivedRequestsCount());
} }
IN_PROC_BROWSER_TEST_F(SupervisionOnboardingTest, NextButtonExitsScreen) { IN_PROC_BROWSER_TEST_F(SupervisionOnboardingTest,
ShowScreen(); NavigateToStartPageAndSkipFlow) {
WaitForScreen(); NavigateToStartPage();
EXPECT_EQ(1u, supervision_server()->GetReceivedRequestsCount());
ClickButton("supervision-onboarding-next-button"); ClickButton("supervision-onboarding-skip-button");
WaitForScreenExit(); WaitForScreenExit();
} }
IN_PROC_BROWSER_TEST_F(SupervisionOnboardingTest, SkipButtonExitsScreen) { IN_PROC_BROWSER_TEST_F(SupervisionOnboardingTest,
ShowScreen(); NavigateToDetailsPageAndBack) {
WaitForScreen(); NavigateToDetailsPage();
EXPECT_EQ(1u, supervision_server()->GetReceivedRequestsCount());
ClickButton("supervision-onboarding-back-button");
WaitForPageWithUrl(supervision::kOnboardingStartPageRelativeUrl);
ClickButton("supervision-onboarding-skip-button"); ClickButton("supervision-onboarding-skip-button");
WaitForScreenExit(); WaitForScreenExit();
} }
IN_PROC_BROWSER_TEST_F(SupervisionOnboardingTest, NavigateToAllSetPageAndBack) {
NavigateToAllSetPage();
ClickButton("supervision-onboarding-back-button");
WaitForPageWithUrl(supervision::kOnboardingDetailsPageRelativeUrl);
ClickButton("supervision-onboarding-back-button");
WaitForPageWithUrl(supervision::kOnboardingStartPageRelativeUrl);
ClickButton("supervision-onboarding-skip-button");
WaitForScreenExit();
}
IN_PROC_BROWSER_TEST_F(SupervisionOnboardingTest,
NavigateToAllSetPageAndFinishFlow) {
NavigateToAllSetPage();
ClickButton("supervision-onboarding-next-button");
WaitForScreenExit();
}
} // namespace chromeos } // namespace chromeos
// Copyright 2019 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/chromeos/supervision/kiosk_next_flow_observer.h"
#include "ash/public/cpp/ash_pref_names.h"
#include "chrome/browser/profiles/profile.h"
#include "chromeos/constants/chromeos_features.h"
#include "components/prefs/pref_service.h"
namespace chromeos {
namespace supervision {
KioskNextFlowObserver::KioskNextFlowObserver(Profile* profile,
OnboardingFlowModel* flow_model)
: profile_(profile), flow_model_(flow_model) {
flow_model_->AddObserver(this);
}
KioskNextFlowObserver::~KioskNextFlowObserver() {
flow_model_->RemoveObserver(this);
}
void KioskNextFlowObserver::StepFinishedLoading(
OnboardingFlowModel::Step step) {
if (step == OnboardingFlowModel::Step::kStart)
profile_->GetPrefs()->SetBoolean(ash::prefs::kKioskNextShellEligible, true);
}
void KioskNextFlowObserver::WillExitFlow(
OnboardingFlowModel::Step step,
OnboardingFlowModel::ExitReason reason) {
if (reason == OnboardingFlowModel::ExitReason::kUserReachedEnd)
profile_->GetPrefs()->SetBoolean(ash::prefs::kKioskNextShellEnabled, true);
}
} // namespace supervision
} // namespace chromeos
// Copyright 2019 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_CHROMEOS_SUPERVISION_KIOSK_NEXT_FLOW_OBSERVER_H_
#define CHROME_BROWSER_CHROMEOS_SUPERVISION_KIOSK_NEXT_FLOW_OBSERVER_H_
#include "base/macros.h"
#include "chrome/browser/chromeos/supervision/mojom/onboarding_controller.mojom.h"
#include "chrome/browser/chromeos/supervision/onboarding_flow_model.h"
class Profile;
namespace chromeos {
namespace supervision {
// Observes the Supervision Onboarding flow and sets prefs related to
// Kiosk Next.
class KioskNextFlowObserver : public OnboardingFlowModel::Observer {
public:
explicit KioskNextFlowObserver(Profile* profile,
OnboardingFlowModel* flow_model);
~KioskNextFlowObserver() override;
private:
// OnboardingFlowModel::Observer:
void StepFinishedLoading(OnboardingFlowModel::Step step) override;
void WillExitFlow(OnboardingFlowModel::Step step,
OnboardingFlowModel::ExitReason reason) override;
Profile* profile_;
OnboardingFlowModel* flow_model_;
DISALLOW_COPY_AND_ASSIGN(KioskNextFlowObserver);
};
} // namespace supervision
} // namespace chromeos
#endif // CHROME_BROWSER_CHROMEOS_SUPERVISION_KIOSK_NEXT_FLOW_OBSERVER_H_
...@@ -22,21 +22,18 @@ struct OnboardingPage { ...@@ -22,21 +22,18 @@ struct OnboardingPage {
// Url for the page that needs to be loaded by the webview host. // Url for the page that needs to be loaded by the webview host.
url.mojom.Url url; url.mojom.Url url;
// Only requests to URLs that pass this pattern should be authenticated // Only requests to URLs that have this prefix should be allowed.
// or have their custom headers extracted. string allowed_urls_prefix;
// Documentation on how to write these patterns can be found in:
// https://developer.chrome.com/extensions/match_patterns
string url_filter_pattern;
// Access token used to authenticate the flow page requests. Note that this // Access token used to authenticate the flow page requests. Note that this
// should only be used in requests to URLs that match |url_filter_pattern|. // should only be used in requests to URLs that match |allowed_urls_prefix|.
string access_token; string access_token;
// Some flow pages are expected to return a custom header in their HTTP // Some flow pages are expected to return a custom header in their HTTP
// responses. If this field is set, we will extract the given header from // responses. If this field is set, we will extract the given header from
// responses and return its value when the page fully loads. // responses and return its value when the page fully loads.
// Note that this should only be used in requests to URLs that match // Note that this should only be used in requests to URLs that match
// |url_filter_pattern|. // |allowed_urls_prefix|.
string? custom_header_name; string? custom_header_name;
}; };
......
...@@ -8,12 +8,18 @@ namespace chromeos { ...@@ -8,12 +8,18 @@ namespace chromeos {
namespace supervision { namespace supervision {
// Default URL prefix for the Supervision Onboarding pages. // Default URL prefix for the Supervision Onboarding pages.
const char kSupervisionServerUrlPrefix[] = const char kSupervisionServerUrlPrefix[] = "https://families.google.com/";
"https://families.google.com/kids/deviceonboarding";
// Relative URL for the onboarding start page. // Relative URL for the onboarding start page.
const char kOnboardingStartPageRelativeUrl[] = "/kids/deviceonboarding/start"; const char kOnboardingStartPageRelativeUrl[] = "/kids/deviceonboarding/start";
// Relative URL for the onboarding details page.
const char kOnboardingDetailsPageRelativeUrl[] =
"/kids/deviceonboarding/details";
// Relative URL for the onboarding "All set" page.
const char kOnboardingAllSetPageRelativeUrl[] = "/kids/deviceonboarding/allset";
// Name of the custom HTTP header returned by the Supervision server containing // Name of the custom HTTP header returned by the Supervision server containing
// a list of experiments that this version of the onboarding supports. // a list of experiments that this version of the onboarding supports.
const char kExperimentHeaderName[] = "supervision-experiments"; const char kExperimentHeaderName[] = "supervision-experiments";
......
...@@ -11,6 +11,8 @@ namespace supervision { ...@@ -11,6 +11,8 @@ namespace supervision {
extern const char kSupervisionServerUrlPrefix[]; extern const char kSupervisionServerUrlPrefix[];
extern const char kOnboardingStartPageRelativeUrl[]; extern const char kOnboardingStartPageRelativeUrl[];
extern const char kOnboardingDetailsPageRelativeUrl[];
extern const char kOnboardingAllSetPageRelativeUrl[];
extern const char kExperimentHeaderName[]; extern const char kExperimentHeaderName[];
extern const char kDeviceOnboardingExperimentName[]; extern const char kDeviceOnboardingExperimentName[];
......
...@@ -4,42 +4,23 @@ ...@@ -4,42 +4,23 @@
#include "chrome/browser/chromeos/supervision/onboarding_controller_impl.h" #include "chrome/browser/chromeos/supervision/onboarding_controller_impl.h"
#include "ash/public/cpp/ash_pref_names.h" #include <utility>
#include "base/bind.h"
#include "base/command_line.h"
#include "base/logging.h" #include "base/logging.h"
#include "base/strings/string_util.h" #include "chrome/browser/chromeos/supervision/kiosk_next_flow_observer.h"
#include "chrome/browser/chromeos/supervision/onboarding_constants.h" #include "chrome/browser/chromeos/supervision/onboarding_flow_model.h"
#include "chrome/browser/profiles/profile_manager.h" #include "chrome/browser/chromeos/supervision/onboarding_logger.h"
#include "chrome/browser/signin/identity_manager_factory.h" #include "chrome/browser/chromeos/supervision/onboarding_presenter.h"
#include "chromeos/constants/chromeos_features.h"
#include "chromeos/constants/chromeos_switches.h"
#include "components/prefs/pref_service.h"
#include "services/identity/public/cpp/access_token_fetcher.h"
#include "url/gurl.h"
namespace chromeos { namespace chromeos {
namespace supervision { namespace supervision {
namespace {
// OAuth scope necessary to access the Supervision server.
const char kSupervisionScope[] =
"https://www.googleapis.com/auth/kid.family.readonly";
GURL SupervisionServerBaseUrl() {
GURL command_line_prefix(
base::CommandLine::ForCurrentProcess()->GetSwitchValueASCII(
switches::kSupervisionOnboardingUrlPrefix));
if (command_line_prefix.is_valid())
return command_line_prefix;
return GURL(kSupervisionServerUrlPrefix);
}
} // namespace
OnboardingControllerImpl::OnboardingControllerImpl(Profile* profile) OnboardingControllerImpl::OnboardingControllerImpl(Profile* profile)
: profile_(profile) { : flow_model_(std::make_unique<OnboardingFlowModel>(profile)),
presenter_(std::make_unique<OnboardingPresenter>(flow_model_.get())),
logger_(std::make_unique<OnboardingLogger>(flow_model_.get())),
kiosk_next_observer_(
std::make_unique<KioskNextFlowObserver>(profile, flow_model_.get())) {
DCHECK(profile); DCHECK(profile);
} }
...@@ -52,97 +33,11 @@ void OnboardingControllerImpl::BindRequest( ...@@ -52,97 +33,11 @@ void OnboardingControllerImpl::BindRequest(
void OnboardingControllerImpl::BindWebviewHost( void OnboardingControllerImpl::BindWebviewHost(
mojom::OnboardingWebviewHostPtr webview_host) { mojom::OnboardingWebviewHostPtr webview_host) {
webview_host_ = std::move(webview_host); flow_model_->StartWithWebviewHost(std::move(webview_host));
auto presentation = mojom::OnboardingPresentation::New();
presentation->state = mojom::OnboardingPresentationState::kLoading;
webview_host_->SetPresentation(std::move(presentation));
identity::IdentityManager* identity_manager =
IdentityManagerFactory::GetForProfile(profile_);
std::string account_id = identity_manager->GetPrimaryAccountId();
OAuth2TokenService::ScopeSet scopes{kSupervisionScope};
// base::Unretained is safe here since |access_token_fetcher_| is owned by
// |this|.
access_token_fetcher_ = identity_manager->CreateAccessTokenFetcherForAccount(
account_id, "supervision_onboarding_controller", scopes,
base::BindOnce(&OnboardingControllerImpl::AccessTokenCallback,
base::Unretained(this)),
identity::AccessTokenFetcher::Mode::kImmediate);
} }
void OnboardingControllerImpl::HandleAction(mojom::OnboardingAction action) { void OnboardingControllerImpl::HandleAction(mojom::OnboardingAction action) {
DCHECK(webview_host_); flow_model_->HandleAction(action);
switch (action) {
// TODO(958985): Implement the full flow state machine.
case mojom::OnboardingAction::kSkipFlow:
case mojom::OnboardingAction::kShowNextPage:
case mojom::OnboardingAction::kShowPreviousPage:
webview_host_->ExitFlow();
}
}
void OnboardingControllerImpl::AccessTokenCallback(
GoogleServiceAuthError error,
identity::AccessTokenInfo access_token_info) {
DCHECK(webview_host_);
if (error.state() != GoogleServiceAuthError::NONE) {
webview_host_->ExitFlow();
return;
}
mojom::OnboardingPage page;
page.access_token = access_token_info.token;
page.custom_header_name = kExperimentHeaderName;
page.url_filter_pattern = SupervisionServerBaseUrl().Resolve("/*").spec();
page.url =
SupervisionServerBaseUrl().Resolve(kOnboardingStartPageRelativeUrl);
webview_host_->LoadPage(
page.Clone(), base::BindOnce(&OnboardingControllerImpl::LoadPageCallback,
base::Unretained(this)));
}
void OnboardingControllerImpl::LoadPageCallback(
mojom::OnboardingLoadPageResultPtr result) {
DCHECK(webview_host_);
// TODO(crbug.com/958995): Log the load page callback results to UMA. We want
// to see how many users get errors, have missing header values or actually
// end up seeing the page.
if (result->net_error != net::Error::OK) {
// TODO(crbug.com/958995): Fail here more gracefully. We should provide a
// way to retry the fetch if the error is recoverable.
LOG(ERROR) << "Supervision Onboarding webview failed to load with error: "
<< net::ErrorToString(result->net_error);
webview_host_->ExitFlow();
return;
}
if (!result->custom_header_value.has_value() ||
!base::EqualsCaseInsensitiveASCII(result->custom_header_value.value(),
kDeviceOnboardingExperimentName)) {
webview_host_->ExitFlow();
return;
}
profile_->GetPrefs()->SetBoolean(ash::prefs::kKioskNextShellEligible, true);
if (!base::FeatureList::IsEnabled(features::kSupervisionOnboardingScreens)) {
webview_host_->ExitFlow();
return;
}
auto presentation = mojom::OnboardingPresentation::New();
presentation->state = mojom::OnboardingPresentationState::kReady;
presentation->can_show_next_page = true;
presentation->can_skip_flow = true;
webview_host_->SetPresentation(std::move(presentation));
} }
} // namespace supervision } // namespace supervision
......
...@@ -6,24 +6,21 @@ ...@@ -6,24 +6,21 @@
#define CHROME_BROWSER_CHROMEOS_SUPERVISION_ONBOARDING_CONTROLLER_IMPL_H_ #define CHROME_BROWSER_CHROMEOS_SUPERVISION_ONBOARDING_CONTROLLER_IMPL_H_
#include <memory> #include <memory>
#include <string>
#include "base/macros.h" #include "base/macros.h"
#include "base/optional.h"
#include "chrome/browser/chromeos/supervision/mojom/onboarding_controller.mojom.h" #include "chrome/browser/chromeos/supervision/mojom/onboarding_controller.mojom.h"
#include "mojo/public/cpp/bindings/binding_set.h" #include "mojo/public/cpp/bindings/binding_set.h"
#include "services/identity/public/cpp/identity_manager.h"
#include "url/gurl.h"
class Profile; class Profile;
namespace identity {
class AccessTokenFetcher;
}
namespace chromeos { namespace chromeos {
namespace supervision { namespace supervision {
class OnboardingFlowModel;
class OnboardingPresenter;
class OnboardingLogger;
class KioskNextFlowObserver;
class OnboardingControllerImpl : public mojom::OnboardingController { class OnboardingControllerImpl : public mojom::OnboardingController {
public: public:
explicit OnboardingControllerImpl(Profile* profile); explicit OnboardingControllerImpl(Profile* profile);
...@@ -36,17 +33,12 @@ class OnboardingControllerImpl : public mojom::OnboardingController { ...@@ -36,17 +33,12 @@ class OnboardingControllerImpl : public mojom::OnboardingController {
void BindWebviewHost(mojom::OnboardingWebviewHostPtr webview_host) override; void BindWebviewHost(mojom::OnboardingWebviewHostPtr webview_host) override;
void HandleAction(mojom::OnboardingAction action) override; void HandleAction(mojom::OnboardingAction action) override;
// Callback to get the access token from the IdentityManager.
void AccessTokenCallback(GoogleServiceAuthError error,
identity::AccessTokenInfo access_token_info);
// Callback to OnboardingWebviewHost::LoadPage.
void LoadPageCallback(mojom::OnboardingLoadPageResultPtr result);
Profile* profile_;
mojom::OnboardingWebviewHostPtr webview_host_;
mojo::BindingSet<mojom::OnboardingController> bindings_; mojo::BindingSet<mojom::OnboardingController> bindings_;
std::unique_ptr<identity::AccessTokenFetcher> access_token_fetcher_;
std::unique_ptr<OnboardingFlowModel> flow_model_;
std::unique_ptr<OnboardingPresenter> presenter_;
std::unique_ptr<OnboardingLogger> logger_;
std::unique_ptr<KioskNextFlowObserver> kiosk_next_observer_;
DISALLOW_COPY_AND_ASSIGN(OnboardingControllerImpl); DISALLOW_COPY_AND_ASSIGN(OnboardingControllerImpl);
}; };
......
// Copyright 2019 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/chromeos/supervision/onboarding_flow_model.h"
#include <utility>
#include "base/bind.h"
#include "base/command_line.h"
#include "base/logging.h"
#include "base/strings/string_util.h"
#include "chrome/browser/chromeos/supervision/onboarding_constants.h"
#include "chrome/browser/signin/identity_manager_factory.h"
#include "chromeos/constants/chromeos_switches.h"
#include "services/identity/public/cpp/primary_account_access_token_fetcher.h"
#include "url/gurl.h"
namespace chromeos {
namespace supervision {
namespace {
// OAuth scope necessary to access the Supervision server.
const char kSupervisionScope[] =
"https://www.googleapis.com/auth/kid.family.readonly";
GURL SupervisionServerBaseUrl() {
GURL command_line_prefix(
base::CommandLine::ForCurrentProcess()->GetSwitchValueASCII(
switches::kSupervisionOnboardingUrlPrefix));
if (command_line_prefix.is_valid())
return command_line_prefix;
if (!command_line_prefix.is_empty())
DLOG(ERROR) << "Supervision server base URL prefix is invalid: "
<< command_line_prefix.possibly_invalid_spec();
return GURL(kSupervisionServerUrlPrefix);
}
} // namespace
OnboardingFlowModel::OnboardingFlowModel(Profile* profile)
: profile_(profile) {}
OnboardingFlowModel::~OnboardingFlowModel() = default;
void OnboardingFlowModel::AddObserver(Observer* observer) {
observer_list_.AddObserver(observer);
}
void OnboardingFlowModel::RemoveObserver(Observer* observer) {
observer_list_.RemoveObserver(observer);
}
void OnboardingFlowModel::StartWithWebviewHost(
mojom::OnboardingWebviewHostPtr webview_host) {
webview_host_ = std::move(webview_host);
LoadStep(Step::kStart);
}
void OnboardingFlowModel::HandleAction(mojom::OnboardingAction action) {
switch (action) {
case mojom::OnboardingAction::kShowNextPage:
ShowNextPage();
return;
case mojom::OnboardingAction::kShowPreviousPage:
ShowPreviousPage();
return;
case mojom::OnboardingAction::kSkipFlow:
SkipFlow();
return;
}
}
void OnboardingFlowModel::ExitFlow(ExitReason reason) {
DCHECK(webview_host_);
for (auto& observer : observer_list_) {
observer.WillExitFlow(current_step_, reason);
}
webview_host_->ExitFlow();
webview_host_ = nullptr;
}
mojom::OnboardingWebviewHost& OnboardingFlowModel::GetWebviewHost() {
DCHECK(webview_host_);
return *webview_host_;
}
//------------------------------------------------------------------------------
// Private methods
mojom::OnboardingPagePtr OnboardingFlowModel::MakePage(
Step step,
const std::string& access_token) {
auto page = mojom::OnboardingPage::New();
page->access_token = access_token;
page->allowed_urls_prefix = SupervisionServerBaseUrl().spec();
std::string relative_page_url;
switch (step) {
case Step::kStart:
page->custom_header_name = kExperimentHeaderName;
relative_page_url = kOnboardingStartPageRelativeUrl;
break;
case Step::kDetails:
relative_page_url = kOnboardingDetailsPageRelativeUrl;
break;
case Step::kAllSet:
relative_page_url = kOnboardingAllSetPageRelativeUrl;
break;
}
page->url = SupervisionServerBaseUrl().Resolve(relative_page_url);
return page;
}
void OnboardingFlowModel::ShowNextPage() {
switch (current_step_) {
case Step::kStart:
LoadStep(Step::kDetails);
return;
case Step::kDetails:
LoadStep(Step::kAllSet);
return;
case Step::kAllSet:
ExitFlow(ExitReason::kUserReachedEnd);
return;
}
}
void OnboardingFlowModel::ShowPreviousPage() {
switch (current_step_) {
case Step::kStart:
NOTREACHED();
return;
case Step::kDetails:
LoadStep(Step::kStart);
return;
case Step::kAllSet:
LoadStep(Step::kDetails);
return;
}
}
void OnboardingFlowModel::SkipFlow() {
switch (current_step_) {
case Step::kStart:
ExitFlow(ExitReason::kFlowSkipped);
return;
case Step::kDetails:
case Step::kAllSet:
NOTREACHED();
return;
}
}
void OnboardingFlowModel::LoadStep(Step step) {
current_step_ = step;
for (auto& observer : observer_list_) {
observer.StepStartedLoading(current_step_);
}
OAuth2TokenService::ScopeSet scopes{kSupervisionScope};
// base::Unretained is safe here since |access_token_fetcher_| is owned by
// |this|.
access_token_fetcher_ =
std::make_unique<identity::PrimaryAccountAccessTokenFetcher>(
"supervision_onboarding_flow",
IdentityManagerFactory::GetForProfile(profile_), scopes,
base::BindOnce(&OnboardingFlowModel::AccessTokenCallback,
base::Unretained(this)),
identity::PrimaryAccountAccessTokenFetcher::Mode::kImmediate);
}
void OnboardingFlowModel::AccessTokenCallback(
GoogleServiceAuthError error,
identity::AccessTokenInfo access_token_info) {
DCHECK(webview_host_);
if (error.state() != GoogleServiceAuthError::NONE) {
ExitFlow(ExitReason::kAuthError);
return;
}
webview_host_->LoadPage(MakePage(current_step_, access_token_info.token),
base::BindOnce(&OnboardingFlowModel::LoadPageCallback,
base::Unretained(this)));
}
void OnboardingFlowModel::LoadPageCallback(
mojom::OnboardingLoadPageResultPtr result) {
DCHECK(webview_host_);
if (result->net_error != net::Error::OK) {
// TODO(crbug.com/958995): Fail here more gracefully. We should provide a
// way to retry the fetch if the error is recoverable.
ExitFlow(ExitReason::kWebviewNetworkError);
return;
}
bool has_experiment =
result->custom_header_value.has_value() &&
base::EqualsCaseInsensitiveASCII(result->custom_header_value.value(),
kDeviceOnboardingExperimentName);
// Only the start step requires the experiment.
if (current_step_ == Step::kStart && !has_experiment) {
ExitFlow(ExitReason::kUserNotEligible);
return;
}
for (auto& observer : observer_list_) {
observer.StepFinishedLoading(current_step_);
}
}
} // namespace supervision
} // namespace chromeos
// Copyright 2019 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_CHROMEOS_SUPERVISION_ONBOARDING_FLOW_MODEL_H_
#define CHROME_BROWSER_CHROMEOS_SUPERVISION_ONBOARDING_FLOW_MODEL_H_
#include <memory>
#include <string>
#include "base/macros.h"
#include "base/observer_list.h"
#include "chrome/browser/chromeos/supervision/mojom/onboarding_controller.mojom.h"
#include "services/identity/public/cpp/identity_manager.h"
class Profile;
namespace identity {
class PrimaryAccountAccessTokenFetcher;
}
namespace chromeos {
namespace supervision {
// Class that manages the onboarding flow state, handling user actions and
// loading new pages. It notifies its observers of flow changes.
class OnboardingFlowModel {
public:
explicit OnboardingFlowModel(Profile* profile);
~OnboardingFlowModel();
// Represents each onboarding flow step.
enum class Step {
// First page, informs the user about supervision features. Has a button to
// skip the whole flow.
kStart,
// Second page, shows additional details about supervision.
kDetails,
// Third and final page, presented when all previous steps have been
// successful.
kAllSet,
};
// Represents possible reasons for the flow to exit.
enum class ExitReason {
// The user navigated through the whole flow and exited successfully.
kUserReachedEnd,
// User chose to skip the flow during its introduction.
kFlowSkipped,
// We found an authentication error before we attempted to load the first
// onboarding page.
kAuthError,
// The webview that presents the pages found a network problem.
kWebviewNetworkError,
// The user is not eligible to see the flow.
kUserNotEligible,
// The onboarding flow screens should not be shown since their feature is
// disabled.
kFeatureDisabled,
};
class Observer : public base::CheckedObserver {
public:
// Step loading notifications. They are called before and after a step has
// successfully loaded, which includes fetching an access token and
// loading a new onboarding page. It is safe to call other flow model
// methods after receiving these notifications.
virtual void StepStartedLoading(Step step) {}
virtual void StepFinishedLoading(Step step) {}
// If we are exiting the flow for any reason, we first notify our observers
// through this method. Observers should *NOT* call other methods in the
// flow model while receiving this notification.
virtual void WillExitFlow(Step current_step, ExitReason reason) {}
};
void AddObserver(Observer* observer);
void RemoveObserver(Observer* observer);
void StartWithWebviewHost(mojom::OnboardingWebviewHostPtr webview_host);
void HandleAction(mojom::OnboardingAction action);
void ExitFlow(ExitReason reason);
mojom::OnboardingWebviewHost& GetWebviewHost();
private:
mojom::OnboardingPagePtr MakePage(Step step, const std::string& access_token);
void ShowNextPage();
void ShowPreviousPage();
void SkipFlow();
void LoadStep(Step step);
void AccessTokenCallback(GoogleServiceAuthError error,
identity::AccessTokenInfo access_token_info);
void LoadPageCallback(mojom::OnboardingLoadPageResultPtr result);
Profile* profile_;
mojom::OnboardingWebviewHostPtr webview_host_;
Step current_step_ = Step::kStart;
base::ObserverList<Observer> observer_list_;
std::unique_ptr<identity::PrimaryAccountAccessTokenFetcher>
access_token_fetcher_;
DISALLOW_COPY_AND_ASSIGN(OnboardingFlowModel);
};
} // namespace supervision
} // namespace chromeos
#endif // CHROME_BROWSER_CHROMEOS_SUPERVISION_ONBOARDING_FLOW_MODEL_H_
// Copyright 2019 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/chromeos/supervision/onboarding_logger.h"
#include "base/logging.h"
namespace chromeos {
namespace supervision {
namespace {
const char* StepDescription(OnboardingFlowModel::Step step) {
switch (step) {
case OnboardingFlowModel::Step::kStart:
return "Start";
case OnboardingFlowModel::Step::kDetails:
return "Details";
case OnboardingFlowModel::Step::kAllSet:
return "AllSet";
}
}
const char* ExitReasonDescription(OnboardingFlowModel::ExitReason reason) {
switch (reason) {
case OnboardingFlowModel::ExitReason::kUserReachedEnd:
return "User reached the end of the flow successfuly.";
case OnboardingFlowModel::ExitReason::kFlowSkipped:
return "User chose to skip the flow.";
case OnboardingFlowModel::ExitReason::kAuthError:
return "Found an error getting an access token.";
case OnboardingFlowModel::ExitReason::kWebviewNetworkError:
return "Webview found a network error.";
case OnboardingFlowModel::ExitReason::kUserNotEligible:
return "User is not eligible to go through the flow.";
case OnboardingFlowModel::ExitReason::kFeatureDisabled:
return "Feature is disabled, we can't present flow pages.";
}
}
bool ExitedDueToError(OnboardingFlowModel::ExitReason reason) {
switch (reason) {
case OnboardingFlowModel::ExitReason::kUserReachedEnd:
case OnboardingFlowModel::ExitReason::kFlowSkipped:
case OnboardingFlowModel::ExitReason::kUserNotEligible:
case OnboardingFlowModel::ExitReason::kFeatureDisabled:
return false;
case OnboardingFlowModel::ExitReason::kAuthError:
case OnboardingFlowModel::ExitReason::kWebviewNetworkError:
return true;
}
}
} // namespace
OnboardingLogger::OnboardingLogger(OnboardingFlowModel* flow_model)
: flow_model_(flow_model) {
flow_model_->AddObserver(this);
}
OnboardingLogger::~OnboardingLogger() {
flow_model_->RemoveObserver(this);
}
void OnboardingLogger::StepStartedLoading(OnboardingFlowModel::Step step) {
DVLOG(1) << "Supervision Onboarding started loading step: "
<< StepDescription(step);
}
void OnboardingLogger::StepFinishedLoading(OnboardingFlowModel::Step step) {
DVLOG(1) << "Supervision Onboarding successfuly loaded step: "
<< StepDescription(step);
}
void OnboardingLogger::WillExitFlow(OnboardingFlowModel::Step step,
OnboardingFlowModel::ExitReason reason) {
if (ExitedDueToError(reason)) {
LOG(ERROR)
<< "Supervision Onboarding is exiting because it found an error: "
<< ExitReasonDescription(reason);
return;
}
DVLOG(1) << "Supervision Onboarding exiting. "
<< ExitReasonDescription(reason);
}
} // namespace supervision
} // namespace chromeos
// Copyright 2019 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_CHROMEOS_SUPERVISION_ONBOARDING_LOGGER_H_
#define CHROME_BROWSER_CHROMEOS_SUPERVISION_ONBOARDING_LOGGER_H_
#include "base/macros.h"
#include "chrome/browser/chromeos/supervision/mojom/onboarding_controller.mojom.h"
#include "chrome/browser/chromeos/supervision/onboarding_flow_model.h"
namespace chromeos {
namespace supervision {
// Records onboarding flow changes.
//
// TODO(crbug.com/958995): Update UMA metrics as well. We want to see how many
// users get errors, have missing header values or actually end up seeing the
// page.
class OnboardingLogger : public OnboardingFlowModel::Observer {
public:
explicit OnboardingLogger(OnboardingFlowModel* flow_model);
~OnboardingLogger() override;
private:
// OnboardingFlowModel::Observer:
void StepStartedLoading(OnboardingFlowModel::Step step) override;
void StepFinishedLoading(OnboardingFlowModel::Step step) override;
void WillExitFlow(OnboardingFlowModel::Step step,
OnboardingFlowModel::ExitReason reason) override;
OnboardingFlowModel* flow_model_;
DISALLOW_COPY_AND_ASSIGN(OnboardingLogger);
};
} // namespace supervision
} // namespace chromeos
#endif // CHROME_BROWSER_CHROMEOS_SUPERVISION_ONBOARDING_LOGGER_H_
// Copyright 2019 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/chromeos/supervision/onboarding_presenter.h"
#include <utility>
#include "chromeos/constants/chromeos_features.h"
namespace chromeos {
namespace supervision {
OnboardingPresenter::OnboardingPresenter(OnboardingFlowModel* flow_model)
: flow_model_(flow_model) {
flow_model_->AddObserver(this);
}
OnboardingPresenter::~OnboardingPresenter() {
flow_model_->RemoveObserver(this);
}
void OnboardingPresenter::StepStartedLoading(OnboardingFlowModel::Step step) {
auto presentation = mojom::OnboardingPresentation::New();
presentation->state = mojom::OnboardingPresentationState::kLoading;
flow_model_->GetWebviewHost().SetPresentation(std::move(presentation));
}
void OnboardingPresenter::StepFinishedLoading(OnboardingFlowModel::Step step) {
if (!base::FeatureList::IsEnabled(features::kSupervisionOnboardingScreens)) {
flow_model_->ExitFlow(OnboardingFlowModel::ExitReason::kFeatureDisabled);
return;
}
auto presentation = mojom::OnboardingPresentation::New();
presentation->state = mojom::OnboardingPresentationState::kReady;
presentation->can_show_next_page = true;
switch (step) {
case OnboardingFlowModel::Step::kStart:
presentation->can_skip_flow = true;
break;
case OnboardingFlowModel::Step::kDetails:
case OnboardingFlowModel::Step::kAllSet:
presentation->can_show_previous_page = true;
break;
}
flow_model_->GetWebviewHost().SetPresentation(std::move(presentation));
}
} // namespace supervision
} // namespace chromeos
// Copyright 2019 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_CHROMEOS_SUPERVISION_ONBOARDING_PRESENTER_H_
#define CHROME_BROWSER_CHROMEOS_SUPERVISION_ONBOARDING_PRESENTER_H_
#include "base/macros.h"
#include "chrome/browser/chromeos/supervision/mojom/onboarding_controller.mojom.h"
#include "chrome/browser/chromeos/supervision/onboarding_flow_model.h"
namespace chromeos {
namespace supervision {
// Sets the onboarding presentation based on observed flow changes.
class OnboardingPresenter : public OnboardingFlowModel::Observer {
public:
explicit OnboardingPresenter(OnboardingFlowModel* flow_model);
~OnboardingPresenter() override;
private:
// OnboardingFlowModel::Observer:
void StepStartedLoading(OnboardingFlowModel::Step step) override;
void StepFinishedLoading(OnboardingFlowModel::Step step) override;
OnboardingFlowModel* flow_model_;
DISALLOW_COPY_AND_ASSIGN(OnboardingPresenter);
};
} // namespace supervision
} // namespace chromeos
#endif // CHROME_BROWSER_CHROMEOS_SUPERVISION_ONBOARDING_PRESENTER_H_
...@@ -29,12 +29,24 @@ ...@@ -29,12 +29,24 @@
*/ */
this.pendingLoadPageCallback_ = null; this.pendingLoadPageCallback_ = null;
/** @private {?chromeos.supervision.mojom.OnboardingLoadPageResult} */ /** @private {!chromeos.supervision.mojom.OnboardingLoadPageResult} */
this.pendingLoadPageResult_ = null; this.pendingLoadPageResult_ = {netError: 0};
// We listen to all requests made to fetch the main frame, but note that
// we end up blocking requests to URLs that don't start with the expected
// server prefix (see onBeforeSendHeaders_).
// This is done because we will only know the prefix when we start to
// load the page, but we add listeners at mojo setup time.
const requestFilter = {urls: ['<all_urls>'], types: ['main_frame']};
this.webviewListener_ = this.webviewFinishedLoading_.bind(this); this.webviewListener_ = this.webviewFinishedLoading_.bind(this);
this.webviewHeaderListener_ = this.onHeadersReceived_.bind(this); this.webview_.request.onBeforeSendHeaders.addListener(
this.webviewAuthListener_ = this.authorizeRequest_.bind(this); this.onBeforeSendHeaders_.bind(this), requestFilter,
['blocking', 'requestHeaders']);
this.webview_.request.onHeadersReceived.addListener(
this.onHeadersReceived_.bind(this), requestFilter,
['responseHeaders', 'extraHeaders']);
} }
/** /**
...@@ -55,15 +67,6 @@ ...@@ -55,15 +67,6 @@
this.pendingLoadPageCallback_ = null; this.pendingLoadPageCallback_ = null;
this.pendingLoadPageResult_ = {netError: 0}; this.pendingLoadPageResult_ = {netError: 0};
this.webview_.request.onBeforeSendHeaders.addListener(
this.webviewAuthListener_,
{urls: [page.urlFilterPattern], types: ['main_frame']},
['blocking', 'requestHeaders']);
this.webview_.request.onHeadersReceived.addListener(
this.webviewHeaderListener_,
{urls: [page.urlFilterPattern], types: ['main_frame']},
['responseHeaders', 'extraHeaders']);
this.webview_.addEventListener('loadstop', this.webviewListener_); this.webview_.addEventListener('loadstop', this.webviewListener_);
this.webview_.addEventListener('loadabort', this.webviewListener_); this.webview_.addEventListener('loadabort', this.webviewListener_);
this.webview_.src = page.url.url; this.webview_.src = page.url.url;
...@@ -74,8 +77,29 @@ ...@@ -74,8 +77,29 @@
} }
/** /**
* @param {!Object<{responseHeaders: * Injects headers into the passed request.
* !Array<{name: string, value:string}>}>} responseEvent *
* @param {!Object} requestEvent
* @return {!BlockingResponse} Modified headers.
* @private
*/
onBeforeSendHeaders_(requestEvent) {
if (!this.page_ ||
!requestEvent.url.startsWith(this.page_.allowedUrlsPrefix)) {
return {cancel: true};
}
requestEvent.requestHeaders.push(
{name: 'Authorization', value: 'Bearer ' + this.page_.accessToken});
return /** @type {!BlockingResponse} */ ({
requestHeaders: requestEvent.requestHeaders,
});
}
/**
* @param {!Object<{responseHeaders: !chrome.webRequest.HttpHeaders}>}
* responseEvent
* @return {!BlockingResponse} * @return {!BlockingResponse}
* @private * @private
*/ */
...@@ -94,31 +118,12 @@ ...@@ -94,31 +118,12 @@
return {}; return {};
} }
/**
* Injects headers into the passed request.
*
* @param {!Object} requestEvent
* @return {!BlockingResponse} Modified headers.
* @private
*/
authorizeRequest_(requestEvent) {
requestEvent.requestHeaders.push(
{name: 'Authorization', value: 'Bearer ' + this.page_.accessToken});
return /** @type {!BlockingResponse} */ ({
requestHeaders: requestEvent.requestHeaders,
});
}
/** /**
* Called when the webview sends a loadstop or loadabort event. * Called when the webview sends a loadstop or loadabort event.
* @private {!Event} e * @param {!Event} e
* @private
*/ */
webviewFinishedLoading_(e) { webviewFinishedLoading_(e) {
this.webview_.request.onBeforeSendHeaders.removeListener(
this.webviewAuthListener_);
this.webview_.request.onHeadersReceived.removeListener(
this.webviewHeaderListener_);
this.webview_.removeEventListener('loadstop', this.webviewListener_); this.webview_.removeEventListener('loadstop', this.webviewListener_);
this.webview_.removeEventListener('loadabort', this.webviewListener_); this.webview_.removeEventListener('loadabort', this.webviewListener_);
......
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