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"
9 #include "base/logging.h"
10 #include "base/memory/scoped_ptr.h"
11 #include "base/prefs/pref_service.h"
12 #include "base/strings/utf_string_conversions.h"
13 #include "chrome/app/chrome_command_ids.h"
14 #include "chrome/browser/browser_process.h"
15 #include "chrome/browser/profiles/profile.h"
16 #include "chrome/browser/status_icons/status_icon.h"
17 #include "chrome/browser/status_icons/status_tray.h"
18 #include "chrome/browser/tab_contents/tab_util.h"
19 #include "chrome/common/pref_names.h"
20 #include "chrome/grit/chromium_strings.h"
21 #include "content/public/browser/browser_thread.h"
22 #include "content/public/browser/content_browser_client.h"
23 #include "content/public/browser/web_contents.h"
24 #include "content/public/browser/web_contents_delegate.h"
25 #include "content/public/browser/web_contents_observer.h"
26 #include "grit/theme_resources.h"
27 #include "net/base/net_util.h"
28 #include "ui/base/l10n/l10n_util.h"
29 #include "ui/base/resource/resource_bundle.h"
30 #include "ui/gfx/image/image_skia.h"
32 #if defined(ENABLE_EXTENSIONS)
33 #include "chrome/common/extensions/extension_constants.h"
34 #include "extensions/browser/extension_registry.h"
35 #include "extensions/common/extension.h"
38 using content::BrowserThread
;
39 using content::WebContents
;
43 #if defined(ENABLE_EXTENSIONS)
44 const extensions::Extension
* GetExtension(WebContents
* web_contents
) {
45 DCHECK_CURRENTLY_ON(BrowserThread::UI
);
50 extensions::ExtensionRegistry
* registry
=
51 extensions::ExtensionRegistry::Get(web_contents
->GetBrowserContext());
52 return registry
->enabled_extensions().GetExtensionOrAppByURL(
53 web_contents
->GetURL());
56 bool IsWhitelistedExtension(const extensions::Extension
* extension
) {
57 DCHECK_CURRENTLY_ON(BrowserThread::UI
);
59 static const char* const kExtensionWhitelist
[] = {
60 extension_misc::kHotwordNewExtensionId
,
63 for (size_t i
= 0; i
< arraysize(kExtensionWhitelist
); ++i
) {
64 if (extension
->id() == kExtensionWhitelist
[i
])
70 #endif // defined(ENABLE_EXTENSIONS)
72 // Gets the security originator of the tab. It returns a string with no '/'
73 // at the end to display in the UI.
74 base::string16
GetSecurityOrigin(WebContents
* web_contents
) {
75 DCHECK_CURRENTLY_ON(BrowserThread::UI
);
78 return base::string16();
80 std::string security_origin
= web_contents
->GetURL().GetOrigin().spec();
82 // Remove the last character if it is a '/'.
83 if (!security_origin
.empty()) {
84 std::string::iterator it
= security_origin
.end() - 1;
86 security_origin
.erase(it
);
89 return base::UTF8ToUTF16(security_origin
);
92 base::string16
GetTitle(WebContents
* web_contents
) {
93 DCHECK_CURRENTLY_ON(BrowserThread::UI
);
96 return base::string16();
98 #if defined(ENABLE_EXTENSIONS)
99 const extensions::Extension
* const extension
= GetExtension(web_contents
);
101 return base::UTF8ToUTF16(extension
->name());
104 base::string16 tab_title
= web_contents
->GetTitle();
106 if (tab_title
.empty()) {
107 // If the page's title is empty use its security originator.
108 tab_title
= GetSecurityOrigin(web_contents
);
110 // If the page's title matches its URL, use its security originator.
112 Profile::FromBrowserContext(web_contents
->GetBrowserContext());
113 std::string languages
=
114 profile
->GetPrefs()->GetString(prefs::kAcceptLanguages
);
115 if (tab_title
== net::FormatUrl(web_contents
->GetURL(), languages
))
116 tab_title
= GetSecurityOrigin(web_contents
);
124 // Stores usage counts for all the capture devices associated with a single
125 // WebContents instance. Instances of this class are owned by
126 // MediaStreamCaptureIndicator. They also observe for the destruction of their
127 // corresponding WebContents and trigger their own deletion from their
128 // MediaStreamCaptureIndicator.
129 class MediaStreamCaptureIndicator::WebContentsDeviceUsage
130 : public content::WebContentsObserver
{
132 WebContentsDeviceUsage(scoped_refptr
<MediaStreamCaptureIndicator
> indicator
,
133 WebContents
* web_contents
)
134 : WebContentsObserver(web_contents
),
135 indicator_(indicator
),
138 mirroring_ref_count_(0),
139 weak_factory_(this) {
142 bool IsCapturingAudio() const { return audio_ref_count_
> 0; }
143 bool IsCapturingVideo() const { return video_ref_count_
> 0; }
144 bool IsMirroring() const { return mirroring_ref_count_
> 0; }
146 scoped_ptr
<content::MediaStreamUI
> RegisterMediaStream(
147 const content::MediaStreamDevices
& devices
);
149 // Increment ref-counts up based on the type of each device provided.
150 void AddDevices(const content::MediaStreamDevices
& devices
);
152 // Decrement ref-counts up based on the type of each device provided.
153 void RemoveDevices(const content::MediaStreamDevices
& devices
);
156 // content::WebContentsObserver overrides.
157 void WebContentsDestroyed() override
{
158 indicator_
->UnregisterWebContents(web_contents());
161 scoped_refptr
<MediaStreamCaptureIndicator
> indicator_
;
162 int audio_ref_count_
;
163 int video_ref_count_
;
164 int mirroring_ref_count_
;
166 base::WeakPtrFactory
<WebContentsDeviceUsage
> weak_factory_
;
168 DISALLOW_COPY_AND_ASSIGN(WebContentsDeviceUsage
);
171 // Implements MediaStreamUI interface. Instances of this class are created for
172 // each MediaStream and their ownership is passed to MediaStream implementation
173 // in the content layer. Each UIDelegate keeps a weak pointer to the
174 // corresponding WebContentsDeviceUsage object to deliver updates about state of
176 class MediaStreamCaptureIndicator::UIDelegate
: public content::MediaStreamUI
{
178 UIDelegate(base::WeakPtr
<WebContentsDeviceUsage
> device_usage
,
179 const content::MediaStreamDevices
& devices
)
180 : device_usage_(device_usage
),
183 DCHECK(!devices_
.empty());
186 ~UIDelegate() override
{
187 if (started_
&& device_usage_
.get())
188 device_usage_
->RemoveDevices(devices_
);
192 // content::MediaStreamUI interface.
193 gfx::NativeViewId
OnStarted(const base::Closure
& close_callback
) override
{
196 if (device_usage_
.get())
197 device_usage_
->AddDevices(devices_
);
201 base::WeakPtr
<WebContentsDeviceUsage
> device_usage_
;
202 content::MediaStreamDevices devices_
;
205 DISALLOW_COPY_AND_ASSIGN(UIDelegate
);
209 scoped_ptr
<content::MediaStreamUI
>
210 MediaStreamCaptureIndicator::WebContentsDeviceUsage::RegisterMediaStream(
211 const content::MediaStreamDevices
& devices
) {
212 return make_scoped_ptr(new UIDelegate( weak_factory_
.GetWeakPtr(), devices
));
215 void MediaStreamCaptureIndicator::WebContentsDeviceUsage::AddDevices(
216 const content::MediaStreamDevices
& devices
) {
217 for (content::MediaStreamDevices::const_iterator it
= devices
.begin();
218 it
!= devices
.end(); ++it
) {
219 if (it
->type
== content::MEDIA_TAB_AUDIO_CAPTURE
||
220 it
->type
== content::MEDIA_TAB_VIDEO_CAPTURE
) {
221 ++mirroring_ref_count_
;
222 } else if (content::IsAudioInputMediaType(it
->type
)) {
224 } else if (content::IsVideoMediaType(it
->type
)) {
232 web_contents()->NotifyNavigationStateChanged(content::INVALIDATE_TYPE_TAB
);
234 indicator_
->UpdateNotificationUserInterface();
237 void MediaStreamCaptureIndicator::WebContentsDeviceUsage::RemoveDevices(
238 const content::MediaStreamDevices
& devices
) {
239 for (content::MediaStreamDevices::const_iterator it
= devices
.begin();
240 it
!= devices
.end(); ++it
) {
241 if (it
->type
== content::MEDIA_TAB_AUDIO_CAPTURE
||
242 it
->type
== content::MEDIA_TAB_VIDEO_CAPTURE
) {
243 --mirroring_ref_count_
;
244 } else if (content::IsAudioInputMediaType(it
->type
)) {
246 } else if (content::IsVideoMediaType(it
->type
)) {
253 DCHECK_GE(audio_ref_count_
, 0);
254 DCHECK_GE(video_ref_count_
, 0);
255 DCHECK_GE(mirroring_ref_count_
, 0);
257 web_contents()->NotifyNavigationStateChanged(content::INVALIDATE_TYPE_TAB
);
258 indicator_
->UpdateNotificationUserInterface();
261 MediaStreamCaptureIndicator::MediaStreamCaptureIndicator()
262 : status_icon_(NULL
),
264 camera_image_(NULL
) {
267 MediaStreamCaptureIndicator::~MediaStreamCaptureIndicator() {
268 // The user is responsible for cleaning up by reporting the closure of any
269 // opened devices. However, there exists a race condition at shutdown: The UI
270 // thread may be stopped before CaptureDevicesClosed() posts the task to
271 // invoke DoDevicesClosedOnUIThread(). In this case, usage_map_ won't be
272 // empty like it should.
273 DCHECK(usage_map_
.empty() ||
274 !BrowserThread::IsMessageLoopValid(BrowserThread::UI
));
277 scoped_ptr
<content::MediaStreamUI
>
278 MediaStreamCaptureIndicator::RegisterMediaStream(
279 content::WebContents
* web_contents
,
280 const content::MediaStreamDevices
& devices
) {
281 WebContentsDeviceUsage
* usage
= usage_map_
.get(web_contents
);
283 usage
= new WebContentsDeviceUsage(this, web_contents
);
284 usage_map_
.add(web_contents
, make_scoped_ptr(usage
));
286 return usage
->RegisterMediaStream(devices
);
289 void MediaStreamCaptureIndicator::ExecuteCommand(int command_id
,
291 DCHECK_CURRENTLY_ON(BrowserThread::UI
);
294 command_id
- IDC_MEDIA_CONTEXT_MEDIA_STREAM_CAPTURE_LIST_FIRST
;
296 DCHECK_GT(static_cast<int>(command_targets_
.size()), index
);
297 WebContents
* web_contents
= command_targets_
[index
];
298 if (ContainsKey(usage_map_
, web_contents
))
299 web_contents
->GetDelegate()->ActivateContents(web_contents
);
302 bool MediaStreamCaptureIndicator::IsCapturingUserMedia(
303 content::WebContents
* web_contents
) const {
304 DCHECK_CURRENTLY_ON(BrowserThread::UI
);
306 WebContentsDeviceUsage
* usage
= usage_map_
.get(web_contents
);
307 return usage
&& (usage
->IsCapturingAudio() || usage
->IsCapturingVideo());
310 bool MediaStreamCaptureIndicator::IsCapturingVideo(
311 content::WebContents
* web_contents
) const {
312 DCHECK_CURRENTLY_ON(BrowserThread::UI
);
314 WebContentsDeviceUsage
* usage
= usage_map_
.get(web_contents
);
315 return usage
&& usage
->IsCapturingVideo();
318 bool MediaStreamCaptureIndicator::IsCapturingAudio(
319 content::WebContents
* web_contents
) const {
320 DCHECK_CURRENTLY_ON(BrowserThread::UI
);
322 WebContentsDeviceUsage
* usage
= usage_map_
.get(web_contents
);
323 return usage
&& usage
->IsCapturingAudio();
326 bool MediaStreamCaptureIndicator::IsBeingMirrored(
327 content::WebContents
* web_contents
) const {
328 DCHECK_CURRENTLY_ON(BrowserThread::UI
);
330 WebContentsDeviceUsage
* usage
= usage_map_
.get(web_contents
);
331 return usage
&& usage
->IsMirroring();
334 void MediaStreamCaptureIndicator::UnregisterWebContents(
335 WebContents
* web_contents
) {
336 usage_map_
.erase(web_contents
);
337 UpdateNotificationUserInterface();
340 void MediaStreamCaptureIndicator::MaybeCreateStatusTrayIcon(bool audio
,
342 DCHECK_CURRENTLY_ON(BrowserThread::UI
);
347 // If there is no browser process, we should not create the status tray.
348 if (!g_browser_process
)
351 StatusTray
* status_tray
= g_browser_process
->status_tray();
355 EnsureStatusTrayIconResources();
357 gfx::ImageSkia image
;
358 base::string16 tool_tip
;
359 GetStatusTrayIconInfo(audio
, video
, &image
, &tool_tip
);
360 DCHECK(!image
.isNull());
361 DCHECK(!tool_tip
.empty());
363 status_icon_
= status_tray
->CreateStatusIcon(
364 StatusTray::MEDIA_STREAM_CAPTURE_ICON
, image
, tool_tip
);
367 void MediaStreamCaptureIndicator::EnsureStatusTrayIconResources() {
368 DCHECK_CURRENTLY_ON(BrowserThread::UI
);
371 mic_image_
= ResourceBundle::GetSharedInstance().GetImageSkiaNamed(
372 IDR_INFOBAR_MEDIA_STREAM_MIC
);
374 if (!camera_image_
) {
375 camera_image_
= ResourceBundle::GetSharedInstance().GetImageSkiaNamed(
376 IDR_INFOBAR_MEDIA_STREAM_CAMERA
);
379 DCHECK(camera_image_
);
382 void MediaStreamCaptureIndicator::MaybeDestroyStatusTrayIcon() {
383 DCHECK_CURRENTLY_ON(BrowserThread::UI
);
388 // If there is no browser process, we should not do anything.
389 if (!g_browser_process
)
392 StatusTray
* status_tray
= g_browser_process
->status_tray();
393 if (status_tray
!= NULL
) {
394 status_tray
->RemoveStatusIcon(status_icon_
);
399 void MediaStreamCaptureIndicator::UpdateNotificationUserInterface() {
400 DCHECK_CURRENTLY_ON(BrowserThread::UI
);
402 scoped_ptr
<StatusIconMenuModel
> menu(new StatusIconMenuModel(this));
405 int command_id
= IDC_MEDIA_CONTEXT_MEDIA_STREAM_CAPTURE_LIST_FIRST
;
406 command_targets_
.clear();
408 for (const auto& it
: usage_map_
) {
409 // Check if any audio and video devices have been used.
410 const WebContentsDeviceUsage
& usage
= *it
.second
;
411 if (!usage
.IsCapturingAudio() && !usage
.IsCapturingVideo())
414 WebContents
* const web_contents
= it
.first
;
416 // The audio/video icon is shown only for non-whitelisted extensions or on
417 // Android. For regular tabs on desktop, we show an indicator in the tab
419 #if defined(ENABLE_EXTENSIONS)
420 const extensions::Extension
* extension
= GetExtension(web_contents
);
421 if (!extension
|| IsWhitelistedExtension(extension
))
425 audio
= audio
|| usage
.IsCapturingAudio();
426 video
= video
|| usage
.IsCapturingVideo();
428 command_targets_
.push_back(web_contents
);
429 menu
->AddItem(command_id
, GetTitle(web_contents
));
431 // If the menu item is not a label, enable it.
432 menu
->SetCommandIdEnabled(command_id
, command_id
!= IDC_MinimumLabelValue
);
434 // If reaching the maximum number, no more item will be added to the menu.
435 if (command_id
== IDC_MEDIA_CONTEXT_MEDIA_STREAM_CAPTURE_LIST_LAST
)
440 if (command_targets_
.empty()) {
441 MaybeDestroyStatusTrayIcon();
445 // The icon will take the ownership of the passed context menu.
446 MaybeCreateStatusTrayIcon(audio
, video
);
448 status_icon_
->SetContextMenu(menu
.Pass());
452 void MediaStreamCaptureIndicator::GetStatusTrayIconInfo(
455 gfx::ImageSkia
* image
,
456 base::string16
* tool_tip
) {
457 DCHECK_CURRENTLY_ON(BrowserThread::UI
);
458 DCHECK(audio
|| video
);
463 if (audio
&& video
) {
464 message_id
= IDS_MEDIA_STREAM_STATUS_TRAY_TEXT_AUDIO_AND_VIDEO
;
465 *image
= *camera_image_
;
466 } else if (audio
&& !video
) {
467 message_id
= IDS_MEDIA_STREAM_STATUS_TRAY_TEXT_AUDIO_ONLY
;
468 *image
= *mic_image_
;
469 } else if (!audio
&& video
) {
470 message_id
= IDS_MEDIA_STREAM_STATUS_TRAY_TEXT_VIDEO_ONLY
;
471 *image
= *camera_image_
;
474 *tool_tip
= l10n_util::GetStringUTF16(message_id
);