| // 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/profiles/dice_web_signin_interception_bubble_view.h" |
| |
| #include <memory> |
| |
| #include "base/bind.h" |
| #include "base/callback_helpers.h" |
| #include "base/check.h" |
| #include "base/feature_list.h" |
| #include "base/memory/ptr_util.h" |
| #include "base/metrics/histogram_functions.h" |
| #include "chrome/browser/profiles/profile.h" |
| #include "chrome/browser/signin/identity_manager_factory.h" |
| #include "chrome/browser/ui/browser.h" |
| #include "chrome/browser/ui/signin/dice_web_signin_interceptor_delegate.h" |
| #include "chrome/browser/ui/ui_features.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/profiles/avatar_toolbar_button.h" |
| #include "chrome/browser/ui/webui/signin/dice_web_signin_intercept_ui.h" |
| #include "chrome/common/webui_url_constants.h" |
| #include "components/signin/public/identity_manager/consent_level.h" |
| #include "components/signin/public/identity_manager/identity_manager.h" |
| #include "content/public/browser/web_contents.h" |
| #include "content/public/browser/web_ui.h" |
| #include "ui/views/bubble/bubble_border.h" |
| #include "ui/views/controls/webview/webview.h" |
| #include "ui/views/layout/fill_layout.h" |
| #include "ui/views/widget/widget.h" |
| |
| namespace { |
| constexpr int kInterceptionBubbleWithGuestHeight = 362; |
| constexpr int kInterceptionBubbleWithoutGuestHeight = 326; |
| constexpr int kInterceptionBubbleWidth = 290; |
| |
| } // namespace |
| |
| DiceWebSigninInterceptionBubbleView::~DiceWebSigninInterceptionBubbleView() { |
| // Cancel if the bubble is destroyed without user interaction. |
| if (callback_) { |
| RecordInterceptionResult(bubble_parameters_, profile_, |
| SigninInterceptionResult::kIgnored); |
| // The callback may synchronously delete a handle, which would attempt to |
| // close this bubble while it is being destroyed. Invalidate the handles now |
| // to prevent this. |
| weak_factory_.InvalidateWeakPtrs(); |
| std::move(callback_).Run(SigninInterceptionResult::kIgnored); |
| } |
| } |
| |
| // static |
| std::unique_ptr<ScopedDiceWebSigninInterceptionBubbleHandle> |
| DiceWebSigninInterceptionBubbleView::CreateBubble( |
| Profile* profile, |
| views::View* anchor_view, |
| const DiceWebSigninInterceptor::Delegate::BubbleParameters& |
| bubble_parameters, |
| base::OnceCallback<void(SigninInterceptionResult)> callback) { |
| auto interception_bubble = |
| base::WrapUnique(new DiceWebSigninInterceptionBubbleView( |
| profile, anchor_view, bubble_parameters, std::move(callback))); |
| std::unique_ptr<ScopedDiceWebSigninInterceptionBubbleHandle> handle = |
| interception_bubble->GetHandle(); |
| // The widget is owned by the views system. |
| views::Widget* widget = views::BubbleDialogDelegateView::CreateBubble( |
| std::move(interception_bubble)); |
| // TODO(droger): Delay showing the bubble until the web view is loaded. |
| widget->Show(); |
| return handle; |
| } |
| |
| DiceWebSigninInterceptionBubbleView::ScopedHandle::~ScopedHandle() { |
| if (!bubble_) |
| return; // The bubble was already closed, do nothing. |
| views::Widget* widget = bubble_->GetWidget(); |
| if (!widget) |
| return; |
| widget->CloseWithReason( |
| bubble_->HasAccepted() ? views::Widget::ClosedReason::kAcceptButtonClicked |
| : views::Widget::ClosedReason::kUnspecified); |
| } |
| |
| DiceWebSigninInterceptionBubbleView::ScopedHandle::ScopedHandle( |
| base::WeakPtr<DiceWebSigninInterceptionBubbleView> bubble) |
| : bubble_(std::move(bubble)) { |
| DCHECK(bubble_); |
| } |
| |
| // static |
| void DiceWebSigninInterceptionBubbleView::RecordInterceptionResult( |
| const DiceWebSigninInterceptor::Delegate::BubbleParameters& |
| bubble_parameters, |
| Profile* profile, |
| SigninInterceptionResult result) { |
| std::string histogram_base_name = "Signin.InterceptResult"; |
| switch (bubble_parameters.interception_type) { |
| case DiceWebSigninInterceptor::SigninInterceptionType::kEnterprise: |
| histogram_base_name.append(".Enterprise"); |
| break; |
| case DiceWebSigninInterceptor::SigninInterceptionType::kMultiUser: |
| histogram_base_name.append(".MultiUser"); |
| break; |
| case DiceWebSigninInterceptor::SigninInterceptionType::kProfileSwitch: |
| histogram_base_name.append(".Switch"); |
| break; |
| } |
| |
| // Record aggregated histogram for each interception type. |
| base::UmaHistogramEnumeration(histogram_base_name, result); |
| // Record histogram sliced by Sync status. |
| signin::IdentityManager* identity_manager = |
| IdentityManagerFactory::GetForProfile(profile); |
| std::string sync_suffix = |
| identity_manager->HasPrimaryAccount(signin::ConsentLevel::kSync) |
| ? ".Sync" |
| : ".NoSync"; |
| base::UmaHistogramEnumeration(histogram_base_name + sync_suffix, result); |
| // For Enterprise, slice per enterprise status for each account. |
| if (bubble_parameters.interception_type == |
| DiceWebSigninInterceptor::SigninInterceptionType::kEnterprise) { |
| if (bubble_parameters.intercepted_account.IsManaged()) { |
| std::string histogram_name = histogram_base_name + ".NewIsEnterprise"; |
| base::UmaHistogramEnumeration(histogram_name, result); |
| } |
| if (bubble_parameters.primary_account.IsManaged()) { |
| std::string histogram_name = histogram_base_name + ".PrimaryIsEnterprise"; |
| base::UmaHistogramEnumeration(histogram_name, result); |
| } |
| } |
| } |
| |
| bool DiceWebSigninInterceptionBubbleView::HasAccepted() const { |
| return has_accepted_; |
| } |
| |
| DiceWebSigninInterceptionBubbleView::DiceWebSigninInterceptionBubbleView( |
| Profile* profile, |
| views::View* anchor_view, |
| const DiceWebSigninInterceptor::Delegate::BubbleParameters& |
| bubble_parameters, |
| base::OnceCallback<void(SigninInterceptionResult)> callback) |
| : views::BubbleDialogDelegateView(anchor_view, |
| views::BubbleBorder::TOP_RIGHT), |
| profile_(profile), |
| bubble_parameters_(bubble_parameters), |
| callback_(std::move(callback)) { |
| DCHECK(profile_); |
| DCHECK(callback_); |
| set_close_on_deactivate(false); |
| |
| // Create the web view in the native bubble. |
| std::unique_ptr<views::WebView> web_view = |
| std::make_unique<views::WebView>(profile); |
| web_view->LoadInitialURL(GURL(chrome::kChromeUIDiceWebSigninInterceptURL)); |
| web_view->SetPreferredSize( |
| gfx::Size(kInterceptionBubbleWidth, |
| DiceWebSigninInterceptUI::ShouldShowGuestOption() |
| ? kInterceptionBubbleWithGuestHeight |
| : kInterceptionBubbleWithoutGuestHeight)); |
| DiceWebSigninInterceptUI* web_ui = web_view->GetWebContents() |
| ->GetWebUI() |
| ->GetController() |
| ->GetAs<DiceWebSigninInterceptUI>(); |
| SetInitiallyFocusedView(web_view.get()); |
| DCHECK(web_ui); |
| // Unretained is fine because this outlives the inner web UI. |
| web_ui->Initialize( |
| bubble_parameters, |
| base::BindOnce(&DiceWebSigninInterceptionBubbleView::OnWebUIUserChoice, |
| base::Unretained(this))); |
| AddChildView(std::move(web_view)); |
| |
| set_margins(gfx::Insets()); |
| SetButtons(ui::DIALOG_BUTTON_NONE); |
| SetLayoutManager(std::make_unique<views::FillLayout>()); |
| } |
| |
| std::unique_ptr<ScopedDiceWebSigninInterceptionBubbleHandle> |
| DiceWebSigninInterceptionBubbleView::GetHandle() const { |
| return std::make_unique<ScopedHandle>(weak_factory_.GetWeakPtr()); |
| } |
| |
| void DiceWebSigninInterceptionBubbleView::OnWebUIUserChoice( |
| SigninInterceptionUserChoice user_choice) { |
| SigninInterceptionResult result; |
| switch (user_choice) { |
| case SigninInterceptionUserChoice::kAccept: |
| result = SigninInterceptionResult::kAccepted; |
| has_accepted_ = true; |
| break; |
| case SigninInterceptionUserChoice::kDecline: |
| result = SigninInterceptionResult::kDeclined; |
| has_accepted_ = false; |
| break; |
| case SigninInterceptionUserChoice::kGuest: |
| result = SigninInterceptionResult::kAcceptedWithGuest; |
| has_accepted_ = true; |
| } |
| |
| RecordInterceptionResult(bubble_parameters_, profile_, result); |
| std::move(callback_).Run(result); |
| if (!has_accepted_) { |
| // Only close the dialog when the user declined. If the user accepted the |
| // dialog displays a spinner until the handle is released. |
| GetWidget()->CloseWithReason( |
| views::Widget::ClosedReason::kCancelButtonClicked); |
| } |
| } |
| |
| // DiceWebSigninInterceptorDelegate -------------------------------------------- |
| |
| std::unique_ptr<ScopedDiceWebSigninInterceptionBubbleHandle> |
| DiceWebSigninInterceptorDelegate::ShowSigninInterceptionBubbleInternal( |
| Browser* browser, |
| const DiceWebSigninInterceptor::Delegate::BubbleParameters& |
| bubble_parameters, |
| base::OnceCallback<void(SigninInterceptionResult)> callback) { |
| DCHECK(browser); |
| |
| views::View* anchor_view = BrowserView::GetBrowserViewForBrowser(browser) |
| ->toolbar_button_provider() |
| ->GetAvatarToolbarButton(); |
| DCHECK(anchor_view); |
| return DiceWebSigninInterceptionBubbleView::CreateBubble( |
| browser->profile(), anchor_view, bubble_parameters, std::move(callback)); |
| } |