// Copyright (c) 2012 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/tabs/tab_utils.h" #include <utility> #include "base/command_line.h" #include "base/strings/string16.h" #include "build/build_config.h" #include "chrome/browser/media/media_capture_devices_dispatcher.h" #include "chrome/browser/media/media_stream_capture_indicator.h" #include "chrome/browser/themes/theme_properties.h" #include "chrome/browser/ui/tabs/tab_strip_model.h" #include "chrome/common/chrome_switches.h" #include "chrome/grit/generated_resources.h" #include "content/public/browser/web_contents.h" #include "ui/base/l10n/l10n_util.h" #include "ui/base/theme_provider.h" #include "ui/gfx/animation/multi_animation.h" #include "ui/gfx/image/image.h" #include "ui/gfx/vector_icons_public.h" #include "ui/native_theme/common_theme.h" #include "ui/native_theme/native_theme.h" #if defined(OS_MACOSX) #include "grit/theme_resources.h" #include "ui/base/resource/resource_bundle.h" #else #include "ui/gfx/paint_vector_icon.h" #endif struct LastMuteMetadata : public content::WebContentsUserData<LastMuteMetadata> { TabMutedReason reason = TAB_MUTED_REASON_NONE; std::string extension_id; private: explicit LastMuteMetadata(content::WebContents* contents) {} friend class content::WebContentsUserData<LastMuteMetadata>; }; DEFINE_WEB_CONTENTS_USER_DATA_KEY(LastMuteMetadata); namespace chrome { namespace { // Interval between frame updates of the tab indicator animations. This is not // the usual 60 FPS because a trade-off must be made between tab UI animation // smoothness and media recording/playback performance on low-end hardware. const int kIndicatorFrameIntervalMs = 50; // 20 FPS // Fade-in/out duration for the tab indicator animations. Fade-in is quick to // immediately notify the user. Fade-out is more gradual, so that the user has // a chance of finding a tab that has quickly "blipped" on and off. const int kIndicatorFadeInDurationMs = 200; const int kIndicatorFadeOutDurationMs = 1000; // Animation that throbs in (towards 1.0) and out (towards 0.0), and ends in the // "in" state. class TabRecordingIndicatorAnimation : public gfx::MultiAnimation { public: ~TabRecordingIndicatorAnimation() override {} // Overridden to provide alternating "towards in" and "towards out" behavior. double GetCurrentValue() const override; static scoped_ptr<TabRecordingIndicatorAnimation> Create(); private: TabRecordingIndicatorAnimation(const gfx::MultiAnimation::Parts& parts, const base::TimeDelta interval) : MultiAnimation(parts, interval) {} // Number of times to "toggle throb" the recording and tab capture indicators // when they first appear. static const int kCaptureIndicatorThrobCycles = 5; }; double TabRecordingIndicatorAnimation::GetCurrentValue() const { return current_part_index() % 2 ? 1.0 - MultiAnimation::GetCurrentValue() : MultiAnimation::GetCurrentValue(); } scoped_ptr<TabRecordingIndicatorAnimation> TabRecordingIndicatorAnimation::Create() { MultiAnimation::Parts parts; static_assert(kCaptureIndicatorThrobCycles % 2 != 0, "odd number of cycles required so animation finishes in showing state"); for (int i = 0; i < kCaptureIndicatorThrobCycles; ++i) { parts.push_back(MultiAnimation::Part( i % 2 ? kIndicatorFadeOutDurationMs : kIndicatorFadeInDurationMs, gfx::Tween::EASE_IN)); } const base::TimeDelta interval = base::TimeDelta::FromMilliseconds(kIndicatorFrameIntervalMs); scoped_ptr<TabRecordingIndicatorAnimation> animation( new TabRecordingIndicatorAnimation(parts, interval)); animation->set_continuous(false); return animation; } } // namespace bool ShouldTabShowFavicon(int capacity, bool is_pinned_tab, bool is_active_tab, bool has_favicon, TabMediaState media_state) { if (!has_favicon) return false; int required_capacity = 1; if (ShouldTabShowCloseButton(capacity, is_pinned_tab, is_active_tab)) ++required_capacity; if (ShouldTabShowMediaIndicator( capacity, is_pinned_tab, is_active_tab, has_favicon, media_state)) { ++required_capacity; } return capacity >= required_capacity; } bool ShouldTabShowMediaIndicator(int capacity, bool is_pinned_tab, bool is_active_tab, bool has_favicon, TabMediaState media_state) { if (media_state == TAB_MEDIA_STATE_NONE) return false; if (ShouldTabShowCloseButton(capacity, is_pinned_tab, is_active_tab)) return capacity >= 2; return capacity >= 1; } bool ShouldTabShowCloseButton(int capacity, bool is_pinned_tab, bool is_active_tab) { if (is_pinned_tab) return false; else if (is_active_tab) return true; else return capacity >= 3; } TabMediaState GetTabMediaStateForContents(content::WebContents* contents) { if (!contents) return TAB_MEDIA_STATE_NONE; scoped_refptr<MediaStreamCaptureIndicator> indicator = MediaCaptureDevicesDispatcher::GetInstance()-> GetMediaStreamCaptureIndicator(); if (indicator.get()) { if (indicator->IsBeingMirrored(contents)) return TAB_MEDIA_STATE_CAPTURING; if (indicator->IsCapturingUserMedia(contents)) return TAB_MEDIA_STATE_RECORDING; } if (contents->IsAudioMuted()) return TAB_MEDIA_STATE_AUDIO_MUTING; if (contents->WasRecentlyAudible()) return TAB_MEDIA_STATE_AUDIO_PLAYING; return TAB_MEDIA_STATE_NONE; } gfx::Image GetTabMediaIndicatorImage(TabMediaState media_state, SkColor button_color) { #if defined(OS_MACOSX) ui::ResourceBundle* rb = &ui::ResourceBundle::GetSharedInstance(); switch (media_state) { case TAB_MEDIA_STATE_AUDIO_PLAYING: return rb->GetNativeImageNamed(IDR_TAB_AUDIO_INDICATOR); case TAB_MEDIA_STATE_AUDIO_MUTING: return rb->GetNativeImageNamed(IDR_TAB_AUDIO_MUTING_INDICATOR); case TAB_MEDIA_STATE_RECORDING: return rb->GetNativeImageNamed(IDR_TAB_RECORDING_INDICATOR); case TAB_MEDIA_STATE_CAPTURING: return rb->GetNativeImageNamed(IDR_TAB_CAPTURE_INDICATOR); case TAB_MEDIA_STATE_NONE: break; } #else gfx::VectorIconId icon_id = gfx::VectorIconId::VECTOR_ICON_NONE; switch (media_state) { case TAB_MEDIA_STATE_AUDIO_PLAYING: icon_id = gfx::VectorIconId::TAB_AUDIO; break; case TAB_MEDIA_STATE_AUDIO_MUTING: icon_id = gfx::VectorIconId::TAB_AUDIO_MUTING; break; case TAB_MEDIA_STATE_RECORDING: icon_id = gfx::VectorIconId::TAB_MEDIA_RECORDING; break; case TAB_MEDIA_STATE_CAPTURING: icon_id = gfx::VectorIconId::TAB_MEDIA_CAPTURING; break; case TAB_MEDIA_STATE_NONE: break; } if (icon_id != gfx::VectorIconId::VECTOR_ICON_NONE) return gfx::Image(gfx::CreateVectorIcon(icon_id, 16, button_color)); #endif NOTREACHED(); return gfx::Image(); } gfx::Image GetTabMediaIndicatorAffordanceImage(TabMediaState media_state, SkColor button_color) { switch (media_state) { case TAB_MEDIA_STATE_AUDIO_PLAYING: return GetTabMediaIndicatorImage(TAB_MEDIA_STATE_AUDIO_MUTING, button_color); case TAB_MEDIA_STATE_AUDIO_MUTING: case TAB_MEDIA_STATE_NONE: case TAB_MEDIA_STATE_RECORDING: case TAB_MEDIA_STATE_CAPTURING: return GetTabMediaIndicatorImage(media_state, button_color); } NOTREACHED(); return GetTabMediaIndicatorImage(media_state, button_color); } scoped_ptr<gfx::Animation> CreateTabMediaIndicatorFadeAnimation( TabMediaState media_state) { if (media_state == TAB_MEDIA_STATE_RECORDING || media_state == TAB_MEDIA_STATE_CAPTURING) { return TabRecordingIndicatorAnimation::Create(); } // Note: While it seems silly to use a one-part MultiAnimation, it's the only // gfx::Animation implementation that lets us control the frame interval. gfx::MultiAnimation::Parts parts; const bool is_for_fade_in = (media_state != TAB_MEDIA_STATE_NONE); parts.push_back(gfx::MultiAnimation::Part( is_for_fade_in ? kIndicatorFadeInDurationMs : kIndicatorFadeOutDurationMs, gfx::Tween::EASE_IN)); const base::TimeDelta interval = base::TimeDelta::FromMilliseconds(kIndicatorFrameIntervalMs); scoped_ptr<gfx::MultiAnimation> animation( new gfx::MultiAnimation(parts, interval)); animation->set_continuous(false); return std::move(animation); } base::string16 AssembleTabTooltipText(const base::string16& title, TabMediaState media_state) { if (media_state == TAB_MEDIA_STATE_NONE) return title; base::string16 result = title; if (!result.empty()) result.append(1, '\n'); switch (media_state) { case TAB_MEDIA_STATE_AUDIO_PLAYING: result.append( l10n_util::GetStringUTF16(IDS_TOOLTIP_TAB_MEDIA_STATE_AUDIO_PLAYING)); break; case TAB_MEDIA_STATE_AUDIO_MUTING: result.append( l10n_util::GetStringUTF16(IDS_TOOLTIP_TAB_MEDIA_STATE_AUDIO_MUTING)); break; case TAB_MEDIA_STATE_RECORDING: result.append( l10n_util::GetStringUTF16(IDS_TOOLTIP_TAB_MEDIA_STATE_RECORDING)); break; case TAB_MEDIA_STATE_CAPTURING: result.append( l10n_util::GetStringUTF16(IDS_TOOLTIP_TAB_MEDIA_STATE_CAPTURING)); break; case TAB_MEDIA_STATE_NONE: NOTREACHED(); break; } return result; } bool AreExperimentalMuteControlsEnabled() { return base::CommandLine::ForCurrentProcess()->HasSwitch( switches::kEnableTabAudioMuting); } bool CanToggleAudioMute(content::WebContents* contents) { switch (GetTabMediaStateForContents(contents)) { case TAB_MEDIA_STATE_NONE: case TAB_MEDIA_STATE_AUDIO_PLAYING: case TAB_MEDIA_STATE_AUDIO_MUTING: return true; case TAB_MEDIA_STATE_RECORDING: case TAB_MEDIA_STATE_CAPTURING: return false; } NOTREACHED(); return false; } TabMutedReason GetTabAudioMutedReason(content::WebContents* contents) { LastMuteMetadata::CreateForWebContents(contents); // Ensures metadata exists. LastMuteMetadata* const metadata = LastMuteMetadata::FromWebContents(contents); if (GetTabMediaStateForContents(contents) == TAB_MEDIA_STATE_CAPTURING) { // For tab capture, libcontent forces muting off. metadata->reason = TAB_MUTED_REASON_MEDIA_CAPTURE; metadata->extension_id.clear(); } return metadata->reason; } const std::string& GetExtensionIdForMutedTab(content::WebContents* contents) { DCHECK_EQ(GetTabAudioMutedReason(contents) != TAB_MUTED_REASON_EXTENSION, LastMuteMetadata::FromWebContents(contents)->extension_id.empty()); return LastMuteMetadata::FromWebContents(contents)->extension_id; } TabMutedResult SetTabAudioMuted(content::WebContents* contents, bool mute, TabMutedReason reason, const std::string& extension_id) { DCHECK(contents); DCHECK_NE(TAB_MUTED_REASON_NONE, reason); if (reason == TAB_MUTED_REASON_AUDIO_INDICATOR && !AreExperimentalMuteControlsEnabled()) { return TAB_MUTED_RESULT_FAIL_NOT_ENABLED; } if (!chrome::CanToggleAudioMute(contents)) return TAB_MUTED_RESULT_FAIL_TABCAPTURE; contents->SetAudioMuted(mute); LastMuteMetadata::CreateForWebContents(contents); // Ensures metadata exists. LastMuteMetadata* const metadata = LastMuteMetadata::FromWebContents(contents); metadata->reason = reason; if (reason == TAB_MUTED_REASON_EXTENSION) { DCHECK(!extension_id.empty()); metadata->extension_id = extension_id; } else { metadata->extension_id.clear(); } return TAB_MUTED_RESULT_SUCCESS; } void UnmuteIfMutedByExtension(content::WebContents* contents, const std::string& extension_id) { LastMuteMetadata::CreateForWebContents(contents); // Ensures metadata exists. LastMuteMetadata* const metadata = LastMuteMetadata::FromWebContents(contents); if (metadata->reason == TAB_MUTED_REASON_EXTENSION && metadata->extension_id == extension_id) { SetTabAudioMuted(contents, false, TAB_MUTED_REASON_EXTENSION, extension_id); } } bool AreAllTabsMuted(const TabStripModel& tab_strip, const std::vector<int>& indices) { for (std::vector<int>::const_iterator i = indices.begin(); i != indices.end(); ++i) { if (!tab_strip.GetWebContentsAt(*i)->IsAudioMuted()) return false; } return true; } } // namespace chrome