1 // Copyright (c) 2012 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
5 #include "chrome/browser/ui/tabs/tab_utils.h"
7 #include "base/command_line.h"
8 #include "base/strings/string16.h"
9 #include "chrome/browser/media/media_capture_devices_dispatcher.h"
10 #include "chrome/browser/media/media_stream_capture_indicator.h"
11 #include "chrome/browser/themes/theme_properties.h"
12 #include "chrome/browser/ui/tabs/tab_strip_model.h"
13 #include "chrome/common/chrome_switches.h"
14 #include "chrome/grit/generated_resources.h"
15 #include "content/public/browser/web_contents.h"
16 #include "grit/theme_resources.h"
17 #include "ui/base/l10n/l10n_util.h"
18 #include "ui/base/resource/resource_bundle.h"
19 #include "ui/base/theme_provider.h"
20 #include "ui/gfx/animation/multi_animation.h"
21 #include "ui/gfx/vector_icons_public.h"
22 #include "ui/native_theme/common_theme.h"
23 #include "ui/native_theme/native_theme.h"
25 #if !defined(OS_MACOSX)
26 #include "ui/gfx/paint_vector_icon.h"
29 struct LastMuteMetadata
30 : public content::WebContentsUserData
<LastMuteMetadata
> {
31 TabMutedReason reason
= TAB_MUTED_REASON_NONE
;
32 std::string extension_id
;
35 explicit LastMuteMetadata(content::WebContents
* contents
) {}
36 friend class content::WebContentsUserData
<LastMuteMetadata
>;
39 DEFINE_WEB_CONTENTS_USER_DATA_KEY(LastMuteMetadata
);
45 // Interval between frame updates of the tab indicator animations. This is not
46 // the usual 60 FPS because a trade-off must be made between tab UI animation
47 // smoothness and media recording/playback performance on low-end hardware.
48 const int kIndicatorFrameIntervalMs
= 50; // 20 FPS
50 // Fade-in/out duration for the tab indicator animations. Fade-in is quick to
51 // immediately notify the user. Fade-out is more gradual, so that the user has
52 // a chance of finding a tab that has quickly "blipped" on and off.
53 const int kIndicatorFadeInDurationMs
= 200;
54 const int kIndicatorFadeOutDurationMs
= 1000;
56 // Animation that throbs in (towards 1.0) and out (towards 0.0), and ends in the
58 class TabRecordingIndicatorAnimation
: public gfx::MultiAnimation
{
60 ~TabRecordingIndicatorAnimation() override
{}
62 // Overridden to provide alternating "towards in" and "towards out" behavior.
63 double GetCurrentValue() const override
;
65 static scoped_ptr
<TabRecordingIndicatorAnimation
> Create();
68 TabRecordingIndicatorAnimation(const gfx::MultiAnimation::Parts
& parts
,
69 const base::TimeDelta interval
)
70 : MultiAnimation(parts
, interval
) {}
72 // Number of times to "toggle throb" the recording and tab capture indicators
73 // when they first appear.
74 static const int kCaptureIndicatorThrobCycles
= 5;
77 double TabRecordingIndicatorAnimation::GetCurrentValue() const {
78 return current_part_index() % 2 ?
79 1.0 - MultiAnimation::GetCurrentValue() :
80 MultiAnimation::GetCurrentValue();
83 scoped_ptr
<TabRecordingIndicatorAnimation
>
84 TabRecordingIndicatorAnimation::Create() {
85 MultiAnimation::Parts parts
;
86 static_assert(kCaptureIndicatorThrobCycles
% 2 != 0,
87 "odd number of cycles required so animation finishes in showing state");
88 for (int i
= 0; i
< kCaptureIndicatorThrobCycles
; ++i
) {
89 parts
.push_back(MultiAnimation::Part(
90 i
% 2 ? kIndicatorFadeOutDurationMs
: kIndicatorFadeInDurationMs
,
91 gfx::Tween::EASE_IN
));
93 const base::TimeDelta interval
=
94 base::TimeDelta::FromMilliseconds(kIndicatorFrameIntervalMs
);
95 scoped_ptr
<TabRecordingIndicatorAnimation
> animation(
96 new TabRecordingIndicatorAnimation(parts
, interval
));
97 animation
->set_continuous(false);
98 return animation
.Pass();
103 bool ShouldTabShowFavicon(int capacity
,
107 TabMediaState media_state
) {
110 int required_capacity
= 1;
111 if (ShouldTabShowCloseButton(capacity
, is_pinned_tab
, is_active_tab
))
113 if (ShouldTabShowMediaIndicator(
114 capacity
, is_pinned_tab
, is_active_tab
, has_favicon
, media_state
)) {
117 return capacity
>= required_capacity
;
120 bool ShouldTabShowMediaIndicator(int capacity
,
124 TabMediaState media_state
) {
125 if (media_state
== TAB_MEDIA_STATE_NONE
)
127 if (ShouldTabShowCloseButton(capacity
, is_pinned_tab
, is_active_tab
))
128 return capacity
>= 2;
129 return capacity
>= 1;
132 bool ShouldTabShowCloseButton(int capacity
,
134 bool is_active_tab
) {
137 else if (is_active_tab
)
140 return capacity
>= 3;
143 TabMediaState
GetTabMediaStateForContents(content::WebContents
* contents
) {
145 return TAB_MEDIA_STATE_NONE
;
147 scoped_refptr
<MediaStreamCaptureIndicator
> indicator
=
148 MediaCaptureDevicesDispatcher::GetInstance()->
149 GetMediaStreamCaptureIndicator();
150 if (indicator
.get()) {
151 if (indicator
->IsBeingMirrored(contents
))
152 return TAB_MEDIA_STATE_CAPTURING
;
153 if (indicator
->IsCapturingUserMedia(contents
))
154 return TAB_MEDIA_STATE_RECORDING
;
157 if (contents
->IsAudioMuted())
158 return TAB_MEDIA_STATE_AUDIO_MUTING
;
159 if (contents
->WasRecentlyAudible())
160 return TAB_MEDIA_STATE_AUDIO_PLAYING
;
162 return TAB_MEDIA_STATE_NONE
;
165 gfx::Image
GetTabMediaIndicatorImage(TabMediaState media_state
,
166 const ui::ThemeProvider
* tp
) {
167 ui::ResourceBundle
& rb
= ui::ResourceBundle::GetSharedInstance();
168 switch (media_state
) {
169 #if !defined(OS_MACOSX)
170 case TAB_MEDIA_STATE_AUDIO_PLAYING
:
171 case TAB_MEDIA_STATE_AUDIO_MUTING
: {
172 SkColor icon_color
= tp
->GetColor(ThemeProperties::COLOR_TAB_ICON
);
174 gfx::CreateVectorIcon(media_state
== TAB_MEDIA_STATE_AUDIO_PLAYING
175 ? gfx::VectorIconId::TAB_AUDIO
176 : gfx::VectorIconId::TAB_AUDIO_MUTING
,
180 case TAB_MEDIA_STATE_AUDIO_PLAYING
:
181 return rb
.GetNativeImageNamed(IDR_TAB_AUDIO_INDICATOR
);
182 case TAB_MEDIA_STATE_AUDIO_MUTING
:
183 return rb
.GetNativeImageNamed(IDR_TAB_AUDIO_MUTING_INDICATOR
);
185 case TAB_MEDIA_STATE_RECORDING
:
186 return rb
.GetNativeImageNamed(IDR_TAB_RECORDING_INDICATOR
);
187 case TAB_MEDIA_STATE_CAPTURING
:
188 return rb
.GetNativeImageNamed(IDR_TAB_CAPTURE_INDICATOR
);
189 case TAB_MEDIA_STATE_NONE
:
193 return rb
.GetNativeImageNamed(IDR_SAD_FAVICON
);
196 gfx::Image
GetTabMediaIndicatorAffordanceImage(TabMediaState media_state
,
197 const ui::ThemeProvider
* tp
) {
198 switch (media_state
) {
199 case TAB_MEDIA_STATE_AUDIO_PLAYING
:
200 return GetTabMediaIndicatorImage(TAB_MEDIA_STATE_AUDIO_MUTING
, tp
);
201 case TAB_MEDIA_STATE_AUDIO_MUTING
:
202 case TAB_MEDIA_STATE_NONE
:
203 case TAB_MEDIA_STATE_RECORDING
:
204 case TAB_MEDIA_STATE_CAPTURING
:
205 return GetTabMediaIndicatorImage(media_state
, tp
);
208 return GetTabMediaIndicatorImage(media_state
, tp
);
211 scoped_ptr
<gfx::Animation
> CreateTabMediaIndicatorFadeAnimation(
212 TabMediaState media_state
) {
213 if (media_state
== TAB_MEDIA_STATE_RECORDING
||
214 media_state
== TAB_MEDIA_STATE_CAPTURING
) {
215 return TabRecordingIndicatorAnimation::Create();
218 // Note: While it seems silly to use a one-part MultiAnimation, it's the only
219 // gfx::Animation implementation that lets us control the frame interval.
220 gfx::MultiAnimation::Parts parts
;
221 const bool is_for_fade_in
= (media_state
!= TAB_MEDIA_STATE_NONE
);
222 parts
.push_back(gfx::MultiAnimation::Part(
223 is_for_fade_in
? kIndicatorFadeInDurationMs
: kIndicatorFadeOutDurationMs
,
224 gfx::Tween::EASE_IN
));
225 const base::TimeDelta interval
=
226 base::TimeDelta::FromMilliseconds(kIndicatorFrameIntervalMs
);
227 scoped_ptr
<gfx::MultiAnimation
> animation(
228 new gfx::MultiAnimation(parts
, interval
));
229 animation
->set_continuous(false);
230 return animation
.Pass();
233 base::string16
AssembleTabTooltipText(const base::string16
& title
,
234 TabMediaState media_state
) {
235 if (media_state
== TAB_MEDIA_STATE_NONE
)
238 base::string16 result
= title
;
240 result
.append(1, '\n');
241 switch (media_state
) {
242 case TAB_MEDIA_STATE_AUDIO_PLAYING
:
244 l10n_util::GetStringUTF16(IDS_TOOLTIP_TAB_MEDIA_STATE_AUDIO_PLAYING
));
246 case TAB_MEDIA_STATE_AUDIO_MUTING
:
248 l10n_util::GetStringUTF16(IDS_TOOLTIP_TAB_MEDIA_STATE_AUDIO_MUTING
));
250 case TAB_MEDIA_STATE_RECORDING
:
252 l10n_util::GetStringUTF16(IDS_TOOLTIP_TAB_MEDIA_STATE_RECORDING
));
254 case TAB_MEDIA_STATE_CAPTURING
:
256 l10n_util::GetStringUTF16(IDS_TOOLTIP_TAB_MEDIA_STATE_CAPTURING
));
258 case TAB_MEDIA_STATE_NONE
:
265 bool AreExperimentalMuteControlsEnabled() {
266 return base::CommandLine::ForCurrentProcess()->HasSwitch(
267 switches::kEnableTabAudioMuting
);
270 bool CanToggleAudioMute(content::WebContents
* contents
) {
271 switch (GetTabMediaStateForContents(contents
)) {
272 case TAB_MEDIA_STATE_NONE
:
273 case TAB_MEDIA_STATE_AUDIO_PLAYING
:
274 case TAB_MEDIA_STATE_AUDIO_MUTING
:
276 case TAB_MEDIA_STATE_RECORDING
:
277 case TAB_MEDIA_STATE_CAPTURING
:
284 TabMutedReason
GetTabAudioMutedReason(content::WebContents
* contents
) {
285 LastMuteMetadata::CreateForWebContents(contents
); // Ensures metadata exists.
286 LastMuteMetadata
* const metadata
=
287 LastMuteMetadata::FromWebContents(contents
);
288 if (GetTabMediaStateForContents(contents
) == TAB_MEDIA_STATE_CAPTURING
) {
289 // For tab capture, libcontent forces muting off.
290 metadata
->reason
= TAB_MUTED_REASON_MEDIA_CAPTURE
;
291 metadata
->extension_id
.clear();
293 return metadata
->reason
;
296 const std::string
& GetExtensionIdForMutedTab(content::WebContents
* contents
) {
297 DCHECK_EQ(GetTabAudioMutedReason(contents
) != TAB_MUTED_REASON_EXTENSION
,
298 LastMuteMetadata::FromWebContents(contents
)->extension_id
.empty());
299 return LastMuteMetadata::FromWebContents(contents
)->extension_id
;
302 TabMutedResult
SetTabAudioMuted(content::WebContents
* contents
,
304 TabMutedReason reason
,
305 const std::string
& extension_id
) {
307 DCHECK_NE(TAB_MUTED_REASON_NONE
, reason
);
309 if (reason
== TAB_MUTED_REASON_AUDIO_INDICATOR
&&
310 !AreExperimentalMuteControlsEnabled()) {
311 return TAB_MUTED_RESULT_FAIL_NOT_ENABLED
;
314 if (!chrome::CanToggleAudioMute(contents
))
315 return TAB_MUTED_RESULT_FAIL_TABCAPTURE
;
317 contents
->SetAudioMuted(mute
);
319 LastMuteMetadata::CreateForWebContents(contents
); // Ensures metadata exists.
320 LastMuteMetadata
* const metadata
=
321 LastMuteMetadata::FromWebContents(contents
);
322 metadata
->reason
= reason
;
323 if (reason
== TAB_MUTED_REASON_EXTENSION
) {
324 DCHECK(!extension_id
.empty());
325 metadata
->extension_id
= extension_id
;
327 metadata
->extension_id
.clear();
330 return TAB_MUTED_RESULT_SUCCESS
;
333 void UnmuteIfMutedByExtension(content::WebContents
* contents
,
334 const std::string
& extension_id
) {
335 LastMuteMetadata::CreateForWebContents(contents
); // Ensures metadata exists.
336 LastMuteMetadata
* const metadata
=
337 LastMuteMetadata::FromWebContents(contents
);
338 if (metadata
->reason
== TAB_MUTED_REASON_EXTENSION
&&
339 metadata
->extension_id
== extension_id
) {
340 SetTabAudioMuted(contents
, false, TAB_MUTED_REASON_EXTENSION
, extension_id
);
344 bool AreAllTabsMuted(const TabStripModel
& tab_strip
,
345 const std::vector
<int>& indices
) {
346 for (std::vector
<int>::const_iterator i
= indices
.begin(); i
!= indices
.end();
348 if (!tab_strip
.GetWebContentsAt(*i
)->IsAudioMuted())
354 } // namespace chrome