| // Copyright 2022 The Chromium Authors |
| // 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/permissions/chip/chip_controller.h" |
| |
| #include <memory> |
| #include <string> |
| #include <utility> |
| |
| #include "base/check.h" |
| #include "base/feature_list.h" |
| #include "base/functional/bind.h" |
| #include "base/functional/callback_forward.h" |
| #include "base/logging.h" |
| #include "base/memory/raw_ptr.h" |
| #include "base/metrics/histogram_functions.h" |
| #include "base/time/time.h" |
| #include "chrome/browser/ui/browser_finder.h" |
| #include "chrome/browser/ui/page_info/page_info_dialog.h" |
| #include "chrome/browser/ui/views/content_setting_bubble_contents.h" |
| #include "chrome/browser/ui/views/location_bar/location_bar_view.h" |
| #include "chrome/browser/ui/views/omnibox/omnibox_view_views.h" |
| #include "chrome/browser/ui/views/page_info/page_info_bubble_specification.h" |
| #include "chrome/browser/ui/views/page_info/page_info_bubble_view.h" |
| #include "chrome/browser/ui/views/permissions/chip/permission_dashboard_controller.h" |
| #include "chrome/browser/ui/views/permissions/chip/permission_dashboard_view.h" |
| #include "chrome/browser/ui/views/permissions/chip/permission_prompt_chip_model.h" |
| #include "chrome/browser/ui/views/permissions/permission_prompt_bubble_view_factory.h" |
| #include "chrome/browser/ui/views/permissions/permission_prompt_style.h" |
| #include "chrome/common/webui_url_constants.h" |
| #include "chrome/grit/generated_resources.h" |
| #include "components/content_settings/core/common/features.h" |
| #include "components/permissions/features.h" |
| #include "components/permissions/permission_prompt.h" |
| #include "components/permissions/permission_request_manager.h" |
| #include "components/permissions/permission_util.h" |
| #include "content/public/browser/navigation_controller.h" |
| #include "content/public/browser/navigation_entry.h" |
| #include "content/public/browser/navigation_handle.h" |
| #include "content/public/browser/visibility.h" |
| #include "ui/base/ui_base_features.h" |
| #include "ui/gfx/paint_vector_icon.h" |
| #include "ui/views/accessibility/view_accessibility.h" |
| #include "ui/views/controls/button/button.h" |
| #include "ui/views/controls/button/button_controller.h" |
| #include "ui/views/widget/widget.h" |
| |
| namespace { |
| |
| constexpr auto kConfirmationDisplayDuration = base::Seconds(4); |
| |
| } // namespace |
| |
| class BubbleButtonController : public views::ButtonController { |
| public: |
| BubbleButtonController( |
| views::Button* button, |
| BubbleOwnerDelegate* bubble_owner, |
| std::unique_ptr<views::ButtonControllerDelegate> delegate) |
| : views::ButtonController(button, std::move(delegate)), |
| bubble_owner_(bubble_owner) {} |
| |
| // TODO(crbug.com/40205454): Add keyboard support. |
| void OnMouseEntered(const ui::MouseEvent& event) override { |
| if (bubble_owner_->IsBubbleShowing() || bubble_owner_->IsAnimating()) { |
| return; |
| } |
| bubble_owner_->RestartTimersOnMouseHover(); |
| } |
| |
| private: |
| raw_ptr<BubbleOwnerDelegate, DanglingUntriaged> bubble_owner_ = nullptr; |
| }; |
| |
| ChipController::ChipController( |
| LocationBarView* location_bar_view, |
| PermissionChipView* chip_view, |
| PermissionDashboardView* permission_dashboard_view, |
| PermissionDashboardController* permission_dashboard_controller) |
| : location_bar_view_(location_bar_view), |
| chip_(chip_view), |
| permission_dashboard_view_(permission_dashboard_view), |
| permission_dashboard_controller_(permission_dashboard_controller) { |
| chip_->SetVisible(false); |
| } |
| |
| ChipController::~ChipController() { |
| views::Widget* current = GetBubbleWidget(); |
| if (current) { |
| current->RemoveObserver(this); |
| current->Close(); |
| } |
| if (active_chip_permission_request_manager_.has_value()) { |
| active_chip_permission_request_manager_.value()->RemoveObserver(this); |
| } |
| observation_.Reset(); |
| } |
| |
| void ChipController::OnPermissionRequestManagerDestructed() { |
| ResetPermissionPromptChip(); |
| if (active_chip_permission_request_manager_.has_value()) { |
| active_chip_permission_request_manager_.value()->RemoveObserver(this); |
| active_chip_permission_request_manager_.reset(); |
| } |
| } |
| |
| void ChipController::OnNavigation( |
| content::NavigationHandle* navigation_handle) { |
| // TODO(crbug.com/40256881): Refactor this so that this observer method is |
| // only called when a non-same-document navigation starts in the primary main |
| // frame. |
| if (!navigation_handle->IsInPrimaryMainFrame() || |
| navigation_handle->IsSameDocument()) { |
| return; |
| } |
| |
| ResetPermissionPromptChip(); |
| } |
| |
| void ChipController::OnTabActiveChanged(bool is_active) { |
| if (!is_active) { |
| ResetPermissionPromptChip(); |
| } |
| } |
| |
| void ChipController::OnRequestsFinalized() { |
| // Due to a permissions requests queue reordering, currently active |
| // permission request may get finalized without a user deciding on a |
| // permission prompt. That means `OnRequestDecided` will not be executed. |
| ResetPermissionRequestChip(); |
| } |
| |
| void ChipController::OnPromptRemoved() { |
| ResetPermissionRequestChip(); |
| } |
| |
| void ChipController::OnRequestDecided( |
| permissions::PermissionAction permission_action) { |
| RemoveBubbleObserverAndResetTimersAndChipCallbacks(); |
| if (!GetLocationBarView()->IsDrawn() || |
| GetLocationBarView()->GetWidget()->GetTopLevelWidget()->IsFullscreen() || |
| permission_action == permissions::PermissionAction::IGNORED || |
| permission_action == permissions::PermissionAction::DISMISSED || |
| permission_action == permissions::PermissionAction::REVOKED || |
| // Do not show the confirmation chip for Camera and Mic because they will |
| // be displayed as LHS indicator. |
| (base::FeatureList::IsEnabled( |
| content_settings::features::kLeftHandSideActivityIndicators) && |
| (permission_prompt_model_->content_settings_type() == |
| ContentSettingsType::MEDIASTREAM_CAMERA || |
| permission_prompt_model_->content_settings_type() == |
| ContentSettingsType::MEDIASTREAM_MIC))) { |
| // Reset everything and hide chip if: |
| // - `LocationBarView` isn't visible |
| // - Permission request was ignored or dismissed as we do not confirm such |
| // actions. |
| // - LHS indicator is displayed. |
| ResetPermissionPromptChip(); |
| } else { |
| HandleConfirmation(permission_action); |
| } |
| } |
| |
| bool ChipController::IsBubbleShowing() { |
| return chip_ != nullptr && (GetBubbleWidget() != nullptr); |
| } |
| |
| bool ChipController::IsAnimating() const { |
| return chip_->is_animating(); |
| } |
| |
| void ChipController::RestartTimersOnMouseHover() { |
| ResetTimers(); |
| if (!permission_prompt_model_ || IsBubbleShowing() || IsAnimating()) { |
| return; |
| } |
| |
| if (is_confirmation_showing_) { |
| collapse_timer_.Start(FROM_HERE, kConfirmationDisplayDuration, this, |
| &ChipController::CollapseConfirmation); |
| } else if (chip_->is_fully_collapsed()) { |
| // Quiet chip can collapse from a verbose state to an icon state. After it |
| // is collapsed, it should be dismissed. |
| StartDismissTimer(); |
| } else { |
| StartCollapseTimer(); |
| } |
| } |
| |
| void ChipController::OnWidgetDestroying(views::Widget* widget) { |
| DCHECK_EQ(GetBubbleWidget(), widget); |
| ResetTimers(); |
| |
| disallowed_custom_cursors_scope_.RunAndReset(); |
| |
| if (!prompt_decision_.has_value()) { |
| observation_.Reset(); |
| widget->RemoveObserver(this); |
| bubble_tracker_.SetView(nullptr); |
| // This method will be called only if a user dismissed permission prompt |
| // popup bubble. In case the user made decision, prompt_decision_ will not |
| // be empty. |
| OnPromptBubbleDismissed(); |
| } |
| |
| permission_prompt_observers_.Notify(&Observer::OnPermissionPromptHidden); |
| } |
| |
| void ChipController::OnWidgetDestroyed(views::Widget* widget) { |
| widget->RemoveObserver(this); |
| bubble_tracker_.SetView(nullptr); |
| |
| if (!prompt_decision_.has_value()) { |
| return; |
| } |
| |
| permissions::PermissionAction action = prompt_decision_.value(); |
| prompt_decision_.reset(); |
| |
| if (!active_chip_permission_request_manager_.has_value() || |
| !active_chip_permission_request_manager_.value()->IsRequestInProgress()) { |
| return; |
| } |
| |
| switch (action) { |
| case permissions::PermissionAction::GRANTED: |
| active_chip_permission_request_manager_.value()->Accept(); |
| break; |
| case permissions::PermissionAction::GRANTED_ONCE: |
| active_chip_permission_request_manager_.value()->AcceptThisTime(); |
| break; |
| case permissions::PermissionAction::DENIED: |
| active_chip_permission_request_manager_.value()->Deny(); |
| break; |
| default: |
| NOTREACHED(); |
| } |
| } |
| |
| void ChipController::OnWidgetActivationChanged(views::Widget* widget, |
| bool active) { |
| // This logic prevents clickjacking. See https://crbug.com/1160485 |
| auto* prompt_bubble_widget = GetBubbleWidget(); |
| if (active && !parent_was_visible_when_activation_changed_) { |
| // If the widget is active and the primary window wasn't active the last |
| // time activation changed, we know that the window just came to the |
| // foreground and trigger input protection. |
| GetPromptBubbleView()->AsDialogDelegate()->TriggerInputProtection( |
| /*force_early*/ true); |
| } |
| parent_was_visible_when_activation_changed_ = |
| prompt_bubble_widget->GetPrimaryWindowWidget()->IsVisible(); |
| } |
| |
| bool ChipController::ShouldWaitForConfirmationToComplete() const { |
| return is_confirmation_showing_ && collapse_timer_.IsRunning(); |
| } |
| |
| bool ChipController::ShouldWaitForLHSIndicatorToCollapse() const { |
| return permission_dashboard_controller_->is_verbose(); |
| } |
| |
| void ChipController::AddObserver(Observer* observer) { |
| permission_prompt_observers_.AddObserver(observer); |
| } |
| |
| void ChipController::RemoveObserver(Observer* observer) { |
| permission_prompt_observers_.RemoveObserver(observer); |
| } |
| |
| void ChipController::InitializePermissionPrompt( |
| base::WeakPtr<permissions::PermissionPrompt::Delegate> delegate, |
| base::OnceCallback<void()> callback) { |
| if (ShouldWaitForConfirmationToComplete()) { |
| delay_prompt_timer_.Start( |
| FROM_HERE, collapse_timer_.GetCurrentDelay(), |
| base::BindOnce(&ChipController::InitializePermissionPrompt, |
| weak_factory_.GetWeakPtr(), delegate, |
| std::move(callback))); |
| return; |
| } |
| |
| ResetPermissionPromptChip(); |
| |
| if (!delegate) { |
| return; |
| } |
| |
| // Here we just initialize the controller with the current request. We might |
| // not yet want to display the chip, for example when a prompt bubble without |
| // a request chip is shown --> only once a confirmation should be displayed, |
| // the chip should become visible. |
| chip_->SetVisible(false); |
| if (permission_dashboard_view_ && |
| !permission_dashboard_view_->GetIndicatorChip()->GetVisible()) { |
| permission_dashboard_view_->SetVisible(false); |
| } |
| permission_prompt_model_ = |
| std::make_unique<PermissionPromptChipModel>(delegate); |
| |
| if (active_chip_permission_request_manager_.has_value()) { |
| active_chip_permission_request_manager_.value()->RemoveObserver(this); |
| } |
| |
| active_chip_permission_request_manager_ = |
| permissions::PermissionRequestManager::FromWebContents( |
| delegate->GetAssociatedWebContents()); |
| active_chip_permission_request_manager_.value()->AddObserver(this); |
| observation_.Observe(chip_); |
| std::move(callback).Run(); |
| } |
| |
| void ChipController::ShowPermissionUi( |
| base::WeakPtr<permissions::PermissionPrompt::Delegate> delegate) { |
| if (permission_dashboard_controller_ && |
| permission_dashboard_controller_->SuppressVerboseIndicator()) { |
| delay_prompt_timer_.Start( |
| FROM_HERE, base::Milliseconds(250), |
| base::BindOnce(&ChipController::ShowPermissionPrompt, |
| weak_factory_.GetWeakPtr(), delegate)); |
| return; |
| } |
| |
| if (ShouldWaitForConfirmationToComplete()) { |
| delay_prompt_timer_.Start( |
| FROM_HERE, collapse_timer_.GetCurrentDelay(), |
| base::BindOnce(&ChipController::ShowPermissionPrompt, |
| weak_factory_.GetWeakPtr(), delegate)); |
| return; |
| } |
| |
| if (!delegate) { |
| return; |
| } |
| |
| InitializePermissionPrompt(delegate); |
| |
| // HaTS surveys may be triggered while a quiet chip is displayed. If that |
| // happens, the quiet chip should not collapse anymore, because otherwise a |
| // user answering a survey would no longer be able to click on the chip. To |
| // enable the PRM to handle this case, we pass a callback to stop the timers. |
| if (delegate->ReasonForUsingQuietUi().has_value()) { |
| delegate->SetHatsShownCallback(base::BindOnce(&ChipController::ResetTimers, |
| weak_factory_.GetWeakPtr())); |
| } |
| |
| // The permission prompt bubble has its own accessibility announcement. We |
| // should not announce the chip. |
| if (!permission_prompt_model_->ShouldBubbleStartOpen()) { |
| AnnouncePermissionRequestForAccessibility( |
| permission_prompt_model_->GetAccessibilityChipText()); |
| } |
| |
| chip_->SetVisible(true); |
| |
| if (permission_dashboard_view_) { |
| permission_dashboard_view_->SetVisible(true); |
| permission_dashboard_view_->UpdateDividerViewVisibility(); |
| } |
| |
| SyncChipWithModel(); |
| |
| chip_->SetButtonController(std::make_unique<BubbleButtonController>( |
| chip_.get(), this, |
| std::make_unique<views::Button::DefaultButtonControllerDelegate>( |
| chip_.get()))); |
| chip_->SetCallback(base::BindRepeating( |
| &ChipController::OnRequestChipButtonPressed, weak_factory_.GetWeakPtr())); |
| chip_->ResetAnimation(); |
| ObservePromptBubble(); |
| |
| if (permission_prompt_model_->IsExpandAnimationAllowed()) { |
| AnimateExpand(); |
| } else { |
| StartDismissTimer(); |
| } |
| } |
| |
| void ChipController::ShowPermissionChip( |
| base::WeakPtr<permissions::PermissionPrompt::Delegate> delegate) { |
| is_bubble_suppressed_ = true; |
| ShowPermissionUi(delegate); |
| } |
| |
| void ChipController::ShowPermissionPrompt( |
| base::WeakPtr<permissions::PermissionPrompt::Delegate> delegate) { |
| is_bubble_suppressed_ = false; |
| ShowPermissionUi(delegate); |
| } |
| |
| void ChipController::ClosePermissionPrompt() { |
| views::Widget* const bubble_widget = GetBubbleWidget(); |
| // If a user decided on a prompt, the widget should not be `nullptr` as it is |
| // 1:1 with the prompt. |
| CHECK(bubble_widget); |
| bubble_widget->Close(); |
| } |
| |
| void ChipController::PromptDecided(permissions::PermissionAction action) { |
| prompt_decision_ = action; |
| // Keep prompt decision inside ChipController and wait for `widget` to be |
| // closed. |
| ClosePermissionPrompt(); |
| } |
| |
| void ChipController::RemoveBubbleObserverAndResetTimersAndChipCallbacks() { |
| views::Widget* const bubble_widget = GetBubbleWidget(); |
| if (bubble_widget) { |
| disallowed_custom_cursors_scope_.RunAndReset(); |
| bubble_widget->RemoveObserver(this); |
| bubble_widget->Close(); |
| permission_prompt_observers_.Notify(&Observer::OnPermissionPromptHidden); |
| } |
| |
| // Reset button click callback |
| chip_->SetCallback(base::RepeatingCallback<void()>(base::DoNothing())); |
| |
| ResetTimers(); |
| } |
| |
| void ChipController::ResetPermissionPromptChip() { |
| RemoveBubbleObserverAndResetTimersAndChipCallbacks(); |
| observation_.Reset(); |
| if (permission_prompt_model_) { |
| // permission_request_manager_ is empty if the PermissionRequestManager |
| // instance has destructed, which triggers the observer method |
| // OnPermissionRequestManagerDestructed() implemented by this controller. |
| if (active_chip_permission_request_manager_.has_value()) { |
| active_chip_permission_request_manager_.value()->RemoveObserver(this); |
| |
| // When the user starts typing into the location bar we need to inform the |
| // PermissionRequestManager to update the PermissionPrompt reference it |
| // is holding. The typical update is for it to destruct the |
| // PermissionPrompt instance and not to hold any PermissionPrompt instance |
| // during the edit time. |
| if (GetLocationBarView()->IsEditingOrEmpty() && |
| (active_chip_permission_request_manager_.value() |
| ->IsRequestInProgress() && |
| (active_chip_permission_request_manager_.value() |
| ->web_contents() |
| ->GetVisibleURL() != GURL(chrome::kChromeUINewTabURL)))) { |
| active_chip_permission_request_manager_.value()->Ignore(); |
| } |
| active_chip_permission_request_manager_.reset(); |
| } |
| permission_prompt_model_.reset(); |
| } |
| |
| HideChip(); |
| is_confirmation_showing_ = false; |
| } |
| |
| void ChipController::ResetPermissionRequestChip() { |
| if (!is_confirmation_showing_) { |
| ResetPermissionPromptChip(); |
| } |
| } |
| |
| void ChipController::ShowPageInfoDialog() { |
| content::WebContents* contents = GetLocationBarView()->GetWebContents(); |
| if (!contents) { |
| return; |
| } |
| |
| content::NavigationEntry* entry = contents->GetController().GetVisibleEntry(); |
| if (entry->IsInitialEntry()) { |
| return; |
| } |
| |
| // Prevent chip from collapsing while prompt bubble is open. |
| ResetTimers(); |
| |
| std::unique_ptr<PageInfoBubbleSpecification> specification = |
| PageInfoBubbleSpecification::Builder( |
| chip_, chip_->GetWidget()->GetNativeWindow(), contents, |
| entry->GetVirtualURL()) |
| .AddInitializedCallback( |
| GetPageInfoDialogCreatedCallbackForTesting() |
| ? std::move(GetPageInfoDialogCreatedCallbackForTesting()) |
| : base::DoNothing()) |
| .AddPageInfoClosingCallback( |
| base::BindOnce(&ChipController::OnPageInfoBubbleClosed, |
| weak_factory_.GetWeakPtr())) |
| .Build(); |
| |
| views::BubbleDialogDelegateView* const bubble = |
| PageInfoBubbleView::CreatePageInfoBubble(std::move(specification)); |
| bubble->GetWidget()->Show(); |
| bubble_tracker_.SetView(bubble); |
| } |
| |
| void ChipController::OnPageInfoBubbleClosed( |
| views::Widget::ClosedReason closed_reason, |
| bool reload_prompt) { |
| HideChip(); |
| } |
| |
| void ChipController::CollapseConfirmation() { |
| is_confirmation_showing_ = false; |
| is_waiting_for_confirmation_collapse_ = true; |
| chip_->AnimateCollapse( |
| gfx::Animation::RichAnimationDuration(base::Milliseconds(75))); |
| } |
| |
| bool ChipController::should_expand_for_testing() { |
| CHECK_IS_TEST(); |
| return permission_prompt_model_->ShouldExpand(); |
| } |
| |
| void ChipController::AnimateExpand() { |
| chip_->ResetAnimation(); |
| chip_->AnimateExpand( |
| gfx::Animation::RichAnimationDuration(base::Milliseconds(350))); |
| chip_->SetVisible(true); |
| if (permission_dashboard_view_) { |
| permission_dashboard_view_->SetVisible(true); |
| } |
| } |
| |
| void ChipController::HandleConfirmation( |
| permissions::PermissionAction user_decision) { |
| DCHECK(permission_prompt_model_); |
| permission_prompt_model_->UpdateWithUserDecision(user_decision); |
| SyncChipWithModel(); |
| if (active_chip_permission_request_manager_.has_value() && |
| !active_chip_permission_request_manager_.value() |
| ->has_pending_requests() && |
| permission_prompt_model_->CanDisplayConfirmation()) { |
| is_confirmation_showing_ = true; |
| |
| if (chip_->GetVisible()) { |
| chip_->AnimateToFit( |
| gfx::Animation::RichAnimationDuration(base::Milliseconds(200))); |
| } else { |
| // No request chip was shown, always expand independently of what contents |
| // were stored in the previous chip. |
| AnimateExpand(); |
| } |
| |
| chip_->SetCallback(base::BindRepeating(&ChipController::ShowPageInfoDialog, |
| weak_factory_.GetWeakPtr())); |
| AnnouncePermissionRequestForAccessibility( |
| permission_prompt_model_->GetAccessibilityChipText()); |
| |
| if (!do_no_collapse_for_testing_) { |
| collapse_timer_.Start(FROM_HERE, kConfirmationDisplayDuration, this, |
| &ChipController::CollapseConfirmation); |
| } |
| } else { |
| ResetPermissionPromptChip(); |
| } |
| } |
| |
| void ChipController::AnnouncePermissionRequestForAccessibility( |
| const std::u16string& text) { |
| chip_->GetViewAccessibility().AnnounceText(text); |
| } |
| |
| void ChipController::CollapsePrompt(bool allow_restart) { |
| if (allow_restart && chip_->IsMouseHovered()) { |
| StartCollapseTimer(); |
| } else { |
| permission_prompt_model_->UpdateAutoCollapsePromptChipState(true); |
| SyncChipWithModel(); |
| |
| if (!chip_->is_fully_collapsed()) { |
| chip_->AnimateCollapse( |
| gfx::Animation::RichAnimationDuration(base::Milliseconds(250))); |
| } |
| |
| StartDismissTimer(); |
| } |
| } |
| |
| void ChipController::OnExpandAnimationEnded() { |
| if (is_confirmation_showing_ || IsBubbleShowing() || |
| !IsPermissionPromptChipVisible() || !permission_prompt_model_) { |
| return; |
| } |
| |
| if (permission_prompt_model_->ShouldBubbleStartOpen() && |
| !is_bubble_suppressed_) { |
| OpenPermissionPromptBubble(); |
| } else { |
| StartCollapseTimer(); |
| } |
| } |
| |
| void ChipController::OnCollapseAnimationEnded() { |
| if (is_waiting_for_confirmation_collapse_) { |
| HideChip(); |
| is_waiting_for_confirmation_collapse_ = false; |
| } |
| } |
| |
| void ChipController::HideChip() { |
| if (!chip_->GetVisible()) { |
| return; |
| } |
| |
| chip_->SetVisible(false); |
| if (permission_dashboard_view_) { |
| // The request chip is gone, the divider view is no longer needed. |
| permission_dashboard_view_->UpdateDividerViewVisibility(); |
| |
| // Hide the parent view `permission_dashboard_view_` if no children are |
| // visible. |
| if (!permission_dashboard_view_->GetIndicatorChip()->GetVisible()) { |
| permission_dashboard_view_->SetVisible(false); |
| } |
| } |
| // When the chip visibility changed from visible -> hidden, the locationbar |
| // layout should be updated. |
| GetLocationBarView()->InvalidateLayout(); |
| } |
| |
| void ChipController::OpenPermissionPromptBubble() { |
| DCHECK(!IsBubbleShowing()); |
| if (!permission_prompt_model_ || !permission_prompt_model_->GetDelegate() || |
| !location_bar_view_->GetWebContents()) { |
| return; |
| } |
| |
| Browser* browser = |
| chrome::FindBrowserWithTab(location_bar_view_->GetWebContents()); |
| if (!browser) { |
| DLOG(WARNING) << "Permission prompt suppressed because the WebContents is " |
| "not attached to any Browser window."; |
| return; |
| } |
| |
| disallowed_custom_cursors_scope_ = |
| permission_prompt_model_->GetDelegate() |
| ->GetAssociatedWebContents() |
| ->CreateDisallowCustomCursorScope(/*max_dimension_dips=*/0); |
| |
| // Prevent chip from collapsing while prompt bubble is open. |
| ResetTimers(); |
| |
| if (permission_prompt_model_->GetPromptStyle() == |
| PermissionPromptStyle::kChip) { |
| // Loud prompt bubble. |
| raw_ptr<PermissionPromptBubbleBaseView> prompt_bubble = |
| CreatePermissionPromptBubbleView( |
| browser, permission_prompt_model_->GetDelegate(), |
| PermissionPromptStyle::kChip); |
| bubble_tracker_.SetView(prompt_bubble); |
| prompt_bubble->Show(); |
| } else if (permission_prompt_model_->GetPromptStyle() == |
| PermissionPromptStyle::kQuietChip) { |
| // Quiet prompt bubble. |
| LocationBarView* lbv = GetLocationBarView(); |
| content::WebContents* web_contents = lbv->GetContentSettingWebContents(); |
| |
| if (web_contents) { |
| std::unique_ptr<ContentSettingQuietRequestBubbleModel> |
| content_setting_bubble_model = |
| std::make_unique<ContentSettingQuietRequestBubbleModel>( |
| lbv->GetContentSettingBubbleModelDelegate(), web_contents); |
| ContentSettingBubbleContents* quiet_request_bubble = |
| new ContentSettingBubbleContents( |
| std::move(content_setting_bubble_model), web_contents, lbv, |
| views::BubbleBorder::TOP_LEFT); |
| quiet_request_bubble->set_close_on_deactivate(false); |
| views::Widget* bubble_widget = |
| views::BubbleDialogDelegateView::CreateBubble(quiet_request_bubble); |
| quiet_request_bubble->set_close_on_deactivate(false); |
| bubble_tracker_.SetView(quiet_request_bubble); |
| bubble_widget->Show(); |
| } |
| } |
| |
| // It is possible that a Chip got reset while the permission prompt bubble was |
| // displayed. |
| if (permission_prompt_model_ && IsBubbleShowing()) { |
| ObservePromptBubble(); |
| permission_prompt_model_->GetDelegate()->SetPromptShown(); |
| permission_prompt_observers_.Notify(&Observer::OnPermissionPromptShown); |
| } |
| } |
| |
| void ChipController::ClosePermissionPromptBubbleWithReason( |
| views::Widget::ClosedReason reason) { |
| DCHECK(IsBubbleShowing()); |
| GetBubbleWidget()->CloseWithReason(reason); |
| } |
| |
| void ChipController::ObservePromptBubble() { |
| views::Widget* prompt_bubble_widget = GetBubbleWidget(); |
| if (prompt_bubble_widget) { |
| parent_was_visible_when_activation_changed_ = |
| prompt_bubble_widget->GetPrimaryWindowWidget()->IsVisible(); |
| prompt_bubble_widget->AddObserver(this); |
| } |
| } |
| |
| void ChipController::OnPromptBubbleDismissed() { |
| DCHECK(permission_prompt_model_); |
| if (!permission_prompt_model_) { |
| return; |
| } |
| |
| if (permission_prompt_model_->GetDelegate()) { |
| permission_prompt_model_->GetDelegate()->SetDismissOnTabClose(); |
| // If the permission prompt bubble is closed, we count it as "Dismissed", |
| // hence it should record the time when the bubble is closed. |
| permission_prompt_model_->GetDelegate()->SetDecisionTime(); |
| // If a permission popup bubble is closed/dismissed, a permission request |
| // should be dismissed as well. |
| permission_prompt_model_->GetDelegate()->Dismiss(); |
| } |
| } |
| |
| void ChipController::OnPromptExpired() { |
| AnnouncePermissionRequestForAccessibility(l10n_util::GetStringUTF16( |
| IDS_PERMISSIONS_EXPIRED_SCREENREADER_ANNOUNCEMENT)); |
| if (active_chip_permission_request_manager_.has_value()) { |
| active_chip_permission_request_manager_.value()->RemoveObserver(this); |
| active_chip_permission_request_manager_.reset(); |
| } |
| |
| // Because `OnPromptExpired` is called async, make sure that there is an |
| // existing permission request before resolving it as `Ignore`. |
| if (permission_prompt_model_ && permission_prompt_model_->GetDelegate() && |
| !permission_prompt_model_->GetDelegate()->Requests().empty()) { |
| permission_prompt_model_->GetDelegate()->Ignore(); |
| } |
| |
| ResetPermissionPromptChip(); |
| } |
| |
| void ChipController::OnRequestChipButtonPressed() { |
| if (IsBubbleShowing()) { |
| // A mouse click on chip while a permission prompt is open should dismiss |
| // the prompt and collapse the chip |
| ClosePermissionPromptBubbleWithReason( |
| views::Widget::ClosedReason::kCloseButtonClicked); |
| } else { |
| OpenPermissionPromptBubble(); |
| } |
| } |
| |
| void ChipController::OnChipVisibilityChanged(bool is_visible) { |
| auto* prompt_bubble = GetBubbleWidget(); |
| if (!chip_->GetVisible() && prompt_bubble) { |
| // In case if the prompt bubble isn't closed on focus loss, manually close |
| // it when chip is hidden. |
| prompt_bubble->Close(); |
| } |
| } |
| |
| void ChipController::SyncChipWithModel() { |
| PermissionPromptChipModel* const model = permission_prompt_model_.get(); |
| chip_->SetChipIcon(model->GetIcon()); |
| chip_->SetTheme(model->GetChipTheme()); |
| chip_->SetMessage(model->GetChipText()); |
| chip_->SetUserDecision(model->GetUserDecision()); |
| chip_->SetPermissionPromptStyle(model->GetPromptStyle()); |
| chip_->SetBlockedIconShowing(model->ShouldDisplayBlockedIcon()); |
| } |
| |
| void ChipController::StartCollapseTimer() { |
| collapse_timer_.Start(FROM_HERE, base::Seconds(12), |
| base::BindOnce(&ChipController::CollapsePrompt, |
| weak_factory_.GetWeakPtr(), |
| /*allow_restart=*/true)); |
| } |
| |
| void ChipController::StartDismissTimer() { |
| if (!permission_prompt_model_) { |
| return; |
| } |
| |
| dismiss_timer_.Start(FROM_HERE, |
| permission_prompt_model_->ShouldExpand() |
| ? base::Seconds(6) |
| // Abusive origins do not support expand animation, |
| // hence the dismiss timer should be longer. |
| : base::Seconds(18), |
| this, &ChipController::OnPromptExpired); |
| } |
| |
| void ChipController::ResetTimers() { |
| collapse_timer_.Stop(); |
| dismiss_timer_.Stop(); |
| delay_prompt_timer_.Stop(); |
| } |
| |
| views::Widget* ChipController::GetBubbleWidget() { |
| // We can't call GetPromptBubbleView() here, because the bubble_tracker may |
| // hold objects that aren't of typ `PermissionPromptBubbleBaseView`. |
| return bubble_tracker_.view() ? bubble_tracker_.view()->GetWidget() : nullptr; |
| } |
| |
| PermissionPromptBubbleBaseView* ChipController::GetPromptBubbleView() { |
| // The tracked bubble view is a `PermissionPromptBubbleBaseView` only when |
| // `kChip` is used. |
| CHECK_EQ(permission_prompt_model_->GetPromptStyle(), |
| PermissionPromptStyle::kChip); |
| auto* view = bubble_tracker_.view(); |
| return view ? static_cast<PermissionPromptBubbleBaseView*>(view) : nullptr; |
| } |
| |
| ContentSettingBubbleContents* |
| ChipController::GetContentSettingBubbleContentsForTesting() { |
| CHECK_IS_TEST(); |
| // The tracked bubble view is a `ContentSettingBubbleContents` only when |
| // `kQuietChip` is used. |
| CHECK_EQ(permission_prompt_model_->GetPromptStyle(), |
| PermissionPromptStyle::kQuietChip); |
| auto* view = bubble_tracker_.view(); |
| return view ? static_cast<ContentSettingBubbleContents*>(view) : nullptr; |
| } |