|  | // Copyright 2015 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/toolbar/media_router_action.h" | 
|  |  | 
|  | #include "base/bind.h" | 
|  | #include "base/location.h" | 
|  | #include "base/metrics/user_metrics.h" | 
|  | #include "base/strings/utf_string_conversions.h" | 
|  | #include "base/task/post_task.h" | 
|  | #include "chrome/browser/media/router/media_router.h" | 
|  | #include "chrome/browser/media/router/media_router_factory.h" | 
|  | #include "chrome/browser/media/router/media_router_metrics.h" | 
|  | #include "chrome/browser/profiles/profile.h" | 
|  | #include "chrome/browser/ui/browser.h" | 
|  | #include "chrome/browser/ui/media_router/media_router_dialog_controller_impl_base.h" | 
|  | #include "chrome/browser/ui/media_router/media_router_ui_service.h" | 
|  | #include "chrome/browser/ui/tabs/tab_strip_model.h" | 
|  | #include "chrome/browser/ui/toolbar/component_toolbar_actions_factory.h" | 
|  | #include "chrome/browser/ui/toolbar/media_router_action_controller.h" | 
|  | #include "chrome/browser/ui/toolbar/toolbar_action_view_delegate.h" | 
|  | #include "chrome/common/media_router/issue.h" | 
|  | #include "chrome/common/media_router/media_route.h" | 
|  | #include "chrome/grit/generated_resources.h" | 
|  | #include "components/vector_icons/vector_icons.h" | 
|  | #include "content/public/browser/browser_task_traits.h" | 
|  | #include "content/public/browser/browser_thread.h" | 
|  | #include "ui/base/l10n/l10n_util.h" | 
|  | #include "ui/gfx/color_palette.h" | 
|  | #include "ui/gfx/image/image_skia.h" | 
|  | #include "ui/gfx/paint_vector_icon.h" | 
|  | #include "ui/gfx/vector_icon_types.h" | 
|  |  | 
|  | using media_router::MediaRouterDialogControllerImplBase; | 
|  |  | 
|  | namespace { | 
|  |  | 
|  | media_router::MediaRouter* GetMediaRouter(Browser* browser) { | 
|  | return media_router::MediaRouterFactory::GetApiForBrowserContext( | 
|  | browser->profile()); | 
|  | } | 
|  |  | 
|  | }  // namespace | 
|  |  | 
|  | MediaRouterAction::MediaRouterAction(Browser* browser, | 
|  | ToolbarActionsBar* toolbar_actions_bar) | 
|  | : media_router::IssuesObserver(GetMediaRouter(browser)->GetIssueManager()), | 
|  | media_router::MediaRoutesObserver(GetMediaRouter(browser)), | 
|  | current_icon_(&vector_icons::kMediaRouterIdleIcon), | 
|  | has_local_display_route_(false), | 
|  | has_dialog_(false), | 
|  | delegate_(nullptr), | 
|  | browser_(browser), | 
|  | toolbar_actions_bar_(toolbar_actions_bar), | 
|  | tab_strip_model_observer_(this), | 
|  | toolbar_actions_bar_observer_(this), | 
|  | skip_close_overflow_menu_for_testing_(false), | 
|  | weak_ptr_factory_(this) { | 
|  | DCHECK(browser_); | 
|  | DCHECK(toolbar_actions_bar_); | 
|  | tab_strip_model_observer_.Add(browser_->tab_strip_model()); | 
|  | toolbar_actions_bar_observer_.Add(toolbar_actions_bar_); | 
|  | IssuesObserver::Init(); | 
|  | } | 
|  |  | 
|  | MediaRouterAction::~MediaRouterAction() { | 
|  | } | 
|  |  | 
|  | // static | 
|  | SkColor MediaRouterAction::GetIconColor(const gfx::VectorIcon& icon_id) { | 
|  | if (&icon_id == &vector_icons::kMediaRouterIdleIcon) | 
|  | return gfx::kChromeIconGrey; | 
|  | if (&icon_id == &vector_icons::kMediaRouterActiveIcon) | 
|  | return gfx::kGoogleBlue500; | 
|  | if (&icon_id == &vector_icons::kMediaRouterWarningIcon) | 
|  | return gfx::kGoogleYellow700; | 
|  | DCHECK_EQ(&vector_icons::kMediaRouterErrorIcon, &icon_id); | 
|  | return gfx::kGoogleRed700; | 
|  | } | 
|  |  | 
|  | std::string MediaRouterAction::GetId() const { | 
|  | return ComponentToolbarActionsFactory::kMediaRouterActionId; | 
|  | } | 
|  |  | 
|  | void MediaRouterAction::SetDelegate(ToolbarActionViewDelegate* delegate) { | 
|  | delegate_ = delegate; | 
|  |  | 
|  | // In cases such as opening a new browser window, SetDelegate() will be | 
|  | // called before the WebContents is set. In those cases, we register with the | 
|  | // dialog controller when ActiveTabChanged() is called. | 
|  | if (delegate_ && delegate_->GetCurrentWebContents()) | 
|  | RegisterWithDialogController(); | 
|  | } | 
|  |  | 
|  | gfx::Image MediaRouterAction::GetIcon(content::WebContents* web_contents, | 
|  | const gfx::Size& size) { | 
|  | return gfx::Image( | 
|  | gfx::CreateVectorIcon(*current_icon_, GetIconColor(*current_icon_))); | 
|  | } | 
|  |  | 
|  | base::string16 MediaRouterAction::GetActionName() const { | 
|  | return l10n_util::GetStringUTF16(IDS_MEDIA_ROUTER_TITLE); | 
|  | } | 
|  |  | 
|  | base::string16 MediaRouterAction::GetAccessibleName( | 
|  | content::WebContents* web_contents) const { | 
|  | return GetTooltip(web_contents); | 
|  | } | 
|  |  | 
|  | base::string16 MediaRouterAction::GetTooltip( | 
|  | content::WebContents* web_contents) const { | 
|  | return l10n_util::GetStringUTF16(IDS_MEDIA_ROUTER_ICON_TOOLTIP_TEXT); | 
|  | } | 
|  |  | 
|  | bool MediaRouterAction::IsEnabled( | 
|  | content::WebContents* web_contents) const { | 
|  | return true; | 
|  | } | 
|  |  | 
|  | bool MediaRouterAction::WantsToRun( | 
|  | content::WebContents* web_contents) const { | 
|  | return false; | 
|  | } | 
|  |  | 
|  | bool MediaRouterAction::HasPopup( | 
|  | content::WebContents* web_contents) const { | 
|  | return true; | 
|  | } | 
|  |  | 
|  | bool MediaRouterAction::IsShowingPopup() const { | 
|  | auto* controller = GetMediaRouterDialogController(); | 
|  | return controller && controller->IsShowingMediaRouterDialog(); | 
|  | } | 
|  |  | 
|  | void MediaRouterAction::HidePopup() { | 
|  | GetMediaRouterDialogController()->HideMediaRouterDialog(); | 
|  | } | 
|  |  | 
|  | gfx::NativeView MediaRouterAction::GetPopupNativeView() { | 
|  | return nullptr; | 
|  | } | 
|  |  | 
|  | ui::MenuModel* MediaRouterAction::GetContextMenu() { | 
|  | // If there is an existing context menu, destroy it before we instantiate a | 
|  | // new one. | 
|  | DestroyContextMenu(); | 
|  | MediaRouterActionController* controller = | 
|  | media_router::MediaRouterUIService::Get(browser_->profile()) | 
|  | ->action_controller(); | 
|  | if (toolbar_actions_bar_->IsActionVisibleOnMainBar(this)) { | 
|  | contextual_menu_ = | 
|  | MediaRouterContextualMenu::CreateForToolbar(browser_, controller); | 
|  | } else { | 
|  | contextual_menu_ = | 
|  | MediaRouterContextualMenu::CreateForOverflowMenu(browser_, controller); | 
|  | } | 
|  | return contextual_menu_->menu_model(); | 
|  | } | 
|  |  | 
|  | void MediaRouterAction::OnContextMenuClosed() { | 
|  | if (toolbar_actions_bar_->popped_out_action() == this && !IsShowingPopup()) | 
|  | toolbar_actions_bar_->UndoPopOut(); | 
|  |  | 
|  | // We must destroy the context menu asynchronously to prevent it from being | 
|  | // destroyed before the command execution. | 
|  | // TODO(takumif): Using task sequence to order operations is fragile. Consider | 
|  | // other ways to do so when we move the icon to the trusted area. | 
|  | base::PostTaskWithTraits( | 
|  | FROM_HERE, {content::BrowserThread::UI}, | 
|  | base::BindOnce(&MediaRouterAction::DestroyContextMenu, | 
|  | weak_ptr_factory_.GetWeakPtr())); | 
|  | } | 
|  |  | 
|  | bool MediaRouterAction::ExecuteAction(bool by_user) { | 
|  | base::RecordAction(base::UserMetricsAction("MediaRouter_Icon_Click")); | 
|  |  | 
|  | if (IsShowingPopup()) { | 
|  | HidePopup(); | 
|  | return false; | 
|  | } | 
|  |  | 
|  | GetMediaRouterDialogController()->ShowMediaRouterDialog(); | 
|  | if (!skip_close_overflow_menu_for_testing_) { | 
|  | // TODO(karandeepb): Instead of checking the return value of | 
|  | // CloseOverflowMenuIfOpen, just check | 
|  | // ToolbarActionsBar::IsActionVisibleOnMainBar. | 
|  | media_router::MediaRouterMetrics::RecordMediaRouterDialogOrigin( | 
|  | toolbar_actions_bar_->CloseOverflowMenuIfOpen() | 
|  | ? media_router::MediaRouterDialogOpenOrigin::OVERFLOW_MENU | 
|  | : media_router::MediaRouterDialogOpenOrigin::TOOLBAR); | 
|  | } | 
|  | return true; | 
|  | } | 
|  |  | 
|  | void MediaRouterAction::UpdateState() { | 
|  | delegate_->UpdateState(); | 
|  | } | 
|  |  | 
|  | bool MediaRouterAction::DisabledClickOpensMenu() const { | 
|  | return false; | 
|  | } | 
|  |  | 
|  | void MediaRouterAction::OnIssue(const media_router::Issue& issue) { | 
|  | current_issue_ = std::make_unique<media_router::IssueInfo>(issue.info()); | 
|  | MaybeUpdateIcon(); | 
|  | } | 
|  |  | 
|  | void MediaRouterAction::OnIssuesCleared() { | 
|  | current_issue_.reset(); | 
|  | MaybeUpdateIcon(); | 
|  | } | 
|  |  | 
|  | void MediaRouterAction::OnRoutesUpdated( | 
|  | const std::vector<media_router::MediaRoute>& routes, | 
|  | const std::vector<media_router::MediaRoute::Id>& joinable_route_ids) { | 
|  | has_local_display_route_ = | 
|  | std::find_if(routes.begin(), routes.end(), | 
|  | [](const media_router::MediaRoute& route) { | 
|  | return route.is_local() && route.for_display(); | 
|  | }) != routes.end(); | 
|  | MaybeUpdateIcon(); | 
|  | } | 
|  |  | 
|  | void MediaRouterAction::OnTabStripModelChanged( | 
|  | TabStripModel* tab_strip_model, | 
|  | const TabStripModelChange& change, | 
|  | const TabStripSelectionChange& selection) { | 
|  | if (!selection.active_tab_changed() || tab_strip_model->empty()) | 
|  | return; | 
|  |  | 
|  | RegisterWithDialogController(); | 
|  | UpdateDialogState(); | 
|  | } | 
|  |  | 
|  | void MediaRouterAction::OnToolbarActionsBarAnimationEnded() { | 
|  | UpdateDialogState(); | 
|  | } | 
|  |  | 
|  | void MediaRouterAction::OnDialogHidden() { | 
|  | if (has_dialog_) { | 
|  | has_dialog_ = false; | 
|  | delegate_->OnPopupClosed(); | 
|  | } | 
|  | } | 
|  |  | 
|  | void MediaRouterAction::OnDialogShown() { | 
|  | if (!has_dialog_) { | 
|  | has_dialog_ = true; | 
|  | // We depress the action regardless of whether ExecuteAction() was user | 
|  | // initiated. | 
|  | delegate_->OnPopupShown(true); | 
|  | } | 
|  | } | 
|  |  | 
|  | void MediaRouterAction::RegisterWithDialogController() { | 
|  | MediaRouterDialogControllerImplBase* controller = | 
|  | GetMediaRouterDialogController(); | 
|  |  | 
|  | if (!controller) | 
|  | return; | 
|  |  | 
|  | // |controller| keeps track of |this| if |this| was created with the browser | 
|  | // window or ephemerally by activating the Cast functionality. If |this| was | 
|  | // created in overflow mode, it will be destroyed when the overflow menu is | 
|  | // closed. | 
|  | if (!toolbar_actions_bar_->in_overflow_mode()) | 
|  | controller->SetMediaRouterAction(weak_ptr_factory_.GetWeakPtr()); | 
|  | } | 
|  |  | 
|  | void MediaRouterAction::UpdateDialogState() { | 
|  | // The WebContents may be null during browser test shutdown, in which case we | 
|  | // cannot call GetMediaRouterDialogController(). | 
|  | if (!delegate_->GetCurrentWebContents()) | 
|  | return; | 
|  |  | 
|  | if (IsShowingPopup()) | 
|  | OnDialogShown(); | 
|  | else | 
|  | OnDialogHidden(); | 
|  | } | 
|  |  | 
|  | MediaRouterDialogControllerImplBase* | 
|  | MediaRouterAction::GetMediaRouterDialogController() { | 
|  | DCHECK(delegate_); | 
|  | content::WebContents* web_contents = delegate_->GetCurrentWebContents(); | 
|  | DCHECK(web_contents); | 
|  | return MediaRouterDialogControllerImplBase::GetOrCreateForWebContents( | 
|  | web_contents); | 
|  | } | 
|  |  | 
|  | const MediaRouterDialogControllerImplBase* | 
|  | MediaRouterAction::GetMediaRouterDialogController() const { | 
|  | DCHECK(delegate_); | 
|  | content::WebContents* web_contents = delegate_->GetCurrentWebContents(); | 
|  | DCHECK(web_contents); | 
|  | return MediaRouterDialogControllerImplBase::FromWebContents(web_contents); | 
|  | } | 
|  |  | 
|  | void MediaRouterAction::MaybeUpdateIcon() { | 
|  | const gfx::VectorIcon& new_icon = GetCurrentIcon(); | 
|  |  | 
|  | // Update the current state if it has changed. | 
|  | if (&new_icon != current_icon_) { | 
|  | current_icon_ = &new_icon; | 
|  |  | 
|  | // Tell the associated view to update its icon to reflect the change made | 
|  | // above. If MaybeUpdateIcon() was called as a result of instantiating | 
|  | // |this|, then |delegate_| may not be set yet. | 
|  | if (delegate_) | 
|  | UpdateState(); | 
|  | } | 
|  | } | 
|  |  | 
|  | const gfx::VectorIcon& MediaRouterAction::GetCurrentIcon() const { | 
|  | // Highest priority is to indicate whether there's an issue. | 
|  | if (current_issue_) { | 
|  | media_router::IssueInfo::Severity severity = current_issue_->severity; | 
|  | if (severity == media_router::IssueInfo::Severity::FATAL) | 
|  | return vector_icons::kMediaRouterErrorIcon; | 
|  | if (severity == media_router::IssueInfo::Severity::WARNING) | 
|  | return vector_icons::kMediaRouterWarningIcon; | 
|  | // Fall through for Severity::NOTIFICATION. | 
|  | } | 
|  |  | 
|  | return has_local_display_route_ ? vector_icons::kMediaRouterActiveIcon | 
|  | : vector_icons::kMediaRouterIdleIcon; | 
|  | } | 
|  |  | 
|  | void MediaRouterAction::DestroyContextMenu() { | 
|  | contextual_menu_.reset(); | 
|  | } |