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/media/media_stream_capture_indicator.h"
8 #include "base/i18n/rtl.h"
9 #include "base/logging.h"
10 #include "base/memory/scoped_ptr.h"
11 #include "base/prefs/pref_service.h"
12 #include "base/utf_string_conversions.h"
13 #include "chrome/app/chrome_command_ids.h"
14 #include "chrome/browser/browser_process.h"
15 #include "chrome/browser/extensions/extension_service.h"
16 #include "chrome/browser/extensions/image_loader.h"
17 #include "chrome/browser/profiles/profile.h"
18 #include "chrome/browser/status_icons/status_icon.h"
19 #include "chrome/browser/status_icons/status_tray.h"
20 #include "chrome/browser/tab_contents/tab_util.h"
21 #include "chrome/browser/ui/screen_capture_notification_ui.h"
22 #include "chrome/common/extensions/extension.h"
23 #include "chrome/common/extensions/manifest_handlers/icons_handler.h"
24 #include "chrome/common/pref_names.h"
25 #include "content/public/browser/browser_thread.h"
26 #include "content/public/browser/content_browser_client.h"
27 #include "content/public/browser/invalidate_type.h"
28 #include "content/public/browser/web_contents.h"
29 #include "content/public/browser/web_contents_delegate.h"
30 #include "content/public/browser/web_contents_observer.h"
31 #include "grit/chromium_strings.h"
32 #include "grit/generated_resources.h"
33 #include "grit/theme_resources.h"
34 #include "net/base/net_util.h"
35 #include "ui/base/l10n/l10n_util.h"
36 #include "ui/base/resource/resource_bundle.h"
37 #include "ui/gfx/image/image_skia.h"
39 using content::BrowserThread
;
40 using content::WebContents
;
44 const extensions::Extension
* GetExtension(WebContents
* web_contents
) {
45 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI
));
51 Profile::FromBrowserContext(web_contents
->GetBrowserContext());
55 ExtensionService
* extension_service
= profile
->GetExtensionService();
56 if (!extension_service
)
59 return extension_service
->extensions()->GetExtensionOrAppByURL(
60 ExtensionURLInfo(web_contents
->GetURL()));
63 // Gets the security originator of the tab. It returns a string with no '/'
64 // at the end to display in the UI.
65 string16
GetSecurityOrigin(WebContents
* web_contents
) {
66 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI
));
71 std::string security_origin
= web_contents
->GetURL().GetOrigin().spec();
73 // Remove the last character if it is a '/'.
74 if (!security_origin
.empty()) {
75 std::string::iterator it
= security_origin
.end() - 1;
77 security_origin
.erase(it
);
80 return UTF8ToUTF16(security_origin
);
83 string16
GetTitle(WebContents
* web_contents
) {
84 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI
));
89 const extensions::Extension
* const extension
= GetExtension(web_contents
);
91 return UTF8ToUTF16(extension
->name());
93 string16 tab_title
= web_contents
->GetTitle();
95 if (tab_title
.empty()) {
96 // If the page's title is empty use its security originator.
97 tab_title
= GetSecurityOrigin(web_contents
);
99 // If the page's title matches its URL, use its security originator.
101 Profile::FromBrowserContext(web_contents
->GetBrowserContext());
102 std::string languages
=
103 profile
->GetPrefs()->GetString(prefs::kAcceptLanguages
);
104 if (tab_title
== net::FormatUrl(web_contents
->GetURL(), languages
))
105 tab_title
= GetSecurityOrigin(web_contents
);
113 // Stores usage counts for all the capture devices associated with a single
114 // WebContents instance, and observes for the destruction of the WebContents
116 class MediaStreamCaptureIndicator::WebContentsDeviceUsage
117 : protected content::WebContentsObserver
{
119 explicit WebContentsDeviceUsage(WebContents
* web_contents
);
121 bool IsWebContentsDestroyed() const { return web_contents() == NULL
; }
123 bool IsCapturingAudio() const { return audio_ref_count_
> 0; }
124 bool IsCapturingVideo() const { return video_ref_count_
> 0; }
125 bool IsMirroring() const { return mirroring_ref_count_
> 0; }
126 bool IsCapturingScreen() const { return screen_capture_ref_count_
> 0; }
127 const base::Closure
& StopScreenCaptureCallback() const {
128 return stop_screen_capture_callback_
;
131 // Increment ref-counts up based on the type of each device provided. The
132 // return value is the message ID for the balloon body to show, or zero if the
133 // balloon should not be shown.
134 int AddDevices(const content::MediaStreamDevices
& devices
,
135 const base::Closure
& close_callback
);
137 // Decrement ref-counts up based on the type of each device provided.
138 void RemoveDevices(const content::MediaStreamDevices
& devices
);
141 int audio_ref_count_
;
142 int video_ref_count_
;
143 int mirroring_ref_count_
;
144 int screen_capture_ref_count_
;
145 base::Closure stop_screen_capture_callback_
;
147 DISALLOW_COPY_AND_ASSIGN(WebContentsDeviceUsage
);
150 MediaStreamCaptureIndicator::WebContentsDeviceUsage::WebContentsDeviceUsage(
151 WebContents
* web_contents
)
152 : content::WebContentsObserver(web_contents
),
155 mirroring_ref_count_(0),
156 screen_capture_ref_count_(0) {
159 int MediaStreamCaptureIndicator::WebContentsDeviceUsage::AddDevices(
160 const content::MediaStreamDevices
& devices
,
161 const base::Closure
& close_callback
) {
162 bool incremented_audio_count
= false;
163 bool incremented_video_count
= false;
164 for (content::MediaStreamDevices::const_iterator it
= devices
.begin();
165 it
!= devices
.end(); ++it
) {
166 if (it
->type
== content::MEDIA_TAB_AUDIO_CAPTURE
||
167 it
->type
== content::MEDIA_TAB_VIDEO_CAPTURE
) {
168 ++mirroring_ref_count_
;
169 } else if (it
->type
== content::MEDIA_SCREEN_VIDEO_CAPTURE
) {
170 ++screen_capture_ref_count_
;
171 stop_screen_capture_callback_
= close_callback
;
172 } else if (content::IsAudioMediaType(it
->type
)) {
174 } else if (content::IsVideoMediaType(it
->type
)) {
181 if (incremented_audio_count
&& incremented_video_count
)
182 return IDS_MEDIA_STREAM_STATUS_TRAY_BALLOON_BODY_AUDIO_AND_VIDEO
;
183 else if (incremented_audio_count
)
184 return IDS_MEDIA_STREAM_STATUS_TRAY_BALLOON_BODY_AUDIO_ONLY
;
185 else if (incremented_video_count
)
186 return IDS_MEDIA_STREAM_STATUS_TRAY_BALLOON_BODY_VIDEO_ONLY
;
191 void MediaStreamCaptureIndicator::WebContentsDeviceUsage::RemoveDevices(
192 const content::MediaStreamDevices
& devices
) {
193 for (content::MediaStreamDevices::const_iterator it
= devices
.begin();
194 it
!= devices
.end(); ++it
) {
195 if (it
->type
== content::MEDIA_TAB_AUDIO_CAPTURE
||
196 it
->type
== content::MEDIA_TAB_VIDEO_CAPTURE
) {
197 --mirroring_ref_count_
;
198 } else if (it
->type
== content::MEDIA_SCREEN_VIDEO_CAPTURE
) {
199 --screen_capture_ref_count_
;
200 } else if (content::IsAudioMediaType(it
->type
)) {
202 } else if (content::IsVideoMediaType(it
->type
)) {
209 DCHECK_GE(audio_ref_count_
, 0);
210 DCHECK_GE(video_ref_count_
, 0);
211 DCHECK_GE(mirroring_ref_count_
, 0);
212 DCHECK_GE(screen_capture_ref_count_
, 0);
215 MediaStreamCaptureIndicator::MediaStreamCaptureIndicator()
216 : status_icon_(NULL
),
219 balloon_image_(NULL
),
220 should_show_balloon_(false) {
223 MediaStreamCaptureIndicator::~MediaStreamCaptureIndicator() {
224 // The user is responsible for cleaning up by reporting the closure of any
225 // opened devices. However, there exists a race condition at shutdown: The UI
226 // thread may be stopped before CaptureDevicesClosed() posts the task to
227 // invoke DoDevicesClosedOnUIThread(). In this case, usage_map_ won't be
228 // empty like it should.
229 DCHECK(usage_map_
.empty() ||
230 !BrowserThread::IsMessageLoopValid(BrowserThread::UI
));
232 // Free any WebContentsDeviceUsage objects left over.
233 for (UsageMap::const_iterator it
= usage_map_
.begin(); it
!= usage_map_
.end();
239 bool MediaStreamCaptureIndicator::IsCommandIdChecked(
240 int command_id
) const {
241 NOTIMPLEMENTED() << "There are no checked items in the MediaStream menu.";
245 bool MediaStreamCaptureIndicator::IsCommandIdEnabled(
246 int command_id
) const {
247 return command_id
!= IDC_MinimumLabelValue
;
250 bool MediaStreamCaptureIndicator::GetAcceleratorForCommandId(
251 int command_id
, ui::Accelerator
* accelerator
) {
252 // No accelerators for status icon context menu.
256 void MediaStreamCaptureIndicator::ExecuteCommand(int command_id
,
258 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI
));
261 command_id
- IDC_MEDIA_CONTEXT_MEDIA_STREAM_CAPTURE_LIST_FIRST
;
263 DCHECK_GT(static_cast<int>(command_targets_
.size()), index
);
264 WebContents
* const web_contents
= command_targets_
[index
];
265 UsageMap::const_iterator it
= usage_map_
.find(web_contents
);
266 if (it
== usage_map_
.end() || it
->second
->IsWebContentsDestroyed())
268 web_contents
->GetDelegate()->ActivateContents(web_contents
);
271 void MediaStreamCaptureIndicator::CaptureDevicesOpened(
272 int render_process_id
,
274 const content::MediaStreamDevices
& devices
,
275 const base::Closure
& close_callback
) {
276 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::IO
));
277 DCHECK(!devices
.empty());
279 BrowserThread::PostTask(
280 BrowserThread::UI
, FROM_HERE
,
281 base::Bind(&MediaStreamCaptureIndicator::AddCaptureDevices
,
282 this, render_process_id
, render_view_id
, devices
,
286 void MediaStreamCaptureIndicator::CaptureDevicesClosed(
287 int render_process_id
,
289 const content::MediaStreamDevices
& devices
) {
290 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::IO
));
291 DCHECK(!devices
.empty());
293 BrowserThread::PostTask(
294 BrowserThread::UI
, FROM_HERE
,
295 base::Bind(&MediaStreamCaptureIndicator::RemoveCaptureDevices
,
296 this, render_process_id
, render_view_id
, devices
));
300 bool MediaStreamCaptureIndicator::IsCapturingUserMedia(
301 content::WebContents
* web_contents
) const {
302 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI
));
304 UsageMap::const_iterator it
= usage_map_
.find(web_contents
);
305 return (it
!= usage_map_
.end() &&
306 (it
->second
->IsCapturingAudio() || it
->second
->IsCapturingVideo()));
309 bool MediaStreamCaptureIndicator::IsBeingMirrored(
310 content::WebContents
* web_contents
) const {
311 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI
));
313 UsageMap::const_iterator it
= usage_map_
.find(web_contents
);
314 return it
!= usage_map_
.end() && it
->second
->IsMirroring();
317 void MediaStreamCaptureIndicator::MaybeCreateStatusTrayIcon() {
318 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI
));
322 // If there is no browser process, we should not create the status tray.
323 if (!g_browser_process
)
326 StatusTray
* status_tray
= g_browser_process
->status_tray();
330 status_icon_
= status_tray
->CreateStatusIcon();
332 EnsureStatusTrayIconResources();
335 void MediaStreamCaptureIndicator::EnsureStatusTrayIconResources() {
336 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI
));
338 mic_image_
= ResourceBundle::GetSharedInstance().GetImageSkiaNamed(
339 IDR_INFOBAR_MEDIA_STREAM_MIC
);
341 if (!camera_image_
) {
342 camera_image_
= ResourceBundle::GetSharedInstance().GetImageSkiaNamed(
343 IDR_INFOBAR_MEDIA_STREAM_CAMERA
);
345 if (!balloon_image_
) {
346 balloon_image_
= ResourceBundle::GetSharedInstance().GetImageSkiaNamed(
347 IDR_PRODUCT_LOGO_32
);
350 DCHECK(camera_image_
);
351 DCHECK(balloon_image_
);
354 void MediaStreamCaptureIndicator::ShowBalloon(
355 WebContents
* web_contents
, int balloon_body_message_id
) {
356 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI
));
357 DCHECK_NE(0, balloon_body_message_id
);
359 // Only show the balloon for extensions.
360 const extensions::Extension
* const extension
= GetExtension(web_contents
);
362 DVLOG(1) << "Balloon is shown only for extensions";
367 l10n_util::GetStringFUTF16(balloon_body_message_id
,
368 UTF8ToUTF16(extension
->name()));
370 Profile::FromBrowserContext(web_contents
->GetBrowserContext());
372 should_show_balloon_
= true;
373 extensions::ImageLoader::Get(profile
)->LoadImageAsync(
375 extensions::IconsInfo::GetIconResource(
376 extension
, 32, ExtensionIconSet::MATCH_BIGGER
),
378 base::Bind(&MediaStreamCaptureIndicator::OnImageLoaded
,
382 void MediaStreamCaptureIndicator::OnImageLoaded(
383 const string16
& message
,
384 const gfx::Image
& image
) {
385 if (!should_show_balloon_
|| !status_icon_
)
388 const gfx::ImageSkia
* image_skia
= !image
.IsEmpty() ? image
.ToImageSkia() :
389 ui::ResourceBundle::GetSharedInstance().GetImageSkiaNamed(
390 IDR_APP_DEFAULT_ICON
);
391 status_icon_
->DisplayBalloon(*image_skia
, string16(), message
);
394 void MediaStreamCaptureIndicator::MaybeDestroyStatusTrayIcon() {
395 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI
));
397 // Make sure images that finish loading don't cause a balloon to be shown.
398 should_show_balloon_
= false;
403 // If there is no browser process, we should not do anything.
404 if (!g_browser_process
)
407 StatusTray
* status_tray
= g_browser_process
->status_tray();
408 if (status_tray
!= NULL
) {
409 status_tray
->RemoveStatusIcon(status_icon_
);
414 void MediaStreamCaptureIndicator::UpdateNotificationUserInterface() {
415 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI
));
416 scoped_ptr
<ui::SimpleMenuModel
> menu(new ui::SimpleMenuModel(this));
420 int command_id
= IDC_MEDIA_CONTEXT_MEDIA_STREAM_CAPTURE_LIST_FIRST
;
421 command_targets_
.clear();
423 WebContents
* screen_capturer
= NULL
;
424 base::Closure close_callback
;
426 for (UsageMap::const_iterator iter
= usage_map_
.begin();
427 iter
!= usage_map_
.end(); ++iter
) {
428 // Check if any audio and video devices have been used.
429 const WebContentsDeviceUsage
& usage
= *iter
->second
;
430 WebContents
* const web_contents
= iter
->first
;
431 if (usage
.IsWebContentsDestroyed()) {
432 // We only show the tray icon for extensions that have not been
433 // destroyed and are capturing audio or video.
437 if (usage
.IsCapturingScreen()) {
438 DCHECK(!screen_capturer
);
439 screen_capturer
= web_contents
;
440 close_callback
= usage
.StopScreenCaptureCallback();
443 // Audio/video icon is shown only for extensions. For regular tabs, we show
444 // an indicator in the tab icon.
445 if (GetExtension(web_contents
) &&
446 (usage
.IsCapturingAudio() || usage
.IsCapturingVideo())) {
447 audio
= audio
|| usage
.IsCapturingAudio();
448 video
= video
|| usage
.IsCapturingVideo();
450 command_targets_
.push_back(web_contents
);
451 menu
->AddItem(command_id
, GetTitle(web_contents
));
453 // If reaching the maximum number, no more item will be added to the menu.
454 if (command_id
== IDC_MEDIA_CONTEXT_MEDIA_STREAM_CAPTURE_LIST_LAST
)
460 if (screen_capturer
) {
461 if (!screen_capture_notification_
) {
462 screen_capture_notification_
= ScreenCaptureNotificationUI::Create();
463 if (!screen_capture_notification_
->Show(
464 base::Bind(&MediaStreamCaptureIndicator::OnStopScreenCapture
,
465 this, close_callback
),
466 GetTitle(screen_capturer
))) {
467 OnStopScreenCapture(close_callback
);
468 screen_capture_notification_
.reset();
472 screen_capture_notification_
.reset();
475 if (command_targets_
.empty()) {
476 MaybeDestroyStatusTrayIcon();
480 // The icon will take the ownership of the passed context menu.
481 MaybeCreateStatusTrayIcon();
483 status_icon_
->SetContextMenu(menu
.release());
484 UpdateStatusTrayIconDisplay(audio
, video
);
488 void MediaStreamCaptureIndicator::UpdateStatusTrayIconDisplay(
489 bool audio
, bool video
) {
490 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI
));
491 DCHECK(audio
|| video
);
492 DCHECK(status_icon_
);
494 if (audio
&& video
) {
495 message_id
= IDS_MEDIA_STREAM_STATUS_TRAY_TEXT_AUDIO_AND_VIDEO
;
496 status_icon_
->SetImage(*camera_image_
);
497 } else if (audio
&& !video
) {
498 message_id
= IDS_MEDIA_STREAM_STATUS_TRAY_TEXT_AUDIO_ONLY
;
499 status_icon_
->SetImage(*mic_image_
);
500 } else if (!audio
&& video
) {
501 message_id
= IDS_MEDIA_STREAM_STATUS_TRAY_TEXT_VIDEO_ONLY
;
502 status_icon_
->SetImage(*camera_image_
);
505 status_icon_
->SetToolTip(l10n_util::GetStringFUTF16(
506 message_id
, l10n_util::GetStringUTF16(IDS_PRODUCT_NAME
)));
509 WebContents
* MediaStreamCaptureIndicator::LookUpByKnownAlias(
510 int render_process_id
, int render_view_id
) const {
511 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI
));
513 WebContents
* result
=
514 tab_util::GetWebContentsByID(render_process_id
, render_view_id
);
516 const RenderViewIDs
key(render_process_id
, render_view_id
);
517 AliasMap::const_iterator it
= aliases_
.find(key
);
518 if (it
!= aliases_
.end())
524 void MediaStreamCaptureIndicator::AddCaptureDevices(
525 int render_process_id
,
527 const content::MediaStreamDevices
& devices
,
528 const base::Closure
& close_callback
) {
529 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI
));
531 WebContents
* const web_contents
=
532 LookUpByKnownAlias(render_process_id
, render_view_id
);
536 // Increase the usage ref-counts.
537 WebContentsDeviceUsage
*& usage
= usage_map_
[web_contents
];
539 usage
= new WebContentsDeviceUsage(web_contents
);
540 const int balloon_body_message_id
=
541 usage
->AddDevices(devices
, close_callback
);
543 // Keep track of the IDs as a known alias to the WebContents instance.
544 const AliasMap::iterator insert_it
= aliases_
.insert(
545 make_pair(RenderViewIDs(render_process_id
, render_view_id
),
546 web_contents
)).first
;
547 DCHECK_EQ(web_contents
, insert_it
->second
)
548 << "BUG: IDs refer to two different WebContents instances.";
550 web_contents
->NotifyNavigationStateChanged(content::INVALIDATE_TYPE_TAB
);
552 UpdateNotificationUserInterface();
553 if (balloon_body_message_id
)
554 ShowBalloon(web_contents
, balloon_body_message_id
);
557 void MediaStreamCaptureIndicator::RemoveCaptureDevices(
558 int render_process_id
,
560 const content::MediaStreamDevices
& devices
) {
561 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI
));
563 WebContents
* const web_contents
=
564 LookUpByKnownAlias(render_process_id
, render_view_id
);
568 // Decrease the usage ref-counts.
569 const UsageMap::iterator it
= usage_map_
.find(web_contents
);
570 if (it
== usage_map_
.end()) {
571 DLOG(FATAL
) << "BUG: Attempt to remove devices more than once.";
574 WebContentsDeviceUsage
* const usage
= it
->second
;
575 usage
->RemoveDevices(devices
);
577 if (!usage
->IsWebContentsDestroyed())
578 web_contents
->NotifyNavigationStateChanged(content::INVALIDATE_TYPE_TAB
);
580 // Remove the usage and alias mappings if all the devices have been closed.
581 if (!usage
->IsCapturingAudio() && !usage
->IsCapturingVideo() &&
582 !usage
->IsMirroring() && !usage
->IsCapturingScreen()) {
583 for (AliasMap::iterator alias_it
= aliases_
.begin();
584 alias_it
!= aliases_
.end(); ) {
585 if (alias_it
->second
== web_contents
)
586 aliases_
.erase(alias_it
++);
591 usage_map_
.erase(it
);
594 UpdateNotificationUserInterface();
597 void MediaStreamCaptureIndicator::OnStopScreenCapture(
598 const base::Closure
& stop
) {
599 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI
));